diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000000..fa74450046a --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,46 @@ +# This is a comment. +# Each line is a file pattern followed by one or more owners. + +# These owners will be the default owners for everything in +# the repo. Unless a later match takes precedence, +# @global-owner1 and @global-owner2 will be requested for +# review when someone opens a pull request. +# * @global-owner1 @global-owner2 + +# Order is important; the last matching pattern takes the most +# precedence. When someone opens a pull request that only +# modifies JS files, only @js-owner and not the global +# owner(s) will be requested for a review. +# *.js @js-owner + +# You can also use email addresses if you prefer. They'll be +# used to look up users just like we do for commit author +# emails. +# *.go docs@example.com + +# In this example, @doctocat owns any files in the build/logs +# directory at the root of the repository and any of its +# subdirectories. +# /build/logs/ @doctocat + +# The `docs/*` pattern will match files like +# `docs/getting-started.md` but not further nested files like +# `docs/build-app/troubleshooting.md`. +# docs/* docs@example.com + +# In this example, @octocat owns any file in an apps directory +# anywhere in your repository. +# apps/ @octocat + +# In this example, @doctocat owns any file in the `/docs` +# directory in the root of your repository and any of its +# subdirectories. +# /docs/ @doctocat + +# Start of NVDA config + +# By default auto request review from NV Access developer team. +* @nvaccess/developers + +# For changes to the userGuide auto request review from userDocs team +/user_docs/en/userGuide.t2t @nvaccess/userDocs diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 00000000000..3797ea2d0a5 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,4 @@ +The NVDA project is open to contributions, thank you for your interest! + +Please first [read our guide to contributing](https://github.com/nvaccess/nvda/wiki/Contributing) on the NVDA +GitHub Wiki. diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 3ee7a74a6f1..2e4343b3d7b 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -10,6 +10,8 @@ Please thoroughly read NVDA's wiki article on how to fill in this template, incl Issues may be closed if the required information is not present. https://github.com/nvaccess/nvda/wiki/Github-issue-template-explanation-and-examples Please also note that the NVDA project has a Citizen and Contributor Code of Conduct which can be found at https://github.com/nvaccess/nvda/blob/master/CODE_OF_CONDUCT.md. NV Access expects that all contributors and other community members read and abide by the rules set out in this document while participating or contributing to this project. This includes creating or commenting on issues and pull requests. + +Each of the questions and sections below start with multiple hash symbols (#). Place your answers and information on the blank line below each question. --> ### Steps to reproduce: @@ -34,6 +36,6 @@ Please also note that the NVDA project has a Citizen and Contributor Code of Con #### Have you tried any other versions of NVDA? If so, please report their behaviors. -#### If addons are disabled, is your problem still occuring? +#### If add-ons are disabled, is your problem still occurring? -#### Did you try to run the COM registry fixing tool in NVDA menu / tools? +#### Does the issue still occur after you run the COM Registration Fixing Tool in NVDA's tools menu? diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 5cb5fec6274..967188fb34a 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -9,6 +9,8 @@ Please thoroughly read NVDA's wiki article on how to fill in this template, incl Issues may be closed if the required information is not present. https://github.com/nvaccess/nvda/wiki/Github-issue-template-explanation-and-examples Please also note that the NVDA project has a Citizen and Contributor Code of Conduct which can be found at https://github.com/nvaccess/nvda/blob/master/CODE_OF_CONDUCT.md. NV Access expects that all contributors and other community members read and abide by the rules set out in this document while participating or contributing to this project. This includes creating or commenting on issues and pull requests. + +Each of the questions and sections below start with multiple hash symbols (#). Place your answers and information on the blank line below each question. --> ### Steps to reproduce: @@ -33,6 +35,6 @@ Please also note that the NVDA project has a Citizen and Contributor Code of Con #### Have you tried any other versions of NVDA? If so, please report their behaviors. -#### If addons are disabled, is your problem still occuring? +#### If add-ons are disabled, is your problem still occurring? -#### Did you try to run the COM registry fixing tool in NVDA menu / tools? +#### Does the issue still occur after you run the COM Registration Fixing Tool in NVDA's tools menu? diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000000..ac2e8b47ddd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: false diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index b2f13d650ce..c2ae3a9ada2 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -9,6 +9,8 @@ Please thoroughly read NVDA's wiki article on how to fill in this template, incl Issues may be closed if the required information is not present. https://github.com/nvaccess/nvda/wiki/Github-issue-template-explanation-and-examples Please also note that the NVDA project has a Citizen and Contributor Code of Conduct which can be found at https://github.com/nvaccess/nvda/blob/master/CODE_OF_CONDUCT.md. NV Access expects that all contributors and other community members read and abide by the rules set out in this document while participating or contributing to this project. This includes creating or commenting on issues and pull requests. + +Each of the questions and sections below start with multiple hash symbols (#). Place your answers and information on the blank line below each question. --> ### Is your feature request related to a problem? Please describe. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index c1a874b202e..6da727c361e 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,6 +1,8 @@ ### Link to issue number: @@ -9,11 +11,30 @@ Please also note that the NVDA project has a Citizen and Contributor Code of Con ### Description of how this pull request fixes the issue: -### Testing performed: +### Testing strategy: ### Known issues with pull request: -### Change log entry: +### Change log entries: +New features +Changes +Bug fixes +For Developers + +### Code Review Checklist: -Section: New features, Changes, Bug fixes + +- [ ] Pull Request description is up to date. +- [ ] Unit tests. +- [ ] System (end to end) tests. +- [ ] Manual tests. +- [ ] User Documentation. +- [ ] Change log entry. +- [ ] Context sensitive help for GUI changes. diff --git a/.gitignore b/.gitignore index 1f9fbdeb99a..b66066c93cf 100644 --- a/.gitignore +++ b/.gitignore @@ -24,20 +24,24 @@ dlldata.c *.pdb .sconsign.dblite user_docs/*/*.html +user_docs/*/*.css +user_docs/*/keyCommands.t2t +user_docs/build.t2tConf extras/controllerClient/x86/nvdaController.h extras/controllerClient/x64 extras/controllerClient/arm64 source/_buildVersion.py -user_docs/*/keyCommands.t2t output testOutput -user_docs/build.t2tConf launcher/nvda_logo.wav uninstaller/UAC.nsh *.pyc *.pyo *.dmp tests/unit/nvda.ini +tests/system/settingsCache/* +!tests/system/settingsCache/2020.4/*.txt source/locale/*/cldr.dic .vscode .vs +.venv diff --git a/.gitmodules b/.gitmodules index 41835aa2324..6fb7620b8b9 100644 --- a/.gitmodules +++ b/.gitmodules @@ -15,31 +15,15 @@ path = include/sonic url = https://github.com/waywardgeek/sonic.git ignore = untracked -[submodule "include/pyserial"] - path = include/pyserial - url = https://github.com/pyserial/pyserial.git - ignore = untracked -[submodule "include/comtypes"] - path = include/comtypes - url = https://github.com/nvaccess/comtypes-bin -[submodule "include/scons"] - path = include/scons - url = https://github.com/SCons/scons [submodule "include/ia2"] path = include/ia2 url = https://github.com/LinuxA11y/IAccessible2.git -[submodule "include/wxPython"] - path = include/wxPython - url = https://github.com/nvaccess/wxPython-bin.git -[submodule "include/configobj"] - path = include/configobj - url = https://github.com/DiffSK/configobj.git -[submodule "include/py2exe"] - path = include/py2exe - url = https://github.com/nvaccess/py2exe-bin [submodule "include/javaAccessBridge32"] path = include/javaAccessBridge32 url = https://github.com/nvaccess/javaAccessBridge32-bin.git +[submodule "include/nvda_dmp"] + path = include/nvda_dmp + url = https://github.com/codeofdusk/nvda_dmp [submodule "include/w3c-aria-practices"] path = include/w3c-aria-practices url = https://github.com/w3c/aria-practices diff --git a/appveyor.yml b/appveyor.yml index 364c5d87591..a66ec823620 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -56,8 +56,6 @@ init: Set-AppveyorBuildVariable "versionType" $versionType } -clone_depth: 1 - install: - cd appveyor # Decrypt files. @@ -95,12 +93,20 @@ build_script: Set-AppveyorBuildVariable "sconsOutTargets" $sconsOutTargets Set-AppveyorBuildVariable "sconsArgs" $sconsArgs - 'echo scons args: %sconsArgs%' - - py scons.py source %sconsArgs% + - scons source %sconsArgs% + - if ERRORLEVEL 1 exit %ERRORLEVEL% # We don't need launcher to run checkPot, so run the checkPot before launcher. - - py scons.py checkPot %sconsArgs% + - ps: | + cmd.exe /c "scons checkPot $sconsArgs" + if($LastExitCode -ne 0) { + $errorCode=$LastExitCode + Add-AppveyorMessage "Translation comments missing or unexpectedly included." + } + if ($errorCode -ne 0) { $host.SetShouldExit($errorCode) } # The pot gets built by tests, but we don't actually need it as a build artifact. - 'echo scons output targets: %sconsOutTargets%' - - py scons.py %sconsOutTargets% %sconsArgs% + - scons %sconsOutTargets% %sconsArgs% + - if ERRORLEVEL 1 exit %ERRORLEVEL% # Build symbol store. - ps: | foreach ($syms in @@ -115,12 +121,10 @@ build_script: } - cd symbols - 7z a -tzip -r ..\output\symbols.zip *.dl_ *.ex_ *.pd_ + - if ERRORLEVEL 1 exit %ERRORLEVEL% - cd .. before_test: - # install required packages - - py -m pip install --upgrade pip - - py -m pip install -r tests/system/requirements.txt -r tests/lint/lintInstall/requirements.txt - mkdir testOutput - mkdir testOutput\unit - mkdir testOutput\system @@ -132,6 +136,7 @@ before_test: $nvdaLauncherFile+="_snapshot" } $nvdaLauncherFile+="_${env:version}.exe" + Set-AppveyorBuildVariable "nvdaLauncherFile" $nvdaLauncherFile echo NVDALauncherFile: $NVDALauncherFile $outputDir=$(resolve-path .\testOutput) $installerLogFilePath="$outputDir\nvda_install.log" @@ -145,6 +150,12 @@ before_test: Add-AppveyorMessage "Unable to install NVDA prior to tests." } Push-AppveyorArtifact $installerLogFilePath + $crashDump = "$outputDir\nvda_crash.dmp" + if (Test-Path -Path $crashDump){ + Push-AppveyorArtifact $crashDump -FileName "nvda_install_crash.dmp" + Add-AppveyorMessage "Install process crashed" + $errorCode=1 + } if($errorCode -ne 0) { $host.SetShouldExit($errorCode) } test_script: @@ -153,7 +164,7 @@ test_script: $errorCode=0 $outDir = (Resolve-Path .\testOutput\unit\) $unitTestsXml = "$outDir\unitTests.xml" - py -m nose -sv --with-xunit --xunit-file="$unitTestsXml" ./tests/unit + .\rununittests.bat --with-xunit --xunit-file="$unitTestsXml" if($LastExitCode -ne 0) { $errorCode=$LastExitCode Add-AppveyorMessage "Unit test failure" @@ -165,18 +176,28 @@ test_script: # Flake8 Linting - ps: | - if($env:APPVEYOR_PULL_REQUEST_NUMBER) { + if ($env:APPVEYOR_PULL_REQUEST_NUMBER -or $env:APPVEYOR_REPO_BRANCH.StartsWith("try-")) { $lintOutput = (Resolve-Path .\testOutput\lint\) $lintSource = (Resolve-Path .\tests\lint\) - git fetch -q origin $env:APPVEYOR_REPO_BRANCH - $prDiff = "$lintOutput\prDiff.patch" - git diff -U0 FETCH_HEAD...HEAD > $prDiff - $flake8Config = "$lintSource\flake8.ini" - $flake8Output = "$lintOutput\PR-Flake8.txt" - type "$prDiff" | py -Xutf8 -m flake8 --diff --output-file="$flake8Output" --tee --config="$flake8Config" + $flake8Output = "$lintOutput\Flake8.txt" + # When Appveyor runs for a pr, + # the build is made from a new temporary commit, + # resulting from the pr branch being merged into its base branch. + # Therefore to create a diff for linting, we must fetch the head of the base branch. + # In a PR, APPVEYOR_REPO_BRANCH points to the head of the base branch. + # Additionally, we can not use a clone_depth of 1, but must use an unlimited clone. + if($env:APPVEYOR_PULL_REQUEST_NUMBER) { + git fetch -q origin $env:APPVEYOR_REPO_BRANCH + $msgBaseLabel = "PR" + } else { + # However in a pushed branch, we must fetch master. + git fetch -q origin master:master + $msgBaseLabel = "Branch" + } + .\runlint.bat FETCH_HEAD "$flake8Output" if($LastExitCode -ne 0) { - $errorCode=$LastExitCode - Add-AppveyorMessage "PR introduces Flake8 errors" + $errorCode=$LastExitCode + Add-AppveyorMessage "$msgBaseLabel introduces Flake8 errors" } Push-AppveyorArtifact $flake8Output $junitXML = "$lintOutput\PR-Flake8.xml" @@ -191,19 +212,13 @@ test_script: - ps: | $testOutput = (Resolve-Path .\testOutput\) $systemTestOutput = (Resolve-Path "$testOutput\system") - $testSource = ".\tests\system\robot" - - $robotArgs = '--argumentfile', '.\tests\system\robotArgs.robot' - # useInstalledNVDA must come after args file to override the whichNVDA value - $useInstalledNVDA = '--variable', 'whichNVDA:installed' - - py -m robot $robotArgs $useInstalledNVDA $testSource - Compress-Archive -Path "$systemTestOutput\*" -DestinationPath "$testOutput\systemTestResult.zip" - Push-AppveyorArtifact "$testOutput\systemTestResult.zip" + .\runsystemtests.bat --variable whichNVDA:installed --variable installDir:"${env:nvdaLauncherFile}" --include installer if($LastExitCode -ne 0) { $errorCode=$LastExitCode Add-AppveyorMessage "System test failure" } + Compress-Archive -Path "$systemTestOutput\*" -DestinationPath "$testOutput\systemTestResult.zip" + Push-AppveyorArtifact "$testOutput\systemTestResult.zip" $wc = New-Object 'System.Net.WebClient' $wc.UploadFile("https://ci.appveyor.com/api/testresults/junit/$($env:APPVEYOR_JOB_ID)", (Resolve-Path "$systemTestOutput\systemTests.xml")) if($errorCode -ne 0) { $host.SetShouldExit($errorCode) } @@ -252,7 +267,13 @@ deploy_script: exe=$exe; hash=$hash; artifacts=$artifacts } - ConvertTo-Json -InputObject $data -Compress | ssh nvaccess@exbi.nvaccess.org nvdaAppveyorHook + ConvertTo-Json -InputObject $data -Compress | Out-File -FilePath deploy.json + Push-AppveyorArtifact deploy.json + # Execute the deploy script on the NV Access server via ssh. + # Warning: if the server address is changed, + # The new address must be also included in appveyor\ssh_known_hosts in this repo + # Otherwise ssh will freeze on input! + cat deploy.json | ssh nvaccess@deploy.nvaccess.org nvdaAppveyorHook # Upload symbols to Mozilla. py -m pip install --no-warn-script-location requests @@ -267,6 +288,9 @@ deploy_script: on_finish: # add a message to point to the NVDA build, to make testing PR's easier. - ps: | + # Save an exact list of the actual Python packages and their versions that got installed, to aide in reproducing a build + .\venvUtils\exportPackageList.bat installed_python_packages.txt + Push-AppveyorArtifact installed_python_packages.txt $appVeyorUrl = "https://ci.appveyor.com" $exe = Get-ChildItem -Name output\*.exe if($?){ diff --git a/appveyor/ssh_known_hosts b/appveyor/ssh_known_hosts index 5d596d692c3..9970fffd9f6 100644 --- a/appveyor/ssh_known_hosts +++ b/appveyor/ssh_known_hosts @@ -1,2 +1 @@ -exbi.nvaccess.org ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJZYcI3sI+IbIaiUcd5/LIL5+wfEoO+NQ8Gw1Ww9bxwpOyP/HYu/1yzuRNTDSckGM3hqaGGliTW2/3DYOvPDUwI= -45.33.21.113 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJZYcI3sI+IbIaiUcd5/LIL5+wfEoO+NQ8Gw1Ww9bxwpOyP/HYu/1yzuRNTDSckGM3hqaGGliTW2/3DYOvPDUwI= +deploy.nvaccess.org,45.33.23.174 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBBZdYWLmrUzPukq3wa15XFFgi2THBc92S9gpvZwrDUDkxNUD5VzRISf2uuFD+cRwn+gwj2jvH77fCFtwqfo4N3w= diff --git a/appx/manifest.xml.subst b/appx/manifest.xml.subst index 6efe97163e1..8b0d8f87149 100644 --- a/appx/manifest.xml.subst +++ b/appx/manifest.xml.subst @@ -24,7 +24,7 @@ diff --git a/devDocs/buildSystemNotes.md b/devDocs/buildSystemNotes.md new file mode 100644 index 00000000000..432b0515e5f --- /dev/null +++ b/devDocs/buildSystemNotes.md @@ -0,0 +1,74 @@ +# Build System Notes +A Python virtual environment is used transparently by the NVDA build system, +and all Python dependencies are installed into this environment using `pip`. + +NVDA's build system commands will handle all aspects of the virtual environment. +Developers should not create or activate the virtual environment manually, unless +working on the build system itself. + +For the documentation on how to _use_ the build system (E.G. building, +running NVDA or tests) see the main repository readme file. + +## How the build system works + +The virtual environment system used is `venv`. +Dependencies are installed with `pip` via the `requirements.txt` file. +Version numbers for dependencies should be used to lock in a version. + +The virtual environment is recreated if it is outdated, either due to: +- Python version. +- `pip` requirements. + +The user is consulted before modifying / removing a virtual environment that can't be identified +as having been created by NVDA's build system. + +### Entry points to the build system + +These are the only files expected to be executed directly by a user/developer: +- `scons.bat` +- `runnvda.bat` +- `rununittests.bat` +- `runsystemtests.bat` +- `runlint.bat` + +**Note:** The `runnvda.bat` script intentionally uses `pyw.exe` to run NVDA as +this is the more common and expected way to run NVDA. +Run NVDA with `py.exe` in order to have standard output/error output to the console. +This is particularly useful if there is an error in NVDA before logging is initialised. +To do this, modify the `runnvda.bat` file. + +**Note:** Executing `source/nvda.pyw` outside of a virtual environment will produce an error message +and early termination. + +### Main implementation files: +The following files contain the main implementation of the virtual environment setup. + +#### `venvUtils/ensureAndActivate.bat` + - Activates the virtual environment. + - If necessary, creates and configures it first. + - The virtual environment is left active. +#### `venvUtils/venvCmd.bat` + - Uses `ensureAndActivate.bat` to run a command within the context + of the virtual environment. + - The virtual environment is deactivated after the command + completes. + - All entry point scripts depend on this. +#### `venvUtils/ensureVenv.py` +- Does the actual work to create and configure the virtual + environment. + +## Motivation for using virtual environments + +Ensures the build environment is clean, and there are no conflicts with other installed packages. + +NVDA and its build system have many Python dependencies. +Using `pip` and a virtual environment means: +- Updating is easier than git submodules. + E.G. wxPython no longer has to be pre-built and stored in our bin repo. +- Developers need to sync/update their submodules less often. +- More consistency for dependencies. +- IDE's can be configured more easily. +- No conflict between NVDA dependencies and Python packages already installed globally on the + developer's system. +- Don't interfere with the developer's system. Installing packages globally may break things + outside of NVDA. diff --git a/devDocs/conf.py b/devDocs/conf.py index 06f073db94b..b01ee4fd6f2 100644 --- a/devDocs/conf.py +++ b/devDocs/conf.py @@ -1,5 +1,5 @@ # A part of NonVisual Desktop Access (NVDA) -# Copyright (C) 2019 NV Access Limited, Leonard de Ruijter +# Copyright (C) 2019-2020 NV Access Limited, Leonard de Ruijter, Joseph Lee # This file is covered by the GNU General Public License. # See the file COPYING for more details. @@ -16,7 +16,7 @@ import languageHandler # noqa: E402 languageHandler.setLanguage("en") -# Initialize globalvars.appArgs to something sensible. +# Initialize globalVars.appArgs to something sensible. import globalVars # noqa: E402 @@ -30,6 +30,11 @@ class AppArgs: globalVars.appArgs = AppArgs() +# #11971: NVDA is not running, therefore app dir is undefined. +# Therefore tell NVDA that apt source directory is app dir. +appDir = os.path.join("..", "source") +globalVars.appDir = os.path.abspath(appDir) + # Import NVDA's versionInfo module. import versionInfo # noqa: E402 diff --git a/devDocs/devDocsInstall/.gitignore b/devDocs/devDocsInstall/.gitignore deleted file mode 100644 index 9ae18088231..00000000000 --- a/devDocs/devDocsInstall/.gitignore +++ /dev/null @@ -1 +0,0 @@ -_executed_requirements.txt diff --git a/devDocs/devDocsInstall/requirements.txt b/devDocs/devDocsInstall/requirements.txt deleted file mode 100644 index 0111644238c..00000000000 --- a/devDocs/devDocsInstall/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -sphinx==2.2.2 -sphinx_rtd_theme diff --git a/devDocs/devDocsInstall/sconscript b/devDocs/devDocsInstall/sconscript deleted file mode 100644 index 032cd142541..00000000000 --- a/devDocs/devDocsInstall/sconscript +++ /dev/null @@ -1,35 +0,0 @@ -### -# This file is a part of the NVDA project. -# URL: http://www.nvaccess.org/ -# Copyright 2019 NV Access Limited. -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2.0, as published by -# the Free Software Foundation. -# 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. -# This license can be found at: -# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html -### - -import sys - -Import("env") - -doInstall = env.Command( - "_executed_requirements.txt", # $TARGET - "requirements.txt", # $SOURCE - [ - # Install deps from requirements file. - [ - sys.executable, "-m", "pip", - "install", "--no-warn-script-location", "-r", "$SOURCE", - ], - # Copy the requirements file, this is used to stop scons from - # triggering pip from attempting re-installing when nothing has - # changed. Pip takes a long time to determine that deps are met. - Copy('$TARGET', '$SOURCE') - ] -) - -Return('doInstall') diff --git a/devDocs/developerGuide.t2t b/devDocs/developerGuide.t2t index 3da21bcea28..0ab3ebd8a8f 100644 --- a/devDocs/developerGuide.t2t +++ b/devDocs/developerGuide.t2t @@ -179,7 +179,7 @@ However, they do differ in some ways. Custom appModules and globalPlugins can be packaged into NVDA add-ons. This allows easy distribution, and provides a safe way for the user to install and uninstall the custom code. -Please refer to the Add-ons section later on in this document. +Please refer to the [Add-ons section #Addons] later on in this document. In order to test the code while developing, you can place it in a special 'scratchpad' directory in your NVDA user configuration directory. You will also need to configure NVDA to enable loading of custom code from the Developer Scratchpad Directory, by enabling this in the Advanced category of NVDA's Settings dialog. @@ -194,7 +194,7 @@ For example, an App Module for notepad would be called notepad.py, as notepad's For apps hosted inside host executables, see the section on app modules for hosted apps. App Module files must be placed in the appModules subdirectory of an add-on, or of the scratchpad directory of the NVDA user configuration directory. - + App Modules must define a class called AppModule, which inherits from appModuleHandler.AppModule. This class can then define event and script methods, gesture bindings and other code. This will all be covered in depth later. @@ -283,7 +283,7 @@ As wwahost app module provides necessary infrastructure for apps hosted inside, ++ Basics of a Global Plugin ++ Global Plugin files have a .py extension, and should have a short unique name which identifies what they do. -Global plugin files must be placed in the globalPlugins subdirectory of an add-on, or of the scratchpad directory of the NVDA user configuration directory. +Global plugin files must be placed in the globalPlugins subdirectory of an add-on, or of the scratchpad directory of the NVDA user configuration directory. Global Plugins must define a class called GlobalPlugin, which inherits from globalPluginHandler.GlobalPlugin. This class can then define event and script methods, gesture bindings and other code. @@ -433,9 +433,9 @@ The order for gesture binding lookup is: +++ Defining script properties +++[DefiningScriptProperties] For NVDA 2018.3 and above, the recommended way to set script properties is by means of the so called script decorator. -In short, a decorator is a function that modifies the behavior of a particular function. +In short, a decorator is a function that modifies the behavior of a particular function or method. The script decorator modifies the script in such a way that it will be properly bound to the desired gestures. -Furthermore, it ensures that the script is listed with the description you specify, and that it is categorised under the desired category in the input gestures dialog. +Furthermore, it ensures that the script is listed with the description you specify, and that it is categorised under the desired category in the input gestures dialog. In order for you to use the script decorator, you will have to import it from the scriptHandler module. ``` @@ -478,6 +478,8 @@ The following keyword arguments can be used when applying the script decorator: This option defaults to False. - bypassInputHelp: A boolean indicating whether this script should run when input help is active. This option defaults to False. +- allowInSleepMode: A boolean indicating whether this script should run when sleep mode is active. + This option defaults to False. - resumeSayAllMode: The say all mode that should be resumed when active before executing this script. The constants for say all mode are prefixed with CURSOR_ and specified in the sayAllHandler modules. If resumeSayAllMode is not specified, say all does not resume after this script. @@ -486,8 +488,10 @@ The following keyword arguments can be used when applying the script decorator: Though the script decorator makes the script definition process a lot easier, there are more ways of binding gestures and setting script properties. For example, a special "__gestures" Python dictionary can be defined as a class variable on an App Module, Global Plugin or NVDA Object. This dictionary should contain gesture identifier strings pointing to the name of the requested script, without the "script_" prefix. -You can also specify a description of the script in the function's docstring. -Furthermore, an alternative way of specifying the script's category is by means of setting a "category" attribute on the script function to a string containing the name of the category. +You can also specify a description of the script in the method's "__doc__" attribute. +However, beware not to include an inline docstring at the start of the method if you do not set the "__doc__" attribute, as it would render the description not translatable. +The script decorator does not suffer from this limitation, so you are encouraged to provide inline docstrings as needed when using it. +Furthermore, an alternative way of specifying the script's category is by means of setting a "category" attribute on the script method to a string containing the name of the category. ++ Example 4: A Global Plugin to Find out Window Class and Control ID ++ The following Global Plugin allows you to press NVDA+leftArrow to have the window class of the current focus announced, and NVDA+rightArrow to have the window control ID of the current focus announced. @@ -688,17 +692,17 @@ class AppModule(appModuleHandler.AppModule): --- end --- ``` -+ Packaging Code as NVDA Add-ons + ++ Packaging Code as NVDA Add-ons +[Addons] To make it easy for users to share and install plugins and drivers, they can be packaged in to a single NVDA add-on package which the user can then install into a copy of NVDA via the Add-ons Manager found under Tools in the NVDA menu. Add-on packages are only supported in NVDA 2012.2 and later. -An add-on package is simply a standard zip archive with the file extension of nvda-addon which contains a manifest file, optional install/uninstall code and one or more directories containing plugins and/or drivers. +An add-on package is simply a standard zip archive with the file extension of nvda-addon which contains a manifest file, optional install/uninstall code and one or more directories containing plugins and/or drivers. ++ Non-ASCII File Names in Zip Archives ++ If your add-on includes files which contain non-ASCII (non-English) characters, you should create the zip archive such that it uses UTF-8 file names. This means that these files can be extracted properly on all systems, regardless of the system's configured language. Unfortunately, many zip archivers do not support this, including Windows Explorer. Generally, it has to be explicitly enabled even in archivers that do support it. -[http://www.7-zip.org/ 7-Zip] supports this, though it must be enabled by specifying the "cu=on" method parameter. +[7-Zip http://www.7-zip.org/] supports this, though it must be enabled by specifying the "cu=on" method parameter. ++ Manifest Files ++ Each add-on package must contain a manifest file named manifest.ini. @@ -720,12 +724,14 @@ Otherwise, the add-on will not install. - e.g "2019.1.1" - Must be a three part version string I.E. Year.Major.Minor, or a two part version string of Year.Major. In the second case, Minor defaults to 0. - Defaults to "0.0.0" - - Must be less than or equal to `lastTestedNVDAVersion` + - Must be less than or equal to ``lastTestedNVDAVersion`` + - - lastTestedNVDAVersion (string): The last version of NVDA this add-on has been tested with. - e.g "2019.1.0" - Must be a three part version string I.E. Year.Major.Minor, or a two part version string of Year.Major. In the second case, Minor defaults to 0. - Defaults to "0.0.0" - - Must be greater than or equal to `minimumNVDAVersion` + - Must be greater than or equal to ``minimumNVDAVersion`` + - - All string values must be enclosed in quotes as shown in the example below. @@ -747,7 +753,7 @@ docFileName = "readme.html" minimumNVDAVersion = "2018.1.0" lastTestedNVDAVersion = "2019.1.0" --- end --- -``` +``` ++ Plugins and Drivers ++ The following plugins and drivers can be included in an add-on: @@ -758,9 +764,9 @@ The following plugins and drivers can be included in an add-on: - ++ Optional install / Uninstall code ++ -If you need to execute code as your add-on is being installed or uninstalled from NVDA (e.g. to validate license information or to copy files to a custom location), you can provide a Python file called installTasks.py in the archive which contains special functions that NVDA will call while installing or uninstalling your add-on. -This file should avoid loading any modules that are not absolutely necessary, especially Python C extensions or dlls from your own add-on, as this could cause later removal of the add-on to fail. -However, if this does happen, the add-on directory will be renamed and then deleted after the next restart of NVDA. +If you need to execute code as your add-on is being installed or uninstalled from NVDA (e.g. to validate license information or to copy files to a custom location), you can provide a Python file called installTasks.py in the archive which contains special functions that NVDA will call while installing or uninstalling your add-on. +This file should avoid loading any modules that are not absolutely necessary, especially Python C extensions or dlls from your own add-on, as this could cause later removal of the add-on to fail. +However, if this does happen, the add-on directory will be renamed and then deleted after the next restart of NVDA. Finally, it should not depend on the existence or state of other add-ons, as they may not be installed, have already been removed or not yet be initialized. +++ the onInstall function +++ @@ -786,7 +792,7 @@ All other fields will be ignored. +++ Locale-specific Messages +++ Each language directory can also contain gettext information, which is the system used to translate the rest of NVDA's user interface and reported messages. As with the rest of NVDA, an nvda.mo compiled gettext database file should be placed in the LC_MESSAGES directory within this directory. -to allow plugins in your add-on to access gettext message information via calls to _(), you must initialize translations at the top of each Python module by calling addonHandler.initTranslation(). +to allow plugins in your add-on to access gettext message information via calls to _(), you must initialize translations at the top of each Python module by calling addonHandler.initTranslation(). For more information about gettext and NVDA translation in general, please read https://github.com/nvaccess/nvda/wiki/Translating @@ -818,6 +824,8 @@ You can navigate through the history of previously entered lines using the up an Output (responses from the interpreter) will be spoken when enter is pressed. The f6 key toggles between the input and output controls. +When on the output control, alt+up/down jumps to the previous/next result (add shift for selecting). +Pressing control+l clears the output. The result of the last executed command is stored in the "_" global variable. This shadows the gettext function which is stored as a built-in with the same name. @@ -856,11 +864,11 @@ If the input is "nav._", attribute names with a single leading underscore are pr Similarly, if the input is "nav.__", attribute names with two leading underscores are proposed. + Remote Python Console + -A remote Python console is available for situations where remote debugging of NVDA is useful. +A remote Python console is available in source builds of NVDA, for situations where remote debugging of NVDA is useful. It is similar to the [local Python console #PythonConsole] discussed above, but is accessed via TCP. Please be aware that this is a huge security risk. -You should only enable it if you are connected to trusted networks. +It is not available in binary builds distributed by NV Access, and You should only enable it if you are connected to trusted networks. ++ Usage ++ To enable the remote Python console, use the local Python console to import remotePythonConsole and call remotePythonConsole.initialize(). diff --git a/devDocs/sconscript b/devDocs/sconscript index c04bd9261b9..12e3f64b8f0 100644 --- a/devDocs/sconscript +++ b/devDocs/sconscript @@ -19,10 +19,6 @@ Import("env", "outputDir", "sourceDir", "t2tBuildConf") env = env.Clone() -devDocsInstall = env.SConscript("devDocsInstall/sconscript", exports=["env"]) -env.Depends(devDocsInstall, env.Dir("devDocs/devDocsInstall/requirements.txt")) -env.Alias("devDocsInstall", devDocsInstall) - devDocsOutputDir=outputDir.Dir('devDocs') #Build the developer guide and move it to the output directory @@ -69,7 +65,6 @@ sphinxAPIDocs = env.Command( ] + [f"{sourceDir}\\{f}" for f in ignorePaths] ] ) -env.Depends(sphinxAPIDocs, devDocsInstall) sphinxHtml = env.Command( "_build", sphinxAPIDocs, diff --git a/extras/controllerClient/x86/example_python.py b/extras/controllerClient/x86/example_python.py index 73e6d457af8..f6abf79992c 100644 --- a/extras/controllerClient/x86/example_python.py +++ b/extras/controllerClient/x86/example_python.py @@ -11,7 +11,7 @@ ctypes.windll.user32.MessageBoxW(0,u"Error: %s"%errorMessage,u"Error communicating with NVDA",0) #Speak and braille some messages -for count in xrange(4): +for count in range(4): clientLib.nvdaController_speakText(u"This is a test client for NVDA") clientLib.nvdaController_brailleMessage(u"Time: %g seconds"%(0.75*count)) time.sleep(0.625) diff --git a/include/cldr b/include/cldr index d1c59aeea6a..367b79b4a86 160000 --- a/include/cldr +++ b/include/cldr @@ -1 +1 @@ -Subproject commit d1c59aeea6a26cd1018e164e8c470e4873925cac +Subproject commit 367b79b4a86a9cb12314158897d950268dc77b02 diff --git a/include/comtypes b/include/comtypes deleted file mode 160000 index 8c45582fe49..00000000000 --- a/include/comtypes +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 8c45582fe497269594f127f93d1a786d6bc868fb diff --git a/include/configobj b/include/configobj deleted file mode 160000 index f9a265c4adf..00000000000 --- a/include/configobj +++ /dev/null @@ -1 +0,0 @@ -Subproject commit f9a265c4adf2e4e64fd65b5555d0a1b2fbf9ec39 diff --git a/include/espeak b/include/espeak index aafd2e72058..cad1c8e87fc 160000 --- a/include/espeak +++ b/include/espeak @@ -1 +1 @@ -Subproject commit aafd2e720582d7ccdadef675ce656903bc775c1d +Subproject commit cad1c8e87fcccf677a445202e340f61980450a84 diff --git a/include/espeak.md b/include/espeak.md new file mode 100644 index 00000000000..3fcca61821d --- /dev/null +++ b/include/espeak.md @@ -0,0 +1,55 @@ +# Espeak-ng submodule + +The submodule contained in the espeak directory is a cross platform open source speech synthesizer. + +### Background +The main authority on build requirements should be `/include/espeak/Makefile.am`. +The `*.vcxproj` files in `/include/espeak/src/windows/` can also be considered, +however these are not always kept up to date. + +We don't use the auto make files or the visual studio files, we maintain our own method of building espeak. +Modifications will need to be made in `/nvdaHelper/espeak` +* `sconscript` for the build process. +* `config.h` to set the eSpeak-ng version that NVDA outputs to the log file. + +### Doing the update + +1. Start from a clean branch off of NVDA `master` + 1. Check out the latest NVDA `origin/master` and create a new branch. + 1. Do a git clean to ensure the working directory is clean. +1. Ensure submodules are up to date + 1. Synchronize submodules with `git submodule sync` + 1. Update submodules with `git submodule update --init --recursive` +1. Checkout the new eSpeak-ng revision to try. + 1. Change to the `include/espeak/` directory + 1. Do `git fetch` to get the latest from the espeak-ng repo + 1. Do `git checkout origin/master` or whichever espeak-ng ref you wish. +1. Look at changes in `Makefile.am` and update our build. + 1. Diff `Makefile.am` with the previously used commit of espeak. + 1. Some modules are intentionally excluded from the build. + If unsure, err on the side of including it and raise it as a question when submitting a PR. + 1. Modify the `/nvdaHelper/espeak/config.h` file as required. +1. Update our record of the version number and build. + 1. Change back to the NVDA repo root + 1. Update the package version in `/nvdaHelper/espeak/config.h` + - Compare to espeak source info: `/include/espeak/src/windows/config.h`. + 1. Update NVDA `readme.md` with espeak version and commit. + 1. Build NVDA +1. Run NVDA (set eSpeak-ng as the synthesizer) and test. +1. Ensure that the log file contains the new version number for eSpeak-NG + +### Troubleshooting + +If python crashes while building, check the log. +If the last thing is compiling some dictionary try excluding it. +This can be done in `/nvdaHelper/espeak/sconscript`. +Remember to report this to the eSpeak-ng project. + +If the build fails, take note of the error, compare the diff of the `Makefile.am` file and mirror +any changes in our `sconscript` file. + +### Known issues +Due to problems with emoji support (causing crashes), emoji dictionary files are being excluded +from the build, they are deleted prior to compiling the dictionaries in the +`/nvdaHelper/espeak/sconscript` file. + diff --git a/include/liblouis b/include/liblouis index 48633b86de7..2e8b61c5605 160000 --- a/include/liblouis +++ b/include/liblouis @@ -1 +1 @@ -Subproject commit 48633b86de7d4dc86e5aa21855f1974ede1b0c26 +Subproject commit 2e8b61c56055370fba697b3c0ec41c26d159d1b0 diff --git a/include/nvda_dmp b/include/nvda_dmp new file mode 160000 index 00000000000..1c601ecd10a --- /dev/null +++ b/include/nvda_dmp @@ -0,0 +1 @@ +Subproject commit 1c601ecd10a5c0256cbaab61b8d38d46bc7f1cf4 diff --git a/include/py2exe b/include/py2exe deleted file mode 160000 index c496ae65e4c..00000000000 --- a/include/py2exe +++ /dev/null @@ -1 +0,0 @@ -Subproject commit c496ae65e4cb67fb5a1c17087ec33e4fd69be859 diff --git a/include/pyserial b/include/pyserial deleted file mode 160000 index c54c81d933b..00000000000 --- a/include/pyserial +++ /dev/null @@ -1 +0,0 @@ -Subproject commit c54c81d933b847458d465cd77e96cd702ff2e7be diff --git a/include/scons b/include/scons deleted file mode 160000 index dfcc78cf334..00000000000 --- a/include/scons +++ /dev/null @@ -1 +0,0 @@ -Subproject commit dfcc78cf3343debc4e01d846fd286779bba3ff1e diff --git a/include/wxPython b/include/wxPython deleted file mode 160000 index 11de9371b6b..00000000000 --- a/include/wxPython +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 11de9371b6b737673ad5ea2703d214ada69a158a diff --git a/launcher/nvdaLauncher.nsi b/launcher/nvdaLauncher.nsi index cef8e853cf1..2711ae7e5b8 100644 --- a/launcher/nvdaLauncher.nsi +++ b/launcher/nvdaLauncher.nsi @@ -1,4 +1,5 @@ !include "fileFunc.nsh" +!include "LogicLib.nsh" !include "mui2.nsh" !define launcher_appExe "nvdaLauncher.exe" @@ -72,10 +73,17 @@ Banner::show /nounload BringToFront setOutPath "$PLUGINSDIR" -;Play NVDA logo sound -File "..\miscDeps\launcher\nvda_logo.wav" -Push "$PLUGINSDIR\nvda_logo.wav" -Call PlaySound +; Get the full param string and puts it in register $0. +; So $0 may then contain eg. "--minimal --install" +; Reference: https://nsis.sourceforge.io/Docs/AppendixE.html#getparameters +${GetParameters} $0 +; From the params string, looks for option "--minimal", tries to get it's (unused) value and stores in $1. +; Sets the error flag if the option is missing. +; Reference: https://nsis.sourceforge.io/Docs/AppendixE.html#getoptions +${GetOptions} $0 "--minimal" $1 +${If} ${Errors} + Call PlayLogoSound +${EndIf} CreateDirectory "$PLUGINSDIR\app" setOutPath "$PLUGINSDIR\app" file /R "${NVDADistDir}\" @@ -87,6 +95,11 @@ execWait "$PLUGINSDIR\app\nvda_noUIAccess.exe $0 -r --launcher" $1 intcmp $1 3 exec +1 SectionEnd +Function PlayLogoSound +File "..\miscDeps\launcher\nvda_logo.wav" +Push "$PLUGINSDIR\nvda_logo.wav" +Call PlaySound +FunctionEnd Function PlaySound ; Retrieve the file to play diff --git a/nvdaHelper/archBuild_sconscript b/nvdaHelper/archBuild_sconscript index ef73eb04b18..be1192483f8 100644 --- a/nvdaHelper/archBuild_sconscript +++ b/nvdaHelper/archBuild_sconscript @@ -93,18 +93,8 @@ env.Append( ] ) env.Append(CCFLAGS=[ - '/W3', - '/WX', - '/std:c++17' + '/std:c++17', ]) -if 'analyze' in debug: - env.Append(CCFLAGS=['/analyze']) - # Disable: Inconsistent annotation for 'x': this instance has no annotations. - # Seems all MIDL-generated code from idl files don't add annotations - env.Append(CCFLAGS='/wd28251') - # Disable: 'x': unreferenced formal parameter - # We use a great deal of hook functions where we have no need for various parameters - env.Append(CCFLAGS='/wd4100') env.Append(CXXFLAGS=['/EHsc']) @@ -155,6 +145,22 @@ if 'debugCRT' in debug: else: env.Append(CCFLAGS=['/MT']) +# Don't enable warnings and warnings as errors or analysis to 3rd party code. +thirdPartyEnv = env.Clone() +env.Append(CCFLAGS=[ + '/W3', + '/WX', +]) +if 'analyze' in debug: + env.Append(CCFLAGS=['/analyze']) + # Disable: Inconsistent annotation for 'x': this instance has no annotations. + # Seems all MIDL-generated code from idl files don't add annotations + env.Append(CCFLAGS='/wd28251') + # Disable: 'x': unreferenced formal parameter + # We use a great deal of hook functions where we have no need for various parameters + env.Append(CCFLAGS='/wd4100') + +Export('thirdPartyEnv') Export('env') acrobatAccessRPCStubs=env.SConscript('acrobatAccess_sconscript') @@ -218,5 +224,5 @@ if TARGET_ARCH in ('x86_64', 'arm64'): env.Install(libInstallDir,remoteLoaderProgram) if TARGET_ARCH=='x86': - env.SConscript('espeak/sconscript') - env.SConscript('liblouis/sconscript') + thirdPartyEnv.SConscript('espeak/sconscript') + thirdPartyEnv.SConscript('liblouis/sconscript') diff --git a/nvdaHelper/espeak/config.h b/nvdaHelper/espeak/config.h index 0c96e87da10..9032f70f805 100644 --- a/nvdaHelper/espeak/config.h +++ b/nvdaHelper/espeak/config.h @@ -1,10 +1,5 @@ -// general headers and types -#define HAVE_STDINT_H 1 -#define __WIN32__ 1 -#define LIBESPEAK_NG_EXPORT +// Supplies the "config.h" include for espeak files +// Replaces the include/espeak/src/windows/config.h file -// Espeak features -#define INCLUDE_KLATT 1 -#define HAVE_SONIC_H 1 - -#define PACKAGE_VERSION "1.49.3 dev" +// Preprocessor definitions have been moved to the build system +// See: nvdaHelper/espeak/sconscript diff --git a/nvdaHelper/espeak/sconscript b/nvdaHelper/espeak/sconscript index 2f8e8147bd2..51838d16811 100644 --- a/nvdaHelper/espeak/sconscript +++ b/nvdaHelper/espeak/sconscript @@ -1,5 +1,5 @@ Import([ - 'env', + 'thirdPartyEnv', 'sourceDir', ]) @@ -30,19 +30,37 @@ class espeak_VOICE(ctypes.Structure): ('spare',ctypes.c_void_p), ] -env=env.Clone() -#Whole-program optimization causes eSpeak to distort and worble with its Klatt4 voice -#Therefore specifically force it off -env.Append(CCFLAGS='/GL-') - -# Don't analyze the code as not our project -if 'analyze' in env['nvdaHelperDebugFlags']: - env.Append(CCFLAGS='/analyze-') - -# Ignore all warnings as the code is not ours -env.Replace(CCFLAGS='/W0') - -env.Append(CPPPATH=['#nvdaHelper/espeak',espeakIncludeDir,espeakIncludeDir.Dir('compat'),sonicSrcDir,espeakSrcDir.Dir('ucd-tools/src/include')]) +env = thirdPartyEnv.Clone() +env.Append( + CCFLAGS=[ + # Whole-program optimization causes eSpeak to distort and warble with its Klatt4 voice + # Therefore specifically force it off + '/GL-', + # Ignore all warnings as the code is not ours. + '/W0', + # Preprocessor definitions. Migrated from 'nvdaHelper/espeak/config.h' + '/DPACKAGE_VERSION=\\"1.51-dev\\"', # See 'include/espeak/src/windows/config.h' + '/DHAVE_STDINT_H=1', + '/D__WIN32__#1', + '/DLIBESPEAK_NG_EXPORT', + # Define WIN32_LEAN_AND_MEAN for preprocessor to prevent windows.h including winsock causing redefinition + # errors when winsock2 is included by espeak\src\include\compat\endian.h + '/DWIN32_LEAN_AND_MEAN', + # Preprocessor definitions. Espeak Features + '/DINCLUDE_SPEECHPLAYER=1', + '/DINCLUDE_KLATT=1', + '/DHAVE_SONIC_H=1', + ]) + +env.Append( + CPPPATH=[ + '#nvdaHelper/espeak', # ensure that nvdaHelper/espeak/config.h is found first. + espeakIncludeDir, + espeakIncludeDir.Dir('compat'), + espeakSrcDir.Dir('speechPlayer/include'), + sonicSrcDir, + espeakSrcDir.Dir('ucd-tools/src/include') + ]) def espeak_compilePhonemeData_buildEmitter(target,source,env): phSourceIgnores=['error_log','error_intonation','compile_prog_log','compile_report','envelopes.png'] @@ -120,6 +138,7 @@ espeakLib=env.SharedLibrary( "phonemelist.c", "readclause.c", "setlengths.c", + "soundIcon.c", "spect.c", "speech.c", "ssml.c", @@ -131,6 +150,14 @@ espeakLib=env.SharedLibrary( "voices.c", "wavegen.c", sonicLib, + # espeak OPT_SPEECHPLAYER block + "sPlayer.c", + "../speechPlayer/src/frame.cpp", + "../speechPlayer/src/speechPlayer.cpp", + "../speechPlayer/src/speechWaveGenerator.cpp", + #"../speak-ng.cpp", + # if not OPT_SPEECHPLAYER + # "../speak-ng.c", # espeak does not need to handle its own audio output so dont include: # pcaudiolib\src\audio.c # pcaudiolib\src\windows.c diff --git a/nvdaHelper/liblouis/sconscript b/nvdaHelper/liblouis/sconscript index 6bb37a5a85c..8404e9c0e67 100644 --- a/nvdaHelper/liblouis/sconscript +++ b/nvdaHelper/liblouis/sconscript @@ -16,11 +16,17 @@ import os import re import glob from SCons.Tool.MSCommon.vc import find_vc_pdir +import typing +from SCons.Environment import Environment +from SCons.Environment import Base Import([ - "env", + "thirdPartyEnv", "sourceDir", ]) +sourceDir:Base = sourceDir +thirdPartyEnv: Environment = thirdPartyEnv +env: Environment = typing.cast(Environment, thirdPartyEnv.Clone()) louisRootDir = env.Dir("#include/liblouis") louisSourceDir = louisRootDir.Dir("liblouis") @@ -37,8 +43,6 @@ def getLouisVersion(): return m.group("version") return "unknown" -env = env.Clone() - # Liblouis is build with Clang, as Microsoft Visual C++ is unable to build C99 code. clangDirs = glob.glob(os.path.join( find_vc_pdir(env, env.get("MSVC_VERSION")), diff --git a/nvdaHelper/remote/sconscript b/nvdaHelper/remote/sconscript index 80a66432707..9604944ed7e 100644 --- a/nvdaHelper/remote/sconscript +++ b/nvdaHelper/remote/sconscript @@ -23,7 +23,6 @@ winIPCUtilsObj=env.Object("./winIPCUtils","../common/winIPCUtils.cpp") vbufBackendLibs=[ env.SConscript('../vbufBase/sconscript'), env.SConscript('../vbufBackends/adobeAcrobat/sconscript'), - env.SConscript('../vbufBackends/adobeFlash/sconscript'), env.SConscript('../vbufBackends/lotusNotesRichText/sconscript'), env.SConscript('../vbufBackends/gecko_ia2/sconscript'), env.SConscript('../vbufBackends/mshtml/sconscript'), diff --git a/nvdaHelper/remote/vbufRemote.cpp b/nvdaHelper/remote/vbufRemote.cpp index 0ce6b35ac16..bfbfb1eefad 100755 --- a/nvdaHelper/remote/vbufRemote.cpp +++ b/nvdaHelper/remote/vbufRemote.cpp @@ -22,7 +22,6 @@ using namespace std; const map VBufBackendFactoryMap { {L"adobeAcrobat",AdobeAcrobatVBufBackend_t_createInstance}, - {L"adobeFlash",AdobeFlashVBufBackend_t_createInstance}, {L"gecko_ia2",GeckoVBufBackend_t_createInstance}, {L"mshtml",MshtmlVBufBackend_t_createInstance}, {L"lotusNotesRichText",lotusNotesRichTextVBufBackend_t_createInstance}, diff --git a/nvdaHelper/vbufBackends/adobeFlash/adobeFlash.cpp b/nvdaHelper/vbufBackends/adobeFlash/adobeFlash.cpp deleted file mode 100644 index d76fd501386..00000000000 --- a/nvdaHelper/vbufBackends/adobeFlash/adobeFlash.cpp +++ /dev/null @@ -1,308 +0,0 @@ -/* -This file is a part of the NVDA project. -URL: http://www.nvda-project.org/ -Copyright 2010-2013 NV Access Limited - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License version 2.0, as published by - the Free Software Foundation. - 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. -This license can be found at: -http://www.gnu.org/licenses/old-licenses/gpl-2.0.html -*/ - -#include -#include -#include -#include -#include -#include -#include -#include -#include "adobeFlash.h" - -using namespace std; - -void AdobeFlashVBufBackend_t::renderThread_initialize() { - registerWinEventHook(renderThread_winEventProcHook); - VBufBackend_t::renderThread_initialize(); -} - -void AdobeFlashVBufBackend_t::renderThread_terminate() { - unregisterWinEventHook(renderThread_winEventProcHook); - if (this->accPropServices) - this->accPropServices->Release(); - VBufBackend_t::renderThread_terminate(); -} - -void CALLBACK AdobeFlashVBufBackend_t::renderThread_winEventProcHook(HWINEVENTHOOK hookID, DWORD eventID, HWND hwnd, long objectID, long childID, DWORD threadID, DWORD time) { - switch(eventID) { - case EVENT_OBJECT_REORDER: - case EVENT_OBJECT_NAMECHANGE: - case EVENT_OBJECT_VALUECHANGE: - case EVENT_OBJECT_STATECHANGE: - break; - default: - return; - } - - int docHandle=HandleToUlong(hwnd); - int ID=(childID==CHILDID_SELF&&objectID>0)?objectID:childID; - for(VBufBackendSet_t::iterator i=runningBackends.begin();i!=runningBackends.end();++i) { - AdobeFlashVBufBackend_t* backend=NULL; - HWND rootWindow=(HWND)UlongToHandle(((*i)->rootDocHandle)); - if(rootWindow!=hwnd) - continue; - backend=static_cast(*i); - VBufStorage_controlFieldNode_t* node=backend->getControlFieldNodeWithIdentifier(docHandle,ID); - if(node) - backend->invalidateSubtree(node); - if(!backend->isWindowless) { - // If this is not windowless, there can only be one backend with this docHandle, - // so stop searching. - break; - } - } -} - -VBufStorage_fieldNode_t* AdobeFlashVBufBackend_t::renderControlContent(VBufStorage_buffer_t* buffer, VBufStorage_controlFieldNode_t* parentNode, VBufStorage_fieldNode_t* previousNode, int docHandle, int id, IAccessible* pacc) { - nhAssert(buffer); - - HRESULT res; - //all IAccessible methods take a variant for childID, get one ready - VARIANT varChild; - varChild.vt=VT_I4; - varChild.lVal=CHILDID_SELF; - - VBufStorage_fieldNode_t* tempNode=NULL; - - //Make sure that we don't already know about this object -- protect from loops - if(buffer->getControlFieldNodeWithIdentifier(docHandle,id)!=NULL) { - return NULL; - } - - //Add this node to the buffer - parentNode=buffer->addControlFieldNode(parentNode,previousNode,docHandle,id,TRUE); - nhAssert(parentNode); //new node must have been created - previousNode=NULL; - - wostringstream s; - - // Get role with accRole - long role = 0; - VARIANT varRole; - VariantInit(&varRole); - if((res=pacc->get_accRole(varChild,&varRole))!=S_OK) { - s<<0; - } else if(varRole.vt==VT_BSTR) { - s << varRole.bstrVal; - } else if(varRole.vt==VT_I4) { - s << varRole.lVal; - role = varRole.lVal; - } - parentNode->addAttribute(L"IAccessible::role",s.str()); - VariantClear(&varRole); - - // Get states with accState - VARIANT varState; - VariantInit(&varState); - if((res=pacc->get_accState(varChild,&varState))!=S_OK) { - varState.vt=VT_I4; - varState.lVal=0; - } - int states=varState.lVal; - VariantClear(&varState); - //Add each state that is on, as an attrib - for(int i=0;i<32;++i) { - int state=1<addAttribute(s.str(),L"1"); - } - } - - BSTR tempBstr=NULL; - wstring name; - wstring value; - wstring content; - - if ((res = pacc->get_accName(varChild, &tempBstr)) == S_OK) { - name = tempBstr; - SysFreeString(tempBstr); - } - if ((res = pacc->get_accValue(varChild, &tempBstr)) == S_OK) { - value = tempBstr; - SysFreeString(tempBstr); - } - if(!value.empty()) { - content=value; - } else if(role!=ROLE_SYSTEM_TEXT&&!name.empty()) { - content=name; - } else if (states & STATE_SYSTEM_FOCUSABLE) { - // This node is focusable, but contains no text. - // Therefore, add it with a space so that the user can get to it. - content = L" "; - } - - if (!content.empty()) { - if (tempNode = buffer->addTextFieldNode(parentNode, previousNode, content)) { - previousNode=tempNode; - } - } - - return parentNode; -} - -long AdobeFlashVBufBackend_t::getAccId(IAccessible* acc) { - IAccIdentity* accId = NULL; - if (acc->QueryInterface(IID_IAccIdentity, (void**)&accId) != S_OK || !accId) - return -1; - BYTE* idString=NULL; - DWORD idLen=0; - HRESULT res; - res = accId->GetIdentityString(CHILDID_SELF, &idString, &idLen); - accId->Release(); - if (res != S_OK || !idString) - return -1; - if (!this->accPropServices) { - // Only retrieve this the first time it's needed. - if (CoCreateInstance(CLSID_AccPropServices, NULL, CLSCTX_SERVER, IID_IAccPropServices, (void**)&this->accPropServices) != S_OK) { - CoTaskMemFree(idString); - return -1; - } - } - HWND hwnd; - DWORD objId; - DWORD childId; - res = this->accPropServices->DecomposeHwndIdentityString(idString, idLen, &hwnd, &objId, &childId); - CoTaskMemFree(idString); - if (res != S_OK) - return -1; - return objId; -} - -void AdobeFlashVBufBackend_t::render(VBufStorage_buffer_t* buffer, int docHandle, int ID, VBufStorage_controlFieldNode_t* oldNode) { - if (!oldNode) { - // This is the initial render. - WCHAR* wclass = (WCHAR*)malloc(sizeof(WCHAR) * 256); - if (!wclass) - return; - if (GetClassName((HWND)UlongToHandle(docHandle), wclass, 256) == 0) { - free(wclass); - return; - } - this->isWindowless = wcscmp(wclass, L"Internet Explorer_Server") == 0; - free(wclass); - } - - DWORD_PTR res=0; - //Get an IAccessible by sending WM_GETOBJECT directly to bypass any proxying, to speed things up. - if (SendMessageTimeout((HWND)UlongToHandle(docHandle), WM_GETOBJECT, 0, isWindowless ? ID : OBJID_CLIENT, SMTO_ABORTIFHUNG, 2000, &res) == 0 || res == 0) { - //Failed to send message or window does not support IAccessible - return; - } - IAccessible* pacc=NULL; - if(ObjectFromLresult(res,IID_IAccessible,0,(void**)&pacc)!=S_OK) { - //Could not get the IAccessible pointer from the WM_GETOBJECT result - return; - } - nhAssert(pacc); //must get a valid IAccessible object - HRESULT hres; - VARIANT varChild; - varChild.vt=VT_I4; - IAccessible* childAcc; - if (ID != CHILDID_SELF && !this->isWindowless) { - // We have the root accessible, but a specific child has been requested. - varChild.lVal = ID; - IDispatch* childDisp = NULL; - hres = pacc->get_accChild(varChild, &childDisp); - pacc->Release(); - if (hres != S_OK || !childDisp) - return; - childAcc = NULL; - hres = childDisp->QueryInterface(IID_IAccessible, (void**)&childAcc); - childDisp->Release(); - if (hres != S_OK || !childAcc) - return; - pacc = childAcc; - } - - if (!oldNode || ID == this->rootID) { - // This is the root node. - VBufStorage_controlFieldNode_t* parentNode=buffer->addControlFieldNode(NULL,NULL,docHandle,ID,TRUE); - parentNode->addAttribute(L"IAccessible::role",L"10"); - VBufStorage_fieldNode_t* previousNode=NULL; - long childCount=0; - pacc->get_accChildCount(&childCount); - - if (this->getAccId(pacc) != -1) { - // We can get IDs from accessibles. - VARIANT* varChildren; - if (!(varChildren = (VARIANT*)malloc(sizeof(VARIANT) * childCount))) - return; - if (FAILED(AccessibleChildren(pacc, 0, childCount, varChildren, &childCount))) - childCount = 0; - for (long i = 0; i < childCount; ++i) { - if (varChildren[i].vt != VT_DISPATCH || !varChildren[i].pdispVal) { - VariantClear(&(varChildren[i])); - continue; - } - childAcc = NULL; - hres = varChildren[i].pdispVal->QueryInterface(IID_IAccessible, (void**)&childAcc); - VariantClear(&(varChildren[i])); - if (hres != S_OK) - continue; - int childId = getAccId(childAcc); - previousNode = this->renderControlContent(buffer, parentNode, previousNode, docHandle, childId, childAcc); - childAcc->Release(); - } - free(varChildren); - - } else { - // We can't get IDs from accessibles. - // The only way to get IDs is to just try them sequentially. - // accessiblesByLocation maps ((x, y), id) to (accessible, id). - // This allows us to order by location and, where that is the same, ID. - // We need this because added children always have larger IDs, - // even if they were inserted between two other children. - map, long>, pair> accessiblesByLocation; - // Keep going until we have childCount children. - for(int i=1;i<1000&&static_cast(accessiblesByLocation.size())get_accChild(varChild, &childDisp) != S_OK || !childDisp) - continue; - childAcc = NULL; - hres = childDisp->QueryInterface(IID_IAccessible, (void**)&childAcc); - childDisp->Release(); - if (hres != S_OK || !childAcc) - continue; - long left=0, top=0, width=0, height=0; - varChild.lVal = CHILDID_SELF; - if (childAcc->accLocation(&left, &top, &width, &height, varChild) != S_OK) - left=top=width=height=0; - accessiblesByLocation[make_pair(make_pair(top + height / 2, left + width / 2), i)] = make_pair(childAcc, i); - } - for (map, long>, pair>::iterator i = accessiblesByLocation.begin(); i != accessiblesByLocation.end(); ++i) { - previousNode = this->renderControlContent(buffer, parentNode, previousNode, docHandle, i->second.second, i->second.first); - i->second.first->Release(); - } - } - - } else { - // This is a child that is being re-rendered. - this->renderControlContent(buffer, NULL, NULL, docHandle, ID, pacc); - } - - pacc->Release(); -} - -AdobeFlashVBufBackend_t::AdobeFlashVBufBackend_t(int docHandle, int ID): VBufBackend_t(docHandle,ID), accPropServices(NULL) { -} - -VBufBackend_t* AdobeFlashVBufBackend_t_createInstance(int docHandle, int ID) { - VBufBackend_t* backend=new AdobeFlashVBufBackend_t(docHandle,ID); - return backend; -} diff --git a/nvdaHelper/vbufBackends/adobeFlash/adobeFlash.h b/nvdaHelper/vbufBackends/adobeFlash/adobeFlash.h deleted file mode 100644 index 39ea00e4c1d..00000000000 --- a/nvdaHelper/vbufBackends/adobeFlash/adobeFlash.h +++ /dev/null @@ -1,51 +0,0 @@ -/* -This file is a part of the NVDA project. -URL: http://www.nvda-project.org/ -Copyright 2010-2013 NV Access Limited - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License version 2.0, as published by - the Free Software Foundation. - 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. -This license can be found at: -http://www.gnu.org/licenses/old-licenses/gpl-2.0.html -*/ - -#ifndef VIRTUALBUFFER_BACKENDS_ADOBEFLASH_H -#define VIRTUALBUFFER_BACKENDS_ADOBEFLASH_H - -#include -#include -#include - -class AdobeFlashVBufBackend_t: public VBufBackend_t { - private: - - VBufStorage_fieldNode_t* renderControlContent(VBufStorage_buffer_t* buffer, VBufStorage_controlFieldNode_t* parentNode, VBufStorage_fieldNode_t* previousNode, int docHandle, int id, IAccessible* pacc); - - IAccPropServices* accPropServices; - - long getAccId(IAccessible* acc); - - bool isWindowless; - - protected: - - static void CALLBACK renderThread_winEventProcHook(HWINEVENTHOOK hookID, DWORD eventID, HWND hwnd, long objectID, long childID, DWORD threadID, DWORD time); - - virtual void renderThread_initialize(); - - virtual void renderThread_terminate(); - - virtual void render(VBufStorage_buffer_t* buffer, int docHandle, int ID, VBufStorage_controlFieldNode_t* oldNode); - - //virtual ~AdobeFlashVBufBackend_t(); - - public: - - AdobeFlashVBufBackend_t(int docHandle, int ID); - -}; - -#endif diff --git a/nvdaHelper/vbufBackends/adobeFlash/sconscript b/nvdaHelper/vbufBackends/adobeFlash/sconscript deleted file mode 100644 index ee49d76bda8..00000000000 --- a/nvdaHelper/vbufBackends/adobeFlash/sconscript +++ /dev/null @@ -1,21 +0,0 @@ -### -#This file is a part of the NVDA project. -#URL: http://www.nvda-project.org/ -#Copyright 2006-2010 NVDA contributers. -#This program is free software: you can redistribute it and/or modify -#it under the terms of the GNU General Public License version 2.0, as published by -#the Free Software Foundation. -#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. -#This license can be found at: -#http://www.gnu.org/licenses/old-licenses/gpl-2.0.html -### - -Import([ - 'env', -]) - -adobeFlashBackendLib=env.Object("adobeFlash.cpp") - -Return('adobeFlashBackendLib') diff --git a/nvdaHelper/vbufBase/backend.h b/nvdaHelper/vbufBase/backend.h index 8cb96a0844f..e73ae9f8307 100644 --- a/nvdaHelper/vbufBase/backend.h +++ b/nvdaHelper/vbufBase/backend.h @@ -184,7 +184,6 @@ typedef VBufBackend_t*(*VBufBackend_create_proc)(int,int); // The backend creation functions VBufBackend_t* AdobeAcrobatVBufBackend_t_createInstance(int docHandle, int ID); -VBufBackend_t* AdobeFlashVBufBackend_t_createInstance(int docHandle, int ID); VBufBackend_t* GeckoVBufBackend_t_createInstance(int docHandle, int ID); VBufBackend_t* lotusNotesRichTextVBufBackend_t_createInstance(int docHandle, int ID); VBufBackend_t* MshtmlVBufBackend_t_createInstance(int docHandle, int ID); diff --git a/readme.md b/readme.md index 9bfb44ce9c8..ed8de569a4d 100644 --- a/readme.md +++ b/readme.md @@ -4,22 +4,22 @@ NVDA (NonVisual Desktop Access) is a free, open source screen reader for Microso It is developed by NV Access in collaboration with a global community of contributors. To learn more about NVDA or download a copy, visit the main [NV Access](http://www.nvaccess.org/) website. -Please note: the NVDA project has a [Citizen and Contributor Code of Conduct](CODE_OF_CONDUCT.md). NV Access expects that all contributors and other community members read and abide by the rules set out in this document while participating or contributing to this project. +Please note: the NVDA project has a [Citizen and Contributor Code of Conduct](CODE_OF_CONDUCT.md). NV Access expects that all contributors and other community members will read and abide by the rules set out in this document while participating or contributing to this project. ## Get support -Either if you are a beginner, an advanced user, a new or a long time developer, or if you are an organization willing to know more or to contribute to NVDA, you can get support through the documentation in place as well as several communication channels dedicated for the NVDA screen reader. Here is an overview of the most important support sources. +Whether you are a beginner, an advanced user, a new or a long time developer; or if you represent an organization wishing to know more or to contribute to NVDA: you can get support through the included documentation as well as several communication channels dedicated to the NVDA screen reader. Here is an overview of the most important support sources. ### Documentation * [NVDA User Guide](https://www.nvaccess.org/files/nvda/documentation/userGuide.html) * [NVDA Developer Guide](https://www.nvaccess.org/files/nvda/documentation/developerGuide.html) * [NVDA Add-ons Development Internals](https://github.com/nvdaaddons/DevGuide/wiki) * [NVDA ControllerClient manual](https://github.com/nvaccess/nvda/tree/master/extras/controllerClient) -* Further documentation is included in the Wiki of this repository and in the [Community Wiki](https://github.com/nvaccess/nvda-community/wiki) +* Further documentation is available in the NVDA repository's [Wiki](https://github.com/nvaccess/nvda/wiki), and in the [Community Wiki](https://github.com/nvaccess/nvda-community/wiki) ### Communication channels * [NVDA Users Mailing List](https://nvda.groups.io/g/nvda) * [NVDA Developers Mailing List](https://groups.io/g/nvda-devel) -* [NVDA Add-ons Mailing List](https://nvda-addons.groups.io/g/nvda-addons) +* [NVDA Add-ons Mailing List](https://groups.io/g/nvda-addons) * [Instant Messaging channel for NVDA Support](https://gitter.im/nvaccess/NVDA) * [Other sources including groups and profiles on social media channels, language specific websites and mailing lists etc.](https://github.com/nvaccess/nvda-community/wiki/Connect) @@ -57,7 +57,6 @@ The following dependencies need to be installed on your system: * [Python](https://www.python.org/), version 3.7, 32 bit * Use latest minor version if possible. - * Don't use `3.7.6` it causes an error while building, see #10696. * Microsoft Visual Studio 2019 Community, Version 16.3 or later: * Download from https://visualstudio.microsoft.com/vs/ * When installing Visual Studio, you need to enable the following: @@ -75,59 +74,44 @@ The following dependencies need to be installed on your system: ### Git Submodules -Most of the dependencies are contained in Git submodules. +Some of the dependencies are contained in Git submodules. If you didn't pass the `--recursive` option to git clone, you will need to run `git submodule update --init`. Whenever a required submodule commit changes (e.g. after git pull), you will need to run `git submodule update`. If you aren't sure, run `git submodule update` after every git pull, merge or checkout. For reference, the following run time dependencies are included in Git submodules: -* [comtypes](https://github.com/enthought/comtypes), version 1.1.7 -* [wxPython](https://www.wxpython.org/), version 4.0.3 -* [eSpeak NG](https://github.com/espeak-ng/espeak-ng), version 1.51-dev commit 1fb68ffffea4 +* [eSpeak NG](https://github.com/espeak-ng/espeak-ng), version 1.51-dev commit cad1c8e8 * [Sonic](https://github.com/waywardgeek/sonic), commit 4f8c1d11 * [IAccessible2](https://wiki.linuxfoundation.org/accessibility/iaccessible2/start), commit cbc1f29631780 -* [ConfigObj](https://github.com/DiffSK/configobj), commit f9a265c -* [Six](https://pypi.python.org/pypi/six), version 1.12.0, required by wxPython and ConfigObj -* [liblouis](http://www.liblouis.org/), version 3.16.1 -* [Unicode Common Locale Data Repository (CLDR)](http://cldr.unicode.org/) Emoji Annotations, version 38.0 +* [liblouis](http://www.liblouis.org/), version 3.17.0 +* [Unicode Common Locale Data Repository (CLDR)](http://cldr.unicode.org/), version 38.1 * NVDA images and sounds * [Adobe Acrobat accessibility interface, version XI](https://download.macromedia.com/pub/developer/acrobat/AcrobatAccess.zip) -* Adobe FlashAccessibility interface typelib * [MinHook](https://github.com/RaMMicHaeL/minhook), tagged version 1.2.2 * brlapi Python bindings, version 0.8 or later, distributed with [BRLTTY for Windows](https://brltty.app/download.html), version 6.1 * lilli.dll, version 2.1.0.0 -* [pySerial](https://pypi.python.org/pypi/pyserial), version 3.4 * [Python interface to FTDI driver/chip](http://fluidmotion.dyndns.org/zenphoto/index.php?p=news&title=Python-interface-to-FTDI-driver-chip) * Java Access Bridge 32 bit, from Zulu Community OpenJDK build 13.0.1+10Zulu (13.28.11) -Additionally, the following build time dependencies are included in Git submodules: +Additionally, the following build time dependencies are included in the miscDeps git submodule: -* [Py2Exe](https://github.com/albertosottile/py2exe/), version 0.9.3.2 commit b372a8e -* [Python Windows Extensions](https://sourceforge.net/projects/pywin32/ ), build 224, required by py2exe * [txt2tags](https://txt2tags.org/), version 2.5 -* [SCons](https://www.scons.org/), version 4.0.1 * [Nulsoft Install System](https://nsis.sourceforge.io/Main_Page/), version 2.51 * [NSIS UAC plug-in](https://nsis.sourceforge.io/UAC_plug-in), version 0.2.4, ansi * xgettext and msgfmt from [GNU gettext](https://sourceforge.net/projects/cppcms/files/boost_locale/gettext_for_windows/) -* [epydoc](http://epydoc.sourceforge.net/), version 3.0.1 with patch for bug #303 * [Boost Optional (stand-alone header)](https://github.com/akrzemi1/Optional), from commit [3922965](https://github.com/akrzemi1/Optional/commit/3922965396fc455c6b1770374b9b4111799588a9) -### Other Dependencies -To lint using Flake 8 locally using our SCons integration, some dependencies are installed (automatically) via pip. -Although this [must be run manually](#linting-your-changes), developers may wish to first configure a Python Virtual Environment to ensure their general install is not affected. -* Flake8 -* Flake8-tabs - - The following dependencies aren't needed by most people, and are not included in Git submodules: - * To generate developer documentation for nvdaHelper: [Doxygen Windows installer](http://www.doxygen.nl/download.html), version 1.8.15: * When you are using Visual Studio Code as your integrated development environment of preference, you can make use of our [prepopulated workspace configuration](https://github.com/nvaccess/vscode-nvda/) for [Visual Studio Code](https://code.visualstudio.com/). While this VSCode project is not included as a submodule in the NVDA repository, you can easily check out the workspace configuration in your repository by executing the following from the root of the repository. ```git clone https://github.com/nvaccess/vscode-nvda.git .vscode``` +### Python dependencies +NVDA and its build system also depend on an extensive list of Python packages. They are all listed with their specific versions in the requirements.txt file in the root of this repository. However, the build system takes care of fetching these itself when needed. These packages will be installed into an isolated Python virtual environment within this repository, and will not affect your system-wide set of packages. + ## Preparing the Source Tree Before you can run the NVDA source code, you must prepare the source tree. You do this by opening a command prompt, changing to the root of the NVDA source distribution and typing: @@ -173,13 +157,8 @@ By default, builds also do not use any compiler optimizations. Please see the `release` keyword argument for what compiler optimizations it will enable. ## Running the Source Code -Most developers run directly from source by: -``` -cd source -pythonw.exe nvda.pyw -``` -Note: Since NVDA is a Windows application (rather than command line), it is best to run it with `pythonw.exe`. -However, if during development you encounter an error early in the startup of NVDA, you can use `python.exe` which is likely to give more information about the error. +It is possible to run NVDA directly from source without having to build the full binary package and launcher. +To launch NVDA from source, using `cmd.exe`, execute `runnvda.bat` in the root of the repository. To view help on the arguments that NVDA will accept, use the `-h` or `--help` option. These arguments are also documented in the user guide. @@ -187,7 +166,7 @@ These arguments are also documented in the user guide. ## Building NVDA A binary build of NVDA can be run on a system without Python and all of NVDA's other dependencies installed (as we do for snapshots and releases). -Binary archives and bundles can be created using scons from the root of the NVDA source distribution. To build any of the following, open a command prompt and change to this directory. +Binary archives and bundles can be created using scons from the root of the NVDA source distribution. To build any of the following, open a command prompt and change to that directory. To make a non-archived binary build (equivalent to an extracted portable archive), type: @@ -216,7 +195,14 @@ scons developerGuide ``` The developer guide will be placed in the `devDocs` folder in the output directory. -Note that the Python 3 sources of NVDA currently do not support building NVDA developer documentation using the `scons devDocs` command. + +To generate the HTML-based source code documentation, type: + +``` +scons devDocs +``` + +The documentation will be placed in the `NVDA` folder in the output directory. To generate developer documentation for nvdaHelper (not included in the devDocs target): @@ -295,24 +281,27 @@ scons checkPot ### Linting your changes In order to ensure your changes comply with NVDA's coding style you can run the Flake8 linter locally. Some developers have found certain linting error messages misleading, these are clarified in `tests/lint/readme.md`. -Running via SCons will use Flake8 to inspect only the differences between your working directory and the specified `base` branch. +runlint.bat will use Flake8 to inspect only the differences between your working directory and the specified `base` branch. If you create a Pull Request, the `base` branch you use here should be the same as the target you would use for a Pull Request. In most cases it will be `origin/master`. ``` -scons lint base=origin/master +runlint origin/master ``` -To be warned about linting errors faster, you may wish to integrate Flake8 other development tools you are using. +To be warned about linting errors faster, you may wish to integrate Flake8 with other development tools you are using. For more details, see `tests/lint/readme.md` +### Unit Tests +Unit tests can be run with the `rununittests.bat` script. +Internally this script uses the Nose Python test framework to execute the tests. +Any arguments given to rununittests.bat are forwarded onto Nose. +Please refer to Nose's own documentation on how to filter tests etc. + ### System Tests -You may also use `scons` to run the system tests, - though this will still require the dependencies to be set up. +System tests can be run with the `runsystemtests.bat` script. +Internally this script uses the Robot test framework to execute the tests. +Any arguments given to runsystemtests.bat are forwarded onto Robot. For more details (including filtering and exclusion of tests) see `tests/system/readme.md`. -``` -scons systemTests -``` - ## Contributing to NVDA If you would like to contribute code or documentation to NVDA, you can read more information in our [contributing guide](https://github.com/nvaccess/nvda/wiki/Contributing). diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000000..03417fe5e32 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,30 @@ +# NVDA's build system is SCons +SCons==4.1.0.post1 + +# NVDA's runtime dependencies +comtypes==1.1.8 +pyserial==3.5 +wxPython==4.1.1 +git+git://github.com/DiffSK/configobj@3e2f4cc#egg=configobj + +#NVDA_DMP requires diff-match-patch +diff_match_patch_python==1.0.2 + +# Packaging NVDA +py2exe==0.10.1.0 + +# For building developer documentation +sphinx==3.4.1 +sphinx_rtd_theme + +# Requirements for automated linting +flake8 ~= 3.7.7 +flake8-tabs == 2.1.0 + +# Requirements for unit tests +nose==1.3.7 + +# Requirements for system tests +robotframework==3.2.2 +robotremoteserver==1.1 +robotframework-screencaplibrary==1.5.0 diff --git a/runlint.bat b/runlint.bat new file mode 100644 index 00000000000..81dcee35af6 --- /dev/null +++ b/runlint.bat @@ -0,0 +1,9 @@ +@echo off +rem runlint [] +rem Lints any changes after base commit up to and including current HEAD, plus any uncommitted changes. +call "%~dp0\venvUtils\venvCmd.bat" py "%~dp0\tests\lint\genDiff.py" %1 "%~dp0\tests\lint\_lint.diff" + if ERRORLEVEL 1 exit /b %ERRORLEVEL% + set flake8Args=--diff --config="%~dp0\tests\lint\flake8.ini" + if "%2" NEQ "" set flake8Args=%flake8Args% --tee --output-file=%2 + type "%~dp0\tests\lint\_lint.diff" | call "%~dp0\venvUtils\venvCmd.bat" py -Xutf8 -m flake8 %flake8Args% + diff --git a/runnvda.bat b/runnvda.bat new file mode 100644 index 00000000000..771b973a5df --- /dev/null +++ b/runnvda.bat @@ -0,0 +1,2 @@ +@echo off +call "%~dp0\venvUtils\venvCmd.bat" start pyw "%~dp0\source\nvda.pyw" %* diff --git a/runsettingsdiff.bat b/runsettingsdiff.bat new file mode 100644 index 00000000000..f367c1ecb41 --- /dev/null +++ b/runsettingsdiff.bat @@ -0,0 +1,2 @@ +@echo off +call "%~dp0\venvUtils\venvCmd.bat" py -m robot --argumentfile "%~dp0\tests\system\guiDiff.robot" %* "%~dp0\tests\system\robot" diff --git a/runsystemtests.bat b/runsystemtests.bat new file mode 100644 index 00000000000..bb2708e2801 --- /dev/null +++ b/runsystemtests.bat @@ -0,0 +1,3 @@ +@echo off +call "%~dp0\venvUtils\venvCmd.bat" py -m robot --argumentfile "%~dp0\tests\system\robotArgs.robot" %* "%~dp0\tests\system\robot" + diff --git a/rununittests.bat b/rununittests.bat new file mode 100644 index 00000000000..c77cfa38b1a --- /dev/null +++ b/rununittests.bat @@ -0,0 +1,2 @@ +@echo off +call "%~dp0\venvUtils\venvCmd.bat" py -m nose -sv --traverse-namespace -w "%~dp0\tests\unit" %* diff --git a/scons.bat b/scons.bat index 5c7001358e6..849e7c1b354 100644 --- a/scons.bat +++ b/scons.bat @@ -1,13 +1,3 @@ @echo off -rem We need this script because .py probably isn't in pathext. -rem We can't just call python -c because it may not be in the path. -rem Instead, find the python launcher (installed by python 3) -where py 1>nul 2>&1 -if "%ERRORLEVEL%" == "0" ( - rem Python launcher is present in the PATH - rem Call python 3.7 for 32 bits - py -3.7-32 "%~dp0\scons.py" %* -) else ( - rem Python registers itself with the .py extension, so call scons.py. - "%~dp0\scons.py" %* -) +rem Executes SScons within the NVDA build system's Python virtual environment. +call "%~dp0\venvUtils\venvCmd.bat" py -m SCons %* diff --git a/scons.py b/scons.py deleted file mode 100644 index 6deab741492..00000000000 --- a/scons.py +++ /dev/null @@ -1,16 +0,0 @@ -"""Run SCons from the NVDA source environment. -""" - -import sys -import os - -sconsPath = os.path.abspath(os.path.join(os.path.dirname(__file__), "include", "scons")) - -if os.path.exists(sconsPath): - # sys.path[0] will always be the current dir, which should take precedence. - # Insert path to the SCons folder after that. - sys.path[1:1] = (sconsPath,) - import SCons.Script - SCons.Script.Main.main() -else: - raise OSError("Path %s does not exist. Perhaps try running git submodule update --init" % sconsPath) diff --git a/sconstruct b/sconstruct index 334351dc61e..c5905140f0e 100755 --- a/sconstruct +++ b/sconstruct @@ -1,5 +1,6 @@ # A part of NonVisual Desktop Access (NVDA) -# Copyright (C) 2010-2020 NV Access Limited, James Teh, Michael Curran, Peter Vágner, Joseph Lee, Reef Turner, Babbage B.V., Leonard de Ruijter, Łukasz Golonka, Accessolutions, Julien Cochuyt # noqa: E501 +# Copyright (C) 2010-2021 NV Access Limited, James Teh, Michael Curran, Peter Vágner, Joseph Lee, +# Reef Turner, Babbage B.V., Leonard de Ruijter, Łukasz Golonka, Accessolutions, Julien Cochuyt # This file may be used under the terms of the GNU General Public License, version 2 or later. # For more details see: https://www.gnu.org/licenses/gpl-2.0.html @@ -7,6 +8,18 @@ import sys import os import platform +# Ensure we are inside the Python virtual environment. +nvdaVenv = os.getenv("NVDA_VENV") +virtualEnv = os.getenv("VIRTUAL_ENV") +if not virtualEnv or not os.path.isdir(virtualEnv): + print( + "Error: SCons cannot detect the NVDA build system Python virtual environment.\n" + "SCons must be executed using scons.bat in the root of this repository." + ) + sys.exit(1) +if nvdaVenv != virtualEnv: + print("Warning: SCons launched within a custom Python virtual environment.") + # Variables for storing required version of Python, and the version which is used to run this script. requiredPythonMajor ="3" requiredPythonMinor = "7" @@ -34,17 +47,12 @@ if ( requiredPythonArchitecture ) ) -if sys.version_info.micro == 6: - # #10696: Building with Python 3.7.6 fails. Innform user and exit. - Py376FailMsg = "Building with Python 3.7.6 is not possible.\nPlease use more recent version of Python 3." - raise RuntimeError(Py376FailMsg) sourceEnvPath = os.path.abspath(os.path.join(Dir('.').srcnode().path, "source")) sys.path.append(sourceEnvPath) import sourceEnv sys.path.remove(sourceEnvPath) import time from glob import glob -from py2exe.dllfinder import pydll import importlib.util import winreg @@ -102,21 +110,6 @@ vars.Add("certTimestampServer", "The URL of the timestamping server to use to ti vars.Add(PathVariable("outputDir", "The directory where the final built archives and such will be placed", "output",PathVariable.PathIsDirCreate)) vars.Add(ListVariable("nvdaHelperDebugFlags", "a list of debugging features you require", 'none', ["debugCRT","RTC","analyze"])) vars.Add(EnumVariable('nvdaHelperLogLevel','The level of logging you wish to see, lower is more verbose','15',allowed_values=[str(x) for x in range(60)])) -if "tests" in COMMAND_LINE_TARGETS: - vars.Add("unitTests", "A list of unit tests to run", "") -if "lint" in COMMAND_LINE_TARGETS: - vars.Add( # pass through variable that lint is requested - "doLint", - "internal use", - True - ) - vars.Add( - "base", - "Lint is done only on a diff, specify the ref to use as base for the diff.", - None - ) -if "systemTests" in COMMAND_LINE_TARGETS: - vars.Add("filter", "A filter for the name of the system test(s) to run. Wildcards accepted.", "") #Base environment for this and sub sconscripts env = Environment(variables=vars,HOST_ARCH='x86',tools=[ @@ -254,6 +247,11 @@ t2tBuildConf = env.Textfile(userDocsDir.File("build.t2tConf"), [ r"%!PreProc: NVDA_COPYRIGHT_YEARS {}".format(versionInfo.copyrightYears), ]) +userGuideConf = os.path.join(userDocsDir.path, 'userGuide.t2tconf') +globalConf = os.path.join(userDocsDir.path, 'global.t2tconf') +changesConf = os.path.join(userDocsDir.path, 'changes.t2tconf') +styles = os.path.join(userDocsDir.path, 'styles.css') + #Allow all t2t files to be converted to html in user_docs #As we use scons Glob this will also include the keyCommands.t2t files for t2tFile in env.Glob(os.path.join(userDocsDir.path,'*','*.t2t')): @@ -261,7 +259,18 @@ for t2tFile in env.Glob(os.path.join(userDocsDir.path,'*','*.t2t')): #txt2tags can not be run in parallel so make sure scons knows this env.SideEffect('_txt2tags',htmlFile) # All of our t2t files require build.t2tConf. - env.Depends(htmlFile,t2tBuildConf) + styleInstallPath = os.path.dirname(t2tFile.abspath) + installedStyle = env.Install(styleInstallPath, styles) + env.Depends( + htmlFile, + [ + t2tBuildConf, + userGuideConf, + globalConf, + changesConf, + styles, + installedStyle + ]) # Build unicode CLDR dictionaries env.SConscript('cldrDict_sconscript',exports=['env', 'sourceDir']) @@ -299,23 +308,27 @@ def NVDADistGenerator(target, source, env, for_signature): action.append(buildCmd) - # Python3 has started signing its main python dll. - # However, Py2exe currently tries to add a string resource to it, invalidating the signature and possibly currupting the certificate. - # Therefore, copy a fresh version of the dll one more time once py2exe has completed. - action.append(Copy(target[0],pydll)) - # #10031: Apps written in Python 3 require Universal CRT to be installed. We cannot assume users have it on their systems. # Therefore , copy required libraries from Windows 10 SDK. try: with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r'SOFTWARE\Microsoft\Microsoft SDKs\Windows\v10.0', 0,winreg.KEY_READ|winreg.KEY_WOW64_32KEY) as SDKKey: - CRTDir = os.path.join(winreg.QueryValueEx(SDKKey, 'InstallationFolder')[0], "Redist", "ucrt", "DLLs", "x86") + sdk_installationFolder = winreg.QueryValueEx(SDKKey, 'InstallationFolder')[0] + sdk_productVersion = winreg.QueryValueEx(SDKKey, 'ProductVersion')[0] except WindowsError: raise RuntimeError("Windows 10 SDK not found") - if os.path.isdir(CRTDir): - with os.scandir(CRTDir) as dir: - for file in dir: - if file.name.endswith(".dll") and file.is_file(): - action.append(Copy(target[0], file.path)) + # The Universal CRT should be in an SDK version-specific directory + # But usually has a '.0' appended after the productVersion found in the registry. + # E.g. 10.0.1941 might e actually 10.0.1941.0. + # Thus try both. + CRTDir = os.path.join(sdk_installationFolder, "Redist", sdk_productVersion+".0", "ucrt", "DLLs", "x86") + if not os.path.isdir(CRTDir): + CRTDir = os.path.join(sdk_installationFolder, "Redist", sdk_productVersion, "ucrt", "DLLs", "x86") + if not os.path.isdir(CRTDir): + raise RuntimeError(f"Could not locate CRT dlls in SDK at {CRTDir}") + with os.scandir(CRTDir) as dir: + for file in dir: + if file.name.endswith(".dll") and file.is_file(): + action.append(Copy(target[0], file.path)) if certFile: for prog in "nvda_noUIAccess.exe", "nvda_uiAccess.exe", "nvda_slave.exe", "nvda_eoaProxy.exe": @@ -402,14 +415,30 @@ env.Alias("launcher", launcher) clientArchive = env.ZipArchive(outputDir.File("%s_controllerClient.zip" % outFilePrefix), clientDir, relativeTo=clientDir) env.Alias("client", clientArchive) +outputStylesFile=env.Command(outputDir.File("styles.css"),userDocsDir.File('styles.css'),Copy('$TARGET','$SOURCE')) changesFile=env.Command(outputDir.File("%s_changes.html" % outFilePrefix),userDocsDir.File('en/changes.html'),Copy('$TARGET','$SOURCE')) +env.Depends(changesFile, outputStylesFile) env.Alias('changes',changesFile) userGuideFile=env.Command(outputDir.File("userGuide.html"),userDocsDir.File('en/userGuide.html'),Copy('$TARGET','$SOURCE')) +env.Depends(userGuideFile, outputStylesFile) env.Alias('userGuide',userGuideFile) + +def makePotSourceFileList(target, sourceFiles, env): + potSourceFiles = [ + os.path.relpath(str(f), str(sourceDir)) for f in sourceFiles + ] + with open(target.abspath, "w") as fileList: + fileList.writelines([f + '\n' for f in potSourceFiles]) + + def makePot(target, source, env): + potSourceFileList = outputDir.File("potSourceFileList.txt") + makePotSourceFileList(potSourceFileList, source, env) # Generate the pot. - if env.Execute([["cd", sourceDir, "&&", + if env.Execute([ + [ + "cd", sourceDir, "&&", XGETTEXT, "-o", target[0].abspath, "--package-name", versionInfo.name, "--package-version", version, @@ -419,7 +448,9 @@ def makePot(target, source, env): "--from-code", "utf-8", # Needed because xgettext doesn't recognise the .pyw extension. "--language=python", - ] + [os.path.relpath(str(f), str(sourceDir)) for f in source] + # Too many files to list on commandline, use a file list instead. + f"--files-from={potSourceFileList.abspath}", + ] ]) != 0: raise RuntimeError("xgettext failed") @@ -439,17 +470,35 @@ def makePot(target, source, env): os.remove(potFn) os.rename(tmpFn, potFn) + env.SConscript("devDocs/sconscript", exports=["env", "outputDir", "sourceDir", "t2tBuildConf"]) -pot = env.Command(outputDir.File("%s.pot" % outFilePrefix), + +def getSubDirs(path): + for root, dirNames, fileNames in os.walk(path): + yield root + + +# The Glob() SCons function doesnt have the ability to go recursive. Instead +# walk the dirs and match patterns. +potSourceFiles = [ # Don't use sourceDir as the source, as this depends on comInterfaces and nvdaHelper. # We only depend on the Python files. - [f for pattern in ("*.py", "*.pyw", r"*\*.py", r"*\*\*.py") - for f in env.Glob(os.path.join(sourceDir.path, pattern)) + f for recurseDirs in getSubDirs(sourceDir.path) + for pattern in ("*.py", "*.pyw") + for f in env.Glob( + os.path.join(recurseDirs, pattern), # Exclude comInterfaces, since these don't contain translatable strings # and they cause unknown encoding warnings. - if not f.path.startswith("source\\comInterfaces\\")], - makePot) + exclude="source/comInterfaces/*" + ) +] + +pot = env.Command( + outputDir.File("%s.pot" % outFilePrefix), + potSourceFiles, + makePot +) env.Alias("pot", pot) diff --git a/source/COMRegistrationFixes/__init__.py b/source/COMRegistrationFixes/__init__.py index 968be869653..dbf5d494ba9 100644 --- a/source/COMRegistrationFixes/__init__.py +++ b/source/COMRegistrationFixes/__init__.py @@ -60,7 +60,8 @@ def fixCOMRegistrations(): Registers most common COM proxies, in case they had accidentally been unregistered or overwritten by 3rd party software installs/uninstalls. """ is64bit=os.environ.get("PROCESSOR_ARCHITEW6432","").endswith('64') - OSMajorMinor=winVersion.winVersion[:2] + winVer = winVersion.getWinVer() + OSMajorMinor = (winVer.major, winVer.minor) log.debug("Fixing COM registration for Windows %s.%s, %s"%(OSMajorMinor[0],OSMajorMinor[1],"64 bit" if is64bit else "32 bit")) # Commands taken from NVDA issue #2807 comment https://github.com/nvaccess/nvda/issues/2807#issuecomment-320149243 # OLEACC (MSAA) proxies diff --git a/source/IAccessibleHandler/__init__.py b/source/IAccessibleHandler/__init__.py index beba0bab96d..9a32972a1bd 100644 --- a/source/IAccessibleHandler/__init__.py +++ b/source/IAccessibleHandler/__init__.py @@ -1,4 +1,3 @@ -# IAccessibleHandler.py # A part of NonVisual Desktop Access (NVDA) # Copyright (C) 2006-2007 NVDA Contributors # This file is covered by the GNU General Public License. @@ -7,8 +6,6 @@ from typing import Tuple import struct import weakref -# Kept for backwards compatibility -from ctypes import * # noqa: F401, F403 from ctypes import ( wintypes, windll, @@ -26,80 +23,9 @@ import oleacc import UIAHandler -# Kept for backwards compatibility -from comInterfaces.Accessibility import * # noqa: F401, F403 -# Specific imports for items we know we use, hopefully in the future we can remove the import for this module. -from comInterfaces.Accessibility import ( - IAccessible, - IAccIdentity, - CAccPropServices, -) -# Kept for backwards compatibility -from comInterfaces.IAccessible2Lib import * # noqa: F401, F403 -# Specific imports for items we know we use, hopefully in the future we can remove the import for this module. -from comInterfaces.IAccessible2Lib import ( - IAccessibleText, - IAccessibleHypertext, - IAccessible2, - IA2_STATE_REQUIRED, - IA2_STATE_INVALID_ENTRY, - IA2_STATE_MODAL, - IA2_STATE_DEFUNCT, - IA2_STATE_SUPPORTS_AUTOCOMPLETION, - IA2_STATE_MULTI_LINE, - IA2_STATE_ICONIFIED, - IA2_STATE_EDITABLE, - IA2_STATE_PINNED, - IA2_STATE_CHECKABLE, - IA2_ROLE_UNKNOWN, - IA2_ROLE_CANVAS, - IA2_ROLE_CAPTION, - IA2_ROLE_CHECK_MENU_ITEM, - IA2_ROLE_COLOR_CHOOSER, - IA2_ROLE_DATE_EDITOR, - IA2_ROLE_DIRECTORY_PANE, - IA2_ROLE_DESKTOP_PANE, - IA2_ROLE_EDITBAR, - IA2_ROLE_EMBEDDED_OBJECT, - IA2_ROLE_ENDNOTE, - IA2_ROLE_FILE_CHOOSER, - IA2_ROLE_FONT_CHOOSER, - IA2_ROLE_FRAME, - IA2_ROLE_FOOTNOTE, - IA2_ROLE_FORM, - IA2_ROLE_GLASS_PANE, - IA2_ROLE_HEADER, - IA2_ROLE_HEADING, - IA2_ROLE_ICON, - IA2_ROLE_IMAGE_MAP, - IA2_ROLE_INPUT_METHOD_WINDOW, - IA2_ROLE_INTERNAL_FRAME, - IA2_ROLE_LABEL, - IA2_ROLE_LAYERED_PANE, - IA2_ROLE_NOTE, - IA2_ROLE_OPTION_PANE, - IA2_ROLE_PAGE, - IA2_ROLE_PARAGRAPH, - IA2_ROLE_RADIO_MENU_ITEM, - IA2_ROLE_REDUNDANT_OBJECT, - IA2_ROLE_ROOT_PANE, - IA2_ROLE_RULER, - IA2_ROLE_SCROLL_PANE, - IA2_ROLE_SECTION, - IA2_ROLE_SHAPE, - IA2_ROLE_SPLIT_PANE, - IA2_ROLE_TEAR_OFF_MENU, - IA2_ROLE_TERMINAL, - IA2_ROLE_TEXT_FRAME, - IA2_ROLE_TOGGLE_BUTTON, - IA2_ROLE_VIEW_PORT, - IA2_ROLE_CONTENT_DELETION, - IA2_ROLE_CONTENT_INSERTION, - IA2_ROLE_BLOCK_QUOTE, - IA2_ROLE_DESKTOP_ICON, - IA2_ROLE_FOOTER, - IA2_ROLE_MARK, -) +from comInterfaces import Accessibility as IA + +from comInterfaces import IAccessible2Lib as IA2 import config @@ -170,15 +96,7 @@ def isMSAADebugLoggingEnabled(): ] from . import internalWinEventHandler -# Imported for backwards compat -from .internalWinEventHandler import ( # noqa: F401 - winEventHookIDs, - winEventLimiter, - winEventIDsToNVDAEventNames, - _shouldGetEvents, -) -from comInterfaces import IAccessible2Lib as IA2 from logHandler import log import JABHandler import eventHandler @@ -276,55 +194,55 @@ def isMSAADebugLoggingEnabled(): oleacc.ROLE_SYSTEM_OUTLINEBUTTON: controlTypes.ROLE_TREEVIEWBUTTON, oleacc.ROLE_SYSTEM_CLOCK: controlTypes.ROLE_CLOCK, # IAccessible2 roles - IA2_ROLE_UNKNOWN: controlTypes.ROLE_UNKNOWN, - IA2_ROLE_CANVAS: controlTypes.ROLE_CANVAS, - IA2_ROLE_CAPTION: controlTypes.ROLE_CAPTION, - IA2_ROLE_CHECK_MENU_ITEM: controlTypes.ROLE_CHECKMENUITEM, - IA2_ROLE_COLOR_CHOOSER: controlTypes.ROLE_COLORCHOOSER, - IA2_ROLE_DATE_EDITOR: controlTypes.ROLE_DATEEDITOR, - IA2_ROLE_DESKTOP_ICON: controlTypes.ROLE_DESKTOPICON, - IA2_ROLE_DESKTOP_PANE: controlTypes.ROLE_DESKTOPPANE, - IA2_ROLE_DIRECTORY_PANE: controlTypes.ROLE_DIRECTORYPANE, - IA2_ROLE_EDITBAR: controlTypes.ROLE_EDITBAR, - IA2_ROLE_EMBEDDED_OBJECT: controlTypes.ROLE_EMBEDDEDOBJECT, - IA2_ROLE_ENDNOTE: controlTypes.ROLE_ENDNOTE, - IA2_ROLE_FILE_CHOOSER: controlTypes.ROLE_FILECHOOSER, - IA2_ROLE_FONT_CHOOSER: controlTypes.ROLE_FONTCHOOSER, - IA2_ROLE_FOOTER: controlTypes.ROLE_FOOTER, - IA2_ROLE_FOOTNOTE: controlTypes.ROLE_FOOTNOTE, - IA2_ROLE_FORM: controlTypes.ROLE_FORM, - IA2_ROLE_FRAME: controlTypes.ROLE_FRAME, - IA2_ROLE_GLASS_PANE: controlTypes.ROLE_GLASSPANE, - IA2_ROLE_HEADER: controlTypes.ROLE_HEADER, - IA2_ROLE_HEADING: controlTypes.ROLE_HEADING, - IA2_ROLE_ICON: controlTypes.ROLE_ICON, - IA2_ROLE_IMAGE_MAP: controlTypes.ROLE_IMAGEMAP, - IA2_ROLE_INPUT_METHOD_WINDOW: controlTypes.ROLE_INPUTWINDOW, - IA2_ROLE_INTERNAL_FRAME: controlTypes.ROLE_INTERNALFRAME, - IA2_ROLE_LABEL: controlTypes.ROLE_LABEL, - IA2_ROLE_LAYERED_PANE: controlTypes.ROLE_LAYEREDPANE, - IA2_ROLE_NOTE: controlTypes.ROLE_NOTE, - IA2_ROLE_OPTION_PANE: controlTypes.ROLE_OPTIONPANE, - IA2_ROLE_PAGE: controlTypes.ROLE_PAGE, - IA2_ROLE_PARAGRAPH: controlTypes.ROLE_PARAGRAPH, - IA2_ROLE_RADIO_MENU_ITEM: controlTypes.ROLE_RADIOMENUITEM, - IA2_ROLE_REDUNDANT_OBJECT: controlTypes.ROLE_REDUNDANTOBJECT, - IA2_ROLE_ROOT_PANE: controlTypes.ROLE_ROOTPANE, - IA2_ROLE_RULER: controlTypes.ROLE_RULER, - IA2_ROLE_SCROLL_PANE: controlTypes.ROLE_SCROLLPANE, - IA2_ROLE_SECTION: controlTypes.ROLE_SECTION, - IA2_ROLE_SHAPE: controlTypes.ROLE_SHAPE, - IA2_ROLE_SPLIT_PANE: controlTypes.ROLE_SPLITPANE, - IA2_ROLE_TEAR_OFF_MENU: controlTypes.ROLE_TEAROFFMENU, - IA2_ROLE_TERMINAL: controlTypes.ROLE_TERMINAL, - IA2_ROLE_TEXT_FRAME: controlTypes.ROLE_TEXTFRAME, - IA2_ROLE_TOGGLE_BUTTON: controlTypes.ROLE_TOGGLEBUTTON, - IA2_ROLE_VIEW_PORT: controlTypes.ROLE_VIEWPORT, - IA2_ROLE_CONTENT_DELETION: controlTypes.ROLE_DELETED_CONTENT, - IA2_ROLE_CONTENT_INSERTION: controlTypes.ROLE_INSERTED_CONTENT, - IA2_ROLE_BLOCK_QUOTE: controlTypes.ROLE_BLOCKQUOTE, + IA2.IA2_ROLE_UNKNOWN: controlTypes.ROLE_UNKNOWN, + IA2.IA2_ROLE_CANVAS: controlTypes.ROLE_CANVAS, + IA2.IA2_ROLE_CAPTION: controlTypes.ROLE_CAPTION, + IA2.IA2_ROLE_CHECK_MENU_ITEM: controlTypes.ROLE_CHECKMENUITEM, + IA2.IA2_ROLE_COLOR_CHOOSER: controlTypes.ROLE_COLORCHOOSER, + IA2.IA2_ROLE_DATE_EDITOR: controlTypes.ROLE_DATEEDITOR, + IA2.IA2_ROLE_DESKTOP_ICON: controlTypes.ROLE_DESKTOPICON, + IA2.IA2_ROLE_DESKTOP_PANE: controlTypes.ROLE_DESKTOPPANE, + IA2.IA2_ROLE_DIRECTORY_PANE: controlTypes.ROLE_DIRECTORYPANE, + IA2.IA2_ROLE_EDITBAR: controlTypes.ROLE_EDITBAR, + IA2.IA2_ROLE_EMBEDDED_OBJECT: controlTypes.ROLE_EMBEDDEDOBJECT, + IA2.IA2_ROLE_ENDNOTE: controlTypes.ROLE_ENDNOTE, + IA2.IA2_ROLE_FILE_CHOOSER: controlTypes.ROLE_FILECHOOSER, + IA2.IA2_ROLE_FONT_CHOOSER: controlTypes.ROLE_FONTCHOOSER, + IA2.IA2_ROLE_FOOTER: controlTypes.ROLE_FOOTER, + IA2.IA2_ROLE_FOOTNOTE: controlTypes.ROLE_FOOTNOTE, + IA2.IA2_ROLE_FORM: controlTypes.ROLE_FORM, + IA2.IA2_ROLE_FRAME: controlTypes.ROLE_FRAME, + IA2.IA2_ROLE_GLASS_PANE: controlTypes.ROLE_GLASSPANE, + IA2.IA2_ROLE_HEADER: controlTypes.ROLE_HEADER, + IA2.IA2_ROLE_HEADING: controlTypes.ROLE_HEADING, + IA2.IA2_ROLE_ICON: controlTypes.ROLE_ICON, + IA2.IA2_ROLE_IMAGE_MAP: controlTypes.ROLE_IMAGEMAP, + IA2.IA2_ROLE_INPUT_METHOD_WINDOW: controlTypes.ROLE_INPUTWINDOW, + IA2.IA2_ROLE_INTERNAL_FRAME: controlTypes.ROLE_INTERNALFRAME, + IA2.IA2_ROLE_LABEL: controlTypes.ROLE_LABEL, + IA2.IA2_ROLE_LAYERED_PANE: controlTypes.ROLE_LAYEREDPANE, + IA2.IA2_ROLE_NOTE: controlTypes.ROLE_NOTE, + IA2.IA2_ROLE_OPTION_PANE: controlTypes.ROLE_OPTIONPANE, + IA2.IA2_ROLE_PAGE: controlTypes.ROLE_PAGE, + IA2.IA2_ROLE_PARAGRAPH: controlTypes.ROLE_PARAGRAPH, + IA2.IA2_ROLE_RADIO_MENU_ITEM: controlTypes.ROLE_RADIOMENUITEM, + IA2.IA2_ROLE_REDUNDANT_OBJECT: controlTypes.ROLE_REDUNDANTOBJECT, + IA2.IA2_ROLE_ROOT_PANE: controlTypes.ROLE_ROOTPANE, + IA2.IA2_ROLE_RULER: controlTypes.ROLE_RULER, + IA2.IA2_ROLE_SCROLL_PANE: controlTypes.ROLE_SCROLLPANE, + IA2.IA2_ROLE_SECTION: controlTypes.ROLE_SECTION, + IA2.IA2_ROLE_SHAPE: controlTypes.ROLE_SHAPE, + IA2.IA2_ROLE_SPLIT_PANE: controlTypes.ROLE_SPLITPANE, + IA2.IA2_ROLE_TEAR_OFF_MENU: controlTypes.ROLE_TEAROFFMENU, + IA2.IA2_ROLE_TERMINAL: controlTypes.ROLE_TERMINAL, + IA2.IA2_ROLE_TEXT_FRAME: controlTypes.ROLE_TEXTFRAME, + IA2.IA2_ROLE_TOGGLE_BUTTON: controlTypes.ROLE_TOGGLEBUTTON, + IA2.IA2_ROLE_VIEW_PORT: controlTypes.ROLE_VIEWPORT, + IA2.IA2_ROLE_CONTENT_DELETION: controlTypes.ROLE_DELETED_CONTENT, + IA2.IA2_ROLE_CONTENT_INSERTION: controlTypes.ROLE_INSERTED_CONTENT, + IA2.IA2_ROLE_BLOCK_QUOTE: controlTypes.ROLE_BLOCKQUOTE, IA2.IA2_ROLE_LANDMARK: controlTypes.ROLE_LANDMARK, - IA2_ROLE_MARK: controlTypes.ROLE_MARKED_CONTENT, + IA2.IA2_ROLE_MARK: controlTypes.ROLE_MARKED_CONTENT, # some common string roles "frame": controlTypes.ROLE_FRAME, "iframe": controlTypes.ROLE_INTERNALFRAME, @@ -371,32 +289,32 @@ def isMSAADebugLoggingEnabled(): } IAccessible2StatesToNVDAStates = { - IA2_STATE_REQUIRED: controlTypes.STATE_REQUIRED, - IA2_STATE_DEFUNCT: controlTypes.STATE_DEFUNCT, - # IA2_STATE_STALE:controlTypes.STATE_DEFUNCT, - IA2_STATE_INVALID_ENTRY: controlTypes.STATE_INVALID_ENTRY, - IA2_STATE_MODAL: controlTypes.STATE_MODAL, - IA2_STATE_SUPPORTS_AUTOCOMPLETION: controlTypes.STATE_AUTOCOMPLETE, - IA2_STATE_MULTI_LINE: controlTypes.STATE_MULTILINE, - IA2_STATE_ICONIFIED: controlTypes.STATE_ICONIFIED, - IA2_STATE_EDITABLE: controlTypes.STATE_EDITABLE, - IA2_STATE_PINNED: controlTypes.STATE_PINNED, - IA2_STATE_CHECKABLE: controlTypes.STATE_CHECKABLE, + IA2.IA2_STATE_REQUIRED: controlTypes.STATE_REQUIRED, + IA2.IA2_STATE_DEFUNCT: controlTypes.STATE_DEFUNCT, + # IA2.IA2_STATE_STALE:controlTypes.STATE_DEFUNCT, + IA2.IA2_STATE_INVALID_ENTRY: controlTypes.STATE_INVALID_ENTRY, + IA2.IA2_STATE_MODAL: controlTypes.STATE_MODAL, + IA2.IA2_STATE_SUPPORTS_AUTOCOMPLETION: controlTypes.STATE_AUTOCOMPLETE, + IA2.IA2_STATE_MULTI_LINE: controlTypes.STATE_MULTILINE, + IA2.IA2_STATE_ICONIFIED: controlTypes.STATE_ICONIFIED, + IA2.IA2_STATE_EDITABLE: controlTypes.STATE_EDITABLE, + IA2.IA2_STATE_PINNED: controlTypes.STATE_PINNED, + IA2.IA2_STATE_CHECKABLE: controlTypes.STATE_CHECKABLE, } def normalizeIAccessible(pacc, childID=0): - if not isinstance(pacc, IAccessible): + if not isinstance(pacc, IA.IAccessible): try: - pacc = pacc.QueryInterface(IAccessible) + pacc = pacc.QueryInterface(IA.IAccessible) except COMError: raise RuntimeError("%s Not an IAccessible" % pacc) # #2558: IAccessible2 doesn't support simple children. # Therefore, it doesn't make sense to use IA2 if the child ID is non-0. - if childID == 0 and not isinstance(pacc, IAccessible2): + if childID == 0 and not isinstance(pacc, IA2.IAccessible2): try: s = pacc.QueryInterface(IServiceProvider) - pacc2 = s.QueryService(IAccessible._iid_, IAccessible2) + pacc2 = s.QueryService(IA.IAccessible._iid_, IA2.IAccessible2) if not pacc2: # QueryService should fail if IA2 is not supported, but some applications such as AIM 7 misbehave # and return a null COM pointer. Treat this as if QueryService failed. @@ -595,7 +513,7 @@ def winEventToNVDAEvent(eventID, window, objectID, childID, useCache=True): f"Creating NVDA event from winEvent: {getWinEventLogInfo(window, objectID, childID, eventID)}, " f"use cache {useCache}" ) - NVDAEventName = winEventIDsToNVDAEventNames.get(eventID, None) + NVDAEventName = internalWinEventHandler.winEventIDsToNVDAEventNames.get(eventID, None) if not NVDAEventName: log.debugWarning(f"No NVDA event name for {getWinEventName(eventID)}") return None @@ -1036,7 +954,7 @@ def _fakeFocus(oldFocus): def initialize(): global accPropServices try: - accPropServices = comtypes.client.CreateObject(CAccPropServices) + accPropServices = comtypes.client.CreateObject(IA.CAccPropServices) except (WindowsError, COMError) as e: log.debugWarning("AccPropServices is not available: %s" % e) internalWinEventHandler.initialize(processDestroyWinEvent) @@ -1044,7 +962,7 @@ def initialize(): # C901 'pumpAll' is too complex def pumpAll(): # noqa: C901 - if not _shouldGetEvents(): + if not internalWinEventHandler._shouldGetEvents(): return focusWinEvents = [] validFocus = False @@ -1058,7 +976,7 @@ def pumpAll(): # noqa: C901 alwaysAllowedObjects.append((focus.event_windowHandle, focus.event_objectID, focus.event_childID)) # Receive all the winEvents from the limiter for this cycle - winEvents = winEventLimiter.flushEvents(alwaysAllowedObjects) + winEvents = internalWinEventHandler.winEventLimiter.flushEvents(alwaysAllowedObjects) for winEvent in winEvents: isEventOnCaret = winEvent[2] == winUser.OBJID_CARET @@ -1074,7 +992,7 @@ def pumpAll(): # noqa: C901 if not focus.shouldAcceptShowHideCaretEvent: continue elif not eventHandler.shouldAcceptEvent( - winEventIDsToNVDAEventNames[winEvent[0]], + internalWinEventHandler.winEventIDsToNVDAEventNames[winEvent[0]], windowHandle=winEvent[1] ): continue @@ -1128,7 +1046,7 @@ def terminate(): def getIAccIdentity(pacc, childID): - IAccIdentityObject = pacc.QueryInterface(IAccIdentity) + IAccIdentityObject = pacc.QueryInterface(IA.IAccIdentity) stringPtr, stringSize = IAccIdentityObject.getIdentityString(childID) try: if accPropServices: @@ -1186,16 +1104,16 @@ def findGroupboxObject(obj): # C901 'getRecursiveTextFromIAccessibleTextObject' def getRecursiveTextFromIAccessibleTextObject(obj, startOffset=0, endOffset=-1): # noqa: C901 - if not isinstance(obj, IAccessibleText): + if not isinstance(obj, IA2.IAccessibleText): try: - textObject = obj.QueryInterface(IAccessibleText) + textObject = obj.QueryInterface(IA2.IAccessibleText) except: # noqa: E722 Bare except textObject = None else: textObject = obj - if not isinstance(obj, IAccessible): + if not isinstance(obj, IA.IAccessible): try: - accObject = obj.QueryInterface(IAccessible) + accObject = obj.QueryInterface(IA.IAccessible) except: # noqa: E722 Bare except return "" else: @@ -1219,7 +1137,7 @@ def getRecursiveTextFromIAccessibleTextObject(obj, startOffset=0, endOffset=-1): description = None return " ".join([x for x in [name, value, description] if x and not x.isspace()]) try: - hypertextObject = accObject.QueryInterface(IAccessibleHypertext) + hypertextObject = accObject.QueryInterface(IA2.IAccessibleHypertext) except: # noqa: E722 Bare except return text textList = [] @@ -1227,7 +1145,7 @@ def getRecursiveTextFromIAccessibleTextObject(obj, startOffset=0, endOffset=-1): if ord(t) == 0xFFFC: try: index = hypertextObject.hyperlinkIndex(i + startOffset) - childTextObject = hypertextObject.hyperlink(index).QueryInterface(IAccessible) + childTextObject = hypertextObject.hyperlink(index).QueryInterface(IA.IAccessible) t = " %s " % getRecursiveTextFromIAccessibleTextObject(childTextObject) except: # noqa: E722 Bare except pass @@ -1322,7 +1240,7 @@ def isMarshalledIAccessible(IAccessibleObject): """Looks at the location of the first function in the IAccessible object's vtable (IUnknown::AddRef) to see if it was implemented in oleacc.dll (its local) or ole32.dll (its marshalled). """ - if not isinstance(IAccessibleObject, IAccessible): + if not isinstance(IAccessibleObject, IA.IAccessible): raise TypeError("object should be of type IAccessible, not %s" % IAccessibleObject) buf = create_unicode_buffer(1024) addr = POINTER(c_void_p).from_address( diff --git a/source/IAccessibleHandler/internalWinEventHandler.py b/source/IAccessibleHandler/internalWinEventHandler.py index 9765ea8213b..43ab1376b97 100644 --- a/source/IAccessibleHandler/internalWinEventHandler.py +++ b/source/IAccessibleHandler/internalWinEventHandler.py @@ -16,13 +16,8 @@ from . import getWinEventLogInfo from . import isMSAADebugLoggingEnabled +from comInterfaces import IAccessible2Lib as IA2 -from comInterfaces.IAccessible2Lib import ( - IA2_EVENT_TEXT_CARET_MOVED, - IA2_EVENT_DOCUMENT_LOAD_COMPLETE, - IA2_EVENT_OBJECT_ATTRIBUTE_CHANGED, - IA2_EVENT_PAGE_CHANGED, -) from .orderedWinEventLimiter import OrderedWinEventLimiter, MENU_EVENTIDS from logHandler import log @@ -61,10 +56,10 @@ winUser.EVENT_OBJECT_STATECHANGE: "stateChange", winUser.EVENT_OBJECT_VALUECHANGE: "valueChange", winUser.EVENT_OBJECT_LIVEREGIONCHANGED: "liveRegionChange", - IA2_EVENT_TEXT_CARET_MOVED: "caret", - IA2_EVENT_DOCUMENT_LOAD_COMPLETE: "documentLoadComplete", - IA2_EVENT_OBJECT_ATTRIBUTE_CHANGED: "IA2AttributeChange", - IA2_EVENT_PAGE_CHANGED: "pageChange", + IA2.IA2_EVENT_TEXT_CARET_MOVED: "caret", + IA2.IA2_EVENT_DOCUMENT_LOAD_COMPLETE: "documentLoadComplete", + IA2.IA2_EVENT_OBJECT_ATTRIBUTE_CHANGED: "IA2AttributeChange", + IA2.IA2_EVENT_PAGE_CHANGED: "pageChange", } _processDestroyWinEvent = None diff --git a/source/JABHandler.py b/source/JABHandler.py index 9466f4489c9..5a9e745cc64 100644 --- a/source/JABHandler.py +++ b/source/JABHandler.py @@ -795,10 +795,10 @@ def initialize(): ): enableBridge() # Accept wm_copydata and any wm_user messages from other processes even if running with higher privileges - if not windll.user32.ChangeWindowMessageFilter(winUser.WM_COPYDATA, 1): + if not windll.user32.ChangeWindowMessageFilter(winUser.WM_COPYDATA, winUser.MSGFLT.ALLOW): raise WinError() for msg in range(winUser.WM_USER + 1, 0xffff): - if not windll.user32.ChangeWindowMessageFilter(msg, 1): + if not windll.user32.ChangeWindowMessageFilter(msg, winUser.MSGFLT.ALLOW): raise WinError() bridgeDll.Windows_run() # Register java events diff --git a/source/NVDAObjects/IAccessible/MSHTML.py b/source/NVDAObjects/IAccessible/MSHTML.py index 80956fd7a0f..bf7286854a8 100644 --- a/source/NVDAObjects/IAccessible/MSHTML.py +++ b/source/NVDAObjects/IAccessible/MSHTML.py @@ -543,11 +543,24 @@ def _get_treeInterceptorClass(self): return virtualBuffers.MSHTML.MSHTML return super(MSHTML,self).treeInterceptorClass - def _get_isCurrent(self): - isCurrent = self.HTMLAttributes["aria-current"] - if isCurrent == "false": - isCurrent = None - return isCurrent + def _get_isCurrent(self) -> controlTypes.IsCurrent: + try: + isCurrent = self.HTMLAttributes["aria-current"] + except LookupError: + return controlTypes.IsCurrent.NO + + # key may be in HTMLAttributes with a value of None + if isCurrent is None: + return controlTypes.IsCurrent.NO + + try: + return controlTypes.IsCurrent(isCurrent) + except ValueError: + log.debugWarning(f"Unknown aria-current value: {isCurrent}") + return controlTypes.IsCurrent.NO + + #: Typing for autoproperty _get_HTMLAttributes + HTMLAttributes: HTMLAttribCache def _get_HTMLAttributes(self): return HTMLAttribCache(self.HTMLNode) diff --git a/source/NVDAObjects/IAccessible/__init__.py b/source/NVDAObjects/IAccessible/__init__.py index ebe78723975..4e1f83f1589 100644 --- a/source/NVDAObjects/IAccessible/__init__.py +++ b/source/NVDAObjects/IAccessible/__init__.py @@ -12,6 +12,7 @@ import itertools import importlib from comInterfaces.tom import ITextDocument +from comInterfaces import IAccessible2Lib as IA2 import tones import languageHandler import textInfos.offsets @@ -124,7 +125,9 @@ def _get_encoding(self): def _getOffsetFromPoint(self,x,y): if self.obj.IAccessibleTextObject.nCharacters>0: - offset = self.obj.IAccessibleTextObject.OffsetAtPoint(x,y,IAccessibleHandler.IA2_COORDTYPE_SCREEN_RELATIVE) + offset = self.obj.IAccessibleTextObject.OffsetAtPoint( + x, y, IA2.IA2_COORDTYPE_SCREEN_RELATIVE + ) # IA2 specifies that a result of -1 indicates that # the point is invalid or there is no character under the point. # Note that Chromium does not follow the spec and returns 0 for invalid or no character points. @@ -138,7 +141,9 @@ def _getOffsetFromPoint(self,x,y): @classmethod def _getBoundingRectFromOffsetInObject(cls,obj,offset): try: - res=RectLTWH(*obj.IAccessibleTextObject.characterExtents(offset,IAccessibleHandler.IA2_COORDTYPE_SCREEN_RELATIVE)) + res = RectLTWH(*obj.IAccessibleTextObject.characterExtents( + offset, IA2.IA2_COORDTYPE_SCREEN_RELATIVE + )) except COMError: raise NotImplementedError if not any(res[2:]): @@ -259,7 +264,7 @@ def _getCharacterOffsets(self,offset): except COMError: pass try: - start,end,text = self.obj.IAccessibleTextObject.TextAtOffset(offset,IAccessibleHandler.IA2_TEXT_BOUNDARY_CHAR) + start, end, text = self.obj.IAccessibleTextObject.TextAtOffset(offset, IA2.IA2_TEXT_BOUNDARY_CHAR) except COMError: return super(IA2TextTextInfo,self)._getCharacterOffsets(offset) if text and (textUtils.isHighSurrogate(text) or textUtils.isLowSurrogate(text)): @@ -275,7 +280,7 @@ def _getWordOffsets(self,offset): except COMError: pass try: - start,end,text=self.obj.IAccessibleTextObject.TextAtOffset(offset,IAccessibleHandler.IA2_TEXT_BOUNDARY_WORD) + start, end, text = self.obj.IAccessibleTextObject.TextAtOffset(offset, IA2.IA2_TEXT_BOUNDARY_WORD) except COMError: return super(IA2TextTextInfo,self)._getWordOffsets(offset) if start>offset or offset>end: @@ -285,7 +290,7 @@ def _getWordOffsets(self,offset): def _getLineOffsets(self,offset): try: - start,end,text=self.obj.IAccessibleTextObject.TextAtOffset(offset,IAccessibleHandler.IA2_TEXT_BOUNDARY_LINE) + start, end, text = self.obj.IAccessibleTextObject.TextAtOffset(offset, IA2.IA2_TEXT_BOUNDARY_LINE) return start,end except COMError: log.debugWarning("IAccessibleText::textAtOffset failed",exc_info=True) @@ -298,7 +303,7 @@ def _getSentenceOffsets(self,offset): except COMError: pass try: - start,end,text=self.obj.IAccessibleTextObject.TextAtOffset(offset,IAccessibleHandler.IA2_TEXT_BOUNDARY_SENTENCE) + start, end, text = self.obj.IAccessibleTextObject.TextAtOffset(offset, IA2.IA2_TEXT_BOUNDARY_SENTENCE) if start==end: raise NotImplementedError return start,end @@ -312,7 +317,7 @@ def _getParagraphOffsets(self,offset): except COMError: pass try: - start,end,text=self.obj.IAccessibleTextObject.TextAtOffset(offset,IAccessibleHandler.IA2_TEXT_BOUNDARY_PARAGRAPH) + start, end, text = self.obj.IAccessibleTextObject.TextAtOffset(offset, IA2.IA2_TEXT_BOUNDARY_PARAGRAPH) if start>=end: raise RuntimeError("did not expand to paragraph correctly") return start,end @@ -485,20 +490,6 @@ def findOverlayClasses(self,clsList): elif windowClassName=="GeckoPluginWindow" and self.event_objectID==0 and self.IAccessibleChildID==0: from .mozilla import GeckoPluginWindowRoot clsList.append(GeckoPluginWindowRoot) - maybeFlash = False - if ((windowClassName in ("MozillaWindowClass", "GeckoPluginWindow") and not isinstance(self.IAccessibleObject, IAccessibleHandler.IAccessible2)) - or windowClassName in ("MacromediaFlashPlayerActiveX", "ApolloRuntimeContentWindow", "ShockwaveFlash", "ShockwaveFlashLibrary", "ShockwaveFlashFullScreen", "GeckoFPSandboxChildWindow")): - maybeFlash = True - elif windowClassName == "Internet Explorer_Server" and self.event_objectID is not None and self.event_objectID > 0: - # #2454: In Windows 8 IE, Flash is exposed in the same HWND as web content. - from .MSHTML import MSHTML - # This is only possibly Flash if it isn't MSHTML. - if not isinstance(self, MSHTML): - maybeFlash = True - if maybeFlash: - # This is possibly a Flash object. - from . import adobeFlash - adobeFlash.findExtraOverlayClasses(self, clsList) elif windowClassName.startswith('Mozilla'): from . import mozilla mozilla.findExtraOverlayClasses(self, clsList) @@ -578,7 +569,11 @@ def findOverlayClasses(self,clsList): clsList.append(IAccessible) - if self.event_objectID==winUser.OBJID_CLIENT and self.event_childID==0 and not isinstance(self.IAccessibleObject,IAccessibleHandler.IAccessible2): + if( + self.event_objectID == winUser.OBJID_CLIENT + and self.event_childID == 0 + and not isinstance(self.IAccessibleObject, IA2.IAccessible2) + ): # This is the main (client) area of the window, so we can use other classes at the window level. # #3872: However, don't do this for IAccessible2 because # IA2 supersedes window level APIs and might conflict with them. @@ -602,7 +597,7 @@ def __init__(self,windowHandle=None,IAccessibleObject=None,IAccessibleChildID=No self.IAccessibleChildID=IAccessibleChildID # Try every trick in the book to get the window handle if we don't have it. - if not windowHandle and isinstance(IAccessibleObject,IAccessibleHandler.IAccessible2): + if not windowHandle and isinstance(IAccessibleObject, IA2.IAccessible2): windowHandle=self.IA2WindowHandle try: Identity=IAccessibleHandler.getIAccIdentity(IAccessibleObject,IAccessibleChildID) @@ -628,7 +623,7 @@ def __init__(self,windowHandle=None,IAccessibleObject=None,IAccessibleChildID=No if not windowHandle: raise InvalidNVDAObject("Can't get a window handle from IAccessible") - if isinstance(IAccessibleObject,IAccessibleHandler.IAccessible2): + if isinstance(IAccessibleObject, IA2.IAccessible2): try: self.IA2UniqueID=IAccessibleObject.uniqueID except COMError: @@ -637,7 +632,7 @@ def __init__(self,windowHandle=None,IAccessibleObject=None,IAccessibleChildID=No # Set the event params based on our calculated/construction info if we must. if event_windowHandle is None: event_windowHandle=windowHandle - if event_objectID is None and isinstance(IAccessibleObject,IAccessibleHandler.IAccessible2): + if event_objectID is None and isinstance(IAccessibleObject, IA2.IAccessible2): event_objectID=winUser.OBJID_CLIENT if event_childID is None: if self.IA2UniqueID is not None: @@ -651,18 +646,18 @@ def __init__(self,windowHandle=None,IAccessibleObject=None,IAccessibleChildID=No super(IAccessible,self).__init__(windowHandle=windowHandle) try: - self.IAccessibleActionObject=IAccessibleObject.QueryInterface(IAccessibleHandler.IAccessibleAction) + self.IAccessibleActionObject = IAccessibleObject.QueryInterface(IA2.IAccessibleAction) except COMError: pass try: - self.IAccessibleTable2Object=self.IAccessibleObject.QueryInterface(IAccessibleHandler.IAccessibleTable2) + self.IAccessibleTable2Object = self.IAccessibleObject.QueryInterface(IA2.IAccessibleTable2) except COMError: try: - self.IAccessibleTableObject=self.IAccessibleObject.QueryInterface(IAccessibleHandler.IAccessibleTable) + self.IAccessibleTableObject = self.IAccessibleObject.QueryInterface(IA2.IAccessibleTable) except COMError: pass try: - self.IAccessibleTextObject=IAccessibleObject.QueryInterface(IAccessibleHandler.IAccessibleText) + self.IAccessibleTextObject = IAccessibleObject.QueryInterface(IA2.IAccessibleText) except COMError: pass if None not in (event_windowHandle,event_objectID,event_childID): @@ -712,7 +707,10 @@ def _isEqual(self,other): return False if self.IAccessibleObject==other.IAccessibleObject: return True - if isinstance(self.IAccessibleObject,IAccessibleHandler.IAccessible2) and isinstance(other.IAccessibleObject,IAccessibleHandler.IAccessible2): + if ( + isinstance(self.IAccessibleObject, IA2.IAccessible2) + and isinstance(other.IAccessibleObject, IA2.IAccessible2) + ): # These are both IAccessible2 objects, so we can test unique ID. # Unique ID is only guaranteed to be unique within a given window, so we must check window handle as well. selfIA2Window=self.IA2WindowHandle @@ -824,7 +822,7 @@ def _get_IAccessibleIdentity(self): return self._IAccessibleIdentity def _get_IAccessibleRole(self): - if isinstance(self.IAccessibleObject,IAccessibleHandler.IAccessible2): + if isinstance(self.IAccessibleObject, IA2.IAccessible2): try: role=self.IAccessibleObject.role() except COMError: @@ -871,7 +869,7 @@ def _get_states(self): log.debugWarning("could not get IAccessible states",exc_info=True) else: states.update(IAccessibleHandler.IAccessibleStatesToNVDAStates[x] for x in (y for y in (1< -#This file is covered by the GNU General Public License. -#See the file COPYING for more details. - -import winUser -import oleacc -from . import IAccessible, getNVDAObjectFromEvent -from NVDAObjects import NVDAObjectTextInfo -from NVDAObjects.behaviors import EditableTextWithoutAutoSelectDetection -from comtypes import COMError, IServiceProvider, hresult -from comtypes.gen.FlashAccessibility import ISimpleTextSelection, IFlashAccessibility -from logHandler import log - -class InputTextFieldTextInfo(NVDAObjectTextInfo): - - def _getStoryText(self): - return self.obj.value or "" - - def _getRawSelectionOffsets(self): - try: - return self.obj.ISimpleTextSelectionObject.GetSelection() - except COMError as e: - if e.hresult == hresult.E_FAIL: - # The documentation says that an empty field should return 0 for both values, but instead, we seem to get E_FAIL. - # An empty field still has a valid caret. - return 0, 0 - else: - raise RuntimeError - except AttributeError: - raise RuntimeError - - def _getCaretOffset(self): - # We want the active (moving) end of the selection. - return self._getRawSelectionOffsets()[1] - - def _getSelectionOffsets(self): - # This might be a backwards selection, but for now, we should always return the values in ascending order. - return sorted(self._getRawSelectionOffsets()) - -class InputTextField(EditableTextWithoutAutoSelectDetection, IAccessible): - TextInfo = InputTextFieldTextInfo - -class Root(IAccessible): - - def _get_presentationType(self): - return self.presType_content - - def _get_treeInterceptorClass(self): - import virtualBuffers.adobeFlash - return virtualBuffers.adobeFlash.AdobeFlash - - #Flash root client has broken accParent, force to return the flash root window root IAccessible - def _get_parent(self): - return getNVDAObjectFromEvent(self.windowHandle,0,0) - -class PluginClientWithBrokenFocus(IAccessible): - """The client of a Flash plugin with broken focus behaviour. - #2546: In Flash protected mode, the Flash content is in another window beneath the plugin window. - Unfortunately, Flash doesn't bother to set focus to this window. - To work around this, when focus hits this object, focus is forced to the child. - """ - - def event_gainFocus(self): - try: - self.firstChild.firstChild.setFocus() - except AttributeError: - super(PluginClientWithBrokenFocus, self).event_gainFocus() - -def findExtraOverlayClasses(obj, clsList): - """Determine the most appropriate class if this is a Flash object. - This works similarly to L{NVDAObjects.NVDAObject.findOverlayClasses} except that it never calls any other findOverlayClasses method. - """ - iaRole = obj.IAccessibleRole - if obj.windowClassName == "GeckoPluginWindow" and iaRole == oleacc.ROLE_SYSTEM_CLIENT and obj.childCount == 1 and obj.firstChild.windowClassName == "GeckoFPSandboxChildWindow": - clsList.append(PluginClientWithBrokenFocus) - return - - try: - servProv = obj.IAccessibleObject.QueryInterface(IServiceProvider) - except COMError: - return - - # Check whether this is the Flash root accessible. - if iaRole == oleacc.ROLE_SYSTEM_CLIENT: - try: - servProv.QueryService(IFlashAccessibility._iid_, IFlashAccessibility) - clsList.append(Root) - except COMError: - pass - # If this is a client and IFlashAccessibility wasn't present, this is not a Flash object. - return - - # Check whether this is a Flash input text field. - try: - # We have to fetch ISimpleTextSelectionObject in order to check whether this is an input text field, so store it on the instance. - obj.ISimpleTextSelectionObject = servProv.QueryService(ISimpleTextSelection._iid_, ISimpleTextSelection) - clsList.append(InputTextField) - except COMError: - pass diff --git a/source/NVDAObjects/IAccessible/chromium.py b/source/NVDAObjects/IAccessible/chromium.py index 59b0566a61e..9cf030797aa 100644 --- a/source/NVDAObjects/IAccessible/chromium.py +++ b/source/NVDAObjects/IAccessible/chromium.py @@ -10,7 +10,6 @@ from comtypes import COMError import oleacc import controlTypes -import IAccessibleHandler from NVDAObjects.IAccessible import IAccessible from virtualBuffers.gecko_ia2 import Gecko_ia2 as GeckoVBuf, Gecko_ia2_TextInfo as GeckoVBufTextInfo from . import ia2Web diff --git a/source/NVDAObjects/IAccessible/ia2TextMozilla.py b/source/NVDAObjects/IAccessible/ia2TextMozilla.py index ba7d7a76b82..0f11fe4142f 100644 --- a/source/NVDAObjects/IAccessible/ia2TextMozilla.py +++ b/source/NVDAObjects/IAccessible/ia2TextMozilla.py @@ -12,7 +12,7 @@ import winUser import textInfos import controlTypes -import IAccessibleHandler +from comInterfaces import IAccessible2Lib as IA2 import api from NVDAObjects import NVDAObject, NVDAObjectTextInfo from . import IA2TextTextInfo, IAccessible @@ -50,7 +50,7 @@ def _getEmbedded(obj, offset): hi = ht.hyperlinkIndex(offset) if hi != -1: hl = ht.hyperlink(hi) - return IAccessible(IAccessibleObject=hl.QueryInterface(IAccessibleHandler.IAccessible2), IAccessibleChildID=0) + return IAccessible(IAccessibleObject=hl.QueryInterface(IA2.IAccessible2), IAccessibleChildID=0) except COMError: pass return None @@ -194,7 +194,7 @@ def _getEmbedding(self, obj): info._endOffset = info._startOffset + 1 return info try: - hl = obj.IAccessibleObject.QueryInterface(IAccessibleHandler.IAccessibleHyperlink) + hl = obj.IAccessibleObject.QueryInterface(IA2.IAccessibleHyperlink) hlOffset = hl.startIndex info._startOffset = hlOffset info._endOffset = hlOffset + 1 diff --git a/source/NVDAObjects/IAccessible/ia2Web.py b/source/NVDAObjects/IAccessible/ia2Web.py index 2f69390c967..06bd5a93ac1 100644 --- a/source/NVDAObjects/IAccessible/ia2Web.py +++ b/source/NVDAObjects/IAccessible/ia2Web.py @@ -10,7 +10,7 @@ from ctypes import c_short from comtypes import COMError, BSTR import oleacc -import IAccessibleHandler +from comInterfaces import IAccessible2Lib as IA2 import controlTypes from logHandler import log from documentBase import DocumentWithTableNavigation @@ -34,11 +34,13 @@ def _get_positionInfo(self): info['level']=level return info - def _get_isCurrent(self): - current = self.IA2Attributes.get("current", None) - if current == "false": - current = None - return current + def _get_isCurrent(self) -> controlTypes.IsCurrent: + ia2attrCurrent: str = self.IA2Attributes.get("current", "false") + try: + return controlTypes.IsCurrent(ia2attrCurrent) + except ValueError: + log.debugWarning(f"Unknown 'current' IA2Attribute value: {ia2attrCurrent}") + return controlTypes.IsCurrent.NO def _get_placeholder(self): placeholder = self.IA2Attributes.get('placeholder', None) @@ -73,7 +75,7 @@ def _get_landmark(self): landmark = next((xr for xr in xmlRoles if xr in aria.landmarkRoles), None) if ( landmark - and self.IAccessibleRole != IAccessibleHandler.IA2_ROLE_LANDMARK + and self.IAccessibleRole != IA2.IA2_ROLE_LANDMARK and landmark != xmlRoles[0] ): # Ignore the landmark role @@ -87,7 +89,10 @@ def event_IA2AttributeChange(self): if self is api.getFocusObject(): # Report aria-current if it changed. speech.speakObjectProperties( - self, current=True, reason=controlTypes.REASON_CHANGE) + self, + current=True, + reason=controlTypes.OutputReason.CHANGE + ) # super calls event_stateChange which updates braille, so no need to # update braille here. @@ -145,10 +150,12 @@ def _getTableCellAt(self,tableID,startPos,destRow,destCol): # We support either IAccessibleTable or IAccessibleTable2 interfaces for locating table cells. # We will be able to get at least one of these. try: - cell = table.IAccessibleTable2Object.cellAt(destRow - 1, destCol - 1).QueryInterface(IAccessibleHandler.IAccessible2) + cell = table.IAccessibleTable2Object.cellAt(destRow - 1, destCol - 1).QueryInterface(IA2.IAccessible2) except AttributeError: # No IAccessibleTable2, try IAccessibleTable instead. - cell = table.IAccessibleTableObject.accessibleAt(destRow - 1, destCol - 1).QueryInterface(IAccessibleHandler.IAccessible2) + cell = table.IAccessibleTableObject.accessibleAt( + destRow - 1, destCol - 1 + ).QueryInterface(IA2.IAccessible2) cell = IAccessible(IAccessibleObject=cell, IAccessibleChildID=0) # If the cell we fetched is marked as hidden, raise LookupError which will instruct calling code to try an adjacent cell instead. if cell.IA2Attributes.get('hidden'): @@ -220,12 +227,12 @@ def findExtraOverlayClasses(obj, clsList, baseClass=Ia2Web, documentClass=None): """ if not documentClass: raise ValueError("documentClass cannot be None") - if not isinstance(obj.IAccessibleObject, IAccessibleHandler.IAccessible2): + if not isinstance(obj.IAccessibleObject, IA2.IAccessible2): return iaRole = obj.IAccessibleRole xmlRoles = obj.IA2Attributes.get("xml-roles", "").split(" ") - if iaRole == IAccessibleHandler.IA2_ROLE_SECTION and obj.IA2Attributes.get("tag", None) == "blockquote": + if iaRole == IA2.IA2_ROLE_SECTION and obj.IA2Attributes.get("tag", None) == "blockquote": clsList.append(BlockQuote) elif iaRole == oleacc.ROLE_SYSTEM_OUTLINE and "treegrid" in xmlRoles: clsList.append(Treegrid) @@ -259,7 +266,7 @@ def findExtraOverlayClasses(obj, clsList, baseClass=Ia2Web, documentClass=None): ): clsList.append(documentClass) - if obj.IA2States & IAccessibleHandler.IA2_STATE_EDITABLE: + if obj.IA2States & IA2.IA2_STATE_EDITABLE: if obj.IAccessibleStates & oleacc.STATE_SYSTEM_FOCUSABLE: clsList.append(Editor) else: diff --git a/source/NVDAObjects/IAccessible/mozilla.py b/source/NVDAObjects/IAccessible/mozilla.py index d727ea50bfd..77a8ca52309 100755 --- a/source/NVDAObjects/IAccessible/mozilla.py +++ b/source/NVDAObjects/IAccessible/mozilla.py @@ -6,6 +6,7 @@ #Copyright (C) 2006-2017 NV Access Limited, Peter Vágner import IAccessibleHandler +from comInterfaces import IAccessible2Lib as IA2 import oleacc import winUser import controlTypes @@ -95,7 +96,7 @@ def findExtraOverlayClasses(obj, clsList): """Determine the most appropriate class if this is a Mozilla object. This works similarly to L{NVDAObjects.NVDAObject.findOverlayClasses} except that it never calls any other findOverlayClasses method. """ - if not isinstance(obj.IAccessibleObject, IAccessibleHandler.IAccessible2): + if not isinstance(obj.IAccessibleObject, IA2.IAccessible2): return iaRole = obj.IAccessibleRole @@ -108,7 +109,7 @@ def findExtraOverlayClasses(obj, clsList): # Not unavailable excludes disabled editable text fields (which also aren't focusable). if not (iaStates & oleacc.STATE_SYSTEM_FOCUSABLE or iaStates & oleacc.STATE_SYSTEM_UNAVAILABLE): # This excludes a non-focusable @role="textbox". - if not (obj.IA2States & IAccessibleHandler.IA2_STATE_EDITABLE): + if not (obj.IA2States & IA2.IA2_STATE_EDITABLE): cls = TextLeaf if not cls: cls = _IAccessibleRolesToOverlayClasses.get(iaRole) @@ -135,7 +136,7 @@ def findExtraOverlayClasses(obj, clsList): #: Maps IAccessible roles to NVDAObject overlay classes. _IAccessibleRolesToOverlayClasses = { - IAccessibleHandler.IA2_ROLE_EMBEDDED_OBJECT: EmbeddedObject, + IA2.IA2_ROLE_EMBEDDED_OBJECT: EmbeddedObject, "embed": EmbeddedObject, "object": EmbeddedObject, } diff --git a/source/NVDAObjects/IAccessible/winConsole.py b/source/NVDAObjects/IAccessible/winConsole.py index 9347169bc5d..9eb85cc8b29 100644 --- a/source/NVDAObjects/IAccessible/winConsole.py +++ b/source/NVDAObjects/IAccessible/winConsole.py @@ -1,22 +1,45 @@ -#NVDAObjects/IAccessible/WinConsole.py -#A part of NonVisual Desktop Access (NVDA) -#This file is covered by the GNU General Public License. -#See the file COPYING for more details. -#Copyright (C) 2007-2019 NV Access Limited, Bill Dengler +# A part of NonVisual Desktop Access (NVDA) +# This file is covered by the GNU General Public License. +# See the file COPYING for more details. +# Copyright (C) 2007-2020 NV Access Limited, Bill Dengler import config -from winVersion import isWin10 +from NVDAObjects.behaviors import KeyboardHandlerBasedTypedCharSupport +from winVersion import getWinVer, WIN10_1607 from . import IAccessible from ..window import winConsole -class WinConsole(winConsole.WinConsole, IAccessible): - "The legacy console implementation for situations where UIA isn't supported." - pass + +class EnhancedLegacyWinConsole(KeyboardHandlerBasedTypedCharSupport, winConsole.WinConsole, IAccessible): + """ + A hybrid approach to console access, using legacy APIs to read output + and KeyboardHandlerBasedTypedCharSupport for input. + """ + #: Legacy consoles take quite a while to send textChange events. + #: This significantly impacts typing performance, so don't queue chars. + _supportsTextChange = False + + +class LegacyWinConsole(winConsole.WinConsole, IAccessible): + """ + NVDA's original console support, used by default on Windows versions + before 1607. + """ + + def _get_diffAlgo(self): + # Non-enhanced legacy consoles use caret proximity to detect + # typed/deleted text. + # Single-character changes are not reported as + # they are confused for typed characters. + # Force difflib to keep meaningful edit reporting in these consoles. + from diffHandler import get_difflib_algo + return get_difflib_algo() + def findExtraOverlayClasses(obj, clsList): - if isWin10(1607) and config.conf['terminals']['keyboardSupportInLegacy']: - from NVDAObjects.behaviors import KeyboardHandlerBasedTypedCharSupport - clsList.append(KeyboardHandlerBasedTypedCharSupport) - clsList.append(WinConsole) + if getWinVer() >= WIN10_1607 and config.conf['terminals']['keyboardSupportInLegacy']: + clsList.append(EnhancedLegacyWinConsole) + else: + clsList.append(LegacyWinConsole) diff --git a/source/NVDAObjects/IAccessible/winword.py b/source/NVDAObjects/IAccessible/winword.py index 57bcce07eb1..acc273a6377 100644 --- a/source/NVDAObjects/IAccessible/winword.py +++ b/source/NVDAObjects/IAccessible/winword.py @@ -269,7 +269,7 @@ def script_caret_moveByCell(self,gesture): isCollapsed=info.isCollapsed if inTable: info.expand(textInfos.UNIT_CELL) - speech.speakTextInfo(info,reason=controlTypes.REASON_FOCUS) + speech.speakTextInfo(info, reason=controlTypes.OutputReason.FOCUS) braille.handler.handleCaretMove(self) def script_reportCurrentComment(self,gesture): @@ -339,7 +339,7 @@ def _moveInTable(self,row=True,forward=True): ui.message(_("Edge of table")) return False newInfo=WordDocumentTextInfo(self,textInfos.POSITION_CARET,_rangeObj=foundCell) - speech.speakTextInfo(newInfo,reason=controlTypes.REASON_CARET, unit=textInfos.UNIT_CELL) + speech.speakTextInfo(newInfo, reason=controlTypes.OutputReason.CARET, unit=textInfos.UNIT_CELL) newInfo.collapse() newInfo.updateCaret() return True diff --git a/source/NVDAObjects/UIA/VisualStudio.py b/source/NVDAObjects/UIA/VisualStudio.py index edd3ec03b8b..2e11df25032 100644 --- a/source/NVDAObjects/UIA/VisualStudio.py +++ b/source/NVDAObjects/UIA/VisualStudio.py @@ -85,7 +85,7 @@ def event_UIA_toolTipOpened(self): def findExtraOverlayClasses(obj, clsList): if obj.UIAAutomationId in _INTELLISENSE_LIST_AUTOMATION_IDS: clsList.insert(0, IntelliSenseList) - elif isinstance(obj.parent, IntelliSenseList) and obj.UIAElement.cachedClassName == "IntellisenseMenuItem": + elif obj.UIAElement.cachedClassName == "IntellisenseMenuItem" and isinstance(obj.parent, IntelliSenseList): clsList.insert(0, IntelliSenseItem) elif ( obj.UIAElement.cachedClassName == "LiveTextBlock" diff --git a/source/NVDAObjects/UIA/__init__.py b/source/NVDAObjects/UIA/__init__.py index 37cd11a9599..31c95b7466e 100644 --- a/source/NVDAObjects/UIA/__init__.py +++ b/source/NVDAObjects/UIA/__init__.py @@ -5,7 +5,7 @@ # Babbage B.V., Leonard de Ruijter, Bill Dengler """Support for UI Automation (UIA) controls.""" - +import typing from ctypes import byref from ctypes.wintypes import POINT, RECT from comtypes import COMError @@ -17,6 +17,7 @@ import colors import languageHandler import UIAHandler +import _UIACustomProps import globalVars import eventHandler import controlTypes @@ -334,7 +335,7 @@ def __init__(self,obj,position,_rangeObj=None): # sometimes rangeFromChild can return a NULL range if not self._rangeObj: raise LookupError elif isinstance(position,locationHelper.Point): - if (winVersion.winVersion.major, winVersion.winVersion.minor) == (6, 1): + if winVersion.getWinVer() <= winVersion.WIN7_SP1: # #9435: RangeFromPoint causes a freeze in UIA client library in the Windows 7 start menu! raise NotImplementedError("RangeFromPoint not supported on Windows 7") self._rangeObj=self.obj.UIATextPattern.RangeFromPoint(position.toPOINT()) @@ -647,21 +648,41 @@ def _getTextWithFieldsForUIARange(self,rootElement,textRange,formatConfig,includ if debug: log.debug("NULL childRange. Skipping") continue - clippedStart=clippedEnd=False - if index==lastChildIndex and childRange.CompareEndpoints(UIAHandler.TextPatternRangeEndpoint_Start,textRange,UIAHandler.TextPatternRangeEndpoint_End)>=0: + clippedStart = False + clippedEnd = False + if childRange.CompareEndpoints( + UIAHandler.TextPatternRangeEndpoint_End, + textRange, + UIAHandler.TextPatternRangeEndpoint_Start + ) <= 0: + if debug: + log.debug("Child completely before textRange. Skipping") + continue + if childRange.CompareEndpoints( + UIAHandler.TextPatternRangeEndpoint_Start, + textRange, + UIAHandler.TextPatternRangeEndpoint_End + ) >= 0: if debug: log.debug("Child at or past end of textRange. Breaking") break - if index==lastChildIndex: - lastChildEndDelta=childRange.CompareEndpoints(UIAHandler.TextPatternRangeEndpoint_End,textRange,UIAHandler.TextPatternRangeEndpoint_End) - if lastChildEndDelta>0: - if debug: - log.debug( - "textRange ended part way through the child. " - "Crop end of childRange to fit" - ) - childRange.MoveEndpointByRange(UIAHandler.TextPatternRangeEndpoint_End,textRange,UIAHandler.TextPatternRangeEndpoint_End) - clippedEnd=True + lastChildEndDelta = childRange.CompareEndpoints( + UIAHandler.TextPatternRangeEndpoint_End, + textRange, + UIAHandler.TextPatternRangeEndpoint_End + ) + if lastChildEndDelta > 0: + if debug: + log.debug( + "textRange ended part way through the child. " + "Crop end of childRange to fit" + ) + childRange.MoveEndpointByRange( + UIAHandler.TextPatternRangeEndpoint_End, + textRange, + UIAHandler.TextPatternRangeEndpoint_End + ) + clippedEnd = True childStartDelta=childRange.CompareEndpoints(UIAHandler.TextPatternRangeEndpoint_Start,tempRange,UIAHandler.TextPatternRangeEndpoint_End) if childStartDelta>0: # plain text before this child @@ -793,6 +814,9 @@ def updateSelection(self): updateCaret = updateSelection class UIA(Window): + _UIACustomProps = _UIACustomProps.CustomPropertiesCommon.get() + + shouldAllowDuplicateUIAFocusEvent = False def _get__coreCycleUIAPropertyCacheElementCache(self): """ @@ -852,6 +876,19 @@ def findOverlayClasses(self,clsList): clsList.append(WpfTextView) elif UIAClassName=="NetUIDropdownAnchor": clsList.append(NetUIDropdownAnchor) + elif self.windowClassName == "EXCEL6" and self.role == controlTypes.ROLE_PANE: + from .excel import BadExcelFormulaEdit + clsList.append(BadExcelFormulaEdit) + elif self.windowClassName == "EXCEL7": + if self.role in (controlTypes.ROLE_DATAITEM, controlTypes.ROLE_HEADERITEM): + from .excel import ExcelCell + clsList.append(ExcelCell) + elif self.role == controlTypes.ROLE_DATAGRID: + from .excel import ExcelWorksheet + clsList.append(ExcelWorksheet) + elif self.role == controlTypes.ROLE_EDITABLETEXT: + from .excel import CellEdit + clsList.append(CellEdit) elif self.TextInfo == UIATextInfo and ( UIAClassName == '_WwG' or self.windowClassName == '_WwG' @@ -880,23 +917,50 @@ def findOverlayClasses(self,clsList): # But not for Internet Explorer and not self.appModule.appName == 'iexplore' ): - from . import edge + from . import spartanEdge if UIAClassName in ("Internet Explorer_Server","WebView") and self.role==controlTypes.ROLE_PANE: - clsList.append(edge.EdgeHTMLRootContainer) - elif (self.UIATextPattern and - # #6998: Edge normally gives its root node a controlType of pane, but ARIA role="document" changes the controlType to document - self.role in (controlTypes.ROLE_PANE,controlTypes.ROLE_DOCUMENT) and - self.parent and (isinstance(self.parent,edge.EdgeHTMLRootContainer) or not isinstance(self.parent,edge.EdgeNode)) + clsList.append(spartanEdge.EdgeHTMLRootContainer) + elif ( + self.UIATextPattern + # #6998: + # Edge normally gives its root node a controlType of pane, but ARIA role="document" + # changes the controlType to document + and self.role in ( + controlTypes.ROLE_PANE, + controlTypes.ROLE_DOCUMENT + ) + and self.parent + and ( + isinstance(self.parent, spartanEdge.EdgeHTMLRootContainer) + or not isinstance(self.parent, spartanEdge.EdgeNode) + ) ): - clsList.append(edge.EdgeHTMLRoot) + clsList.append(spartanEdge.EdgeHTMLRoot) elif self.role==controlTypes.ROLE_LIST: - clsList.append(edge.EdgeList) + clsList.append(spartanEdge.EdgeList) + else: + clsList.append(spartanEdge.EdgeNode) + elif self.windowClassName == "Chrome_RenderWidgetHostHWND": + from . import chromium + from . import web + if ( + self.UIATextPattern + and self.role == controlTypes.ROLE_DOCUMENT + and self.parent + and self.parent.role == controlTypes.ROLE_PANE + ): + clsList.append(chromium.ChromiumUIADocument) else: - clsList.append(edge.EdgeNode) - elif self.role == controlTypes.ROLE_DOCUMENT and UIAAutomationId == "Microsoft.Windows.PDF.DocumentView": + if self.role == controlTypes.ROLE_LIST: + clsList.append(web.List) + clsList.append(chromium.ChromiumUIA) + elif ( + self.role == controlTypes.ROLE_DOCUMENT + and self.UIAElement.cachedAutomationId == "Microsoft.Windows.PDF.DocumentView" + ): # PDFs - from . import edge - clsList.append(edge.EdgeHTMLRoot) + from . import spartanEdge + clsList.append(spartanEdge.EdgeHTMLRoot) elif ( UIAAutomationId == "RichEditControl" and "DevExpress.XtraRichEdit" in self.UIAElement.cachedProviderDescription @@ -1121,8 +1185,72 @@ def _get_UIASelectionItemPattern(self): self.UIASelectionItemPattern=self._getUIAPattern(UIAHandler.UIA_SelectionItemPatternId,UIAHandler.IUIAutomationSelectionItemPattern) return self.UIASelectionItemPattern + def _get_UIASelectionPattern(self): + self.UIASelectionPattern = self._getUIAPattern( + UIAHandler.UIA_SelectionPatternId, + UIAHandler.IUIAutomationSelectionPattern + ) + return self.UIASelectionPattern + + def _get_UIASelectionPattern2(self): + try: + self.UIASelectionPattern2 = self._getUIAPattern( + UIAHandler.UIA_SelectionPattern2Id, + UIAHandler.IUIAutomationSelectionPattern2 + ) + except COMError: + # SelectionPattern2 is not available on older Operating Systems such as Windows 7 + self.UIASelectionPattern2 = None + return self.UIASelectionPattern2 + + def getSelectedItemsCount(self, maxItems=None): + p = self.UIASelectionPattern2 + if p: + return p.currentItemCount + return 0 + + #: Typing information for auto-property: _get_selectionContainer + selectionContainer: "typing.Optional[UIA]" + + def _get_selectionContainer(self) -> "typing.Optional[UIA]": + p = self.UIASelectionItemPattern + if not p: + return None + e = p.currentSelectionContainer + if not e: + # Some implementations of SelectionItemPattern, such as the Outlook attachment list + # give back a NULL selectionContainer + return None + e = e.buildUpdatedCache(UIAHandler.handler.baseCacheRequest) + obj = UIA(UIAElement=e) + if obj.UIASelectionPattern2: + return obj + return None + + #: typing for auto-property: UIAAnnotationObjects + UIAAnnotationObjects: typing.Dict[int, UIAHandler.IUIAutomationElement] + + def _get_UIAAnnotationObjects(self) -> typing.Dict[int, UIAHandler.IUIAutomationElement]: + """ + Returns this UIAElement's annotation objects, + in a dict keyed by their annotation type ID. + """ + objsByTypeID = {} + objs = self._getUIACacheablePropertyValue(UIAHandler.UIA_AnnotationObjectsPropertyId) + if objs: + objs = objs.QueryInterface(UIAHandler.IUIAutomationElementArray) + for index in range(objs.length): + obj = objs.getElement(index) + typeID = obj.GetCurrentPropertyValue(UIAHandler.UIA_AnnotationAnnotationTypeIdPropertyId) + objsByTypeID[typeID] = obj + return objsByTypeID + def _get_UIATextPattern(self): - self.UIATextPattern=self._getUIAPattern(UIAHandler.UIA_TextPatternId,UIAHandler.IUIAutomationTextPattern,cache=True) + self.UIATextPattern = self._getUIAPattern( + UIAHandler.UIA_TextPatternId, + UIAHandler.IUIAutomationTextPattern, + cache=False + ) return self.UIATextPattern def _get_UIATextEditPattern(self): @@ -1200,7 +1328,10 @@ def _get_UIAAutomationId(self): # #11445: due to timing errors, elements will be instantiated with no automation Id present. return "" - def _get_name(self): + #: Typing info for auto property _get_name() + name: str + + def _get_name(self) -> str: try: return self._getUIACacheablePropertyValue(UIAHandler.UIA_NamePropertyId) except COMError: @@ -1273,6 +1404,7 @@ def _get_keyboardShortcut(self): UIAHandler.UIA_IsSelectionItemPatternAvailablePropertyId, UIAHandler.UIA_IsEnabledPropertyId, UIAHandler.UIA_IsOffscreenPropertyId, + UIAHandler.UIA_AnnotationTypesPropertyId, } def _get_states(self): @@ -1338,6 +1470,14 @@ def _get_states(self): states.add(controlTypes.STATE_CHECKABLE) if s==UIAHandler.ToggleState_On: states.add(controlTypes.STATE_CHECKED) + try: + annotationTypes = self._getUIACacheablePropertyValue(UIAHandler.UIA_AnnotationTypesPropertyId) + except COMError: + # annotationTypes cannot be fetched on older Operating Systems such as Windows 7. + annotationTypes = None + if annotationTypes: + if UIAHandler.AnnotationType_Comment in annotationTypes: + states.add(controlTypes.STATE_HASCOMMENT) return states def _getReadOnlyState(self) -> bool: @@ -1400,7 +1540,10 @@ def _get_previous(self): return None return self.correctAPIForRelation(UIA(UIAElement=previousElement)) - def _get_next(self): + #: Typing information for auto-property: _get_next + next: "typing.Optional[UIA]" + + def _get_next(self) -> "typing.Optional[UIA]": try: nextElement=UIAHandler.handler.baseTreeWalker.GetNextSiblingElementBuildCache(self.UIAElement,UIAHandler.handler.baseCacheRequest) except COMError: @@ -1668,7 +1811,7 @@ def event_UIA_systemAlert(self): This just reports the element that received the alert in speech and braille, similar to how focus is presented. Skype for business toast notifications being one example. """ - speech.speakObject(self, reason=controlTypes.REASON_FOCUS) + speech.speakObject(self, reason=controlTypes.OutputReason.FOCUS) # Ideally, we wouldn't use getPropertiesBraille directly. braille.handler.message(braille.getPropertiesBraille(name=self.name, role=self.role)) @@ -1737,7 +1880,7 @@ def _get_positionInfo(self): info={} itemIndex=0 try: - itemIndex=self._getUIACacheablePropertyValue(UIAHandler.handler.ItemIndex_PropertyId) + itemIndex = self._getUIACacheablePropertyValue(self._UIACustomProps.itemIndex.id) except COMError: pass if itemIndex>0: @@ -1749,7 +1892,7 @@ def _get_positionInfo(self): e=None if e: try: - itemCount=e.getCurrentPropertyValue(UIAHandler.handler.ItemCount_PropertyId) + itemCount = e.getCurrentPropertyValue(self._UIACustomProps.itemCount.id) except COMError: itemCount=0 if itemCount>0: @@ -1768,7 +1911,7 @@ def event_focusEntered(self): def event_valueChange(self): focusParent=api.getFocusObject().parent if self==focusParent: - speech.speakObjectProperties(self,value=True,reason=controlTypes.REASON_CHANGE) + speech.speakObjectProperties(self, value=True, reason=controlTypes.OutputReason.CHANGE) else: super(SensitiveSlider,self).event_valueChange() diff --git a/source/NVDAObjects/UIA/anaheimEdge.py b/source/NVDAObjects/UIA/anaheimEdge.py new file mode 100644 index 00000000000..71fb096b7a0 --- /dev/null +++ b/source/NVDAObjects/UIA/anaheimEdge.py @@ -0,0 +1,14 @@ +# A part of NonVisual Desktop Access (NVDA) +# This file is covered by the GNU General Public License. +# See the file COPYING for more details. +# Copyright (C) 2020 NV Access Limited + +""" +UIA.anaheim_edge module for specialisations required for Chromium based Edge (code name anaheim). +Note that the UIA.chromium module provides base behaviour for all Chromium based browsers, whereas this +(anaheim_edge) module is only concerned with the window chrome / widgets that are specific to the browser. + +This file is currently a placeholder, this line can be deleted when this file is populated. Normally blank +files would not be included in NVDA's repository, however in this case it has been in order to head off any +confusion between the two very different implementations of Edge. +""" diff --git a/source/NVDAObjects/UIA/chromium.py b/source/NVDAObjects/UIA/chromium.py new file mode 100644 index 00000000000..6d8ea2ee5d1 --- /dev/null +++ b/source/NVDAObjects/UIA/chromium.py @@ -0,0 +1,68 @@ +# A part of NonVisual Desktop Access (NVDA) +# This file is covered by the GNU General Public License. +# See the file COPYING for more details. +# Copyright (C) 2020 NV Access limited, Leonard de Ruijter + +import UIAHandler +from . import web +import controlTypes + +""" +This module provides UIA behaviour specific to the chromium family of browsers. +Note this is more specialised than UIA.web but less so than browser specific modules such as UIA.spartan_edge +or UIA.anaheim_edge. +""" + + +class ChromiumUIATextInfo(web.UIAWebTextInfo): + + def _getFormatFieldAtRange(self, textRange, formatConfig, ignoreMixedValues=False): + formatField = super()._getFormatFieldAtRange(textRange, formatConfig, ignoreMixedValues=ignoreMixedValues) + # Headings are also exposed in the element tree, + # And therefore exposing in a formatField is redundant and causes duplicate reporting. + # So remove heading-level from the formatField if it exists. + try: + del formatField.field['heading-level'] + except KeyError: + pass + return formatField + + def _getControlFieldForObject(self, obj, isEmbedded=False, startOfNode=False, endOfNode=False): + field = super()._getControlFieldForObject( + obj, + isEmbedded=isEmbedded, + startOfNode=startOfNode, + endOfNode=endOfNode + ) + # use the value of comboboxes as content. + if obj.role == controlTypes.ROLE_COMBOBOX: + field['content'] = obj.value + # Layout tables do not have the UIA table pattern + if field['role'] == controlTypes.ROLE_TABLE: + if not obj._getUIACacheablePropertyValue(UIAHandler.UIA_IsTablePatternAvailablePropertyId): + field['table-layout'] = True + # Currently no way to tell if author has explicitly set name. + # Therefore always report the name if the control is not of a type that + # by definition uses its name for content. + # this may cause some duplicate speaking, + # But that is currently better than nothing at all. + if not field.get('nameIsContent') and field.get('name'): + field['alwaysReportName'] = True + return field + + +class ChromiumUIA(web.UIAWeb): + _TextInfo = ChromiumUIATextInfo + + +class ChromiumUIATreeInterceptor(web.UIAWebTreeInterceptor): + + def _get_documentConstantIdentifier(self): + return self.rootNVDAObject.parent._getUIACacheablePropertyValue(UIAHandler.UIA_AutomationIdPropertyId) + + +class ChromiumUIADocument(ChromiumUIA): + treeInterceptorClass = ChromiumUIATreeInterceptor + + def _get_shouldCreateTreeInterceptor(self): + return self.role == controlTypes.ROLE_DOCUMENT diff --git a/source/NVDAObjects/UIA/edge.py b/source/NVDAObjects/UIA/edge.py deleted file mode 100644 index cbd84185868..00000000000 --- a/source/NVDAObjects/UIA/edge.py +++ /dev/null @@ -1,619 +0,0 @@ -#NVDAObjects/UIA/edge.py -#A part of NonVisual Desktop Access (NVDA) -#This file is covered by the GNU General Public License. -#See the file COPYING for more details. -#Copyright (C) 2015-2017 NV Access Limited, Babbage B.V. - -from comtypes import COMError -from comtypes.automation import VARIANT -from ctypes import byref -import winVersion -from logHandler import log -import eventHandler -import config -import controlTypes -import cursorManager -import re -import aria -import textInfos -import UIAHandler -from UIABrowseMode import UIABrowseModeDocument, UIABrowseModeDocumentTextInfo, UIATextRangeQuickNavItem,UIAControlQuicknavIterator -from UIAUtils import * -from . import UIA, UIATextInfo - -def splitUIAElementAttribs(attribsString): - """Split an UIA Element attributes string into a dict of attribute keys and values. - An invalid attributes string does not cause an error, but strange results may be returned. - @param attribsString: The UIA Element attributes string to convert. - @type attribsString: str - @return: A dict of the attribute keys and values, where values are strings - @rtype: {str: str} - """ - attribsDict = {} - tmp = "" - key = "" - inEscape = False - for char in attribsString: - if inEscape: - tmp += char - inEscape = False - elif char == "\\": - inEscape = True - elif char == "=": - # We're about to move on to the value, so save the key and clear tmp. - key = tmp - tmp = "" - elif char == ";": - # We're about to move on to a new attribute. - if key: - # Add this key/value pair to the dict. - attribsDict[key] = tmp - key = "" - tmp = "" - else: - tmp += char - # If there was no trailing semi-colon, we need to handle the last attribute. - if key: - # Add this key/value pair to the dict. - attribsDict[key] = tmp - return attribsDict - -class EdgeTextInfo(UIATextInfo): - - def _get_UIAElementAtStartWithReplacedContent(self): - """Fetches the deepest UIAElement at the start of the text range whos name has been overridden by the author (such as aria-label).""" - element=self.UIAElementAtStart - condition=createUIAMultiPropertyCondition({UIAHandler.UIA_ControlTypePropertyId:self.UIAControlTypesWhereNameIsContent}) - # A part from the condition given, we must always match on the root of the document so we know when to stop walking - runtimeID=VARIANT() - self.obj.UIAElement._IUIAutomationElement__com_GetCurrentPropertyValue(UIAHandler.UIA_RuntimeIdPropertyId,byref(runtimeID)) - condition=UIAHandler.handler.clientObject.createOrCondition(UIAHandler.handler.clientObject.createPropertyCondition(UIAHandler.UIA_RuntimeIdPropertyId,runtimeID),condition) - walker=UIAHandler.handler.clientObject.createTreeWalker(condition) - cacheRequest=UIAHandler.handler.clientObject.createCacheRequest() - cacheRequest.addProperty(UIAHandler.UIA_NamePropertyId) - cacheRequest.addProperty(UIAHandler.UIA_AriaPropertiesPropertyId) - element=walker.normalizeElementBuildCache(element,cacheRequest) - while element and not UIAHandler.handler.clientObject.compareElements(element,self.obj.UIAElement): - name=element.getCachedPropertyValue(UIAHandler.UIA_NamePropertyId) - if name: - ariaProperties=element.getCachedPropertyValue(UIAHandler.UIA_AriaPropertiesPropertyId) - if ('label=' in ariaProperties) or ('labelledby=' in ariaProperties): - return element - try: - textRange=self.obj.UIATextPattern.rangeFromChild(element) - except COMError: - return - text = textRange.getText(-1) - if not text or text.isspace(): - return element - element=walker.getParentElementBuildCache(element,cacheRequest) - - def _moveToEdgeOfReplacedContent(self,back=False): - """If within replaced content (E.g. aria-label is used), moves to the first or last character covered, so that a following call to move in the same direction will move out of the replaced content, in order to ensure that the content only takes up one character stop.""" - element=self.UIAElementAtStartWithReplacedContent - if not element: - return - try: - textRange=self.obj.UIATextPattern.rangeFromChild(element) - except COMError: - return - if not back: - textRange.MoveEndpointByRange(UIAHandler.TextPatternRangeEndpoint_Start, textRange, UIAHandler.TextPatternRangeEndpoint_End) - textRange.move(UIAHandler.TextUnit_Character, -1) - else: - textRange.MoveEndpointByRange(UIAHandler.TextPatternRangeEndpoint_End, textRange, UIAHandler.TextPatternRangeEndpoint_Start) - self._rangeObj=textRange - - def _collapsedMove(self,unit,direction,skipReplacedContent): - """A simple collapsed move (i.e. both ends move together), but whether it classes replaced content as one character stop can be configured via the skipReplacedContent argument.""" - if not skipReplacedContent: - return super(EdgeTextInfo,self).move(unit,direction) - if direction==0: - return - chunk=1 if direction>0 else -1 - finalRes=0 - while finalRes!=direction: - self._moveToEdgeOfReplacedContent(back=direction<0) - res=super(EdgeTextInfo,self).move(unit,chunk) - if res==0: - break - finalRes+=res - return finalRes - - def move(self,unit,direction,endPoint=None,skipReplacedContent=True): - if not endPoint: - return self._collapsedMove(unit,direction,skipReplacedContent) - else: - tempInfo=self.copy() - res=tempInfo.move(unit,direction,skipReplacedContent=skipReplacedContent) - if res!=0: - self.setEndPoint(tempInfo,"endToEnd" if endPoint=="end" else "startToStart") - return res - - def _getControlFieldForObject(self,obj,isEmbedded=False,startOfNode=False,endOfNode=False): - field=super(EdgeTextInfo,self)._getControlFieldForObject(obj,isEmbedded=isEmbedded,startOfNode=startOfNode,endOfNode=endOfNode) - field['embedded']=isEmbedded - role=field.get('role') - # Fields should be treated as block for certain roles. - # This can affect whether the field is presented as a container (e.g. announcing entering and exiting) - if role in ( - controlTypes.ROLE_GROUPING, - controlTypes.ROLE_SECTION, - controlTypes.ROLE_PARAGRAPH, - controlTypes.ROLE_ARTICLE, - controlTypes.ROLE_LANDMARK, - controlTypes.ROLE_REGION, - ): - field['isBlock']=True - ariaProperties = splitUIAElementAttribs( - obj._getUIACacheablePropertyValue(UIAHandler.UIA_AriaPropertiesPropertyId) - ) - # ARIA roledescription and landmarks - field['roleText'] = ariaProperties.get('roledescription') - # provide landmarks - field['landmark']=obj.landmark - # Combo boxes with a text pattern are editable - if obj.role==controlTypes.ROLE_COMBOBOX and obj.UIATextPattern: - field['states'].add(controlTypes.STATE_EDITABLE) - # report if the field is 'current' - field['current']=obj.isCurrent - if obj.placeholder and obj._isTextEmpty: - field['placeholder']=obj.placeholder - # For certain controls, if ARIA overrides the label, then force the field's content (value) to the label - # Later processing in Edge's getTextWithFields will remove descendant content from fields with a content attribute. - hasAriaLabel = 'label' in ariaProperties - hasAriaLabelledby = 'labelledby' in ariaProperties - if field.get('nameIsContent'): - content="" - field.pop('name',None) - if hasAriaLabel or hasAriaLabelledby: - content=obj.name - if not content: - text=self.obj.makeTextInfo(obj).text - if not text or text.isspace(): - content=obj.name or field.pop('description',None) - if content: - field['content']=content - elif isEmbedded: - field['content']=obj.value - if field['role']==controlTypes.ROLE_GROUPING: - field['role']=controlTypes.ROLE_EMBEDDEDOBJECT - if not obj.value: - field['content']=obj.name - elif hasAriaLabel or hasAriaLabelledby: - field['alwaysReportName'] = True - # Give lists an item count - if obj.role==controlTypes.ROLE_LIST: - child=UIAHandler.handler.clientObject.ControlViewWalker.GetFirstChildElement(obj.UIAElement) - if child: - field['_childcontrolcount']=child.getCurrentPropertyValue(UIAHandler.UIA_SizeOfSetPropertyId) - return field - - def getTextWithFields(self,formatConfig=None): - # We don't want fields for collapsed ranges. - # This would normally be a general rule, but MS Word currently needs fields for collapsed ranges, thus this code is not in the base. - if self.isCollapsed: - return [] - fields=super(EdgeTextInfo,self).getTextWithFields(formatConfig) - seenText=False - curStarts=[] - # remove clickable state on descendants of controls with clickable state - clickableField=None - for field in fields: - if isinstance(field,textInfos.FieldCommand) and field.command=="controlStart": - states=field.field['states'] - if clickableField: - states.discard(controlTypes.STATE_CLICKABLE) - elif controlTypes.STATE_CLICKABLE in states: - clickableField=field.field - elif clickableField and isinstance(field,textInfos.FieldCommand) and field.command=="controlEnd" and field.field is clickableField: - clickableField=None - # Chop extra whitespace off the end incorrectly put there by Edge - numFields=len(fields) - index=0 - while index1 and isinstance(field,str) and field.isspace(): - prevField=fields[index-2] - if isinstance(prevField,textInfos.FieldCommand) and prevField.command=="controlEnd": - del fields[index-1:index+1] - index+=1 - # chop fields off the end incorrectly placed there by Edge - # This can happen if expanding to line covers element start chars at its end - startCount=0 - lastStartIndex=None - numFields=len(fields) - for index in range(numFields-1,-1,-1): - field=fields[index] - if isinstance(field,str): - break - elif isinstance(field,textInfos.FieldCommand) and field.command=="controlStart" and not field.field.get('embedded'): - startCount+=1 - lastStartIndex=index - if lastStartIndex: - del fields[lastStartIndex:lastStartIndex+(startCount*2)] - # Remove any content from fields with a content attribute - numFields=len(fields) - curField=None - for index in range(numFields-1,-1,-1): - field=fields[index] - if not curField and isinstance(field,textInfos.FieldCommand) and field.command=="controlEnd" and field.field.get('content'): - curField=field.field - endIndex=index - elif curField and isinstance(field,textInfos.FieldCommand) and field.command=="controlStart" and field.field is curField: - fields[index+1:endIndex]=" " - curField=None - return fields - -class EdgeTextInfo_preGapRemoval(EdgeTextInfo): - - def _hasEmbedded(self): - """Is this textInfo positioned on an embedded child?""" - children=self._rangeObj.getChildren() - if children.length: - child=children.getElement(0) - if not child.getCurrentPropertyValue(UIAHandler.UIA_IsTextPatternAvailablePropertyId): - childRange=self.obj.UIATextPattern.rangeFromChild(child) - if childRange: - childChildren=childRange.getChildren() - if childChildren.length==1 and UIAHandler.handler.clientObject.compareElements(child,childChildren.getElement(0)): - return True - return False - - def move(self,unit,direction,endPoint=None,skipReplacedContent=True): - # Skip over non-text element starts and ends - if not endPoint: - if direction>0 and unit in (textInfos.UNIT_LINE,textInfos.UNIT_PARAGRAPH): - return self._collapsedMove(unit,direction,skipReplacedContent) - elif direction>0: - res=self._collapsedMove(unit,direction,skipReplacedContent) - if res!=0: - # Ensure we move past the start of any elements - tempInfo=self.copy() - while super(EdgeTextInfo,tempInfo).move(textInfos.UNIT_CHARACTER,1)!=0: - tempInfo.setEndPoint(self,"startToStart") - if tempInfo.text or tempInfo._hasEmbedded(): - break - tempInfo.collapse(True) - self._rangeObj=tempInfo._rangeObj.clone() - return res - elif direction<0: - tempInfo=self.copy() - res=self._collapsedMove(unit,direction,skipReplacedContent) - if res!=0: - while True: - tempInfo.setEndPoint(self,"startToStart") - if tempInfo.text or tempInfo._hasEmbedded(): - break - if super(EdgeTextInfo,self).move(textInfos.UNIT_CHARACTER,-1)==0: - break - return res - else: - tempInfo=self.copy() - res=tempInfo.move(unit,direction,skipReplacedContent=skipReplacedContent) - if res!=0: - self.setEndPoint(tempInfo,"endToEnd" if endPoint=="end" else "startToStart") - return res - - def expand(self,unit): - # Ensure expanding to character/word correctly covers embedded controls - if unit in (textInfos.UNIT_CHARACTER,textInfos.UNIT_WORD): - tempInfo=self.copy() - tempInfo.move(textInfos.UNIT_CHARACTER,1,endPoint="end",skipReplacedContent=False) - if tempInfo._hasEmbedded(): - self.setEndPoint(tempInfo,"endToEnd") - return - super(EdgeTextInfo,self).expand(unit) - return - - def _getTextWithFieldsForUIARange(self,rootElement,textRange,formatConfig,includeRoot=True,recurseChildren=True,alwaysWalkAncestors=True,_rootElementClipped=(True,True)): - # Edge zooms into its children at the start. - # Thus you are already in the deepest first child. - # Therefore get the deepest enclosing element at the start, get its content, Then do the whole thing again on the content from the end of the enclosing element to the end of its parent, and repete! - # In other words, get the content while slowly zooming out from the start. - log.debug("_getTextWithFieldsForUIARange (unbalanced)") - if not recurseChildren: - log.debug("recurseChildren is False. Falling back to super") - for field in super(EdgeTextInfo,self)._getTextWithFieldsForUIARange(rootElement,textRange,formatConfig,includeRoot=includeRoot,alwaysWalkAncestors=True,recurseChildren=False,_rootElementClipped=_rootElementClipped): - yield field - return - if log.isEnabledFor(log.DEBUG): - log.debug("rootElement: %s"%rootElement.currentLocalizedControlType) - log.debug("full text: %s"%textRange.getText(-1)) - log.debug("includeRoot: %s"%includeRoot) - startRange=textRange.clone() - startRange.MoveEndpointByRange(UIAHandler.TextPatternRangeEndpoint_End,startRange,UIAHandler.TextPatternRangeEndpoint_Start) - enclosingElement=getEnclosingElementWithCacheFromUIATextRange(startRange,self._controlFieldUIACacheRequest) - if not enclosingElement: - log.debug("No enclosingElement. Returning") - return - enclosingRange=self.obj.getNormalizedUIATextRangeFromElement(enclosingElement) - if not enclosingRange: - log.debug("enclosingRange is NULL. Returning") - return - if log.isEnabledFor(log.DEBUG): - log.debug("enclosingElement: %s"%enclosingElement.currentLocalizedControlType) - startRange.MoveEndpointByRange(UIAHandler.TextPatternRangeEndpoint_End,enclosingRange,UIAHandler.TextPatternRangeEndpoint_End) - if startRange.CompareEndpoints(UIAHandler.TextPatternRangeEndpoint_End,textRange,UIAHandler.TextPatternRangeEndpoint_End)>0: - startRange.MoveEndpointByRange(UIAHandler.TextPatternRangeEndpoint_End,textRange,UIAHandler.TextPatternRangeEndpoint_End) - # Ensure we don't now have a collapsed range - if startRange.CompareEndpoints(UIAHandler.TextPatternRangeEndpoint_End,startRange,UIAHandler.TextPatternRangeEndpoint_Start)<=0: - log.debug("Collapsed range. Returning") - return - # check for an embedded child - childElements=getChildrenWithCacheFromUIATextRange(startRange,self._controlFieldUIACacheRequest) - if childElements.length==1 and UIAHandler.handler.clientObject.compareElements(rootElement,childElements.getElement(0)): - log.debug("Using single embedded child as enclosingElement") - for field in super(EdgeTextInfo,self)._getTextWithFieldsForUIARange(rootElement,startRange,formatConfig,_rootElementClipped=_rootElementClipped,includeRoot=includeRoot,alwaysWalkAncestors=False,recurseChildren=False): - yield field - return - parents=[] - parentElement=enclosingElement - log.debug("Generating ancestors:") - hasAncestors=False - while parentElement: - if log.isEnabledFor(log.DEBUG): - log.debug("parentElement: %s"%parentElement.currentLocalizedControlType) - isRoot=UIAHandler.handler.clientObject.compareElements(parentElement,rootElement) - log.debug("isRoot: %s"%isRoot) - if not isRoot: - hasAncestors=True - if parentElement is not enclosingElement: - if includeRoot or not isRoot: - try: - obj=UIA(windowHandle=self.obj.windowHandle,UIAElement=parentElement,initialUIACachedPropertyIDs=self._controlFieldUIACachedPropertyIDs) - field=self._getControlFieldForObject(obj) - except LookupError: - log.debug("Failed to fetch controlField data for parentElement. Breaking") - break - parents.append((parentElement,field)) - else: - # This is the root but it was not requested for inclusion - # However we still need the root element itself for further recursion - parents.append((parentElement,None)) - if isRoot: - log.debug("Hit root. Breaking") - break - log.debug("Fetching next parentElement") - parentElement=UIAHandler.handler.baseTreeWalker.getParentElementBuildCache(parentElement,self._controlFieldUIACacheRequest) - log.debug("Done generating parents") - log.debug("Yielding parents in reverse order") - for parentElement,field in reversed(parents): - if field: yield textInfos.FieldCommand("controlStart",field) - log.debug("Done yielding parents") - log.debug("Yielding balanced fields for startRange") - clippedStart=enclosingRange.CompareEndpoints(UIAHandler.TextPatternRangeEndpoint_Start,startRange,UIAHandler.TextPatternRangeEndpoint_Start)<0 - clippedEnd=enclosingRange.CompareEndpoints(UIAHandler.TextPatternRangeEndpoint_End,startRange,UIAHandler.TextPatternRangeEndpoint_End)>0 - for field in super(EdgeTextInfo,self)._getTextWithFieldsForUIARange(enclosingElement,startRange,formatConfig,_rootElementClipped=(clippedStart,clippedEnd),includeRoot=includeRoot or hasAncestors,alwaysWalkAncestors=False,recurseChildren=True): - yield field - tempRange=startRange.clone() - log.debug("Walking parents to yield controlEnds and recurse unbalanced endRanges") - for parentElement,field in parents: - if log.isEnabledFor(log.DEBUG): - log.debug("parentElement: %s"%parentElement.currentLocalizedControlType) - tempRange.MoveEndpointByRange(UIAHandler.TextPatternRangeEndpoint_Start,tempRange,UIAHandler.TextPatternRangeEndpoint_End) - parentRange=self.obj.getNormalizedUIATextRangeFromElement(parentElement) - if parentRange: - tempRange.MoveEndpointByRange(UIAHandler.TextPatternRangeEndpoint_End,parentRange,UIAHandler.TextPatternRangeEndpoint_End) - if tempRange.CompareEndpoints(UIAHandler.TextPatternRangeEndpoint_End,textRange,UIAHandler.TextPatternRangeEndpoint_End)>0: - tempRange.MoveEndpointByRange(UIAHandler.TextPatternRangeEndpoint_End,textRange,UIAHandler.TextPatternRangeEndpoint_End) - clippedEnd=True - else: - clippedEnd=False - if field: - clippedStart=parentRange.CompareEndpoints(UIAHandler.TextPatternRangeEndpoint_Start,textRange,UIAHandler.TextPatternRangeEndpoint_Start)<0 - field['_startOfNode']=not clippedStart - field['_endOfNode']=not clippedEnd - if tempRange.CompareEndpoints(UIAHandler.TextPatternRangeEndpoint_End,tempRange,UIAHandler.TextPatternRangeEndpoint_Start)>0: - log.debug("Recursing endRange") - for endField in self._getTextWithFieldsForUIARange(parentElement,tempRange,formatConfig,_rootElementClipped=(clippedStart,clippedEnd),includeRoot=False,alwaysWalkAncestors=True,recurseChildren=True): - yield endField - log.debug("Done recursing endRange") - else: - log.debug("No content after parent") - if field: - log.debug("Yielding controlEnd for parent") - yield textInfos.FieldCommand("controlEnd",field) - log.debug("Done walking parents to yield controlEnds and recurse unbalanced endRanges") - log.debug("_getTextWithFieldsForUIARange (unbalanced) end") - -class EdgeNode(UIA): - - _edgeIsPreGapRemoval=winVersion.winVersion.build<15048 - - _TextInfo=EdgeTextInfo_preGapRemoval if _edgeIsPreGapRemoval else EdgeTextInfo - - def getNormalizedUIATextRangeFromElement(self,UIAElement): - textRange = super().getNormalizedUIATextRangeFromElement(UIAElement) - if not textRange or not self._edgeIsPreGapRemoval: - return textRange - #Move the start of a UIA text range past any element start character stops - lastCharInfo = EdgeTextInfo_preGapRemoval(self,None, _rangeObj=textRange) - lastCharInfo._rangeObj = textRange - charInfo = lastCharInfo.copy() - charInfo.collapse() - while super(EdgeTextInfo,charInfo).move(textInfos.UNIT_CHARACTER,1)!=0: - charInfo.setEndPoint(lastCharInfo,"startToStart") - if charInfo.text or charInfo._hasEmbedded(): - break - lastCharInfo.setEndPoint(charInfo,"startToEnd") - charInfo.collapse(True) - return textRange - - def _get_role(self): - role=super(EdgeNode,self).role - if not isinstance(self,EdgeHTMLRoot) and role==controlTypes.ROLE_PANE and self.UIATextPattern: - return controlTypes.ROLE_INTERNALFRAME - ariaRole=self._getUIACacheablePropertyValue(UIAHandler.UIA_AriaRolePropertyId).lower() - # #7333: It is valid to provide multiple, space separated aria roles in HTML - # The role used is the first role in the list that has an associated NVDA role in aria.ariaRolesToNVDARoles - for ariaRole in ariaRole.split(): - newRole=aria.ariaRolesToNVDARoles.get(ariaRole) - if newRole: - role=newRole - break - return role - - def _get_states(self): - states=super(EdgeNode,self).states - if self.role in (controlTypes.ROLE_STATICTEXT,controlTypes.ROLE_GROUPING,controlTypes.ROLE_SECTION,controlTypes.ROLE_GRAPHIC) and self.UIAInvokePattern: - states.add(controlTypes.STATE_CLICKABLE) - return states - - def _get_ariaProperties(self): - return splitUIAElementAttribs(self.UIAElement.currentAriaProperties) - - # RegEx to get the value for the aria-current property. This will be looking for a the value of 'current' - # in a list of strings like "something=true;current=date;". We want to capture one group, after the '=' - # character and before the ';' character. - # This could be one of: "false", "true", "page", "step", "location", "date", "time" - # "false" is ignored by the regEx and will not produce a match - RE_ARIA_CURRENT_PROP_VALUE = re.compile("current=(?!false)(\w+);") - - def _get_isCurrent(self): - ariaProperties=self._getUIACacheablePropertyValue(UIAHandler.UIA_AriaPropertiesPropertyId) - match = self.RE_ARIA_CURRENT_PROP_VALUE.search(ariaProperties) - log.debug("aria props = %s" % ariaProperties) - if match: - valueOfAriaCurrent = match.group(1) - log.debug("aria current value = %s" % valueOfAriaCurrent) - return valueOfAriaCurrent - return None - - def _get_roleText(self): - roleText = self.ariaProperties.get('roledescription', None) - if roleText: - return roleText - return super().roleText - - def _get_placeholder(self): - ariaPlaceholder = self.ariaProperties.get('placeholder', None) - return ariaPlaceholder - - def _get__isTextEmpty(self): - # NOTE: we can not check the result of the EdgeTextInfo move implementation to determine if we added - # any characters to the range, since it seems to return 1 even when the text property has not changed. - # Also we can not move (repeatedly by one character) since this can overrun the end of the field in edge. - # So instead, we use self to make a text info (which should have the right range) and then use the UIA - # specific _rangeObj.getText function to get a subset of the full range of characters. - ti = self.makeTextInfo(self) - if ti.isCollapsed: - # it is collapsed therefore it is empty. - # exit early so we do not have to do not have to fetch `ti.text` which - # is potentially costly to performance. - return True - numberOfCharacters = 2 - text = ti._rangeObj.getText(numberOfCharacters) - # Edge can report newline for empty fields: - if text == "\n": - return True - return False - - def _get_landmark(self): - landmarkId=self._getUIACacheablePropertyValue(UIAHandler.UIA_LandmarkTypePropertyId) - if not landmarkId: # will be 0 for non-landmarks - return None - landmarkRole = UIAHandler.UIALandmarkTypeIdsToLandmarkNames.get(landmarkId) - if landmarkRole: - return landmarkRole - ariaRoles=self._getUIACacheablePropertyValue(UIAHandler.UIA_AriaRolePropertyId).lower() - # #7333: It is valid to provide multiple, space separated aria roles in HTML - # If multiple roles or even multiple landmark roles are provided, the first one is used - ariaRole = ariaRoles.split(" ")[0] - if ariaRole in aria.landmarkRoles and (ariaRole != 'region' or self.name): - return ariaRole - return None - - -class EdgeList(EdgeNode): - - # non-focusable lists are readonly lists (ensures correct NVDA presentation category) - def _get_states(self): - states=super(EdgeList,self).states - if controlTypes.STATE_FOCUSABLE not in states: - states.add(controlTypes.STATE_READONLY) - return states - -class EdgeHTMLRootContainer(EdgeNode): - - def event_gainFocus(self): - firstChild=self.firstChild - if isinstance(firstChild,UIA): - eventHandler.executeEvent("gainFocus",firstChild) - return - return super(EdgeHTMLRootContainer,self).event_gainFocus() - -class EdgeHeadingQuickNavItem(UIATextRangeQuickNavItem): - - @property - def level(self): - if not hasattr(self,'_level'): - styleVal=getUIATextAttributeValueFromRange(self.textInfo._rangeObj,UIAHandler.UIA_StyleIdAttributeId) - self._level=styleVal-(UIAHandler.StyleId_Heading1-1) if UIAHandler.StyleId_Heading1<=styleVal<=UIAHandler.StyleId_Heading6 else None - return self._level - - def isChild(self,parent): - return self.level>parent.level - -def EdgeHeadingQuicknavIterator(itemType,document,position,direction="next"): - """ - A helper for L{EdgeHTMLTreeInterceptor._iterNodesByType} that specifically yields L{EdgeHeadingQuickNavItem} objects found in the given document, starting the search from the given position, searching in the given direction. - See L{browseMode._iterNodesByType} for details on these specific arguments. - """ - # Edge exposes all headings as UIA elements with a controlType of text, and a level. Thus we can quickly search for these. - # However, sometimes when ARIA is used, the level on the element may not match the level in the text attributes. - # Therefore we need to search for all levels 1 through 6, even if a specific level is specified. - # Though this is still much faster than searching text attributes alone - # #9078: this must be wrapped inside a list, as Python 3 will treat this as iteration. - levels=list(range(1,7)) - condition=createUIAMultiPropertyCondition({UIAHandler.UIA_ControlTypePropertyId:UIAHandler.UIA_TextControlTypeId,UIAHandler.UIA_LevelPropertyId:levels}) - levelString=itemType[7:] - for item in UIAControlQuicknavIterator(itemType,document,position,condition,direction=direction,itemClass=EdgeHeadingQuickNavItem): - # Verify this is the correct heading level via text attributes - if item.level and (not levelString or levelString==str(item.level)): - yield item - -class EdgeHTMLTreeInterceptor(cursorManager.ReviewCursorManager,UIABrowseModeDocument): - - TextInfo=UIABrowseModeDocumentTextInfo - - def _get_documentConstantIdentifier(self): - return self.rootNVDAObject.parent.name - - def _iterNodesByType(self,nodeType,direction="next",pos=None): - if nodeType.startswith("heading"): - return EdgeHeadingQuicknavIterator(nodeType,self,pos,direction=direction) - else: - return super(EdgeHTMLTreeInterceptor,self)._iterNodesByType(nodeType,direction=direction,pos=pos) - - def shouldPassThrough(self,obj,reason=None): - # Enter focus mode for selectable list items ( and role=listbox) + if ( + reason == controlTypes.OutputReason.FOCUS + and obj.role == controlTypes.ROLE_LISTITEM + and controlTypes.STATE_SELECTABLE in obj.states + ): + return True + return super().shouldPassThrough(obj, reason=reason) + + def _iterNodesByType(self, nodeType, direction="next", pos=None): + if nodeType.startswith("heading"): + return HeadingControlQuicknavIterator(nodeType, self, pos, direction=direction) + else: + return super()._iterNodesByType(nodeType, direction=direction, pos=pos) diff --git a/source/NVDAObjects/UIA/winConsoleUIA.py b/source/NVDAObjects/UIA/winConsoleUIA.py index 03513bf48f1..c0d847b0bd3 100644 --- a/source/NVDAObjects/UIA/winConsoleUIA.py +++ b/source/NVDAObjects/UIA/winConsoleUIA.py @@ -1,4 +1,3 @@ -# NVDAObjects/UIA/winConsoleUIA.py # A part of NonVisual Desktop Access (NVDA) # This file is covered by the GNU General Public License. # See the file COPYING for more details. @@ -363,20 +362,6 @@ def _get_TextInfo(self): movement.""" return consoleUIATextInfo if self.is21H1Plus else consoleUIATextInfoPre21H1 - def _getTextLines(self): - if self.is21H1Plus: - # #11760: the 21H1 UIA console wraps across lines. - # When text wraps, NVDA starts reading from the beginning of the visible text for every new line of output. - # Use the superclass _getTextLines instead. - return super()._getTextLines() - # This override of _getTextLines takes advantage of the fact that - # the console text contains linefeeds for every line - # Thus a simple string splitlines is much faster than splitting by unit line. - ti = self.makeTextInfo(textInfos.POSITION_ALL) - text = ti.text or "" - return text.splitlines() - - def detectPossibleSelectionChange(self): try: return super().detectPossibleSelectionChange() diff --git a/source/NVDAObjects/UIA/wordDocument.py b/source/NVDAObjects/UIA/wordDocument.py index 08bf414210e..bccd3d03783 100644 --- a/source/NVDAObjects/UIA/wordDocument.py +++ b/source/NVDAObjects/UIA/wordDocument.py @@ -1,7 +1,7 @@ -# A part of NonVisual Desktop Access (NVDA) # This file is covered by the GNU General Public License. +# A part of NonVisual Desktop Access (NVDA) # See the file COPYING for more details. -# Copyright (C) 2016-2020 NV Access Limited, Joseph Lee +# Copyright (C) 2016-2021 NV Access Limited, Joseph Lee, Jakub Lukowicz from comtypes import COMError from collections import defaultdict @@ -18,6 +18,8 @@ from UIAUtils import * from . import UIA, UIATextInfo from NVDAObjects.window.winword import WordDocument as WordDocumentBase +from scriptHandler import script + """Support for Microsoft Word via UI Automation.""" @@ -67,15 +69,40 @@ def getCommentInfoFromPosition(position): for index in range(UIAElementArray.length): UIAElement=UIAElementArray.getElement(index) UIAElement=UIAElement.buildUpdatedCache(UIAHandler.handler.baseCacheRequest) - obj=UIA(UIAElement=UIAElement) - if not obj.parent or obj.parent.name!='Comment': - continue - comment=obj.makeTextInfo(textInfos.POSITION_ALL).text - dateObj=obj.previous - date=dateObj.name - authorObj=dateObj.previous - author=authorObj.name - return dict(comment=comment,author=author,date=date) + typeID = UIAElement.GetCurrentPropertyValue(UIAHandler.UIA_AnnotationAnnotationTypeIdPropertyId) + # Use Annotation Type Comment if available + if typeID == UIAHandler.AnnotationType_Comment: + comment = UIAElement.GetCurrentPropertyValue(UIAHandler.UIA_NamePropertyId) + author = UIAElement.GetCurrentPropertyValue(UIAHandler.UIA_AnnotationAuthorPropertyId) + date = UIAElement.GetCurrentPropertyValue(UIAHandler.UIA_AnnotationDateTimePropertyId) + return dict(comment=comment, author=author, date=date) + else: + obj = UIA(UIAElement=UIAElement) + if ( + not obj.parent + # Because the name of this object is language sensetive check if it has UIA Annotation Pattern + or not obj.parent.UIAElement.getCurrentPropertyValue( + UIAHandler.UIA_IsAnnotationPatternAvailablePropertyId + ) + ): + continue + comment = obj.makeTextInfo(textInfos.POSITION_ALL).text + tempObj = obj.previous.previous + authorObj = tempObj or obj.previous + author = authorObj.name + if not tempObj: + return dict(comment=comment, author=author) + dateObj = obj.previous + date = dateObj.name + return dict(comment=comment, author=author, date=date) + + +def getPresentableCommentInfoFromPosition(commentInfo): + if "date" not in commentInfo: + # Translators: The message reported for a comment in Microsoft Word + return _("Comment: {comment} by {author}").format(**commentInfo) + # Translators: The message reported for a comment in Microsoft Word + return _("Comment: {comment} by {author} on {date}").format(**commentInfo) class CommentUIATextInfoQuickNavItem(TextAttribUIATextInfoQuickNavItem): attribID=UIAHandler.UIA_AnnotationTypesAttributeId @@ -84,8 +111,7 @@ class CommentUIATextInfoQuickNavItem(TextAttribUIATextInfoQuickNavItem): @property def label(self): commentInfo=getCommentInfoFromPosition(self.textInfo) - # Translators: The message reported for a comment in Microsoft Word - return _("Comment: {comment} by {author} on {date}").format(**commentInfo) + return getPresentableCommentInfoFromPosition(commentInfo) class WordDocumentTextInfo(UIATextInfo): @@ -292,7 +318,7 @@ def script_tab(self,gesture): return info=self.makeTextInfo(textInfos.POSITION_SELECTION) if not info.isCollapsed: - speech.speakTextInfo(info,reason=controlTypes.REASON_FOCUS) + speech.speakTextInfo(info, reason=controlTypes.OutputReason.FOCUS) script_shiftTab=script_tab def _iterNodesByType(self,nodeType,direction="next",pos=None): @@ -329,31 +355,17 @@ def event_UIA_notification(self, activityId=None, **kwargs): return super(WordDocument, self).event_UIA_notification(**kwargs) + @script( + gesture="kb:NVDA+alt+c", + # Translators: a description for a script that reports the comment at the caret. + description=_("Reports the text of the comment where the System caret is located.") + ) def script_reportCurrentComment(self,gesture): caretInfo=self.makeTextInfo(textInfos.POSITION_CARET) - caretInfo.expand(textInfos.UNIT_CHARACTER) - val=caretInfo._rangeObj.getAttributeValue(UIAHandler.UIA_AnnotationObjectsAttributeId) - if not val: - return - try: - UIAElementArray=val.QueryInterface(UIAHandler.IUIAutomationElementArray) - except COMError: - return - for index in range(UIAElementArray.length): - UIAElement=UIAElementArray.getElement(index) - UIAElement=UIAElement.buildUpdatedCache(UIAHandler.handler.baseCacheRequest) - obj=UIA(UIAElement=UIAElement) - if not obj.parent or obj.parent.name!='Comment': - continue - comment=obj.makeTextInfo(textInfos.POSITION_ALL).text - dateObj=obj.previous - date=dateObj.name - authorObj=dateObj.previous - author=authorObj.name - # Translators: The message reported for a comment in Microsoft Word - ui.message(_("{comment} by {author} on {date}").format(comment=comment,date=date,author=author)) - return - - __gestures={ - "kb:NVDA+alt+c":"reportCurrentComment", - } + commentInfo = getCommentInfoFromPosition(caretInfo) + if commentInfo is not None: + ui.message(getPresentableCommentInfoFromPosition(commentInfo)) + else: + # Translators: a message when there is no comment to report in Microsoft Word + ui.message(_("No comments")) + return diff --git a/source/NVDAObjects/__init__.py b/source/NVDAObjects/__init__.py index 47021689e36..56c3206b4da 100644 --- a/source/NVDAObjects/__init__.py +++ b/source/NVDAObjects/__init__.py @@ -11,6 +11,7 @@ import os import time import re +import typing import weakref from logHandler import log import review @@ -417,10 +418,12 @@ def _get_name(self): """ return "" - def _get_role(self): + #: Type definition for auto prop '_get_role' + role: int + + def _get_role(self) -> int: """The role or type of control this object represents (example: button, list, dialog). @return: a ROLE_* constant from L{controlTypes} - @rtype: int """ return controlTypes.ROLE_UNKNOWN @@ -444,15 +447,20 @@ def _get_roleTextBraille(self): return f"{braille.roleLabels[controlTypes.ROLE_LANDMARK]} {braille.landmarkLabels[self.landmark]}" return self.roleText - def _get_value(self): - """The value of this object (example: the current percentage of a scrollbar, the selected option in a combo box). - @rtype: str + #: Typing information for auto property _get_value + value: str + + def _get_value(self) -> str: + """The value of this object + (example: the current percentage of a scrollbar, the selected option in a combo box). """ return "" - def _get_description(self): + #: Typing information for auto property _get_description + description: str + + def _get_description(self) -> str: """The description or help text of this object. - @rtype: str """ return "" @@ -497,12 +505,11 @@ def _get_isInForeground(self): raise NotImplementedError # Type info for auto property: - states: set + states: typing.Set[int] - def _get_states(self): + def _get_states(self) -> typing.Set[int]: """Retrieves the current states of this object (example: selected, focused). @return: a set of STATE_* constants from L{controlTypes}. - @rtype: set of int """ return set() @@ -638,13 +645,15 @@ def _get_presentationalColumnNumber(self): """ raise NotImplementedError - def _get_cellCoordsText(self): + #: Typing information for auto-property: _get_cellCoordsText + cellCoordsText: typing.Optional[str] + + def _get_cellCoordsText(self) -> typing.Optional[str]: """ An alternative text representation of cell coordinates e.g. "a1". Will override presentation of rowNumber and columnNumber. Only implement if the representation is really different. """ return None - def _get_rowCount(self): """Retrieves the number of rows this object contains if its a table. @@ -975,12 +984,13 @@ def _get_statusBar(self): """ return None - def _get_isCurrent(self): + isCurrent: controlTypes.IsCurrent #: type info for auto property _get_isCurrent + + def _get_isCurrent(self) -> controlTypes.IsCurrent: """Gets the value that indicates whether this object is the current element in a set of related - elements. This maps to aria-current. Normally returns None. If this object is current - it will return one of the following values: "true", "page", "step", "location", "date", "time" + elements. This maps to aria-current. """ - return None + return controlTypes.IsCurrent.NO def _get_shouldAcceptShowHideCaretEvent(self): """Some objects/applications send show/hide caret events when we don't expect it, such as when the cursor is blinking. @@ -992,7 +1002,7 @@ def _get_shouldAcceptShowHideCaretEvent(self): def reportFocus(self): """Announces this object in a way suitable such that it gained focus. """ - speech.speakObject(self,reason=controlTypes.REASON_FOCUS) + speech.speakObject(self, reason=controlTypes.OutputReason.FOCUS) def _get_placeholder(self): """If it exists for this object get the value of the placeholder text. @@ -1109,7 +1119,7 @@ def event_mouseMove(self,x,y): def event_stateChange(self): if self is api.getFocusObject(): - speech.speakObjectProperties(self,states=True, reason=controlTypes.REASON_CHANGE) + speech.speakObjectProperties(self, states=True, reason=controlTypes.OutputReason.CHANGE) braille.handler.handleUpdate(self) vision.handler.handleUpdate(self, property="states") @@ -1118,7 +1128,7 @@ def event_focusEntered(self): speech.cancelSpeech() return if self.isPresentableFocusAncestor: - speech.speakObject(self,reason=controlTypes.REASON_FOCUSENTERED) + speech.speakObject(self, reason=controlTypes.OutputReason.FOCUSENTERED) def event_gainFocus(self): """ @@ -1156,19 +1166,19 @@ def event_becomeNavigatorObject(self, isFocus=False): def event_valueChange(self): if self is api.getFocusObject(): - speech.speakObjectProperties(self, value=True, reason=controlTypes.REASON_CHANGE) + speech.speakObjectProperties(self, value=True, reason=controlTypes.OutputReason.CHANGE) braille.handler.handleUpdate(self) vision.handler.handleUpdate(self, property="value") def event_nameChange(self): if self is api.getFocusObject(): - speech.speakObjectProperties(self, name=True, reason=controlTypes.REASON_CHANGE) + speech.speakObjectProperties(self, name=True, reason=controlTypes.OutputReason.CHANGE) braille.handler.handleUpdate(self) vision.handler.handleUpdate(self, property="name") def event_descriptionChange(self): if self is api.getFocusObject(): - speech.speakObjectProperties(self, description=True, reason=controlTypes.REASON_CHANGE) + speech.speakObjectProperties(self, description=True, reason=controlTypes.OutputReason.CHANGE) braille.handler.handleUpdate(self) vision.handler.handleUpdate(self, property="description") diff --git a/source/NVDAObjects/behaviors.py b/source/NVDAObjects/behaviors.py index e09d93ec28e..6d94181831b 100755 --- a/source/NVDAObjects/behaviors.py +++ b/source/NVDAObjects/behaviors.py @@ -1,5 +1,4 @@ # -*- coding: UTF-8 -*- -# NVDAObjects/behaviors.py # A part of NonVisual Desktop Access (NVDA) # This file is covered by the GNU General Public License. # See the file COPYING for more details. @@ -12,7 +11,6 @@ import os import time import threading -import difflib import tones import queueHandler import eventHandler @@ -30,6 +28,8 @@ import braille import nvwave import globalVars +from typing import List +import diffHandler class ProgressBar(NVDAObject): @@ -265,15 +265,31 @@ def event_textChange(self): """ self._event.set() - def _getTextLines(self): - """Retrieve the text of this object in lines. + def _get_diffAlgo(self): + """ + This property controls which diffing algorithm should be used by + this object. Most subclasses should simply use the base + implementation, which returns DMP (character-based diffing). + + @Note: DMP is experimental, and can be disallowed via user + preference. In this case, the prior stable implementation, Difflib + (line-based diffing), will be used. + """ + return diffHandler.get_dmp_algo() + + def _get_devInfo(self): + info = super().devInfo + info.append(f"diffing algorithm: {self.diffAlgo}") + return info + + def _getText(self) -> str: + """Retrieve the text of this object. This will be used to determine the new text to speak. The base implementation uses the L{TextInfo}. However, subclasses should override this if there is a better way to retrieve the text. - @return: The current lines of text. - @rtype: list of str """ - return list(self.makeTextInfo(textInfos.POSITION_ALL).getTextInChunks(textInfos.UNIT_LINE)) + ti = self.makeTextInfo(textInfos.POSITION_ALL) + return self.diffAlgo._getText(ti) def _reportNewLines(self, lines): """ @@ -291,10 +307,10 @@ def _reportNewText(self, line): def _monitor(self): try: - oldLines = self._getTextLines() + oldText = self._getText() except: - log.exception("Error getting initial lines") - oldLines = [] + log.exception("Error getting initial text") + oldText = "" while self._keepMonitoring: self._event.wait() @@ -309,9 +325,9 @@ def _monitor(self): self._event.clear() try: - newLines = self._getTextLines() + newText = self._getText() if config.conf["presentation"]["reportDynamicContentChanges"]: - outLines = self._calculateNewText(newLines, oldLines) + outLines = self._calculateNewText(newText, oldText) if len(outLines) == 1 and len(outLines[0].strip()) == 1: # This is only a single character, # which probably means it is just a typed character, @@ -319,61 +335,13 @@ def _monitor(self): del outLines[0] if outLines: queueHandler.queueFunction(queueHandler.eventQueue, self._reportNewLines, outLines) - oldLines = newLines + oldText = newText except: - log.exception("Error getting lines or calculating new text") - - def _calculateNewText(self, newLines, oldLines): - outLines = [] - - prevLine = None - for line in difflib.ndiff(oldLines, newLines): - if line[0] == "?": - # We're never interested in these. - continue - if line[0] != "+": - # We're only interested in new lines. - prevLine = line - continue - text = line[2:] - if not text or text.isspace(): - prevLine = line - continue - - if prevLine and prevLine[0] == "-" and len(prevLine) > 2: - # It's possible that only a few characters have changed in this line. - # If so, we want to speak just the changed section, rather than the entire line. - prevText = prevLine[2:] - textLen = len(text) - prevTextLen = len(prevText) - # Find the first character that differs between the two lines. - for pos in range(min(textLen, prevTextLen)): - if text[pos] != prevText[pos]: - start = pos - break - else: - # We haven't found a differing character so far and we've hit the end of one of the lines. - # This means that the differing text starts here. - start = pos + 1 - # Find the end of the differing text. - if textLen != prevTextLen: - # The lines are different lengths, so assume the rest of the line changed. - end = textLen - else: - for pos in range(textLen - 1, start - 1, -1): - if text[pos] != prevText[pos]: - end = pos + 1 - break - - if end - start < 15: - # Less than 15 characters have changed, so only speak the changed chunk. - text = text[start:end] + log.exception("Error getting or calculating new text") - if text and not text.isspace(): - outLines.append(text) - prevLine = line + def _calculateNewText(self, newText: str, oldText: str) -> List[str]: + return self.diffAlgo.diff(newText, oldText) - return outLines class Terminal(LiveText, EditableText): """An object which both accepts text input and outputs text which should be reported automatically. @@ -566,7 +534,7 @@ def _moveToColumn(self, obj): # Use the focused copy of the row as the parent for all cells to make comparison faster. obj.parent = self api.setNavigatorObject(obj) - speech.speakObject(obj, reason=controlTypes.REASON_FOCUS) + speech.speakObject(obj, reason=controlTypes.OutputReason.FOCUS) def _moveToColumnNumber(self, column): child = column - 1 @@ -764,7 +732,7 @@ class ToolTip(NVDAObject): def event_show(self): if not config.conf["presentation"]["reportTooltips"]: return - speech.speakObject(self, reason=controlTypes.REASON_FOCUS) + speech.speakObject(self, reason=controlTypes.OutputReason.FOCUS) # Ideally, we wouldn't use getPropertiesBraille directly. braille.handler.message(braille.getPropertiesBraille(name=self.name, role=self.role)) @@ -777,7 +745,7 @@ class Notification(NVDAObject): def event_alert(self): if not config.conf["presentation"]["reportHelpBalloons"]: return - speech.speakObject(self, reason=controlTypes.REASON_FOCUS) + speech.speakObject(self, reason=controlTypes.OutputReason.FOCUS) # Ideally, we wouldn't use getPropertiesBraille directly. braille.handler.message(braille.getPropertiesBraille(name=self.name, role=self.role)) diff --git a/source/NVDAObjects/window/__init__.py b/source/NVDAObjects/window/__init__.py index 9e73257a1f7..0149372ae22 100644 --- a/source/NVDAObjects/window/__init__.py +++ b/source/NVDAObjects/window/__init__.py @@ -1,8 +1,7 @@ -#NVDAObjects/window.py -#A part of NonVisual Desktop Access (NVDA) -#Copyright (C) 2006-2019 NV Access Limited, Babbage B.V., Bill Dengler -#This file is covered by the GNU General Public License. -#See the file COPYING for more details. +# A part of NonVisual Desktop Access (NVDA) +# Copyright (C) 2006-2020 NV Access Limited, Babbage B.V., Bill Dengler +# This file is covered by the GNU General Public License. +# See the file COPYING for more details. import re import ctypes @@ -136,7 +135,7 @@ def findOverlayClasses(self,clsList): if not any(issubclass(cls,EditableText) for cls in clsList): gi=winUser.getGUIThreadInfo(self.windowThreadID) if gi.hwndCaret==self.windowHandle and gi.flags&winUser.GUI_CARETBLINKING: - if self.windowTextLineCount: + if self.windowTextLineCount and self.windowText: from .edit import UnidentifiedEdit clsList.append(UnidentifiedEdit) else: diff --git a/source/NVDAObjects/window/edit.py b/source/NVDAObjects/window/edit.py index de855a54d3a..437007c91b1 100644 --- a/source/NVDAObjects/window/edit.py +++ b/source/NVDAObjects/window/edit.py @@ -24,7 +24,6 @@ import textInfos.offsets from keyboardHandler import KeyboardInputGesture from scriptHandler import isScriptWaiting -import IAccessibleHandler import controlTypes from . import Window from .. import NVDAObjectTextInfo @@ -508,6 +507,9 @@ def _getFormatFieldAtRange(self, textRange, formatConfig): formatField["italic"]=bool(fontObj.italic) formatField["underline"]=bool(fontObj.underline) formatField["strikethrough"]=bool(fontObj.StrikeThrough) + if formatConfig["reportSuperscriptsAndSubscripts"]: + if not fontObj: + fontObj = textRange.font if fontObj.superscript: formatField["text-position"]="super" elif fontObj.subscript: diff --git a/source/NVDAObjects/window/excel.py b/source/NVDAObjects/window/excel.py index af7f98d335d..63211ea56f0 100755 --- a/source/NVDAObjects/window/excel.py +++ b/source/NVDAObjects/window/excel.py @@ -994,6 +994,12 @@ def _getFormatFieldAndOffsets(self,offset,formatConfig,calculateOffsets=True): formatField['italic']=fontObj.italic underline=fontObj.underline formatField['underline']=False if underline is None or underline==xlUnderlineStyleNone else True + formatField['strikethrough'] = fontObj.strikethrough + if formatConfig['reportSuperscriptsAndSubscripts']: + if fontObj.superscript: + formatField['text-position'] = 'super' + elif fontObj.subscript: + formatField['text-position'] = 'sub' if formatConfig['reportStyle']: try: styleName=self.obj.excelCellObject.style.nameLocal @@ -1471,7 +1477,7 @@ def reportFocus(self): formatField.update(field.field) if not hasattr(self.parent,'_formatFieldSpeechCache'): self.parent._formatFieldSpeechCache = textInfos.Field() - if formatField: + if formatField or self.parent._formatFieldSpeechCache: sequence = speech.getFormatFieldSpeech( formatField, attrsCache=self.parent._formatFieldSpeechCache, diff --git a/source/NVDAObjects/window/scintilla.py b/source/NVDAObjects/window/scintilla.py index de90d6e3540..b6829c5014b 100755 --- a/source/NVDAObjects/window/scintilla.py +++ b/source/NVDAObjects/window/scintilla.py @@ -5,7 +5,6 @@ # See the file COPYING for more details. import ctypes -import IAccessibleHandler import speech import textInfos.offsets import winKernel diff --git a/source/NVDAObjects/window/winConsole.py b/source/NVDAObjects/window/winConsole.py index 96d3d93155b..01ecec4e75e 100644 --- a/source/NVDAObjects/window/winConsole.py +++ b/source/NVDAObjects/window/winConsole.py @@ -1,8 +1,7 @@ -#NVDAObjects/WinConsole.py -#A part of NonVisual Desktop Access (NVDA) -#This file is covered by the GNU General Public License. -#See the file COPYING for more details. -#Copyright (C) 2007-2019 NV Access Limited, Bill Dengler +# A part of NonVisual Desktop Access (NVDA) +# This file is covered by the GNU General Public License. +# See the file COPYING for more details. +# Copyright (C) 2007-2020 NV Access Limited, Bill Dengler import winConsoleHandler from . import Window @@ -14,18 +13,12 @@ class WinConsole(Terminal, EditableTextWithoutAutoSelectDetection, Window): """ - NVDA's legacy Windows Console support. + Base class for NVDA's legacy Windows Console support. This is used in situations where UIA isn't available. Please consider using NVDAObjects.UIA.winConsoleUIA instead. """ STABILIZE_DELAY = 0.03 - def initOverlayClass(self): - # Legacy consoles take quite a while to send textChange events. - # This significantly impacts typing performance, so don't queue chars. - if isinstance(self, KeyboardHandlerBasedTypedCharSupport): - self._supportsTextChange = False - def _get_windowThreadID(self): # #10113: Windows forces the thread of console windows to match the thread of the first attached process. # However, To correctly handle speaking of typed characters, @@ -69,8 +62,8 @@ def event_loseFocus(self): def event_nameChange(self): pass - def _getTextLines(self): - return winConsoleHandler.getConsoleVisibleLines() + def _getText(self): + return '\n'.join(winConsoleHandler.getConsoleVisibleLines()) def script_caret_backspaceCharacter(self, gesture): super(WinConsole, self).script_caret_backspaceCharacter(gesture) diff --git a/source/NVDAObjects/window/winword.py b/source/NVDAObjects/window/winword.py index 98f05c76c74..2d83596adba 100755 --- a/source/NVDAObjects/window/winword.py +++ b/source/NVDAObjects/window/winword.py @@ -672,7 +672,7 @@ def activate(self): self.updateCaret() tiCopy = self.copy() tiCopy.expand(textInfos.UNIT_LINE) - speech.speakTextInfo(tiCopy,reason=controlTypes.REASON_FOCUS) + speech.speakTextInfo(tiCopy, reason=controlTypes.OutputReason.FOCUS) braille.handler.handleCaretMove(self) return @@ -1402,6 +1402,9 @@ def script_tab(self,gesture): * If not in a table, announces the distance of the caret from the left edge of the document, and any remaining text on that line. """ gesture.send() + self.reportTab() + + def reportTab(self): selectionObj=self.WinwordSelectionObject inTable=selectionObj.tables.count>0 if selectionObj else False info=self.makeTextInfo(textInfos.POSITION_SELECTION) @@ -1410,7 +1413,7 @@ def script_tab(self,gesture): info.expand(textInfos.UNIT_PARAGRAPH) isCollapsed=info.isCollapsed if not isCollapsed: - speech.speakTextInfo(info,reason=controlTypes.REASON_FOCUS) + speech.speakTextInfo(info, reason=controlTypes.OutputReason.FOCUS) braille.handler.handleCaretMove(self) if selectionObj and isCollapsed: offset=selectionObj.information(wdHorizontalPositionRelativeToPage) @@ -1418,7 +1421,7 @@ def script_tab(self,gesture): ui.message(msg) if selectionObj.paragraphs[1].range.start==selectionObj.start: info.expand(textInfos.UNIT_LINE) - speech.speakTextInfo(info,unit=textInfos.UNIT_LINE,reason=controlTypes.REASON_CARET) + speech.speakTextInfo(info, unit=textInfos.UNIT_LINE, reason=controlTypes.OutputReason.CARET) def getLocalizedMeasurementTextForPointSize(self,offset): options=self.WinwordApplicationObject.options diff --git a/source/UIABrowseMode.py b/source/UIABrowseMode.py index 7354f2e7a23..275aae674af 100644 --- a/source/UIABrowseMode.py +++ b/source/UIABrowseMode.py @@ -360,6 +360,14 @@ class UIABrowseModeDocument(UIADocumentWithTableNavigation,browseMode.BrowseMode # Because UIA TextRanges are opaque and are tied specifically to one particular document. shouldRememberCaretPositionAcrossLoads=False + def event_UIA_activeTextPositionChanged(self, obj, nextHandler, textRange=None): + if not self.isReady: + self._initialScrollObj = obj + return nextHandler() + scrollInfo = self.makeTextInfo(textRange) + if not self._handleScrollTo(scrollInfo): + return nextHandler() + def _iterNodesByType(self,nodeType,direction="next",pos=None): if nodeType.startswith("heading"): return UIAHeadingQuicknavIterator(nodeType,self,pos,direction=direction) diff --git a/source/UIAHandler.py b/source/UIAHandler.py index 528af0a81c0..a866cd4f195 100644 --- a/source/UIAHandler.py +++ b/source/UIAHandler.py @@ -1,20 +1,21 @@ -#UIAHandler.py -#A part of NonVisual Desktop Access (NVDA) -#Copyright (C) 2008-2018 NV Access Limited, Joseph Lee -#This file is covered by the GNU General Public License. -#See the file COPYING for more details. +# A part of NonVisual Desktop Access (NVDA) +# Copyright (C) 2008-2018 NV Access Limited, Joseph Lee +# This file is covered by the GNU General Public License. +# See the file COPYING for more details. +from typing import Optional from comtypes import COMError import config from logHandler import log # Maintain backwards compatibility: F403 (unable to detect undefined names) flake8 warning. from _UIAHandler import * # noqa: F403 +from _UIAHandler import UIAHandler # Make the _UIAHandler._isDebug function available to this module, # ignoring the fact that it is not used here directly. from _UIAHandler import _isDebug # noqa: F401 -handler=None +handler: Optional[UIAHandler] = None def initialize(): global handler diff --git a/source/_UIAConstants.py b/source/_UIAConstants.py new file mode 100644 index 00000000000..f458e8b33bb --- /dev/null +++ b/source/_UIAConstants.py @@ -0,0 +1,62 @@ +# A part of NonVisual Desktop Access (NVDA) +# Copyright (C) 2021 NV Access Limited +# This file is covered by the GNU General Public License. +# See the file COPYING for more details. + +import enum + + +class FillType(enum.IntEnum): + NONE = 0 + COLOR = 1 + GRADIENT = 2 + PICTURE = 3 + PATTERN = 4 + + +FillTypeLabels = { + # Translators: a style of fill type (to color the inside of a control or text) + FillType.NONE: pgettext("UIAHandler.FillType", "none"), + # Translators: a style of fill type (to color the inside of a control or text) + FillType.COLOR: pgettext("UIAHandler.FillType", "color"), + # Translators: a style of fill type (to color the inside of a control or text) + FillType.GRADIENT: pgettext("UIAHandler.FillType", "gradient"), + # Translators: a style of fill type (to color the inside of a control or text) + FillType.PICTURE: pgettext("UIAHandler.FillType", "picture"), + # Translators: a style of fill type (to color the inside of a control or text) + FillType.PATTERN: pgettext("UIAHandler.FillType", "pattern"), +} + + +# Some newer UIA constants that could be missing +class UIAutomationType(enum.IntEnum): + INT = 1 + BOOL = 2 + STRING = 3 + DOUBLE = 4 + POINT = 5 + RECT = 6 + ELEMENT = 7 + ARRAY = 8 + OUT = 9 + INT_ARRAY = 10 + BOOL_ARRAY = 11 + STRING_ARRAY = 12 + DOUBLE_ARRAY = 13 + POINT_ARRAY = 14 + RECT_ARRAY = 15 + ELEMENT_ARRAY = 16 + OUT_INT = 17 + OUT_BOOL = 18 + OUT_STRING = 19 + OUT_DOUBLE = 20 + OUT_POINT = 21 + OUT_RECT = 22 + OUT_ELEMENT = 23 + OUT_INT_ARRAY = 24 + OUT_BOOL_ARRAY = 25 + OUT_STRING_ARRAY = 26 + OUT_DOUBLE_ARRAY = 27 + OUT_POINT_ARRAY = 28 + OUT_RECT_ARRAY = 29 + OUT_ELEMENT_ARRAY = 30 diff --git a/source/_UIACustomProps.py b/source/_UIACustomProps.py new file mode 100644 index 00000000000..354c9bf91e2 --- /dev/null +++ b/source/_UIACustomProps.py @@ -0,0 +1,90 @@ +# A part of NonVisual Desktop Access (NVDA) +# Copyright (C) 2021 NV Access Limited +# This file is covered by the GNU General Public License. +# See the file COPYING for more details. + +from dataclasses import ( + dataclass, + field, +) +from typing import Optional + +from comtypes import ( + GUID, + byref, +) +from _UIAConstants import ( + UIAutomationType, +) + +""" +This module provides helpers and a common format to define UIA custom properties. +The common custom properties are defined here. +Custom properties specific to an application should be defined within a NVDAObjects/UIA +submodule specific to that application, E.G. 'NVDAObjects/UIA/excel.py' + +UIA originally had hard coded 'static' ID's for properties. +For an example see 'UIA_SelectionPatternId' in +`source/comInterfaces/_944DE083_8FB8_45CF_BCB7_C477ACB2F897_0_1_0.py` +imported via `UIAutomationClient.py`. +When a new property was added the UIA spec had to be updated. +Now a mechanism is in place to allow applications to register "custom properties". +This relies on both the UIA server application and the UIA client application sharing a known +GUID for the property. +""" + + +@dataclass +class CustomPropertyInfo: + """Holds information about a CustomProperty + This makes it easy to define custom properties to be loaded. + """ + guid: GUID + programmaticName: str + uiaType: UIAutomationType + id: int = field(init=False) + + def __post_init__(self) -> None: + """ The id field must be initialised at runtime. + UIA will return the id to use when given the GUID. + Any application can be first to register a custom property, subsequent applications + will be given the same id. + """ + import NVDAHelper + self.id = NVDAHelper.localLib.registerUIAProperty( + byref(self.guid), + self.programmaticName, + self.uiaType + ) + + +class CustomPropertiesCommon: + """UIA 'custom properties' common to all applications. + Once registered, all subsequent registrations will return the same ID value. + This class should be used as a singleton via CustomPropertiesCommon.get() + to prevent unnecessary work by repeatedly interacting with UIA. + """ + #: Singleton instance + _instance: "Optional[CustomPropertiesCommon]" = None + + @classmethod + def get(cls) -> "CustomPropertiesCommon": + """Get the singleton instance or initialise it. + """ + if cls._instance is None: + cls._instance = cls() + return cls._instance + + def __init__(self): + + self.itemIndex = CustomPropertyInfo( + guid=GUID("{92A053DA-2969-4021-BF27-514CFC2E4A69}"), + programmaticName="ItemIndex", + uiaType=UIAutomationType.INT, + ) + + self.itemCount = CustomPropertyInfo( + guid=GUID("{ABBF5C45-5CCC-47b7-BB4E-87CB87BBD162}"), + programmaticName="ItemCount", + uiaType=UIAutomationType.INT, + ) diff --git a/source/_UIAHandler.py b/source/_UIAHandler.py index cbc465c5926..e377a88c045 100644 --- a/source/_UIAHandler.py +++ b/source/_UIAHandler.py @@ -1,47 +1,57 @@ -# _UIAHandler.py # A part of NonVisual Desktop Access (NVDA) -# Copyright (C) 2011-2020 NV Access Limited, Joseph Lee, Babbage B.V., Leonard de Ruijter +# Copyright (C) 2011-2021 NV Access Limited, Joseph Lee, Babbage B.V., Leonard de Ruijter # This file is covered by the GNU General Public License. # See the file COPYING for more details. -from ctypes import * -from ctypes.wintypes import * +import ctypes +import ctypes.wintypes +from ctypes import ( + oledll, + windll, +) +from enum import ( + Enum, +) + import comtypes.client from comtypes.automation import VT_EMPTY -from comtypes import COMError -from comtypes import * -import weakref +from comtypes import ( + COMError, + COMObject, + byref, + CLSCTX_INPROC_SERVER, + CoCreateInstance, +) + import threading import time -from collections import namedtuple import config import api import appModuleHandler -import queueHandler import controlTypes -import NVDAHelper import winKernel import winUser import winVersion import eventHandler from logHandler import log import UIAUtils -from comtypes.gen import UIAutomationClient as UIA -from comtypes.gen.UIAutomationClient import * +from comInterfaces import UIAutomationClient as UIA +# F403: unable to detect undefined names +from comInterfaces.UIAutomationClient import * # noqa: F403 import textInfos from typing import Dict from queue import Queue import aria -#Some newer UIA constants that could be missing -ItemIndex_Property_GUID=GUID("{92A053DA-2969-4021-BF27-514CFC2E4A69}") -ItemCount_Property_GUID=GUID("{ABBF5C45-5CCC-47b7-BB4E-87CB87BBD162}") + HorizontalTextAlignment_Left=0 HorizontalTextAlignment_Centered=1 HorizontalTextAlignment_Right=2 HorizontalTextAlignment_Justified=3 + + # The name of the WDAG (Windows Defender Application Guard) process WDAG_PROCESS_NAME=u'hvsirdpclient' @@ -65,13 +75,9 @@ "RichEdit20", "RICHEDIT50W", "SysListView32", - "EXCEL7", "Button", # #8944: The Foxit UIA implementation is incomplete and should not be used for now. "FoxitDocWnd", - # All Chromium implementations (including Edge) should not be UIA, - # As their IA2 implementation is still better at the moment. - "Chrome_RenderWidgetHostHWND", ] # #8405: used to detect UIA dialogs prior to Windows 10 RS5. @@ -188,7 +194,7 @@ localEventHandlerGroupUIAEventIds = set() autoSelectDetectionAvailable = False -if winVersion.isWin10(): +if winVersion.getWinVer() >= winVersion.WIN10: UIAEventIdsToNVDAEventNames.update({ UIA.UIA_Text_TextChangedEventId: "textChange", UIA.UIA_Text_TextSelectionChangedEventId: "caret", @@ -207,12 +213,28 @@ for id in UIAEventIdsToNVDAEventNames.keys(): ignoreWinEventsMap[id] = [0] + +class AllowUiaInChromium(Enum): + _DEFAULT = 0 # maps to 'when necessary' + WHEN_NECESSARY = 1 # the current default + YES = 2 + NO = 3 + + @staticmethod + def getConfig() -> 'AllowUiaInChromium': + allow = AllowUiaInChromium(config.conf['UIA']['allowInChromium']) + if allow == AllowUiaInChromium._DEFAULT: + return AllowUiaInChromium.WHEN_NECESSARY + return allow + + class UIAHandler(COMObject): _com_interfaces_ = [ UIA.IUIAutomationEventHandler, UIA.IUIAutomationFocusChangedEventHandler, UIA.IUIAutomationPropertyChangedEventHandler, - UIA.IUIAutomationNotificationEventHandler + UIA.IUIAutomationNotificationEventHandler, + UIA.IUIAutomationActiveTextPositionChangedEventHandler, ] def __init__(self): @@ -234,7 +256,13 @@ def __init__(self): raise self.MTAThreadInitException def terminate(self): - MTAThreadHandle=HANDLE(windll.kernel32.OpenThread(winKernel.SYNCHRONIZE,False,self.MTAThread.ident)) + MTAThreadHandle = ctypes.wintypes.HANDLE( + windll.kernel32.OpenThread( + winKernel.SYNCHRONIZE, + False, + self.MTAThread.ident + ) + ) self.MTAThreadQueue.put_nowait(None) #Wait for the MTA thread to die (while still message pumping) if windll.user32.MsgWaitForMultipleObjects(1,byref(MTAThreadHandle),False,200,0)!=0: @@ -295,9 +323,6 @@ def MTAThreadFunc(self): self.UIAWindowHandleCache={} self.baseTreeWalker=self.clientObject.RawViewWalker self.baseCacheRequest=self.windowCacheRequest.Clone() - import UIAHandler - self.ItemIndex_PropertyId=NVDAHelper.localLib.registerUIAProperty(byref(ItemIndex_Property_GUID),u"ItemIndex",1) - self.ItemCount_PropertyId=NVDAHelper.localLib.registerUIAProperty(byref(ItemCount_Property_GUID),u"ItemCount",1) for propertyId in (UIA_FrameworkIdPropertyId,UIA_AutomationIdPropertyId,UIA_ClassNamePropertyId,UIA_ControlTypePropertyId,UIA_ProviderDescriptionPropertyId,UIA_ProcessIdPropertyId,UIA_IsTextPatternAvailablePropertyId,UIA_IsContentElementPropertyId,UIA_IsControlElementPropertyId): self.baseCacheRequest.addProperty(propertyId) self.baseCacheRequest.addPattern(UIA_TextPatternId) @@ -356,6 +381,12 @@ def _registerGlobalEventHandlers(self): self.baseCacheRequest, self ) + if isinstance(self.clientObject, UIA.IUIAutomation6): + self.globalEventHandlerGroup.AddActiveTextPositionChangedEventHandler( + UIA.TreeScope_Subtree, + self.baseCacheRequest, + self + ) self.addEventHandlerGroup(self.rootElement, self.globalEventHandlerGroup) def _createLocalEventHandlerGroup(self): @@ -506,14 +537,15 @@ def IUIAutomationFocusChangedEventHandler_HandleFocusChangedEvent(self,sender): return import NVDAObjects.UIA if isinstance(eventHandler.lastQueuedFocusObject,NVDAObjects.UIA.UIA): - lastFocus=eventHandler.lastQueuedFocusObject.UIAElement + lastFocusObj = eventHandler.lastQueuedFocusObject # Ignore duplicate focus events. # It seems that it is possible for compareElements to return True, even though the objects are different. # Therefore, don't ignore the event if the last focus object has lost its hasKeyboardFocus state. try: if ( - self.clientObject.compareElements(sender, lastFocus) - and lastFocus.currentHasKeyboardFocus + not lastFocusObj.shouldAllowDuplicateUIAFocusEvent + and self.clientObject.compareElements(sender, lastFocusObj.UIAElement) + and lastFocusObj.UIAElement.currentHasKeyboardFocus ): if _isDebug(): log.debugWarning("HandleFocusChangedEvent: Ignoring duplicate focus event") @@ -644,6 +676,34 @@ def IUIAutomationNotificationEventHandler_HandleNotificationEvent( return eventHandler.queueEvent("UIA_notification",obj, notificationKind=NotificationKind, notificationProcessing=NotificationProcessing, displayString=displayString, activityId=activityId) + def IUIAutomationActiveTextPositionChangedEventHandler_HandleActiveTextPositionChangedEvent( + self, + sender, + textRange + ): + if not self.MTAThreadInitEvent.isSet(): + # UIAHandler hasn't finished initialising yet, so just ignore this event. + if _isDebug(): + log.debug("HandleActiveTextPositionchangedEvent: event received while not fully initialized") + return + import NVDAObjects.UIA + try: + obj = NVDAObjects.UIA.UIA(UIAElement=sender) + except Exception: + if _isDebug(): + log.debugWarning( + "HandleActiveTextPositionChangedEvent: Exception while creating object: ", + exc_info=True + ) + return + if not obj: + if _isDebug(): + log.debug( + "HandleActiveTextPositionchangedEvent: Ignoring because no object: " + ) + return + eventHandler.queueEvent("UIA_activeTextPositionChanged", obj, textRange=textRange) + def _isBadUIAWindowClassName(self, windowClass): "Given a windowClassName, returns True if this is a known problematic UIA implementation." # #7497: Windows 10 Fall Creators Update has an incomplete UIA @@ -697,20 +757,56 @@ def _isUIAWindowHelper(self,hwnd): # Ask the window if it supports UIA natively res=windll.UIAutomationCore.UiaHasServerSideProvider(hwnd) if res: - # the window does support UIA natively, but - # MS Word documents now have a fairly usable UI Automation implementation. However, - # Builds of MS Office 2016 before build 9000 or so had bugs which we cannot work around. - # And even current builds of Office 2016 are still missing enough info from UIA that it is still impossible to switch to UIA completely. - # Therefore, if we can inject in-process, refuse to use UIA and instead fall back to the MS Word object model. + # The window does support UIA natively, but MS Word documents now + # have a fairly usable UI Automation implementation. + # However, builds of MS Office 2016 before build 9000 or so had bugs which + # we cannot work around. + # And even current builds of Office 2016 are still missing enough info from + # UIA that it is still impossible to switch to UIA completely. + # Therefore, if we can inject in-process, refuse to use UIA and instead + # fall back to the MS Word object model. + canUseOlderInProcessApproach = bool(appModule.helperLocalBindingHandle) if ( # An MS Word document window windowClass=="_WwG" # Disabling is only useful if we can inject in-process (and use our older code) - and appModule.helperLocalBindingHandle - # Allow the user to explisitly force UIA support for MS Word documents no matter the Office version + and canUseOlderInProcessApproach + # Allow the user to explicitly force UIA support for MS Word documents + # no matter the Office version and not config.conf['UIA']['useInMSWordWhenAvailable'] ): return False + # MS Excel spreadsheets now have a fairly usable UI Automation implementation. + # However, builds of MS Office 2016 before build 9000 or so had bugs which we + # cannot work around. + # And even current builds of Office 2016 are still missing enough info from UIA + # that it is still impossible to switch to UIA completely. + # Therefore, if we can inject in-process, refuse to use UIA and instead fall + # back to the MS Excel object model. + elif ( + # An MS Excel spreadsheet window + windowClass == "EXCEL7" + # Disabling is only useful if we can inject in-process (and use our older code) + and appModule.helperLocalBindingHandle + # Allow the user to explicitly force UIA support for MS Excel spreadsheets + # no matter the Office version + and not config.conf['UIA']['useInMSExcelWhenAvailable'] + ): + return False + # Unless explicitly allowed, all Chromium implementations (including Edge) should not be UIA, + # As their IA2 implementation is still better at the moment. + elif ( + windowClass == "Chrome_RenderWidgetHostHWND" + and ( + AllowUiaInChromium.getConfig() == AllowUiaInChromium.NO + # Disabling is only useful if we can inject in-process (and use our older code) + or ( + canUseOlderInProcessApproach + and AllowUiaInChromium.getConfig() != AllowUiaInChromium.YES # Users can prefer to use UIA + ) + ) + ): + return False return bool(res) def isUIAWindow(self,hwnd): diff --git a/source/addonHandler/__init__.py b/source/addonHandler/__init__.py index bf9bd6d4421..6248a163ce3 100644 --- a/source/addonHandler/__init__.py +++ b/source/addonHandler/__init__.py @@ -1,9 +1,10 @@ # -*- coding: UTF-8 -*- -#addonHandler.py -#A part of NonVisual Desktop Access (NVDA) -#Copyright (C) 2012-2019 Rui Batista, NV Access Limited, Noelia Ruiz Martínez, Joseph Lee, Babbage B.V., Arnold Loubriat -#This file is covered by the GNU General Public License. -#See the file COPYING for more details. +# addonHandler.py +# A part of NonVisual Desktop Access (NVDA) +# Copyright (C) 2012-2019 Rui Batista, NV Access Limited, Noelia Ruiz Martínez, +# Joseph Lee, Babbage B.V., Arnold Loubriat +# This file is covered by the GNU General Public License. +# See the file COPYING for more details. import sys import os.path diff --git a/source/appModuleHandler.py b/source/appModuleHandler.py index 0d17bfea131..463d87bb190 100644 --- a/source/appModuleHandler.py +++ b/source/appModuleHandler.py @@ -389,9 +389,7 @@ def _setProductInfo(self): if not self.processHandle: raise RuntimeError("processHandle is 0") # No need to worry about immersive (hosted) apps and friends until Windows 8. - # Python 3.7 introduces platform_version to sys.getwindowsversion tuple, - # which returns major, minor, build. - if winVersion.winVersion.platform_version >= (6, 2, 9200): + if winVersion.getWinVer() >= winVersion.WIN8: # Some apps such as File Explorer says it is an immersive process but error 15700 is shown. # Therefore resort to file version info behavior because it is not a hosted app. # Others such as Store version of Office are not truly hosted apps, @@ -501,7 +499,7 @@ def _get_isWindowsStoreApp(self): e.g. File Explorer reports itself as immersive when it is not. @rtype: bool """ - if winVersion.winVersion.platform_version < (6, 2, 9200): + if winVersion.getWinVer() < winVersion.WIN8: # Windows Store/UWP apps were introduced in Windows 8. self.isWindowsStoreApp = False return False diff --git a/source/appModules/excel.py b/source/appModules/excel.py index afe6cac130e..adac3378961 100644 --- a/source/appModules/excel.py +++ b/source/appModules/excel.py @@ -5,6 +5,7 @@ # For more details see: https://www.gnu.org/licenses/gpl-2.0.html import time +import config import eventHandler import api import UIAHandler @@ -15,6 +16,8 @@ from NVDAObjects.window.edit import UnidentifiedEdit from NVDAObjects.window import Window from NVDAObjects.window.excel import ExcelCell +from NVDAObjects.IAccessible import IAccessible + class Excel6(Window): """ @@ -43,6 +46,17 @@ def _get_focusRedirect(self): self.focusRedirect=obj return obj + +class Excel6_WhenUIAEnabled(IAccessible): + """ + #12303: When accessing Microsoft Excel via UI Automation + MSAA focus events on the old formula edit window should be completely ignored. + UI Automation will fire its own ones. + """ + + shouldAllowIAccessibleFocusEvent = False + + class AppModule(appModuleHandler.AppModule): def chooseNVDAObjectOverlayClasses(self, obj, clsList): @@ -55,4 +69,14 @@ def chooseNVDAObjectOverlayClasses(self, obj, clsList): pass clsList.insert(0, DisplayModelEditableText) if windowClass=="EXCEL6": - clsList.insert(0,Excel6) + if config.conf["UIA"]["useInMSExcelWhenAvailable"]: + # #12303: When accessing Microsoft Excel via UI Automation + # MSAA focus events on the old formula edit window should be completely ignored. + # UI Automation will fire its own ones. + clsList.insert(0, Excel6_WhenUIAEnabled) + else: + # #12303: The old Formula Edit window in recent versions of Excel + # may not be accessible with display models as GDI is no longer used. + # However, UI Automation does expose an accessible edit control within the active cell, + # So use a class that will redirect focus to that. + clsList.insert(0, Excel6) diff --git a/source/appModules/explorer.py b/source/appModules/explorer.py index f155ccce45b..20e025c4c34 100644 --- a/source/appModules/explorer.py +++ b/source/appModules/explorer.py @@ -242,7 +242,7 @@ class MetadataEditField(RichEdit50): but to avoid Windows Explorer crashes we need to use EditTextInfo here. """ @classmethod def _get_TextInfo(cls): - if ((winVersion.winVersion.major, winVersion.winVersion.minor) == (6, 1)): + if winVersion.getWinVer() <= winVersion.WIN7_SP1: cls.TextInfo = EditTextInfo else: cls.TextInfo = super().TextInfo @@ -255,8 +255,8 @@ def event_gainFocus(self): # as it causes 'pane" to be announced when minimizing windows or moving to desktop. # However when closing Windows 7 Start Menu in some cases # focus lands on it instead of the focused desktop item. - # Simply ignore the event if running on anything never than Win 7. - if ((winVersion.winVersion.major, winVersion.winVersion.minor) != (6, 1)): + # Simply ignore the event if running on anything other than Win 7. + if winVersion.getWinVer() > winVersion.WIN7_SP1: return if eventHandler.isPendingEvents("gainFocus"): return @@ -416,7 +416,10 @@ def event_gainFocus(self, obj, nextHandler): def isGoodUIAWindow(self, hwnd): # #9204: shell raises window open event for emoji panel in build 18305 and later. - if winVersion.isWin10(version=1903) and winUser.getClassName(hwnd) == "ApplicationFrameWindow": + if ( + winVersion.getWinVer() >= winVersion.WIN10_1903 + and winUser.getClassName(hwnd) == "ApplicationFrameWindow" + ): return True return False diff --git a/source/appModules/kindle.py b/source/appModules/kindle.py index c0b6fa358a4..4a282019582 100644 --- a/source/appModules/kindle.py +++ b/source/appModules/kindle.py @@ -20,11 +20,10 @@ import textInfos from speech.types import SpeechSequence from textInfos import DocumentWithPageTurns -from IAccessibleHandler import IAccessible2 from NVDAObjects.IAccessible import IAccessible from globalCommands import SCRCAT_SYSTEMCARET from NVDAObjects.IAccessible.ia2TextMozilla import MozillaCompoundTextInfo -import IAccessibleHandler +from comInterfaces import IAccessible2Lib as IA2 import winUser import mouseHandler from logHandler import log @@ -59,7 +58,7 @@ def event_pageChange(self, obj, nextHandler): # Don't report anything if the book area isn't focused. return info.expand(textInfos.UNIT_LINE) - speech.speakTextInfo(info, unit=textInfos.UNIT_LINE, reason=controlTypes.REASON_CARET) + speech.speakTextInfo(info, unit=textInfos.UNIT_LINE, reason=OutputReason.CARET) def isAlive(self): return winUser.isWindow(self.rootNVDAObject.windowHandle) @@ -78,7 +77,9 @@ def _getTableCellAt(self,tableID,startPos,destRow,destCol): raise LookupError table = obj.table try: - cell = table.IAccessibleTable2Object.cellAt(destRow - 1, destCol - 1).QueryInterface(IAccessible2) + cell = table.IAccessibleTable2Object.cellAt( + destRow - 1, destCol - 1 + ).QueryInterface(IA2.IAccessible2) cell = IAccessible(IAccessibleObject=cell, IAccessibleChildID=0) # If the cell we fetched is marked as hidden, raise LookupError which will instruct calling code to try an adjacent cell instead. if cell.IA2Attributes.get('hidden'): @@ -100,7 +101,8 @@ def _changePageScriptHelper(self,gesture,previous=False): info=self.makeTextInfo(textInfos.POSITION_FIRST) self.selection=info info.expand(textInfos.UNIT_LINE) - if not willSayAllResume(gesture): speech.speakTextInfo(info,unit=textInfos.UNIT_LINE,reason=controlTypes.REASON_CARET) + if not willSayAllResume(gesture): + speech.speakTextInfo(info, unit=textInfos.UNIT_LINE, reason=OutputReason.CARET) def script_moveByPage_forward(self,gesture): self._changePageScriptHelper(gesture) @@ -159,7 +161,7 @@ def _iterEmbeddedObjs(self, hypertext, startIndex, direction): log.debug("Starting at hyperlink index %d" % startIndex) for index in range(startIndex, hypertext.nHyperlinks if direction == "next" else -1, 1 if direction == "next" else -1): hl = hypertext.hyperlink(index) - obj = IAccessible(IAccessibleObject=hl.QueryInterface(IAccessibleHandler.IAccessible2), IAccessibleChildID=0) + obj = IAccessible(IAccessibleObject=hl.QueryInterface(IA2.IAccessible2), IAccessibleChildID=0) log.debug("Yielding object at index %d" % index) yield obj try: @@ -226,7 +228,7 @@ def _iterNodesByType(self, nodeType, direction="next", pos=None): if not getattr(parent,'IAccessibleTextObject',None): obj=parent continue - hl = obj.IAccessibleObject.QueryInterface(IAccessibleHandler.IAccessibleHyperlink) + hl = obj.IAccessibleObject.QueryInterface(IA2.IAccessibleHyperlink) offset = hl.startIndex obj=parent hli = obj.iaHypertext.hyperlinkIndex(offset) @@ -408,7 +410,11 @@ class AppModule(appModuleHandler.AppModule): def chooseNVDAObjectOverlayClasses(self,obj,clsList): if isinstance(obj,IAccessible): clsList.insert(0,PageTurnFocusIgnorer) - if ((isinstance(obj.IAccessibleObject, IAccessibleHandler.IAccessible2) and obj.IA2Attributes.get("class") == "KindleBookPageView") + if ( + ( + isinstance(obj.IAccessibleObject, IA2.IAccessible2) + and obj.IA2Attributes.get("class") == "KindleBookPageView" + ) # We must rely on .name in Kindle <= 1.19. or (hasattr(obj,'IAccessibleTextObject') and obj.name=="Book Page View") ): @@ -418,7 +424,11 @@ def chooseNVDAObjectOverlayClasses(self,obj,clsList): return clsList def event_NVDAObject_init(self, obj): - if isinstance(obj, IAccessible) and isinstance(obj.IAccessibleObject, IAccessibleHandler.IAccessible2) and obj.role == controlTypes.ROLE_LINK: + if ( + isinstance(obj, IAccessible) + and isinstance(obj.IAccessibleObject, IA2.IAccessible2) + and obj.role == controlTypes.ROLE_LINK + ): xRoles = obj.IA2Attributes.get("xml-roles", "").split(" ") if "kindle-footnoteref" in xRoles: obj.role = controlTypes.ROLE_FOOTNOTE diff --git a/source/appModules/miranda32.py b/source/appModules/miranda32.py index 54e83fb1950..2cb3b1c1785 100644 --- a/source/appModules/miranda32.py +++ b/source/appModules/miranda32.py @@ -166,7 +166,7 @@ def script_changeItem(self,gesture): gesture.send() if not isScriptWaiting(): api.processPendingEvents() - speech.speakObject(self,reason=controlTypes.REASON_FOCUS) + speech.speakObject(self, reason=controlTypes.OutputReason.FOCUS) braille.handler.handleGainFocus(self) __changeItemGestures = ( diff --git a/source/appModules/nvda.py b/source/appModules/nvda.py index ba37154d879..4f0fd2a529b 100755 --- a/source/appModules/nvda.py +++ b/source/appModules/nvda.py @@ -1,20 +1,30 @@ -#appModules/nvda.py -#A part of NonVisual Desktop Access (NVDA) -#Copyright (C) 2008-2017 NV Access Limited -#This file is covered by the GNU General Public License. -#See the file COPYING for more details. +# A part of NonVisual Desktop Access (NVDA) +# Copyright (C) 2008-2021 NV Access Limited, James Teh, Michael Curran, Leonard de Ruijter, Reef Turner, +# Julien Cochuyt +# This file may be used under the terms of the GNU General Public License, version 2 or later. +# For more details see: https://www.gnu.org/licenses/gpl-2.0.html + + +import typing import appModuleHandler import api import controlTypes import versionInfo from NVDAObjects.IAccessible import IAccessible +from baseObject import ScriptableObject import gui +from scriptHandler import script import speech +import textInfos import braille import config from logHandler import log +if typing.TYPE_CHECKING: + import inputCore + + nvdaMenuIaIdentity = None class NvdaDialog(IAccessible): @@ -44,6 +54,118 @@ def _get_description(self): """ return "" + +# Translators: The name of a category of NVDA commands. +SCRCAT_PYTHON_CONSOLE = _("Python Console") + + +class NvdaPythonConsoleUIOutputClear(ScriptableObject): + + # Allow the bound gestures to be edited through the Input Gestures dialog (see L{gui.prePopup}) + isPrevFocusOnNvdaPopup = True + + @script( + gesture="kb:control+l", + # Translators: Description of a command to clear the Python Console output pane + description=_("Clear the output pane"), + category=SCRCAT_PYTHON_CONSOLE, + ) + def script_clearOutput(self, gesture: "inputCore.InputGesture"): + from pythonConsole import consoleUI + consoleUI.clear() + + +class NvdaPythonConsoleUIOutputCtrl(ScriptableObject): + + # Allow the bound gestures to be edited through the Input Gestures dialog (see L{gui.prePopup}) + isPrevFocusOnNvdaPopup = True + + @script( + gesture="kb:alt+downArrow", + # Translators: Description of a command to move to the next result in the Python Console output pane + description=_("Move to the next result"), + category=SCRCAT_PYTHON_CONSOLE + ) + def script_moveToNextResult(self, gesture: "inputCore.InputGesture"): + self._resultNavHelper(direction="next", select=False) + + @script( + gesture="kb:alt+upArrow", + # Translators: Description of a command to move to the previous result + # in the Python Console output pane + description=_("Move to the previous result"), + category=SCRCAT_PYTHON_CONSOLE + ) + def script_moveToPrevResult(self, gesture: "inputCore.InputGesture"): + self._resultNavHelper(direction="previous", select=False) + + @script( + gesture="kb:alt+downArrow+shift", + # Translators: Description of a command to select from the current caret position to the end + # of the current result in the Python Console output pane + description=_("Select until the end of the current result"), + category=SCRCAT_PYTHON_CONSOLE + ) + def script_selectToResultEnd(self, gesture: "inputCore.InputGesture"): + self._resultNavHelper(direction="next", select=True) + + @script( + gesture="kb:alt+shift+upArrow", + # Translators: Description of a command to select from the current caret position to the start + # of the current result in the Python Console output pane + description=_("Select until the start of the current result"), + category=SCRCAT_PYTHON_CONSOLE + ) + def script_selectToResultStart(self, gesture: "inputCore.InputGesture"): + self._resultNavHelper(direction="previous", select=True) + + def _resultNavHelper(self, direction: str = "next", select: bool = False): + from pythonConsole import consoleUI + startPos, endPos = consoleUI.outputCtrl.GetSelection() + if self.isTextSelectionAnchoredAtStart: + curPos = endPos + else: + curPos = startPos + if direction == "previous": + for pos in reversed(consoleUI.outputPositions): + if pos < curPos: + break + else: + # Translators: Reported when attempting to move to the previous result in the Python Console + # output pane while there is no previous result. + speech.speakMessage(_("Top")) + return + elif direction == "next": + for pos in consoleUI.outputPositions: + if pos > curPos: + break + else: + # Translators: Reported when attempting to move to the next result in the Python Console + # output pane while there is no next result. + speech.speakMessage(_("Bottom")) + return + else: + raise ValueError(u"Unexpected direction: {!r}".format(direction)) + if select: + consoleUI.outputCtrl.Freeze() + anchorPos = startPos if self.isTextSelectionAnchoredAtStart else endPos + consoleUI.outputCtrl.SetSelection(anchorPos, pos) + consoleUI.outputCtrl.Thaw() + self.detectPossibleSelectionChange() + self.isTextSelectionAnchoredAtStart = anchorPos < pos + else: + consoleUI.outputCtrl.SetSelection(pos, pos) + info = self.makeTextInfo(textInfos.POSITION_CARET) + copy = info.copy() + info.expand(textInfos.UNIT_LINE) + if ( + copy.move(textInfos.UNIT_CHARACTER, 4, endPoint="end") == 4 + and copy.text == ">>> " + ): + info.move(textInfos.UNIT_CHARACTER, 4, endPoint="start") + speech.speakTextInfo(info, reason=controlTypes.OutputReason.CARET) + + class AppModule(appModuleHandler.AppModule): # The configuration profile that has been previously edited. # This ought to be a class property. @@ -108,8 +230,26 @@ def isNvdaSettingsDialog(self, obj): return True return False + def isNvdaPythonConsoleUIInputCtrl(self, obj): + from pythonConsole import consoleUI + if not consoleUI: + return + return obj.windowHandle == consoleUI.inputCtrl.GetHandle() + + def isNvdaPythonConsoleUIOutputCtrl(self, obj): + from pythonConsole import consoleUI + if not consoleUI: + return + return obj.windowHandle == consoleUI.outputCtrl.GetHandle() + def chooseNVDAObjectOverlayClasses(self, obj, clsList): if obj.windowClassName == "#32770" and obj.role == controlTypes.ROLE_DIALOG: clsList.insert(0, NvdaDialog) if self.isNvdaSettingsDialog(obj): clsList.insert(0, NvdaDialogEmptyDescription) + return + if self.isNvdaPythonConsoleUIInputCtrl(obj): + clsList.insert(0, NvdaPythonConsoleUIOutputClear) + elif self.isNvdaPythonConsoleUIOutputCtrl(obj): + clsList.insert(0, NvdaPythonConsoleUIOutputClear) + clsList.insert(0, NvdaPythonConsoleUIOutputCtrl) diff --git a/source/appModules/outlook.py b/source/appModules/outlook.py index f24131cd74f..b5e1b7e278a 100644 --- a/source/appModules/outlook.py +++ b/source/appModules/outlook.py @@ -1,8 +1,8 @@ -#appModules/outlook.py -#A part of NonVisual Desktop Access (NVDA) -#Copyright (C) 2006-2018 NV Access Limited, Yogesh Kumar, Manish Agrawal, Joseph Lee, Davy Kager, Babbage B.V. -#This file is covered by the GNU General Public License. -#See the file COPYING for more details. +# A part of NonVisual Desktop Access (NVDA) +# Copyright (C) 2006-2021 NV Access Limited, Yogesh Kumar, Manish Agrawal, Joseph Lee, Davy Kager, +# Babbage B.V., Leonard de Ruijter +# This file is covered by the GNU General Public License. +# See the file COPYING for more details. from comtypes import COMError from comtypes.hresult import S_OK @@ -11,6 +11,7 @@ import ctypes from hwPortUtils import SYSTEMTIME import scriptHandler +from scriptHandler import script import winKernel import comHelper import NVDAHelper @@ -29,11 +30,14 @@ import ui from NVDAObjects.IAccessible import IAccessible from NVDAObjects.window import Window +from NVDAObjects.window.winword import WordDocument as BaseWordDocument from NVDAObjects.IAccessible.winword import WordDocument, WordDocumentTreeInterceptor, BrowseModeWordDocumentTextInfo, WordDocumentTextInfo from NVDAObjects.IAccessible.MSHTML import MSHTML from NVDAObjects.behaviors import RowWithFakeNavigation, Dialog from NVDAObjects.UIA import UIA from NVDAObjects.UIA.wordDocument import WordDocument as UIAWordDocument +import languageHandler +from gettext import ngettext PR_LAST_VERB_EXECUTED=0x10810003 VERB_REPLYTOSENDER=102 @@ -242,10 +246,10 @@ def event_gainFocus(self): return super(SuperGridClient2010,self).event_gainFocus() try: kwargs = {} - UIA.kwargsFromSuper(kwargs, relation="focus") - obj=UIA(**kwargs) - except: - log.debugWarning("Retrieving UIA focus failed", exc_info=True) + UIA.kwargsFromSuper(kwargs, relation="focus", ignoreNonNativeElementsWithFocus=False) + obj = UIA(**kwargs) + except Exception: + log.error("Retrieving UIA focus failed", exc_info=True) return super(SuperGridClient2010,self).event_gainFocus() if not isinstance(obj,UIAGridRow): return super(SuperGridClient2010,self).event_gainFocus() @@ -332,6 +336,30 @@ def _generateTimeRangeText(self,startTime,endTime): # Translators: a message reporting the time range (i.e. start time to end time) of an Outlook calendar entry return _("{startTime} to {endTime}").format(startTime=startText,endTime=endText) + @staticmethod + def _generateCategoriesText(appointment): + categories = appointment.Categories + if not categories: + return None + # Categories is a delimited string of category names that have been assigned to an Outlook item. + # This property uses the user locale's list separator to separate entries. + # See also https://docs.microsoft.com/en-us/office/vba/api/outlook.appointmentitem.categories + bufLength = 4 + separatorBuf = ctypes.create_unicode_buffer(bufLength) + if ctypes.windll.kernel32.GetLocaleInfoW( + languageHandler.LOCALE_USER_DEFAULT, + languageHandler.LOCALE_SLIST, + separatorBuf, + bufLength + ) == 0: + raise ctypes.WinError() + categoriesCount = len(categories.split(f"{separatorBuf.value} ")) + + # Translators: Part of a message reported when on a calendar appointment with one or more categories + # in Microsoft Outlook. + categoriesText = ngettext("category", "categories", categoriesCount) + return f"{categoriesText} {categories}" + def isDuplicateIAccessibleEvent(self,obj): return False @@ -353,8 +381,15 @@ def reportFocus(self): except COMError: return super(CalendarView,self).reportFocus() t=self._generateTimeRangeText(start,end) - # Translators: A message reported when on a calendar appointment in Microsoft Outlook - ui.message(_("Appointment {subject}, {time}").format(subject=p.subject,time=t)) + # Translators: A message reported when on a calendar appointment with category in Microsoft Outlook + message = _("Appointment {subject}, {time}").format(subject=p.subject, time=t) + try: + categoriesText = self._generateCategoriesText(p) + except COMError: + categoriesText = None + if categoriesText is not None: + message = f"{message}, {categoriesText}" + ui.message(message) else: v=e.currentView try: @@ -363,9 +398,31 @@ def reportFocus(self): except COMError: return super(CalendarView,self).reportFocus() timeSlotText=self._generateTimeRangeText(selectedStartTime,selectedEndTime) - startLimit=u"%s %s"%(winKernel.GetDateFormatEx(winKernel.LOCALE_NAME_USER_DEFAULT, winKernel.DATE_LONGDATE, selectedStartTime, None),winKernel.GetTimeFormat(winKernel.LOCALE_USER_DEFAULT, winKernel.TIME_NOSECONDS, selectedStartTime, None)) - endLimit=u"%s %s"%(winKernel.GetDateFormatEx(winKernel.LOCALE_NAME_USER_DEFAULT, winKernel.DATE_LONGDATE, selectedEndTime, None),winKernel.GetTimeFormat(winKernel.LOCALE_USER_DEFAULT, winKernel.TIME_NOSECONDS, selectedEndTime, None)) - query=u'[Start] < "{endLimit}" And [End] > "{startLimit}"'.format(startLimit=startLimit,endLimit=endLimit) + startDate = winKernel.GetDateFormatEx( + winKernel.LOCALE_NAME_USER_DEFAULT, + winKernel.DATE_LONGDATE, + selectedStartTime, + None + ) + startTime = winKernel.GetTimeFormatEx( + winKernel.LOCALE_NAME_USER_DEFAULT, + winKernel.TIME_NOSECONDS, + selectedStartTime, + None + ) + endDate = winKernel.GetDateFormatEx( + winKernel.LOCALE_NAME_USER_DEFAULT, + winKernel.DATE_LONGDATE, + selectedEndTime, + None + ) + endTime = winKernel.GetTimeFormatEx( + winKernel.LOCALE_NAME_USER_DEFAULT, + winKernel.TIME_NOSECONDS, + selectedEndTime, + None + ) + query = f'[Start] < "{endDate} {endTime}" And [End] > "{startDate} {startTime}"' i=e.currentFolder.items i.sort('[Start]') i.IncludeRecurrences =True @@ -544,7 +601,7 @@ def script_tab(self,gesture): info.expand(textInfos.UNIT_CELL) isCollapsed=False if not isCollapsed: - speech.speakTextInfo(info,reason=controlTypes.REASON_FOCUS) + speech.speakTextInfo(info, reason=controlTypes.OutputReason.FOCUS) braille.handler.handleCaretMove(self) __gestures={ @@ -552,7 +609,20 @@ def script_tab(self,gesture): "kb:shift+tab":"tab", } -class OutlookWordDocument(WordDocument): + +class BaseOutlookWordDocument(BaseWordDocument): + + @script(gestures=["kb:tab", "kb:shift+tab"]) + def script_tab(self, gesture): + bookmark = self.makeTextInfo(textInfos.POSITION_SELECTION).bookmark + gesture.send() + info, caretMoved = self._hasCaretMoved(bookmark) + if not caretMoved: + return + self.reportTab() + + +class OutlookWordDocument(WordDocument, BaseOutlookWordDocument): def _get_isReadonlyViewer(self): # #2975: The only way we know an email is read-only is if the underlying email has been sent. @@ -575,7 +645,8 @@ def _get_role(self): ignoreEditorRevisions=True ignorePageNumbers=True # This includes page sections, and page columns. None of which are appropriate for outlook. -class OutlookUIAWordDocument(UIAWordDocument): + +class OutlookUIAWordDocument(UIAWordDocument, BaseOutlookWordDocument): """ Forces browse mode to be used on the UI Automation Outlook message viewer if the message is being read).""" def _get_isReadonlyViewer(self): diff --git a/source/appModules/powerpnt.py b/source/appModules/powerpnt.py index f79b77229b3..d05bc596c8f 100644 --- a/source/appModules/powerpnt.py +++ b/source/appModules/powerpnt.py @@ -1085,7 +1085,7 @@ def event_treeInterceptor_gainFocus(self): speech.speakPreselectedText(info.text) else: info.expand(textInfos.UNIT_LINE) - speech.speakTextInfo(info, reason=controlTypes.REASON_CARET, unit=textInfos.UNIT_LINE) + speech.speakTextInfo(info, reason=controlTypes.OutputReason.CARET, unit=textInfos.UNIT_LINE) def event_gainFocus(self,obj,nextHandler): pass diff --git a/source/appModules/putty.py b/source/appModules/putty.py index cd116342d28..60907e0a831 100644 --- a/source/appModules/putty.py +++ b/source/appModules/putty.py @@ -12,7 +12,7 @@ from NVDAObjects.window import DisplayModelEditableText, DisplayModelLiveText import appModuleHandler from NVDAObjects.IAccessible import IAccessible -from winVersion import isWin10 +from winVersion import getWinVer, WIN10_1607 class AppModule(appModuleHandler.AppModule): # Allow this to be overridden for derived applications. @@ -24,7 +24,7 @@ def chooseNVDAObjectOverlayClasses(self, obj, clsList): clsList.remove(DisplayModelEditableText) except ValueError: pass - if isWin10(1607): + if getWinVer() >= WIN10_1607: clsList[0:0] = (KeyboardHandlerBasedTypedCharSupport, DisplayModelLiveText) else: clsList[0:0] = (Terminal, DisplayModelLiveText) diff --git a/source/appModules/soffice.py b/source/appModules/soffice.py index 478a84c2ddc..e08e2af779d 100755 --- a/source/appModules/soffice.py +++ b/source/appModules/soffice.py @@ -5,6 +5,7 @@ #Copyright (C) 2006-2019 NV Access Limited, Bill Dengler from comtypes import COMError +from comInterfaces import IAccessible2Lib as IA2 import IAccessibleHandler import appModuleHandler import controlTypes @@ -150,7 +151,7 @@ def _getFormatFieldAndOffsets(self,offset,formatConfig,calculateOffsets=True): # optimisation: Assume a hyperlink occupies a full attribute run. try: - if obj.IAccessibleTextObject.QueryInterface(IAccessibleHandler.IAccessibleHypertext).hyperlinkIndex(offset) != -1: + if obj.IAccessibleTextObject.QueryInterface(IA2.IAccessibleHypertext).hyperlinkIndex(offset) != -1: formatField["link"] = True except COMError: pass diff --git a/source/appModules/winamp.py b/source/appModules/winamp.py index 3984800fb7c..52e6f907ea8 100644 --- a/source/appModules/winamp.py +++ b/source/appModules/winamp.py @@ -127,7 +127,7 @@ def script_changeItem(self,gesture): gesture.send() if not isScriptWaiting(): api.processPendingEvents() - speech.speakObject(self,reason=controlTypes.REASON_FOCUS) + speech.speakObject(self, reason=controlTypes.OutputReason.FOCUS) braille.handler.handleGainFocus(self) def event_nameChange(self): diff --git a/source/appModules/windowsinternal_composableshell_experiences_textinput_inputapp.py b/source/appModules/windowsinternal_composableshell_experiences_textinput_inputapp.py index ab73a7db1f8..04c179d1215 100644 --- a/source/appModules/windowsinternal_composableshell_experiences_textinput_inputapp.py +++ b/source/appModules/windowsinternal_composableshell_experiences_textinput_inputapp.py @@ -1,8 +1,7 @@ -# App module for Composable Shell (CShell) input panel -#A part of NonVisual Desktop Access (NVDA) -#Copyright (C) 2017-2018 NV Access Limited, Joseph Lee -#This file is covered by the GNU General Public License. -#See the file COPYING for more details. +# A part of NonVisual Desktop Access (NVDA) +# Copyright (C) 2017-2021 NV Access Limited, Joseph Lee +# This file is covered by the GNU General Public License. +# See the file COPYING for more details. """App module for Windows 10 Modern Keyboard aka new touch keyboard panel. The chief feature is allowing NVDA to announce selected emoji when using the keyboard to search for and select one. @@ -173,9 +172,17 @@ def event_UIA_window_windowOpen(self, obj, nextHandler): return # #9104: different aspects of modern input panel are represented by automation iD's. childAutomationID = obj.firstChild.UIAElement.cachedAutomationID - # Emoji panel for build 16299 and 17134. + # Emoji panel for 1709 (build 16299) and 1803 (17134). + emojiPanelInitial = winVersion.WIN10_1709 # This event is properly raised in build 17134. - if winVersion.winVersion.build <= 17134 and childAutomationID in ("TEMPLATE_PART_ExpressiveInputFullViewFuntionBarItemControl", "TEMPLATE_PART_ExpressiveInputFullViewFuntionBarCloseButton"): + emojiPanelWindowOpenEvent = winVersion.WIN10_1803 + if ( + emojiPanelInitial <= winVersion.getWinVer() <= emojiPanelWindowOpenEvent + and childAutomationID in ( + "TEMPLATE_PART_ExpressiveInputFullViewFuntionBarItemControl", + "TEMPLATE_PART_ExpressiveInputFullViewFuntionBarCloseButton" + ) + ): self.event_UIA_elementSelected(obj.lastChild.firstChild, nextHandler) # Handle hardware keyboard suggestions. # Treat it the same as CJK composition list - don't announce this if candidate announcement setting is off. @@ -221,8 +228,9 @@ def event_nameChange(self, obj, nextHandler): or (self._recentlySelected is not None and self._recentlySelected in obj.name)): return # The word "blank" is kept announced, so suppress this on build 17666 and later. - if winVersion.winVersion.build > 17134: - # In build 17672 and later, return immediatley when element selected event on clipboard item was fired just prior to this. + if winVersion.getWinVer().build > 17134: + # In build 17672 and later, + # return immediately when element selected event on clipboard item was fired just prior to this. # In some cases, parent will be None, as seen when emoji panel is closed in build 18267. try: if obj.UIAElement.cachedAutomationID == "TEMPLATE_PART_ClipboardItemIndex" or obj.parent.UIAElement.cachedAutomationID == "TEMPLATE_PART_ClipboardItemsList": return diff --git a/source/braille.py b/source/braille.py index 7b2e0da7718..7c153aa5d62 100644 --- a/source/braille.py +++ b/source/braille.py @@ -39,6 +39,8 @@ import bdDetect import queueHandler import brailleViewer +from autoSettingsUtils.driverSetting import BooleanDriverSetting, NumericDriverSetting + roleLabels = { # Translators: Displayed in braille for an object which is a @@ -405,8 +407,7 @@ def __init__(self): #: @type: [int, ...] self.brailleToRawPos = [] #: The position of the cursor in L{brailleCells}, C{None} if the cursor is not in this region. - #: @type: int - self.brailleCursorPos = None + self.brailleCursorPos: Optional[int] = None #: The position of the selection start in L{brailleCells}, C{None} if there is no selection in this region. #: @type: int self.brailleSelectionStart = None @@ -524,7 +525,15 @@ def getPropertiesBraille(**propertyValues) -> str: # noqa: C901 textList.append(value) if states is not None: textList.extend( - controlTypes.processAndLabelStates(role, states, controlTypes.REASON_FOCUS, states, None, positiveStateLabels, negativeStateLabels) + controlTypes.processAndLabelStates( + role, + states, + controlTypes.OutputReason.FOCUS, + states, + None, + positiveStateLabels, + negativeStateLabels + ) ) if roleText: textList.append(roleText) @@ -571,13 +580,9 @@ def getPropertiesBraille(**propertyValues) -> str: # noqa: C901 # %s is replaced with the column number. columnStr = _("c{columnNumber}").format(columnNumber=columnNumber) textList.append(columnStr) - current = propertyValues.get('current', False) - if current: - try: - textList.append(controlTypes.isCurrentLabels[current]) - except KeyError: - log.debugWarning("Aria-current value not handled: %s"%current) - textList.append(controlTypes.isCurrentLabels[True]) + isCurrent = propertyValues.get('current', controlTypes.IsCurrent.NO) + if isCurrent != controlTypes.IsCurrent.NO: + textList.append(isCurrent.displayString) placeholder = propertyValues.get('placeholder', None) if placeholder: textList.append(placeholder) @@ -662,7 +667,7 @@ def getControlFieldBraille(info, field, ancestors, reportStart, formatConfig): states = field.get("states", set()) value=field.get('value',None) - current=field.get('current', None) + current = field.get('current', controlTypes.IsCurrent.NO) placeholder=field.get('placeholder', None) roleText = field.get('roleTextBraille', field.get('roleText')) landmark = field.get("landmark") @@ -1087,15 +1092,21 @@ def routeTo(self, braillePos): brailleInput.handler.updateDisplay() return - if braillePos == self.brailleCursorPos: - # The cursor is already at this position, - # so activate the position. - try: - self._getSelection().activate() - except NotImplementedError: - pass - return dest = self.getTextInfoForBraillePos(braillePos) + # When there is a selection, brailleCursorPos will be None + # Don't activate, but move the cursor to the new cell (dropping the + # selection). An alternative behavior may be to activate on the selection. + # Moving the cursor was considered more intuitive. + if self.brailleCursorPos is not None: + cursor = self.getTextInfoForBraillePos(self.brailleCursorPos) + if dest.compareEndPoints(cursor, "startToStart") == 0: + # The cursor is already at this position, + # so activate the position. + try: + self._getSelection().activate() + except NotImplementedError: + pass + return self._setCursor(dest) def nextLine(self): @@ -2228,19 +2239,23 @@ def terminate(): class BrailleDisplayDriver(driverHandler.Driver): """Abstract base braille display driver. - Each braille display driver should be a separate Python module in the root brailleDisplayDrivers directory containing a BrailleDisplayDriver class which inherits from this base class. + Each braille display driver should be a separate Python module in the root brailleDisplayDrivers directory + containing a BrailleDisplayDriver class which inherits from this base class. At a minimum, drivers must set L{name} and L{description} and override the L{check} method. To display braille, L{numCells} and L{display} must be implemented. - Drivers should dispatch input such as presses of buttons, wheels or other controls using the L{inputCore} framework. - They should subclass L{BrailleDisplayGesture} and execute instances of those gestures using L{inputCore.manager.executeGesture}. + Drivers should dispatch input such as presses of buttons, wheels or other controls + using the L{inputCore} framework. + They should subclass L{BrailleDisplayGesture} + and execute instances of those gestures using L{inputCore.manager.executeGesture}. These gestures can be mapped in L{gestureMap}. A driver can also inherit L{baseObject.ScriptableObject} to provide display specific scripts. @see: L{hwIo} for raw serial and HID I/O. - There are factory functions to create L{driverHandler.DriverSetting} instances for common display specific settings; e.g. L{DotFirmnessSetting}. + There are factory functions to create L{autoSettingsUtils.driverSetting.DriverSetting} instances + for common display specific settings; e.g. L{DotFirmnessSetting}. """ _configSection = "braille" # Most braille display drivers don't have settings yet. @@ -2458,7 +2473,7 @@ def _handleAck(self): @classmethod def DotFirmnessSetting(cls,defaultVal,minVal,maxVal,useConfig=False): """Factory function for creating dot firmness setting.""" - return driverHandler.NumericDriverSetting( + return NumericDriverSetting( "dotFirmness", # Translators: Label for a setting in braille settings dialog. _("Dot firm&ness"), @@ -2471,7 +2486,7 @@ def DotFirmnessSetting(cls,defaultVal,minVal,maxVal,useConfig=False): @classmethod def BrailleInputSetting(cls, useConfig=True): """Factory function for creating braille input setting.""" - return driverHandler.BooleanDriverSetting( + return BooleanDriverSetting( "brailleInput", # Translators: Label for a setting in braille settings dialog. _("Braille inp&ut"), @@ -2481,7 +2496,7 @@ def BrailleInputSetting(cls, useConfig=True): @classmethod def HIDInputSetting(cls, useConfig): """Factory function for creating HID input setting.""" - return driverHandler.BooleanDriverSetting( + return BooleanDriverSetting( "hidKeyboardInput", # Translators: Label for a setting in braille settings dialog. _("&HID keyboard input simulation"), diff --git a/source/brailleTables.py b/source/brailleTables.py index a640bae931b..e99c0cf4ec8 100644 --- a/source/brailleTables.py +++ b/source/brailleTables.py @@ -114,6 +114,12 @@ def listTables(): addTable("be-in-g1.utb", _("Bengali grade 1")) # Translators: The name of a braille table displayed in the # braille settings dialog. +addTable("bel-comp.utb", _("Belarusian computer braille")) +# Translators: The name of a braille table displayed in the +# braille settings dialog. +addTable("bel.utb", _("Belarusian literary braille"), input=False) +# Translators: The name of a braille table displayed in the +# braille settings dialog. addTable("bg.ctb", _("Bulgarian 8 dot computer braille")) # Translators: The name of a braille table displayed in the # braille settings dialog. @@ -432,6 +438,12 @@ def listTables(): addTable("uk-comp.utb", _("Ukrainian computer braille")) # Translators: The name of a braille table displayed in the # braille settings dialog. +addTable("ur-pk-g1.utb", _("Urdu grade 1")) +# Translators: The name of a braille table displayed in the +# braille settings dialog. +addTable("ur-pk-g2.ctb", _("Urdu grade 2")) +# Translators: The name of a braille table displayed in the +# braille settings dialog. addTable("uz-g1.utb", _("Uzbek grade 1")) # Translators: The name of a braille table displayed in the # braille settings dialog. diff --git a/source/brailleViewer/brailleViewerGui.py b/source/brailleViewer/brailleViewerGui.py index b781a29f0ce..33f3ead0a1a 100644 --- a/source/brailleViewer/brailleViewerGui.py +++ b/source/brailleViewer/brailleViewerGui.py @@ -1,5 +1,5 @@ # A part of NonVisual Desktop Access (NVDA) -# Copyright (C) 2014-2019 NV Access Limited +# Copyright (C) 2014-2021 NV Access Limited, Accessolutions, Julien Cochuyt # This file is covered by the GNU General Public License. # See the file COPYING for more details. import enum @@ -280,7 +280,7 @@ def __init__(self, numCells, onDestroyed): gui.mainFrame, title=self._title, pos=dialogPos, - style=wx.CAPTION | wx.STAY_ON_TOP + style=wx.CAPTION | wx.CLOSE_BOX | wx.STAY_ON_TOP ) self.Bind(wx.EVT_CLOSE, self._onClose) self.Bind(wx.EVT_WINDOW_DESTROY, self._onDestroy) diff --git a/source/browseMode.py b/source/browseMode.py index b00b4d1c42b..31cd021f35d 100644 --- a/source/browseMode.py +++ b/source/browseMode.py @@ -1,9 +1,10 @@ # A part of NonVisual Desktop Access (NVDA) -# Copyright (C) 2007-2020 NV Access Limited, Babbage B.V., James Teh, Leonard de Ruijter, -# Thomas Stivers +# Copyright (C) 2007-2021 NV Access Limited, Babbage B.V., James Teh, Leonard de Ruijter, +# Thomas Stivers, Accessolutions, Julien Cochuyt # This file is covered by the GNU General Public License. # See the file COPYING for more details. +from typing import Union import os import itertools import collections @@ -23,7 +24,7 @@ import gui import ui import cursorManager -from scriptHandler import isScriptWaiting, willSayAllResume +from scriptHandler import script, isScriptWaiting, willSayAllResume import aria import controlTypes from controlTypes import OutputReason @@ -45,8 +46,6 @@ from typing import Optional -REASON_QUICKNAV = OutputReason.QUICKNAV - def reportPassThrough(treeInterceptor,onlyIfChanged=True): """Reports the pass through mode if it has changed. @param treeInterceptor: The current Browse Mode treeInterceptor. @@ -203,7 +202,7 @@ def report(self,readUnit=None): if info.compareEndPoints(fieldInfo, "endToEnd") > 0: # We've expanded past the end of the field, so limit to the end of the field. info.setEndPoint(fieldInfo, "endToEnd") - speech.speakTextInfo(info, reason=controlTypes.REASON_FOCUS) + speech.speakTextInfo(info, reason=OutputReason.FOCUS) def activate(self): self.textInfo.obj._activatePosition(info=self.textInfo) @@ -217,7 +216,7 @@ def moveTo(self): reportPassThrough(self.document) info = self.textInfo.copy() info.collapse() - self.document._set_selection(info, reason=REASON_QUICKNAV) + self.document._set_selection(info, reason=OutputReason.QUICKNAV) @property def isAfterSelection(self): @@ -251,9 +250,17 @@ def _getLabelForProperties(self, labelPropertyGetter): # Translators: Reported label in the elements list for an element which which has no name and value unlabeled = _("Unlabeled") realStates = labelPropertyGetter("states") - labeledStates = " ".join(controlTypes.processAndLabelStates(role, realStates, controlTypes.REASON_FOCUS)) + labeledStates = " ".join(controlTypes.processAndLabelStates(role, realStates, OutputReason.FOCUS)) if self.itemType == "formField": - if role in (controlTypes.ROLE_BUTTON,controlTypes.ROLE_DROPDOWNBUTTON,controlTypes.ROLE_TOGGLEBUTTON,controlTypes.ROLE_SPLITBUTTON,controlTypes.ROLE_MENUBUTTON,controlTypes.ROLE_DROPDOWNBUTTONGRID,controlTypes.ROLE_SPINBUTTON,controlTypes.ROLE_TREEVIEWBUTTON): + if role in ( + controlTypes.ROLE_BUTTON, + controlTypes.ROLE_DROPDOWNBUTTON, + controlTypes.ROLE_TOGGLEBUTTON, + controlTypes.ROLE_SPLITBUTTON, + controlTypes.ROLE_MENUBUTTON, + controlTypes.ROLE_DROPDOWNBUTTONGRID, + controlTypes.ROLE_TREEVIEWBUTTON + ): # Example output: Mute; toggle button; pressed labelParts = (content or name or unlabeled, roleText, labeledStates) else: @@ -323,17 +330,18 @@ def shouldPassThrough(self, obj, reason: Optional[OutputReason] = None): """Determine whether pass through mode should be enabled (focus mode) or disabled (browse mode) for a given object. @param obj: The object in question. @type obj: L{NVDAObjects.NVDAObject} - @param reason: The reason for this query; one of the output reasons, L{REASON_QUICKNAV}, or C{None} for manual pass through mode activation by the user. + @param reason: The reason for this query; + one of the output reasons, or C{None} for manual pass through mode activation by the user. @return: C{True} if pass through mode (focus mode) should be enabled, C{False} if it should be disabled (browse mode). """ if reason and ( self.disableAutoPassThrough - or (reason == controlTypes.REASON_FOCUS and not config.conf["virtualBuffers"]["autoPassThroughOnFocusChange"]) - or (reason == controlTypes.REASON_CARET and not config.conf["virtualBuffers"]["autoPassThroughOnCaretMove"]) + or (reason == OutputReason.FOCUS and not config.conf["virtualBuffers"]["autoPassThroughOnFocusChange"]) + or (reason == OutputReason.CARET and not config.conf["virtualBuffers"]["autoPassThroughOnCaretMove"]) ): # This check relates to auto pass through and auto pass through is disabled, so don't change the pass through state. return self.passThrough - if reason == REASON_QUICKNAV: + if reason == OutputReason.QUICKNAV: return False states = obj.states role = obj.role @@ -351,7 +359,7 @@ def shouldPassThrough(self, obj, reason: Optional[OutputReason] = None): if role in self.ALWAYS_SWITCH_TO_PASS_THROUGH_ROLES or controlTypes.STATE_EDITABLE in states: return True # focus is moving to this control. Perhaps after pressing tab or clicking a button that brings up a menu (via javascript) - if reason == controlTypes.REASON_FOCUS: + if reason == OutputReason.FOCUS: if role in self.SWITCH_TO_PASS_THROUGH_ON_FOCUS_ROLES: return True # If this is a focus change, pass through should be enabled for certain ancestor containers. @@ -519,7 +527,7 @@ def _activatePosition(self, obj=None): reportPassThrough(self) elif obj.role == controlTypes.ROLE_EMBEDDEDOBJECT or obj.role in self.APPLICATION_ROLES: obj.setFocus() - speech.speakObject(obj, reason=controlTypes.REASON_FOCUS) + speech.speakObject(obj, reason=OutputReason.FOCUS) else: self._activateNVDAObject(obj) @@ -547,7 +555,7 @@ def _focusLastFocusableObject(self, activatePosition=False): # this object to change (e.g. checking a check box). However, we won't # actually get the focus event until after the change has occurred. # Therefore, we must cache properties for speech before the change occurs. - speech.speakObject(obj, controlTypes.REASON_ONLYCACHE) + speech.speakObject(obj, OutputReason.ONLYCACHE) self._objPendingFocusBeforeActivate = obj if activatePosition: # Make sure we activate the object at the caret, which is not necessarily focusable. @@ -964,9 +972,10 @@ def __init__(self, document): contentsSizer.AddSpacer(gui.guiHelper.SPACE_BETWEEN_VERTICAL_DIALOG_ITEMS) bHelper = gui.guiHelper.ButtonHelper(wx.HORIZONTAL) - # Translators: The label of a button to activate an element - # in the browse mode Elements List dialog. - self.activateButton = bHelper.addButton(self, label=_("&Activate")) + # Translators: The label of a button to activate an element in the browse mode Elements List dialog. + # Beware not to set an accelerator that would collide with other controls in this dialog, such as an + # element type radio label. + self.activateButton = bHelper.addButton(self, label=_("Activate")) self.activateButton.Bind(wx.EVT_BUTTON, lambda evt: self.onAction(True)) # Translators: The label of a button to move to an element @@ -1278,13 +1287,13 @@ def event_treeInterceptor_gainFocus(self): if not self.passThrough: if doSayAll: - speech.speakObjectProperties(self.rootNVDAObject,name=True,states=True,reason=controlTypes.REASON_FOCUS) + speech.speakObjectProperties(self.rootNVDAObject, name=True, states=True, reason=OutputReason.FOCUS) sayAllHandler.readText(sayAllHandler.CURSOR_CARET) else: # Speak it like we would speak focus on any other document object. # This includes when entering the treeInterceptor for the first time: if not hadFirstGainFocus: - speech.speakObject(self.rootNVDAObject, reason=controlTypes.REASON_FOCUS) + speech.speakObject(self.rootNVDAObject, reason=OutputReason.FOCUS) else: # And when coming in from an outside object # #4069 But not when coming up from a non-rendered descendant. @@ -1295,13 +1304,13 @@ def event_treeInterceptor_gainFocus(self): except ValueError: tl=len(ancestors) if fdl<=tl: - speech.speakObject(self.rootNVDAObject, reason=controlTypes.REASON_FOCUS) + speech.speakObject(self.rootNVDAObject, reason=OutputReason.FOCUS) info = self.selection if not info.isCollapsed: speech.speakPreselectedText(info.text) else: info.expand(textInfos.UNIT_LINE) - speech.speakTextInfo(info, reason=controlTypes.REASON_CARET, unit=textInfos.UNIT_LINE) + speech.speakTextInfo(info, reason=OutputReason.CARET, unit=textInfos.UNIT_LINE) reportPassThrough(self) braille.handler.handleGainFocus(self) @@ -1325,7 +1334,7 @@ def _activatePosition(self, obj=None, info=None): return super(BrowseModeDocumentTreeInterceptor,self)._activatePosition(obj=obj) - def _set_selection(self, info, reason=controlTypes.REASON_CARET): + def _set_selection(self, info, reason=OutputReason.CARET): super(BrowseModeDocumentTreeInterceptor, self)._set_selection(info) if isScriptWaiting() or not info.isCollapsed: return @@ -1336,7 +1345,7 @@ def _set_selection(self, info, reason=controlTypes.REASON_CARET): caret.collapse() self._lastCaretPosition = caret.bookmark review.handleCaretMove(caret) - if reason == controlTypes.REASON_FOCUS: + if reason == OutputReason.FOCUS: self._lastCaretMoveWasFocus = True focusObj = api.getFocusObject() if focusObj==self.rootNVDAObject: @@ -1471,11 +1480,11 @@ def _tabOverride(self, direction): # This node is already focused, so we need to move to and speak this node here. newCaret = newInfo.copy() newCaret.collapse() - self._set_selection(newCaret,reason=controlTypes.REASON_FOCUS) + self._set_selection(newCaret, reason=OutputReason.FOCUS) if self.passThrough: obj.event_gainFocus() else: - speech.speakTextInfo(newInfo,reason=controlTypes.REASON_FOCUS) + speech.speakTextInfo(newInfo, reason=OutputReason.FOCUS) else: # This node doesn't have the focus, so just set focus to it. The gainFocus event will handle the rest. obj.setFocus() @@ -1553,6 +1562,13 @@ def event_gainFocus(self, obj, nextHandler): return if not self.passThrough and self._shouldIgnoreFocus(obj): return + + # If the previous focus object was removed, we might hit a false positive for overlap detection. + # Track the previous focus target so that we can account for this scenario. + previousFocusObjIsDefunct = False + if self._lastFocusObj and controlTypes.STATE_DEFUNCT in self._lastFocusObj.states: + previousFocusObjIsDefunct = True + self._lastFocusObj=obj try: @@ -1560,7 +1576,7 @@ def event_gainFocus(self, obj, nextHandler): except: # This object is not in the treeInterceptor, even though it resides beneath the document. # Automatic pass through should be enabled in certain circumstances where this occurs. - if not self.passThrough and self.shouldPassThrough(obj,reason=controlTypes.REASON_FOCUS): + if not self.passThrough and self.shouldPassThrough(obj, reason=OutputReason.FOCUS): self.passThrough=True reportPassThrough(self) self._replayFocusEnteredEvents() @@ -1570,10 +1586,11 @@ def event_gainFocus(self, obj, nextHandler): caretInfo=self.makeTextInfo(textInfos.POSITION_CARET) # Expand to one character, as isOverlapping() doesn't treat, for example, (4,4) and (4,5) as overlapping. caretInfo.expand(textInfos.UNIT_CHARACTER) - if not self._hadFirstGainFocus or not focusInfo.isOverlapping(caretInfo): + isOverlapping = focusInfo.isOverlapping(caretInfo) + if not self._hadFirstGainFocus or not isOverlapping or (isOverlapping and previousFocusObjIsDefunct): # The virtual caret is not within the focus node. oldPassThrough=self.passThrough - passThrough=self.shouldPassThrough(obj,reason=controlTypes.REASON_FOCUS) + passThrough = self.shouldPassThrough(obj, reason=OutputReason.FOCUS) if not oldPassThrough and (passThrough or sayAllHandler.isRunning()): # If pass-through is disabled, cancel speech, as a focus change should cause page reading to stop. # This must be done before auto-pass-through occurs, as we want to stop page reading even if pass-through will be automatically enabled by this focus change. @@ -1581,9 +1598,9 @@ def event_gainFocus(self, obj, nextHandler): self.passThrough=passThrough if not self.passThrough: # We read the info from the browseMode document instead of the control itself. - speech.speakTextInfo(focusInfo,reason=controlTypes.REASON_FOCUS) + speech.speakTextInfo(focusInfo, reason=OutputReason.FOCUS) # However, we still want to update the speech property cache so that property changes will be spoken properly. - speech.speakObject(obj,controlTypes.REASON_ONLYCACHE) + speech.speakObject(obj, controlTypes.OutputReason.ONLYCACHE) # As we do not call nextHandler which would trigger the vision framework to handle gain focus, # we need to call it manually here. vision.handler.handleGainFocus(obj) @@ -1591,17 +1608,17 @@ def event_gainFocus(self, obj, nextHandler): # Although we are going to speak the object rather than textInfo content, we still need to silently speak the textInfo content so that the textInfo speech cache is updated correctly. # Not doing this would cause later browseMode speaking to either not speak controlFields it had entered, or speak controlField exits after having already exited. # See #7435 for a discussion on this. - speech.speakTextInfo(focusInfo,reason=controlTypes.REASON_ONLYCACHE) + speech.speakTextInfo(focusInfo, reason=OutputReason.ONLYCACHE) self._replayFocusEnteredEvents() nextHandler() focusInfo.collapse() - self._set_selection(focusInfo,reason=controlTypes.REASON_FOCUS) + self._set_selection(focusInfo, reason=OutputReason.FOCUS) else: # The virtual caret was already at the focused node. if not self.passThrough: # This focus change was caused by a virtual caret movement, so don't speak the focused node to avoid double speaking. # However, we still want to update the speech property cache so that property changes will be spoken properly. - speech.speakObject(obj,controlTypes.REASON_ONLYCACHE) + speech.speakObject(obj, OutputReason.ONLYCACHE) if config.conf["virtualBuffers"]["autoFocusFocusableElements"]: # As we do not call nextHandler which would trigger the vision framework to handle gain focus, # we need to call it manually here. @@ -1622,7 +1639,8 @@ def event_gainFocus(self, obj, nextHandler): # the properties before the activation/key, so use that to speak any # changes. speech.speakObject( - self._objPendingFocusBeforeActivate, controlTypes.REASON_CHANGE + self._objPendingFocusBeforeActivate, + OutputReason.CHANGE ) self._objPendingFocusBeforeActivate = None else: @@ -1633,14 +1651,15 @@ def event_gainFocus(self, obj, nextHandler): event_gainFocus.ignoreIsReady=True - def _handleScrollTo(self, obj): + def _handleScrollTo( + self, + obj: Union[NVDAObject, textInfos.TextInfo], + ) -> bool: """Handle scrolling the browseMode document to a given object in response to an event. Subclasses should call this from an event which indicates that the document has scrolled. @postcondition: The virtual caret is moved to L{obj} and the buffer content for L{obj} is reported. @param obj: The object to which the document should scroll. - @type obj: L{NVDAObjects.NVDAObject} @return: C{True} if the document was scrolled, C{False} if not. - @rtype: bool @note: If C{False} is returned, calling events should probably call their nextHandler. """ if self.programmaticScrollMayFireEvent and self._lastProgrammaticScrollTime and time.time() - self._lastProgrammaticScrollTime < 0.4: @@ -1649,10 +1668,15 @@ def _handleScrollTo(self, obj): # However, pretend we handled it, as we don't want it to be passed on to the object either. return True - try: - scrollInfo = self.makeTextInfo(obj) - except: - return False + if isinstance(obj, NVDAObject): + try: + scrollInfo = self.makeTextInfo(obj) + except (NotImplementedError, RuntimeError): + return False + elif isinstance(obj, textInfos.TextInfo): + scrollInfo = obj.copy() + else: + raise ValueError(f"{obj} is not a supported type") #We only want to update the caret and speak the field if we're not in the same one as before caretInfo=self.makeTextInfo(textInfos.POSITION_CARET) @@ -1661,7 +1685,7 @@ def _handleScrollTo(self, obj): if not scrollInfo.isOverlapping(caretInfo): if scrollInfo.isCollapsed: scrollInfo.expand(textInfos.UNIT_LINE) - speech.speakTextInfo(scrollInfo,reason=controlTypes.REASON_CARET) + speech.speakTextInfo(scrollInfo, reason=OutputReason.CARET) scrollInfo.collapse() self.selection = scrollInfo return True @@ -1794,10 +1818,10 @@ def script_moveToStartOfContainer(self,gesture): ui.message(_("Not in a container")) return container.collapse() - self._set_selection(container, reason=REASON_QUICKNAV) + self._set_selection(container, reason=OutputReason.QUICKNAV) if not willSayAllResume(gesture): container.expand(textInfos.UNIT_LINE) - speech.speakTextInfo(container, reason=controlTypes.REASON_FOCUS) + speech.speakTextInfo(container, reason=OutputReason.FOCUS) script_moveToStartOfContainer.resumeSayAllMode=sayAllHandler.CURSOR_CARET # Translators: Description for the Move to start of container command in browse mode. script_moveToStartOfContainer.__doc__=_("Moves to the start of the container element, such as a list or table") @@ -1819,10 +1843,10 @@ def script_movePastEndOfContainer(self,gesture): # Review cursor is at the bottom line of the current navigator object. # Landing at the end of a browse mode document when trying to jump to the end of the current container. ui.message(_("Bottom")) - self._set_selection(container, reason=REASON_QUICKNAV) + self._set_selection(container, reason=OutputReason.QUICKNAV) if not willSayAllResume(gesture): container.expand(textInfos.UNIT_LINE) - speech.speakTextInfo(container, reason=controlTypes.REASON_FOCUS) + speech.speakTextInfo(container, reason=OutputReason.FOCUS) script_movePastEndOfContainer.resumeSayAllMode=sayAllHandler.CURSOR_CARET # Translators: Description for the Move past end of container command in browse mode. script_movePastEndOfContainer.__doc__=_("Moves past the end of the container element, such as a list or table") @@ -1860,3 +1884,14 @@ def _iterNotLinkBlock(self, direction="next", pos=None): "kb:shift+,": "moveToStartOfContainer", "kb:,": "movePastEndOfContainer", } + + @script( + description=_( + # Translators: the description for the toggleScreenLayout script. + "Toggles on and off if the screen layout is preserved while rendering the document content" + ), + gesture="kb:NVDA+v", + ) + def script_toggleScreenLayout(self, gesture): + # Translators: The message reported for not supported toggling of screen layout + ui.message(_("Not supported in this document.")) diff --git a/source/buildVersion.py b/source/buildVersion.py index 1333239a4cc..dd10aafa766 100644 --- a/source/buildVersion.py +++ b/source/buildVersion.py @@ -1,7 +1,7 @@ -#A part of NonVisual Desktop Access (NVDA) -#Copyright (C) 2006-2019 NV Access Limited -#This file is covered by the GNU General Public License. -#See the file COPYING for more details. +# A part of NonVisual Desktop Access (NVDA) +# Copyright (C) 2006-2020 NV Access Limited +# This file is covered by the GNU General Public License. +# See the file COPYING for more details. import os @@ -65,8 +65,8 @@ def formatVersionForGUI(year, major, minor): # Version information for NVDA name = "NVDA" -version_year = 2020 -version_major = 4 +version_year = 2021 +version_major = 1 version_minor = 0 version_build = 0 # Should not be set manually. Set in 'sconscript' provided by 'appVeyor.yml' version=_formatDevVersionString() diff --git a/source/colors.py b/source/colors.py index ead8d7b77a0..327eff5a922 100644 --- a/source/colors.py +++ b/source/colors.py @@ -14,7 +14,7 @@ class RGB(namedtuple('RGB',('red','green','blue'))): """Represents a color as an RGB (red green blue) value""" @classmethod - def fromCOLORREF(cls,c): + def fromCOLORREF(cls, c) -> "RGB": """factory method to create an RGB from a COLORREF ctypes instance""" if isinstance(c,COLORREF): c=c.value diff --git a/source/comInterfaces/UIAutomationClient.py b/source/comInterfaces/UIAutomationClient.py index a24fbb96e65..2ccf200f4a8 100644 --- a/source/comInterfaces/UIAutomationClient.py +++ b/source/comInterfaces/UIAutomationClient.py @@ -1,3 +1,7 @@ -from comtypes.gen import _944DE083_8FB8_45CF_BCB7_C477ACB2F897_0_1_0 +try: + from comtypes.gen import _944DE083_8FB8_45CF_BCB7_C477ACB2F897_0_1_0 +except ModuleNotFoundError: + import _944DE083_8FB8_45CF_BCB7_C477ACB2F897_0_1_0 + from _944DE083_8FB8_45CF_BCB7_C477ACB2F897_0_1_0 import * globals().update(_944DE083_8FB8_45CF_BCB7_C477ACB2F897_0_1_0.__dict__) __name__ = 'comtypes.gen.UIAutomationClient' \ No newline at end of file diff --git a/source/comInterfaces/readme.md b/source/comInterfaces/readme.md new file mode 100644 index 00000000000..b28d495ea72 --- /dev/null +++ b/source/comInterfaces/readme.md @@ -0,0 +1,16 @@ +The comInterfaces package is generated via SCons. +The logic for this is in `comInterfaces_sconscript`, which uses `comtypes.gen` to read `*.tlb` +files or via interface IDs. + +The interface files have an ID named file (a GUID, followed by a version number) as well as a +"friendly name" file. + +The "friendly name" file generated by comtypes is not consumed easily by tools and IDEs, +runtime logic is used to expose symbols. +To remedy this, the file is then processed by `comInterfaces_sconscript` to extract the module +name and replace the import statement with a more elaborate approach which includes a fallback +for the purposes of IDEs and tools. + +Only UIAutomation.py is not generated, UIA has historically been updated regularly and the version on +Appveyor build servers could not be guaranteed. + diff --git a/source/comInterfaces_sconscript b/source/comInterfaces_sconscript index 3a6d93a5d75..ef4a25b46dc 100755 --- a/source/comInterfaces_sconscript +++ b/source/comInterfaces_sconscript @@ -11,6 +11,10 @@ #This license can be found at: #http://www.gnu.org/licenses/old-licenses/gpl-2.0.html ### +from typing import List +import fileinput +import re +from SCons.Node.FS import File Import( 'env', @@ -26,18 +30,49 @@ def new_my_import(fullname): return old_my_import(fullname) comtypes.client._generate._my_import = new_my_import -def interfaceAction(target,source,env): - clsid=env.get('clsid') +def makeIDEFriendly(path:str) -> None: + """ + Add a local import of * so that tools and IDE's can find definitions. + Prefer to import from comtypes.gen, at runtime behavior will not have changed. + @param path: Path to the friendly name comInterfaces module. + """ + importTemplate = ( +"""try: + from comtypes.gen import {libIdentifier} +except ModuleNotFoundError: + import {libIdentifier} + from {libIdentifier} import * +""") + + importPattern = re.compile(r"from comtypes\.gen import ([\w]+)\n") + for line in fileinput.input(path, inplace=True): + # Note: The fileinput module temporarily redirects stdout to the file. + # So print is used to write the file. + match = importPattern.match(line) + if match: + libId = match.group(1) + print(importTemplate.format(libIdentifier=libId), end='') + else: + print(line, end='') + +def interfaceAction(target:List[File], source, env): + clsid = env.get('clsid') if clsid: - source=(clsid,env['majorVersion'],env['minorVersion']) + source=(clsid, env['majorVersion'], env['minorVersion']) else: source=str(source[0]) comtypes.client.GetModule(source) + # re-write the the "friendlyNameFile" so that tools/IDEs can find the + # definitions + for t in target: + path: str = t.abspath + if path.endswith(".py"): + makeIDEFriendly(path) interfaceBuilder=env.Builder( action=env.Action(interfaceAction), ) -env['BUILDERS']['comtypesInterface']=interfaceBuilder +env['BUILDERS']['comtypesInterface'] = interfaceBuilder # Force comtypes generated interfaces in to our directory import comtypes.client @@ -46,23 +81,41 @@ comtypes.client.gen_dir=Dir('comInterfaces').abspath COM_INTERFACES = { "IAccessible2Lib.py": "typelibs/ia2.tlb", "ISimpleDOM.py": "typelibs/ISimpleDOMNode.tlb", - "mathPlayer.py": "typelibs/mathPlayerDLL.tlb", - #"Accessibility.py": ('{1EA4DBF0-3C3B-11CF-810C-00AA00389B71}',1,0), + "MathPlayer.py": "typelibs/mathPlayerDLL.tlb", + "Accessibility.py": ('{1EA4DBF0-3C3B-11CF-810C-00AA00389B71}',1,0), "tom.py": ('{8CC497C9-A1DF-11CE-8098-00AA0047BE5D}',1,0), "SpeechLib.py": ('{C866CA3A-32F7-11D2-9602-00C04F8EE628}',5,0), "AcrobatAccessLib.py": "typelibs/AcrobatAccess.tlb", - "FlashAccessibility.py": "typelibs/FlashAccessibility.tlb", + # We don't generate UIAutomationClient (and _944DE083_8FB8_45CF_BCB7_C477ACB2F897_0_1_0.py) + # because we don't know what version is available on Appveyor. + # Instead we locally generate the file and include it in the repository. + # Ensuring that adjustments performed by 'makeIDEFriendly' are incorporated. + # "UIAutomationClient.py": ('{944DE083-8FB8-45CF-BCB7-C477ACB2F897}', 1, 0), } for k,v in COM_INTERFACES.items(): - targets=[Dir('comInterfaces').File(k), - # This buillds a .pyc file as well. - Dir('comInterfaces').File(importlib.util.cache_from_source(k))] - source=clsid=majorVersion=None + targets=[ + Dir('comInterfaces').File(k), + # This builds a .pyc file as well. + Dir('comInterfaces').File(importlib.util.cache_from_source(k)) + ] + source = clsid = majorVersion = None if isinstance(v, str): - env.comtypesInterface(targets,v) + env.comtypesInterface(targets, v) else: - env.comtypesInterface(targets,Dir('comInterfaces').File('__init__.py'),clsid=v[0],majorVersion=v[1],minorVersion=v[2]) + env.comtypesInterface( + targets, + Dir('comInterfaces').File('__init__.py'), + clsid=v[0], + majorVersion=v[1], + minorVersion=v[2] + ) + -#When cleaning comInterfaces get rid of everything except for things starting with __ (e.g. __init__.py) -env.Clean(Dir('comInterfaces'),Glob('comInterfaces/[!_]*')+Glob('comInterfaces/_[!_]*')) + +# When cleaning comInterfaces get rid of everything +# except for things starting with __ (e.g. __init__.py) +env.Clean( + Dir('comInterfaces'), + Glob('comInterfaces/[!_]*') + Glob('comInterfaces/_[!_]*') +) diff --git a/source/compoundDocuments.py b/source/compoundDocuments.py index 97a437db056..8087b02c3dd 100644 --- a/source/compoundDocuments.py +++ b/source/compoundDocuments.py @@ -449,7 +449,7 @@ def _get_caretObject(self): return eventHandler.lastQueuedFocusObject def event_treeInterceptor_gainFocus(self): - speech.speakObject(self.rootNVDAObject, reason=controlTypes.REASON_FOCUS) + speech.speakObject(self.rootNVDAObject, reason=controlTypes.OutputReason.FOCUS) try: info = self.makeTextInfo(textInfos.POSITION_SELECTION) except RuntimeError: @@ -457,7 +457,7 @@ def event_treeInterceptor_gainFocus(self): else: if info.isCollapsed: info.expand(textInfos.UNIT_LINE) - speech.speakTextInfo(info, unit=textInfos.UNIT_LINE, reason=controlTypes.REASON_CARET) + speech.speakTextInfo(info, unit=textInfos.UNIT_LINE, reason=controlTypes.OutputReason.CARET) else: speech.speakPreselectedText(info.text) braille.handler.handleGainFocus(self) diff --git a/source/comtypesMonkeyPatches.py b/source/comtypesMonkeyPatches.py index 99248a79a1c..beff7550681 100644 --- a/source/comtypesMonkeyPatches.py +++ b/source/comtypesMonkeyPatches.py @@ -8,6 +8,7 @@ import ctypes import _ctypes import importlib +import sys # A version of ctypes.WINFUNCTYPE @@ -95,14 +96,23 @@ def new__call__(self,*args,**kwargs): # This causes Release() to be called more than it should, which is very nasty and will eventually cause us to access pointers which have been freed. from comtypes import _compointer_base -_cpbDel = _compointer_base.__del__ +_compointer_base._oldCpbDel = _compointer_base.__del__ def newCpbDel(self): + # __del__ may be called while Python is exiting. + # In this state, global symbols may be set to None + # Therefore avoid calling into garbageHandler or log, + # unless isFinalizing is checked first to ensure they are still available + # Using local variables or calling other methods on this class is still okay. + isFinalizingFunc = getattr(sys, 'is_finalizing', lambda: True) + isFinalizing = isFinalizingFunc() if hasattr(self, "_deleted"): # Don't allow this to be called more than once. - log.debugWarning("COM pointer %r already deleted" % self) + if not isFinalizing: + log.debugWarning("COM pointer %r already deleted" % self) return - garbageHandler.notifyObjectDeletion(self) - _cpbDel(self) + if not isFinalizing: + garbageHandler.notifyObjectDeletion(self) + self._oldCpbDel() self._deleted = True newCpbDel.__name__ = "__del__" _compointer_base.__del__ = newCpbDel @@ -130,7 +140,9 @@ def newGetTypeInfo(self,index,lcid=0): # comtypes doesn't let us disable this when running from source, so we need to monkey patch. # This is just the code from the original comtypes._check_version excluding the time check. import comtypes -def _check_version(actual): + + +def _check_version(actual, tlib_cached_mtime=None): from comtypes.tools.codegenerator import version as required if actual != required: raise ImportError("Wrong version") diff --git a/source/config/__init__.py b/source/config/__init__.py index 55c689897e5..fe3e38958ea 100644 --- a/source/config/__init__.py +++ b/source/config/__init__.py @@ -1,6 +1,5 @@ -# -*- coding: UTF-8 -*- # A part of NonVisual Desktop Access (NVDA) -# Copyright (C) 2006-2020 NV Access Limited, Aleksey Sadovoy, Peter Vágner, Rui Batista, Zahari Yurukov, +# Copyright (C) 2006-2021 NV Access Limited, Aleksey Sadovoy, Peter Vágner, Rui Batista, Zahari Yurukov, # Joseph Lee, Babbage B.V., Łukasz Golonka, Julien Cochuyt # This file is covered by the GNU General Public License. # See the file COPYING for more details. @@ -34,7 +33,7 @@ import extensionPoints from . import profileUpgrader from .configSpec import confspec -from typing import Optional, List +from typing import Any, Dict, List, Optional, Set #: True if NVDA is running as a Windows Store Desktop Bridge application isAppX=False @@ -191,8 +190,10 @@ def initConfigPath(configPath=None): RUN_REGKEY = r"SOFTWARE\Microsoft\Windows\CurrentVersion\Run" def getStartAfterLogon(): - if (easeOfAccess.isSupported and easeOfAccess.canConfigTerminateOnDesktopSwitch - and easeOfAccess.willAutoStart(winreg.HKEY_CURRENT_USER)): + if ( + easeOfAccess.canConfigTerminateOnDesktopSwitch + and easeOfAccess.willAutoStart(winreg.HKEY_CURRENT_USER) + ): return True try: k = winreg.OpenKey(winreg.HKEY_CURRENT_USER, RUN_REGKEY) @@ -204,7 +205,7 @@ def getStartAfterLogon(): def setStartAfterLogon(enable): if getStartAfterLogon() == enable: return - if easeOfAccess.isSupported and easeOfAccess.canConfigTerminateOnDesktopSwitch: + if easeOfAccess.canConfigTerminateOnDesktopSwitch: easeOfAccess.setAutoStart(winreg.HKEY_CURRENT_USER, enable) if enable: return @@ -222,10 +223,6 @@ def setStartAfterLogon(enable): except WindowsError: pass -def canStartOnSecureScreens(): - # No more need to check for the NVDA service nor presence of Ease of Access, as only Windows 7 SP1 and higher is supported. - # This function will be transformed into a flag in a future release. - return isInstalledCopy() SLAVE_FILENAME = os.path.join(globalVars.appDir, "nvda_slave.exe") @@ -235,7 +232,7 @@ def canStartOnSecureScreens(): NVDA_REGKEY = r"SOFTWARE\NVDA" def getStartOnLogonScreen(): - if easeOfAccess.isSupported and easeOfAccess.willAutoStart(winreg.HKEY_LOCAL_MACHINE): + if easeOfAccess.willAutoStart(winreg.HKEY_LOCAL_MACHINE): return True try: k = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, NVDA_REGKEY) @@ -244,13 +241,7 @@ def getStartOnLogonScreen(): return False def _setStartOnLogonScreen(enable): - if easeOfAccess.isSupported: - # The installer will have migrated service config to EoA if appropriate, - # so we only need to deal with EoA here. - easeOfAccess.setAutoStart(winreg.HKEY_LOCAL_MACHINE, enable) - else: - k = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, NVDA_REGKEY, 0, winreg.KEY_WRITE) - winreg.SetValueEx(k, u"startOnLogonScreen", None, winreg.REG_DWORD, int(enable)) + easeOfAccess.setAutoStart(winreg.HKEY_LOCAL_MACHINE, enable) def setSystemConfigToCurrentConfig(): fromPath = globalVars.appArgs.configPath @@ -310,18 +301,6 @@ def setStartOnLogonScreen(enable): ) != 0: raise RuntimeError("Slave failed to set startOnLogonScreen") -def getConfigDirs(subpath=None): - """Retrieve all directories that should be used when searching for configuration. - IF C{subpath} is provided, it will be added to each directory returned. - @param subpath: The path to be added to each directory, C{None} for none. - @type subpath: str - @return: The configuration directories in the order in which they should be searched. - @rtype: list of str - """ - log.warning("getConfigDirs is deprecated. Use globalVars.appArgs.configPath instead") - return [os.path.join(dir, subpath) if subpath else dir - for dir in (globalVars.appArgs.configPath,) - ] def addConfigDirsToPythonPackagePath(module, subdir=None): """Add the configuration directories to the module search path (__path__) of a Python package. @@ -1179,16 +1158,3 @@ def exit(self): def __exit__(self, excType, excVal, traceback): self.exit() -# The below functions are moved to systemUtils module. -# They are kept here for backwards compatibility. -# They would be removed from the config module in NVDA 2021.1. - - -def execElevated(*args, **kwargs): - import systemUtils - systemUtils.execElevated(*args, **kwargs) - - -def hasUiAccess(*args, **kwargs): - import systemUtils - systemUtils.hasUiAccess(*args, **kwargs) diff --git a/source/config/configSpec.py b/source/config/configSpec.py index 9e23fbe4fe7..6e95716537f 100644 --- a/source/config/configSpec.py +++ b/source/config/configSpec.py @@ -219,12 +219,16 @@ [UIA] enabled = boolean(default=true) useInMSWordWhenAvailable = boolean(default=false) + useInMSExcelWhenAvailable = boolean(default=false) winConsoleImplementation= option("auto", "legacy", "UIA", default="auto") selectiveEventRegistration = boolean(default=false) + # 0:default, 1:Only when necessary, 2:yes, 3:no + allowInChromium = integer(0, 3, default=0) [terminals] speakPasswords = boolean(default=false) keyboardSupportInLegacy = boolean(default=True) + diffAlgo = option("auto", "dmp", "difflib", default="auto") [update] autoCheck = boolean(default=true) diff --git a/source/contentRecog/recogUi.py b/source/contentRecog/recogUi.py index 888c2d0cc0d..cd21ad15a53 100644 --- a/source/contentRecog/recogUi.py +++ b/source/contentRecog/recogUi.py @@ -52,29 +52,8 @@ def makeTextInfo(self, position): ti.collapse() else: ti = self.result.makeTextInfo(self, position) - if not isinstance(ti, BaseContentRecogTextInfo): - # Support of TextInfos that do not inherit from BaseContentRecogTextInfo is deprecated - # and will be removed in NVDA 2020.1. - log.warning( - f"Deprecation: {type(ti)} must inherit from {BaseContentRecogTextInfo} to avoid reference cycles." - ) - ti = self._patchTextInfo(ti) return ti - def _patchTextInfo(self, info): - # Patch TextInfos so that updateSelection/Caret updates our fake selection. - info.updateCaret = lambda: self._setSelection(info, True) - info.updateSelection = lambda: self._setSelection(info, False) - # Ensure any copies get patched too. - oldCopy = info.copy - info.copy = lambda: self._patchTextInfo(oldCopy()) - return info - - def _setSelection(self, textInfo, collapse): - self._selection = textInfo.copy() - if collapse: - self._selection.collapse() - def setFocus(self): ti = self.parent.treeInterceptor if isinstance(ti, browseMode.BrowseModeDocumentTreeInterceptor): diff --git a/source/controlTypes.py b/source/controlTypes.py index d74e8b73636..1a9395a7cb8 100644 --- a/source/controlTypes.py +++ b/source/controlTypes.py @@ -1,11 +1,13 @@ -#controlTypes.py -#A part of NonVisual Desktop Access (NVDA) -#This file is covered by the GNU General Public License. -#See the file COPYING for more details. -#Copyright (C) 2007-2016 NV Access Limited, Babbage B.V. +# A part of NonVisual Desktop Access (NVDA) +# This file is covered by the GNU General Public License. +# See the file COPYING for more details. +# Copyright (C) 2007-2021 NV Access Limited, Babbage B.V. + from typing import Dict, Union, Set, Any, Optional, List from enum import Enum, auto +from logHandler import log + ROLE_UNKNOWN=0 ROLE_WINDOW=1 ROLE_TITLEBAR=2 @@ -109,7 +111,6 @@ ROLE_MENUBUTTON=102 ROLE_DROPDOWNBUTTONGRID=103 ROLE_MATH=104 -ROLE_EQUATION=ROLE_MATH # Deprecated; for backwards compatibility. ROLE_GRIP=105 ROLE_HOTKEYFIELD=106 ROLE_INDICATOR=107 @@ -334,7 +335,7 @@ ROLE_ICON:_("icon"), # Translators: Identifies a directory pane. ROLE_DIRECTORYPANE:_("directory pane"), - # Translators: Identifies an embedded object such as flash content on webpages. + # Translators: Identifies an object that is embedded in a document. ROLE_EMBEDDEDOBJECT:_("embedded object"), # Translators: Identifies an end note. ROLE_ENDNOTE:_("end note"), @@ -648,35 +649,50 @@ class OutputReason(Enum): QUICKNAV = auto() -# The following constants are kept for backwards compatibility. -# In future, OutputReason should be used directly +class IsCurrent(Enum): + """Values to use within NVDA to denote 'current' values. + These describe if an item is the current item within a particular kind of selection. + EG aria-current + """ + NO = "false" + YES = "true" + PAGE = "page" + STEP = "step" + LOCATION = "location" + DATE = "date" + TIME = "time" -REASON_FOCUS = OutputReason.FOCUS -REASON_FOCUSENTERED = OutputReason.FOCUSENTERED -REASON_MOUSE = OutputReason.MOUSE -REASON_QUERY = OutputReason.QUERY -REASON_CHANGE = OutputReason.CHANGE -REASON_MESSAGE = OutputReason.MESSAGE -REASON_SAYALL = OutputReason.SAYALL -REASON_CARET = OutputReason.CARET -REASON_ONLYCACHE = OutputReason.ONLYCACHE + @property + def displayString(self): + """ + @return: The translated UI display string that should be used for this value of the IsCurrent enum + """ + try: + return _isCurrentLabels[self] + except KeyError: + log.debugWarning(f"No translation mapping for: {self}") + # there is a value for 'current' but NVDA hasn't learned about it yet, + # at least describe in the general sense that this item is 'current' + return _isCurrentLabels[IsCurrent.YES] -#: Text to use for 'current' values. These describe if an item is the current item -#: within a particular kind of selection. -isCurrentLabels: Dict[Union[bool, str], str] = { + +#: Text to use for 'current' values. These describe if an item is the current item +#: within a particular kind of selection. EG aria-current +_isCurrentLabels: Dict[Enum, str] = { + IsCurrent.NO: "", # There is nothing extra to say for items that are not current. # Translators: Presented when an item is marked as current in a collection of items - True:_("current"), + IsCurrent.YES: _("current"), # Translators: Presented when a page item is marked as current in a collection of page items - "page":_("current page"), + IsCurrent.PAGE: _("current page"), # Translators: Presented when a step item is marked as current in a collection of step items - "step":_("current step"), + IsCurrent.STEP: _("current step"), # Translators: Presented when a location item is marked as current in a collection of location items - "location":_("current location"), + IsCurrent.LOCATION: _("current location"), # Translators: Presented when a date item is marked as current in a collection of date items - "date":_("current date"), + IsCurrent.DATE: _("current date"), # Translators: Presented when a time item is marked as current in a collection of time items - "time":_("current time"), + IsCurrent.TIME: _("current time"), } @@ -687,8 +703,8 @@ def processPositiveStates(role, states, reason: OutputReason, positiveStates=Non @type role: int @param states: The raw states for an object to process. @type states: set - @param reason: The reason to process the states (e.g. C{REASON_FOCUS}. - @param positiveStates: Used for C{REASON_CHANGE}, specifies states changed from negative to positive; + @param reason: The reason to process the states (e.g. C{OutputReason.FOCUS}. + @param positiveStates: Used for C{OutputReason.CHANGE}, specifies states changed from negative to positive; @type positiveStates: set @return: The processed positive states. @rtype: set @@ -714,14 +730,14 @@ def processPositiveStates(role, states, reason: OutputReason, positiveStates=Non # or reporting clickable just isn't useful, # or the user has explicitly requested no reporting clickable positiveStates.discard(STATE_CLICKABLE) - if reason == REASON_QUERY: + if reason == OutputReason.QUERY: return positiveStates positiveStates.discard(STATE_DEFUNCT) positiveStates.discard(STATE_MODAL) positiveStates.discard(STATE_FOCUSED) positiveStates.discard(STATE_OFFSCREEN) positiveStates.discard(STATE_INVISIBLE) - if reason != REASON_CHANGE: + if reason != OutputReason.CHANGE: positiveStates.discard(STATE_LINKED) if role in ( ROLE_LISTITEM, @@ -751,13 +767,13 @@ def processNegativeStates(role, states, reason: OutputReason, negativeStates=Non @type role: int @param states: The raw states for an object to process. @type states: set - @param reason: The reason to process the states (e.g. C{REASON_FOCUS}. - @param negativeStates: Used for C{REASON_CHANGE}, specifies states changed from positive to negative; + @param reason: The reason to process the states (e.g. C{OutputReason.FOCUS}. + @param negativeStates: Used for C{OutputReason.CHANGE}, specifies states changed from positive to negative; @type negativeStates: set @return: The processed negative states. @rtype: set """ - if reason == REASON_CHANGE and not isinstance(negativeStates, set): + if reason == OutputReason.CHANGE and not isinstance(negativeStates, set): raise TypeError("negativeStates must be a set for this reason") speakNegatives = set() # Add the negative selected state if the control is selectable, @@ -771,7 +787,7 @@ def processNegativeStates(role, states, reason: OutputReason, negativeStates=Non and STATE_FOCUSABLE in states # Only include if reporting the focus or when states are changing on the focus. # This is to avoid exposing it for things like caret movement in browse mode. - and (reason == REASON_FOCUS or (reason == REASON_CHANGE and STATE_FOCUSED in states)) + and (reason == OutputReason.FOCUS or (reason == OutputReason.CHANGE and STATE_FOCUSED in states)) and role in ( ROLE_LISTITEM, ROLE_TREEVIEWITEM, @@ -784,11 +800,15 @@ def processNegativeStates(role, states, reason: OutputReason, negativeStates=Non ): speakNegatives.add(STATE_SELECTED) # Restrict "not checked" in a similar way to "not selected". - if (role in (ROLE_CHECKBOX, ROLE_RADIOBUTTON, ROLE_CHECKMENUITEM) or STATE_CHECKABLE in states) and (STATE_HALFCHECKED not in states) and (reason != REASON_CHANGE or STATE_FOCUSED in states): + if( + (role in (ROLE_CHECKBOX, ROLE_RADIOBUTTON, ROLE_CHECKMENUITEM) or STATE_CHECKABLE in states) + and (STATE_HALFCHECKED not in states) + and (reason != OutputReason.CHANGE or STATE_FOCUSED in states) + ): speakNegatives.add(STATE_CHECKED) if role == ROLE_TOGGLEBUTTON: speakNegatives.add(STATE_PRESSED) - if reason == REASON_CHANGE: + if reason == OutputReason.CHANGE: # We want to speak this state only if it is changing to negative. speakNegatives.add(STATE_DROPTARGET) # We were given states which have changed to negative. @@ -821,9 +841,9 @@ def processAndLabelStates( """Processes the states for an object and returns the appropriate state labels for both positive and negative states. @param role: The role of the object to process states for (e.g. C{ROLE_CHECKBOX}. @param states: The raw states for an object to process. - @param reason: The reason to process the states (e.g. C{REASON_FOCUS}. - @param positiveStates: Used for C{REASON_CHANGE}, specifies states changed from negative to positive; - @param negativeStates: Used for C{REASON_CHANGE}, specifies states changed from positive to negative; + @param reason: The reason to process the states (e.g. C{OutputReason.FOCUS}. + @param positiveStates: Used for C{OutputReason.CHANGE}, specifies states changed from negative to positive; + @param negativeStates: Used for C{OutputReason.CHANGE}, specifies states changed from positive to negative; @param positiveStateLabelDict: Dictionary containing state identifiers as keys and associated positive labels as their values. @param negativeStateLabelDict: Dictionary containing state identifiers as keys and associated negative labels as their values. @return: The labels of the relevant positive and negative states. diff --git a/source/core.py b/source/core.py index 00d34fdf9aa..3304d197ae6 100644 --- a/source/core.py +++ b/source/core.py @@ -1,10 +1,11 @@ -# -*- coding: UTF-8 -*- # A part of NonVisual Desktop Access (NVDA) -# Copyright (C) 2006-2019 NV Access Limited, Aleksey Sadovoy, Christopher Toth, Joseph Lee, Peter Vágner, +# Copyright (C) 2006-2021 NV Access Limited, Aleksey Sadovoy, Christopher Toth, Joseph Lee, Peter Vágner, # Derek Riemer, Babbage B.V., Zahari Yurukov, Łukasz Golonka # This file is covered by the GNU General Public License. # See the file COPYING for more details. +from typing import Optional + """NVDA core""" RPC_E_CALL_CANCELED = -2147418110 @@ -42,6 +43,8 @@ class CallCancelled(Exception): # inform those who want to know that NVDA has finished starting up. postNvdaStartup = extensionPoints.Action() +# inform those who want to know that NVDA has begun to exit. +preNVDAExit = extensionPoints.Action() PUMP_MAX_DELAY = 10 @@ -78,7 +81,8 @@ def doStartupDialogs(): _("Configuration File Error"), wx.OK | wx.ICON_EXCLAMATION) if config.conf["general"]["showWelcomeDialogAtStartup"]: - gui.WelcomeDialog.run() + from gui.startupDialogs import WelcomeDialog + WelcomeDialog.run() if config.conf["brailleViewer"]["showBrailleViewerAtStartup"]: gui.mainFrame.onToggleBrailleViewerCommand(evt=None) if config.conf["speechViewer"]["showSpeechViewerAtStartup"]: @@ -104,14 +108,13 @@ def onResult(ID): except: pass # Ask the user if usage stats can be collected. - gui.runScriptModalDialog(gui.AskAllowUsageStatsDialog(None),onResult) + gui.runScriptModalDialog(gui.startupDialogs.AskAllowUsageStatsDialog(None), onResult) def restart(disableAddons=False, debugLogging=False): """Restarts NVDA by starting a new copy.""" if globalVars.appArgs.launcher: - import wx globalVars.exitCode=3 - wx.GetApp().ExitMainLoop() + triggerNVDAExit() return import subprocess import winUser @@ -207,6 +210,85 @@ def _setInitialFocus(): except: log.exception("Error retrieving initial focus") + +def getWxLangOrNone() -> Optional['wx.LanguageInfo']: + import languageHandler + import wx + lang = languageHandler.getLanguage() + locale = wx.Locale() + wxLang = locale.FindLanguageInfo(lang) + if not wxLang and '_' in lang: + wxLang = locale.FindLanguageInfo(lang.split('_')[0]) + # #8064: Wx might know the language, but may not actually contain a translation database for that language. + # If we try to initialize this language, wx will show a warning dialog. + # #9089: some languages (such as Aragonese) do not have language info, causing language getter to fail. + # In this case, wxLang is already set to None. + # Therefore treat these situations like wx not knowing the language at all. + if wxLang and not locale.IsAvailable(wxLang.Language): + wxLang = None + if not wxLang: + log.debugWarning("wx does not support language %s" % lang) + return wxLang + + +def triggerNVDAExit(): + preNVDAExit.notify() + # ensure NVDA only runs exit procedures once + handlers = list(preNVDAExit.handlers) # don't mutate .handlers directly with unregister while iterating + for handler in handlers: + preNVDAExit.unregister(handler) + + +def _closeAllWindows(): + """ + Should only be used by calling triggerNVDAExit and after handleNVDAModuleCleanupBeforeGUIExit. + Ensures the wx mainloop is exited by all the top windows being destroyed. + wx objects that don't inherit from wx.Window (eg sysTrayIcon, Menu) need to be manually destroyed. + """ + import gui + from gui.settingsDialogs import SettingsDialog + from typing import Dict + import wx + + app = wx.GetApp() + + # prevent race condition with object deletion + # prevent deletion of the object while we work on it. + _SettingsDialog = SettingsDialog + nonWeak: Dict[_SettingsDialog, _SettingsDialog] = dict(_SettingsDialog._instances) + + for instance, state in nonWeak.items(): + if state is _SettingsDialog.DialogState.DESTROYED: + log.error( + "Destroyed but not deleted instance of gui.SettingsDialog exists" + f": {instance.title} - {instance.__class__.__qualname__} - {instance}" + ) + else: + log.debug("Exiting NVDA with an open settings dialog: {!r}".format(instance)) + + # wx.Windows destroy child Windows automatically but wx.Menu and TaskBarIcon don't inherit from wx.Window. + # They must be manually destroyed when exiting the app. + # Note: this doesn't consistently clean them from the tray and appears to be a wx issue. (#12286, #12238) + log.debug("destroying system tray icon and menu") + app.ScheduleForDestruction(gui.mainFrame.sysTrayIcon.menu) + gui.mainFrame.sysTrayIcon.RemoveIcon() + app.ScheduleForDestruction(gui.mainFrame.sysTrayIcon) + + for window in wx.GetTopLevelWindows(): + if isinstance(window, wx.Dialog) and window.IsModal(): + log.debug(f"ending modal {window} during exit process") + wx.CallAfter(window.EndModal, wx.ID_CLOSE_ALL) + elif not isinstance(window, gui.MainFrame): + log.debug(f"closing window {window} during exit process") + wx.CallAfter(window.Close) + + wx.Yield() # creates a temporary event loop and uses it instead to process pending messages + log.debug("destroying main frame during exit process") + # the MainFrame has EVT_CLOSE bound to the ExitDialog + # which calls this function on exit, so destroy this window + app.ScheduleForDestruction(gui.mainFrame) + + def main(): """NVDA's core main loop. This initializes all modules such as audio, IAccessible, keyboard, mouse, and GUI. @@ -227,17 +309,6 @@ def main(): log.debug("loading config") import config config.initialize() - if globalVars.appArgs.configPath == config.getUserDefaultConfigPath(useInstalledPathIfExists=True): - # Make sure not to offer the ability to copy the current configuration to the user account. - # This case always applies to the launcher when configPath is not overridden by the user, - # which is the default. - # However, if a user wants to run the launcher with a custom configPath, - # it is likely that he wants to copy that configuration when installing. - # This check also applies to cases where a portable copy is run using the installed configuration, - # in which case we want to avoid copying a configuration to itself. - # We set the value to C{None} in order for the gui to determine - # when to disable the checkbox for this feature. - globalVars.appArgs.copyPortableConfig = None if config.conf['development']['enableScratchpadDir']: log.info("Developer Scratchpad mode enabled") if not globalVars.appArgs.minimal and config.conf["general"]["playStartAndExitSounds"]: @@ -253,7 +324,7 @@ def main(): languageHandler.setLanguage(lang) except: log.warning("Could not set language to %s"%lang) - log.info("Using Windows version %s" % winVersion.winVersionText) + log.info(f"Windows version: {winVersion.getWinVer()}") log.info("Using Python version %s"%sys.version) log.info("Using comtypes version %s"%comtypes.__version__) import configobj @@ -285,15 +356,20 @@ def main(): # Translators: This is spoken when NVDA is starting. speech.speakMessage(_("Loading NVDA. Please wait...")) import wx - # wxPython 4 no longer has either of these constants (despite the documentation saying so), some add-ons may rely on - # them so we add it back into wx. https://wxpython.org/Phoenix/docs/html/wx.Window.html#wx.Window.Centre - wx.CENTER_ON_SCREEN = wx.CENTRE_ON_SCREEN = 0x2 import six log.info("Using wx version %s with six version %s"%(wx.version(), six.__version__)) class App(wx.App): def OnAssert(self,file,line,cond,msg): message="{file}, line {line}:\nassert {cond}: {msg}".format(file=file,line=line,cond=cond,msg=msg) log.debugWarning(message,codepath="WX Widgets",stack_info=True) + + def InitLocale(self): + # Backport of `InitLocale` from wx Python 4.1.2 as the current version tries to set a Python + # locale to an nonexistent one when creating an instance of `wx.App`. + # This causes a crash when running under a particular version of Universal CRT (#12160) + import locale + locale.setlocale(locale.LC_ALL, "C") + app = App(redirect=False) # We support queryEndSession events, but in general don't do anything for them. # However, when running as a Windows Store application, we do want to request to be restarted for updates @@ -418,26 +494,17 @@ def handlePowerStatusChange(self): # initialize wxpython localization support locale = wx.Locale() - lang=languageHandler.getLanguage() - wxLang=locale.FindLanguageInfo(lang) - if not wxLang and '_' in lang: - wxLang=locale.FindLanguageInfo(lang.split('_')[0]) + wxLang = getWxLangOrNone() if hasattr(sys,'frozen'): locale.AddCatalogLookupPathPrefix(os.path.join(globalVars.appDir, "locale")) - # #8064: Wx might know the language, but may not actually contain a translation database for that language. - # If we try to initialize this language, wx will show a warning dialog. - # #9089: some languages (such as Aragonese) do not have language info, causing language getter to fail. - # In this case, wxLang is already set to None. - # Therefore treat these situations like wx not knowing the language at all. - if wxLang and not locale.IsAvailable(wxLang.Language): - wxLang=None if wxLang: try: locale.Init(wxLang.Language) except: log.error("Failed to initialize wx locale",exc_info=True) - else: - log.debugWarning("wx does not support language %s" % lang) + finally: + # Revert wx's changes to the python locale + languageHandler.setLocale(languageHandler.curLang) log.debug("Initializing garbageHandler") garbageHandler.initialize() @@ -509,7 +576,8 @@ def handlePowerStatusChange(self): except: log.error("", exc_info=True) if globalVars.appArgs.launcher: - gui.LauncherDialog.run() + from gui.startupDialogs import LauncherDialog + LauncherDialog.run() # LauncherDialog will call doStartupDialogs() afterwards if required. else: wx.CallAfter(doStartupDialogs) @@ -561,17 +629,42 @@ def run(self): log.debug("initializing updateCheck") updateCheck.initialize() log.info("NVDA initialized") - postNvdaStartup.notify() + + # Queue the firing of the postNVDAStartup notification. + # This is queued so that it will run from within the core loop, + # and initial focus has been reported. + def _doPostNvdaStartupAction(): + log.debug("Notify of postNvdaStartup action") + postNvdaStartup.notify() + + queueHandler.queueFunction(queueHandler.eventQueue, _doPostNvdaStartupAction) + + def handleNVDAModuleCleanupBeforeGUIExit(): + """ Terminates various modules that rely on the GUI. This should be used before closing all windows + and terminating the GUI + """ + import brailleViewer + # before the GUI is terminated we must terminate the update checker + if updateCheck: + _terminate(updateCheck) + + # The core is expected to terminate, so we should not treat this as a crash + _terminate(watchdog) + # plugins must be allowed to close safely before we terminate the GUI as dialogs may be unsaved + _terminate(globalPluginHandler) + # the brailleViewer should be destroyed safely before closing the window + brailleViewer.destroyBrailleViewer() + + preNVDAExit.register(handleNVDAModuleCleanupBeforeGUIExit) + preNVDAExit.register(_closeAllWindows) log.debug("entering wx application main loop") app.MainLoop() log.info("Exiting") - if updateCheck: - _terminate(updateCheck) - - _terminate(watchdog) - _terminate(globalPluginHandler, name="global plugin handler") + # If MainLoop is terminated through WM_QUIT, such as starting an NVDA instance older than 2021.1, + # triggerNVDAExit has not been called yet + triggerNVDAExit() _terminate(gui) config.saveOnExit() @@ -605,6 +698,14 @@ def run(self): _terminate(speech) _terminate(addonHandler) _terminate(garbageHandler) + # DMP is only started if needed. + # Terminate manually (and let it write to the log if necessary) + # as core._terminate always writes an entry. + try: + import diffHandler + diffHandler._dmp._terminate() + except Exception: + log.exception("Exception while terminating DMP") if not globalVars.appArgs.minimal and config.conf["general"]["playStartAndExitSounds"]: try: diff --git a/source/cursorManager.py b/source/cursorManager.py index 3c2e17aaea1..a297b649de2 100644 --- a/source/cursorManager.py +++ b/source/cursorManager.py @@ -158,7 +158,8 @@ def _caretMovementScriptHelper(self,gesture,unit,direction=None,posConstant=text # info if it is offset-based. selection = info.copy() info.expand(unit) - if not willSayAllResume(gesture): speech.speakTextInfo(info,unit=unit,reason=controlTypes.REASON_CARET) + if not willSayAllResume(gesture): + speech.speakTextInfo(info, unit=unit, reason=controlTypes.OutputReason.CARET) if not oldInfo.isCollapsed: speech.speakSelectionChange(oldInfo, selection) self.selection = selection @@ -173,7 +174,7 @@ def doFindText(self, text, reverse=False, caseSensitive=False, willSayAllResume= speech.cancelSpeech() info.move(textInfos.UNIT_LINE,1,endPoint="end") if not willSayAllResume: - speech.speakTextInfo(info, reason=controlTypes.REASON_CARET) + speech.speakTextInfo(info, reason=controlTypes.OutputReason.CARET) else: wx.CallAfter(gui.messageBox,_('text "%s" not found')%text,_("Find Error"),wx.OK|wx.ICON_ERROR) CursorManager._lastFindText=text diff --git a/source/diffHandler.py b/source/diffHandler.py new file mode 100644 index 00000000000..4bacff6ae38 --- /dev/null +++ b/source/diffHandler.py @@ -0,0 +1,194 @@ +# A part of NonVisual Desktop Access (NVDA) +# This file is covered by the GNU General Public License. +# See the file COPYING for more details. +# Copyright (C) 2020 Bill Dengler + +import config +import globalVars +import os +import struct +import subprocess +import sys +from abc import abstractmethod +from baseObject import AutoPropertyObject +from difflib import ndiff +from logHandler import log +from textInfos import TextInfo, UNIT_LINE +from threading import Lock +from typing import List + + +class DiffAlgo(AutoPropertyObject): + @abstractmethod + def diff(self, newText: str, oldText: str) -> List[str]: + raise NotImplementedError + + @abstractmethod + def _getText(self, ti: TextInfo) -> str: + raise NotImplementedError + + +class DiffMatchPatch(DiffAlgo): + """A character-based diffing approach, using the Google Diff Match Patch + library in a proxy process (to work around a licence conflict). + """ + #: A subprocess.Popen object for the nvda_dmp process. + _proc = None + #: A lock to control access to the nvda_dmp process. + #: Control access to avoid synchronization problems if multiple threads + #: attempt to use nvda_dmp at the same time. + _lock = Lock() + + def _initialize(self): + """Start the nvda_dmp process if it is not already running. + @note: This should be run from within the context of an acquired lock.""" + if not DiffMatchPatch._proc: + log.debug("Starting diff-match-patch proxy") + if hasattr(sys, "frozen"): + dmp_path = (os.path.join(globalVars.appDir, "nvda_dmp.exe"),) + else: + dmp_path = (sys.executable, os.path.join( + globalVars.appDir, "..", "include", "nvda_dmp", "nvda_dmp.py" + )) + DiffMatchPatch._proc = subprocess.Popen( + dmp_path, + creationflags=subprocess.CREATE_NO_WINDOW, + bufsize=0, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE + ) + + def _getText(self, ti: TextInfo) -> str: + return ti.text + + def diff(self, newText: str, oldText: str) -> List[str]: + try: + if not newText and not oldText: + # Return an empty list here to avoid exiting + # nvda_dmp uses two zero-length texts as a sentinal value + return [] + with DiffMatchPatch._lock: + self._initialize() + old = oldText.encode("utf-8") + new = newText.encode("utf-8") + # Sizes are packed as 32-bit ints in native byte order. + # Since nvda and nvda_dmp are running on the same Python + # platform/version, this is okay. + tl = struct.pack("=II", len(old), len(new)) + DiffMatchPatch._proc.stdin.write(tl) + DiffMatchPatch._proc.stdin.write(old) + DiffMatchPatch._proc.stdin.write(new) + buf = b"" + sizeb = b"" + SIZELEN = 4 + while len(sizeb) < SIZELEN: + try: + sizeb += DiffMatchPatch._proc.stdout.read(SIZELEN - len(sizeb)) + except TypeError: + pass + (size,) = struct.unpack("=I", sizeb) + while len(buf) < size: + buf += DiffMatchPatch._proc.stdout.read(size - len(buf)) + DiffMatchPatch._proc.stdin.flush() + DiffMatchPatch._proc.stdout.flush() + return [ + line + for line in buf.decode("utf-8").splitlines() + if line and not line.isspace() + ] + except Exception: + log.exception("Exception in DMP, falling back to difflib") + return Difflib().diff(newText, oldText) + + def _terminate(self): + with DiffMatchPatch._lock: + if DiffMatchPatch._proc: + log.debug("Terminating diff-match-patch proxy") + # nvda_dmp exits when it receives two zero-length texts. + DiffMatchPatch._proc.stdin.write(struct.pack("=II", 0, 0)) + DiffMatchPatch._proc.wait(timeout=5) + + +class Difflib(DiffAlgo): + "A line-based diffing approach in pure Python, using the Python standard library." + + def diff(self, newText: str, oldText: str) -> List[str]: + newLines = newText.splitlines() + oldLines = oldText.splitlines() + outLines = [] + + prevLine = None + + for line in ndiff(oldLines, newLines): + if line[0] == "?": + # We're never interested in these. + continue + if line[0] != "+": + # We're only interested in new lines. + prevLine = line + continue + text = line[2:] + if not text or text.isspace(): + prevLine = line + continue + + if prevLine and prevLine[0] == "-" and len(prevLine) > 2: + # It's possible that only a few characters have changed in this line. + # If so, we want to speak just the changed section, rather than the entire line. + prevText = prevLine[2:] + textLen = len(text) + prevTextLen = len(prevText) + # Find the first character that differs between the two lines. + for pos in range(min(textLen, prevTextLen)): + if text[pos] != prevText[pos]: + start = pos + break + else: + # We haven't found a differing character so far and we've hit the end of one of the lines. + # This means that the differing text starts here. + start = pos + 1 + # Find the end of the differing text. + if textLen != prevTextLen: + # The lines are different lengths, so assume the rest of the line changed. + end = textLen + else: + for pos in range(textLen - 1, start - 1, -1): + if text[pos] != prevText[pos]: + end = pos + 1 + break + + if end - start < 15: + # Less than 15 characters have changed, so only speak the changed chunk. + text = text[start:end] + + if text and not text.isspace(): + outLines.append(text) + prevLine = line + + return outLines + + def _getText(self, ti: TextInfo) -> str: + return "\n".join(ti.getTextInChunks(UNIT_LINE)) + + +def get_dmp_algo(): + """ + This function returns a Diff Match Patch object if allowed by the user. + DMP is experimental and can be explicitly enabled/disabled by a user + setting to opt in or out of the experiment. If config does not allow + DMP, this function returns a Difflib instance instead. + """ + return ( + _dmp + if config.conf["terminals"]["diffAlgo"] == "dmp" + else _difflib + ) + + +def get_difflib_algo(): + "Returns an instance of the difflib diffAlgo." + return _difflib + + +_difflib = Difflib() +_dmp = DiffMatchPatch() diff --git a/source/documentBase.py b/source/documentBase.py index 51aee3d3b24..84e949e26e4 100644 --- a/source/documentBase.py +++ b/source/documentBase.py @@ -162,7 +162,7 @@ def _tableMovementScriptHelper(self, movement="next", axis=None): # Retrieve the cell on which we started. info = self._getTableCellAt(tableID, self.selection,origRow, origCol) - speech.speakTextInfo(info,formatConfig=formatConfig,reason=controlTypes.REASON_CARET) + speech.speakTextInfo(info, formatConfig=formatConfig, reason=controlTypes.OutputReason.CARET) info.collapse() self.selection = info diff --git a/source/documentationUtils.py b/source/documentationUtils.py new file mode 100644 index 00000000000..db80df9c5eb --- /dev/null +++ b/source/documentationUtils.py @@ -0,0 +1,52 @@ +# -*- coding: UTF-8 -*- +# A part of NonVisual Desktop Access (NVDA) +# Copyright (C) 2006-2021 NV Access Limited, Łukasz Golonka +# This file may be used under the terms of the GNU General Public License, version 2 or later. +# For more details see: https://www.gnu.org/licenses/gpl-2.0.html + +import os +import sys + +import globalVars +import languageHandler + + +def getDocFilePath(fileName, localized=True): + if not getDocFilePath.rootPath: + if hasattr(sys, "frozen"): + getDocFilePath.rootPath = os.path.join(globalVars.appDir, "documentation") + else: + getDocFilePath.rootPath = os.path.join(globalVars.appDir, "..", "user_docs") + + if localized: + lang = languageHandler.getLanguage() + tryLangs = [lang] + if "_" in lang: + # This locale has a sub-locale, but documentation might not exist for the sub-locale, so try stripping it. + tryLangs.append(lang.split("_")[0]) + # If all else fails, use English. + tryLangs.append("en") + + fileName, fileExt = os.path.splitext(fileName) + for tryLang in tryLangs: + tryDir = os.path.join(getDocFilePath.rootPath, tryLang) + if not os.path.isdir(tryDir): + continue + + # Some out of date translations might include .txt files which are now .html files in newer translations. + # Therefore, ignore the extension and try both .html and .txt. + for tryExt in ("html", "txt"): + tryPath = os.path.join(tryDir, f"{fileName}.{tryExt}") + if os.path.isfile(tryPath): + return tryPath + return None + else: + # Not localized. + if not hasattr(sys, "frozen") and fileName in ("copying.txt", "contributors.txt"): + # If running from source, these two files are in the root dir. + return os.path.join(globalVars.appDir, "..", fileName) + else: + return os.path.join(getDocFilePath.rootPath, fileName) + + +getDocFilePath.rootPath = None diff --git a/source/driverHandler.py b/source/driverHandler.py index cee0ea31f53..889e1adaf59 100644 --- a/source/driverHandler.py +++ b/source/driverHandler.py @@ -7,18 +7,6 @@ """Handler for driver functionality that is global to synthesizers and braille displays.""" from autoSettingsUtils.autoSettings import AutoSettings -# F401: the following imports, while unused in this file, are provided for backwards compatibility. -from autoSettingsUtils.driverSetting import ( # noqa: F401 - DriverSetting, - BooleanDriverSetting, - NumericDriverSetting, - AutoPropertyObject, -) -from autoSettingsUtils.utils import ( # noqa: F401 - UnsupportedConfigParameterError, - StringParameterInfo, -) - class Driver(AutoSettings): """ diff --git a/source/easeOfAccess.py b/source/easeOfAccess.py index ba4729c5271..a1aa4ddae31 100644 --- a/source/easeOfAccess.py +++ b/source/easeOfAccess.py @@ -1,8 +1,7 @@ -#easeOfAccess.py -#A part of NonVisual Desktop Access (NVDA) -#Copyright (C) 2014 NV Access Limited -#This file is covered by the GNU General Public License. -#See the file COPYING for more details. +# A part of NonVisual Desktop Access (NVDA) +# Copyright (C) 2014-2021 NV Access Limited +# This file is covered by the GNU General Public License. +# See the file COPYING for more details. """Utilities for working with the Windows Ease of Access Center. """ @@ -10,12 +9,10 @@ import winreg import ctypes import winUser -from winVersion import winVersion +import winVersion -# Windows >= Vista -isSupported = winVersion.major >= 6 # Windows >= 8 -canConfigTerminateOnDesktopSwitch = isSupported and (winVersion.major, winVersion.minor) >= (6, 2) +canConfigTerminateOnDesktopSwitch: bool = winVersion.getWinVer() >= winVersion.WIN8 ROOT_KEY = r"Software\Microsoft\Windows NT\CurrentVersion\Accessibility" APP_KEY_NAME = "nvda_nvda_v1" diff --git a/source/editableText.py b/source/editableText.py index bcc8c9b5a7b..92f5c8278a0 100755 --- a/source/editableText.py +++ b/source/editableText.py @@ -1,7 +1,7 @@ # A part of NonVisual Desktop Access (NVDA) # This file is covered by the GNU General Public License. # See the file COPYING for more details. -# Copyright (C) 2006-2020 NV Access Limited, Davy Kager, Julien Cochuyt +# Copyright (C) 2006-2021 NV Access Limited, Davy Kager, Julien Cochuyt """Common support for editable text. @note: If you want editable text functionality for an NVDAObject, @@ -146,7 +146,7 @@ def _caretScriptPostMovedHelper(self, speakUnit, gesture, info=None): review.handleCaretMove(info) if speakUnit and not willSayAllResume(gesture): info.expand(speakUnit) - speech.speakTextInfo(info, unit=speakUnit, reason=controlTypes.REASON_CARET) + speech.speakTextInfo(info, unit=speakUnit, reason=controlTypes.OutputReason.CARET) braille.handler.handleCaretMove(self) def _caretMovementScriptHelper(self, gesture, unit): @@ -200,7 +200,13 @@ def script_caret_newLine(self,gesture): onlyInitial=True else: onlyInitial=False - speech.speakTextInfo(lineInfo,unit=textInfos.UNIT_LINE,reason=controlTypes.REASON_CARET,onlyInitialFields=onlyInitial,suppressBlanks=True) + speech.speakTextInfo( + lineInfo, + unit=textInfos.UNIT_LINE, + reason=controlTypes.OutputReason.CARET, + onlyInitialFields=onlyInitial, + suppressBlanks=True + ) def _caretMoveBySentenceHelper(self, gesture, direction): if isScriptWaiting(): @@ -254,7 +260,8 @@ def _backspaceScriptHelper(self,unit,gesture): caretMoved,newInfo=self._hasCaretMoved(oldBookmark) if not caretMoved: return - if len(delChunk)>1: + delChunk = delChunk.replace("\r\n", "\n") # Occurs with at least with Scintilla + if len(delChunk) > 1: speech.speakMessage(delChunk) else: speech.speakSpelling(delChunk) diff --git a/source/eventHandler.py b/source/eventHandler.py index 1d722135dd4..f456944c985 100755 --- a/source/eventHandler.py +++ b/source/eventHandler.py @@ -213,7 +213,7 @@ def _getFocusLossCancellableSpeechCommand( obj, reason: controlTypes.OutputReason ) -> Optional[_CancellableSpeechCommand]: - if reason != controlTypes.REASON_FOCUS or not speech.manager._shouldCancelExpiredFocusEvents(): + if reason != controlTypes.OutputReason.FOCUS or not speech.manager._shouldCancelExpiredFocusEvents(): return None from NVDAObjects import NVDAObject if not isinstance(obj, NVDAObject): diff --git a/source/garbageHandler.py b/source/garbageHandler.py index b5cb2592ee4..aa90fab0f0a 100644 --- a/source/garbageHandler.py +++ b/source/garbageHandler.py @@ -5,6 +5,7 @@ # See the file COPYING for more details. +import sys import gc import threading from logHandler import log @@ -20,7 +21,11 @@ class TrackedObject: """ def __del__(self): - notifyObjectDeletion(self) + # __del__ may still be called while Python is exiting. + # And therefore some symbols may be set to None. + isFinalizing = getattr(sys, 'is_finalizing', lambda: True)() + if not isFinalizing: + notifyObjectDeletion(self) _collectionThreadID = 0 diff --git a/source/globalCommands.py b/source/globalCommands.py index e9ecfdb5d77..c85490c8a55 100644 --- a/source/globalCommands.py +++ b/source/globalCommands.py @@ -2,15 +2,13 @@ # A part of NonVisual Desktop Access (NVDA) # This file is covered by the GNU General Public License. # See the file COPYING for more details. -# Copyright (C) 2006-2020 NV Access Limited, Peter Vágner, Aleksey Sadovoy, Rui Batista, Joseph Lee, +# Copyright (C) 2006-2021 NV Access Limited, Peter Vágner, Aleksey Sadovoy, Rui Batista, Joseph Lee, # Leonard de Ruijter, Derek Riemer, Babbage B.V., Davy Kager, Ethan Holliger, Łukasz Golonka, Accessolutions, # Julien Cochuyt -import time import itertools from typing import Optional -import tones import audioDucking import touchHandler import keyboardHandler @@ -25,7 +23,6 @@ from NVDAObjects import NVDAObject, NVDAObjectTextInfo import globalVars from logHandler import log -from synthDriverHandler import * import gui import wx import config @@ -34,13 +31,13 @@ import winKernel import treeInterceptorHandler import browseMode +import languageHandler import scriptHandler from scriptHandler import script import ui import braille import brailleInput import inputCore -import virtualBuffers import characterProcessing from baseObject import ScriptableObject import core @@ -98,6 +95,13 @@ class GlobalCommands(ScriptableObject): """Commands that are available at all times, regardless of the current focus. """ + @script( + description=_( + # Translators: Describes the Cycle audio ducking mode command. + "Cycles through audio ducking modes which determine when NVDA lowers the volume of other sounds" + ), + gesture="kb:NVDA+shift+d" + ) def script_cycleAudioDuckingMode(self,gesture): if not audioDucking.isAudioDuckingSupported(): # Translators: a message when audio ducking is not supported on this machine @@ -110,9 +114,17 @@ def script_cycleAudioDuckingMode(self,gesture): config.conf['audio']['audioDuckingMode']=nextMode nextLabel=audioDucking.audioDuckingModes[nextMode] ui.message(nextLabel) - # Translators: Describes the Cycle audio ducking mode command. - script_cycleAudioDuckingMode.__doc__=_("Cycles through audio ducking modes which determine when NVDA lowers the volume of other sounds") + @script( + description=_( + # Translators: Input help mode message for toggle input help command. + "Turns input help on or off. " + "When on, any input such as pressing a key on the keyboard " + "will tell you what script is associated with that input, if any." + ), + category=SCRCAT_INPUT, + gesture="kb:NVDA+1" + ) def script_toggleInputHelp(self,gesture): inputCore.manager.isInputHelpActive = not inputCore.manager.isInputHelpActive # Translators: This will be presented when the input help is toggled. @@ -121,10 +133,13 @@ def script_toggleInputHelp(self,gesture): stateOff = _("input help off") state = stateOn if inputCore.manager.isInputHelpActive else stateOff ui.message(state) - # Translators: Input help mode message for toggle input help command. - script_toggleInputHelp.__doc__=_("Turns input help on or off. When on, any input such as pressing a key on the keyboard will tell you what script is associated with that input, if any.") - script_toggleInputHelp.category=SCRCAT_INPUT + @script( + # Translators: Input help mode message for toggle sleep mode command. + description=_("Toggles sleep mode on and off for the active application."), + gestures=("kb(desktop):NVDA+shift+s", "kb(laptop):NVDA+shift+z"), + allowInSleepMode=True + ) def script_toggleCurrentAppSleepMode(self,gesture): curFocus=api.getFocusObject() curApp=curFocus.appModule @@ -138,10 +153,17 @@ def script_toggleCurrentAppSleepMode(self,gesture): curApp.sleepMode=True # Translators: This is presented when sleep mode is activated, the focused application is self voicing, such as klango or openbook. ui.message(_("Sleep mode on")) - # Translators: Input help mode message for toggle sleep mode command. - script_toggleCurrentAppSleepMode.__doc__=_("Toggles sleep mode on and off for the active application.") - script_toggleCurrentAppSleepMode.allowInSleepMode=True + @script( + description=_( + # Translators: Input help mode message for report current line command. + "Reports the current line under the application cursor. " + "Pressing this key twice will spell the current line. " + "Pressing three times will spell the line using character descriptions." + ), + category=SCRCAT_SYSTEMCARET, + gestures=("kb(desktop):NVDA+upArrow", "kb(laptop):NVDA+l") + ) def script_reportCurrentLine(self,gesture): obj=api.getFocusObject() treeInterceptor=obj.treeInterceptor @@ -154,31 +176,40 @@ def script_reportCurrentLine(self,gesture): info.expand(textInfos.UNIT_LINE) scriptCount=scriptHandler.getLastScriptRepeatCount() if scriptCount==0: - speech.speakTextInfo(info,unit=textInfos.UNIT_LINE,reason=controlTypes.REASON_CARET) + speech.speakTextInfo(info, unit=textInfos.UNIT_LINE, reason=controlTypes.OutputReason.CARET) else: speech.spellTextInfo(info,useCharacterDescriptions=scriptCount>1) - # Translators: Input help mode message for report current line command. - script_reportCurrentLine.__doc__=_("Reports the current line under the application cursor. Pressing this key twice will spell the current line. Pressing three times will spell the line using character descriptions.") - script_reportCurrentLine.category=SCRCAT_SYSTEMCARET + @script( + # Translators: Input help mode message for left mouse click command. + description=_("Clicks the left mouse button once at the current mouse position"), + category=SCRCAT_MOUSE, + gestures=("kb:numpadDivide", "kb(laptop):NVDA+[") + ) def script_leftMouseClick(self,gesture): # Translators: Reported when left mouse button is clicked. ui.message(_("Left click")) mouseHandler.executeMouseEvent(winUser.MOUSEEVENTF_LEFTDOWN,0,0) mouseHandler.executeMouseEvent(winUser.MOUSEEVENTF_LEFTUP,0,0) - # Translators: Input help mode message for left mouse click command. - script_leftMouseClick.__doc__=_("Clicks the left mouse button once at the current mouse position") - script_leftMouseClick.category=SCRCAT_MOUSE + @script( + # Translators: Input help mode message for right mouse click command. + description=_("Clicks the right mouse button once at the current mouse position"), + category=SCRCAT_MOUSE, + gestures=("kb:numpadMultiply", "kb(laptop):NVDA+]") + ) def script_rightMouseClick(self,gesture): # Translators: Reported when right mouse button is clicked. ui.message(_("Right click")) mouseHandler.executeMouseEvent(winUser.MOUSEEVENTF_RIGHTDOWN,0,0) mouseHandler.executeMouseEvent(winUser.MOUSEEVENTF_RIGHTUP,0,0) - # Translators: Input help mode message for right mouse click command. - script_rightMouseClick.__doc__=_("Clicks the right mouse button once at the current mouse position") - script_rightMouseClick.category=SCRCAT_MOUSE + @script( + # Translators: Input help mode message for left mouse lock/unlock toggle command. + description=_("Locks or unlocks the left mouse button"), + category=SCRCAT_MOUSE, + gestures=("kb:shift+numpadDivide", "kb(laptop):NVDA+control+[") + ) def script_toggleLeftMouseButton(self,gesture): if winUser.getKeyState(winUser.VK_LBUTTON)&32768: # Translators: This is presented when the left mouse button lock is released (used for drag and drop). @@ -188,10 +219,13 @@ def script_toggleLeftMouseButton(self,gesture): # Translators: This is presented when the left mouse button is locked down (used for drag and drop). ui.message(_("Left mouse button lock")) mouseHandler.executeMouseEvent(winUser.MOUSEEVENTF_LEFTDOWN,0,0) - # Translators: Input help mode message for left mouse lock/unlock toggle command. - script_toggleLeftMouseButton.__doc__=_("Locks or unlocks the left mouse button") - script_toggleLeftMouseButton.category=SCRCAT_MOUSE + @script( + # Translators: Input help mode message for right mouse lock/unlock command. + description=_("Locks or unlocks the right mouse button"), + category=SCRCAT_MOUSE, + gestures=("kb:shift+numpadMultiply", "kb(laptop):NVDA+control+]") + ) def script_toggleRightMouseButton(self,gesture): if winUser.getKeyState(winUser.VK_RBUTTON)&32768: # Translators: This is presented when the right mouse button lock is released (used for drag and drop). @@ -201,10 +235,16 @@ def script_toggleRightMouseButton(self,gesture): # Translators: This is presented when the right mouse button is locked down (used for drag and drop). ui.message(_("Right mouse button lock")) mouseHandler.executeMouseEvent(winUser.MOUSEEVENTF_RIGHTDOWN,0,0) - # Translators: Input help mode message for right mouse lock/unlock command. - script_toggleRightMouseButton.__doc__=_("Locks or unlocks the right mouse button") - script_toggleRightMouseButton.category=SCRCAT_MOUSE + @script( + description=_( + # Translators: Input help mode message for report current selection command. + "Announces the current selection in edit controls and documents. " + "If there is no selection it says so." + ), + category=SCRCAT_SYSTEMCARET, + gestures=("kb(desktop):NVDA+shift+upArrow", "kb(laptop):NVDA+shift+s") + ) def script_reportCurrentSelection(self,gesture): obj=api.getFocusObject() treeInterceptor=obj.treeInterceptor @@ -218,20 +258,26 @@ def script_reportCurrentSelection(self,gesture): speech.speakMessage(_("No selection")) else: speech.speakTextSelected(info.text) - # Translators: Input help mode message for report current selection command. - script_reportCurrentSelection.__doc__=_("Announces the current selection in edit controls and documents. If there is no selection it says so.") - script_reportCurrentSelection.category=SCRCAT_SYSTEMCARET + @script( + # Translators: Input help mode message for report date and time command. + description=_("If pressed once, reports the current time. If pressed twice, reports the current date"), + category=SCRCAT_SYSTEM, + gesture="kb:NVDA+f12" + ) def script_dateTime(self,gesture): if scriptHandler.getLastScriptRepeatCount()==0: text=winKernel.GetTimeFormatEx(winKernel.LOCALE_NAME_USER_DEFAULT, winKernel.TIME_NOSECONDS, None, None) else: text=winKernel.GetDateFormatEx(winKernel.LOCALE_NAME_USER_DEFAULT, winKernel.DATE_LONGDATE, None, None) ui.message(text) - # Translators: Input help mode message for report date and time command. - script_dateTime.__doc__=_("If pressed once, reports the current time. If pressed twice, reports the current date") - script_dateTime.category=SCRCAT_SYSTEM + @script( + # Translators: Input help mode message for increase synth setting value command. + description=_("Increases the currently active setting in the synth settings ring"), + category=SCRCAT_SPEECH, + gestures=("kb(desktop):NVDA+control+upArrow", "kb(laptop):NVDA+shift+control+upArrow") + ) def script_increaseSynthSetting(self,gesture): settingName=globalVars.settingsRing.currentSettingName if not settingName: @@ -240,10 +286,13 @@ def script_increaseSynthSetting(self,gesture): return settingValue=globalVars.settingsRing.increase() ui.message("%s %s" % (settingName,settingValue)) - # Translators: Input help mode message for increase synth setting value command. - script_increaseSynthSetting.__doc__=_("Increases the currently active setting in the synth settings ring") - script_increaseSynthSetting.category=SCRCAT_SPEECH + @script( + # Translators: Input help mode message for decrease synth setting value command. + description=_("Decreases the currently active setting in the synth settings ring"), + category=SCRCAT_SPEECH, + gestures=("kb(desktop):NVDA+control+downArrow", "kb(laptop):NVDA+control+shift+downArrow") + ) def script_decreaseSynthSetting(self,gesture): settingName=globalVars.settingsRing.currentSettingName if not settingName: @@ -251,10 +300,13 @@ def script_decreaseSynthSetting(self,gesture): return settingValue=globalVars.settingsRing.decrease() ui.message("%s %s" % (settingName,settingValue)) - # Translators: Input help mode message for decrease synth setting value command. - script_decreaseSynthSetting.__doc__=_("Decreases the currently active setting in the synth settings ring") - script_decreaseSynthSetting.category=SCRCAT_SPEECH + @script( + # Translators: Input help mode message for next synth setting command. + description=_("Moves to the next available setting in the synth settings ring"), + category=SCRCAT_SPEECH, + gestures=("kb(desktop):NVDA+control+rightArrow", "kb(laptop):NVDA+shift+control+rightArrow") + ) def script_nextSynthSetting(self,gesture): nextSettingName=globalVars.settingsRing.next() if not nextSettingName: @@ -262,10 +314,13 @@ def script_nextSynthSetting(self,gesture): return nextSettingValue=globalVars.settingsRing.currentSettingValue ui.message("%s %s"%(nextSettingName,nextSettingValue)) - # Translators: Input help mode message for next synth setting command. - script_nextSynthSetting.__doc__=_("Moves to the next available setting in the synth settings ring") - script_nextSynthSetting.category=SCRCAT_SPEECH + @script( + # Translators: Input help mode message for previous synth setting command. + description=_("Moves to the previous available setting in the synth settings ring"), + category=SCRCAT_SPEECH, + gestures=("kb(desktop):NVDA+control+leftArrow", "kb(laptop):NVDA+shift+control+leftArrow") + ) def script_previousSynthSetting(self,gesture): previousSettingName=globalVars.settingsRing.previous() if not previousSettingName: @@ -273,10 +328,13 @@ def script_previousSynthSetting(self,gesture): return previousSettingValue=globalVars.settingsRing.currentSettingValue ui.message("%s %s"%(previousSettingName,previousSettingValue)) - # Translators: Input help mode message for previous synth setting command. - script_previousSynthSetting.__doc__=_("Moves to the previous available setting in the synth settings ring") - script_previousSynthSetting.category=SCRCAT_SPEECH + @script( + # Translators: Input help mode message for toggle speaked typed characters command. + description=_("Toggles on and off the speaking of typed characters"), + category=SCRCAT_SPEECH, + gesture="kb:NVDA+2" + ) def script_toggleSpeakTypedCharacters(self,gesture): if config.conf["keyboard"]["speakTypedCharacters"]: # Translators: The message announced when toggling the speak typed characters keyboard setting. @@ -287,10 +345,13 @@ def script_toggleSpeakTypedCharacters(self,gesture): state = _("speak typed characters on") config.conf["keyboard"]["speakTypedCharacters"]=True ui.message(state) - # Translators: Input help mode message for toggle speaked typed characters command. - script_toggleSpeakTypedCharacters.__doc__=_("Toggles on and off the speaking of typed characters") - script_toggleSpeakTypedCharacters.category=SCRCAT_SPEECH + @script( + # Translators: Input help mode message for toggle speak typed words command. + description=_("Toggles on and off the speaking of typed words"), + category=SCRCAT_SPEECH, + gesture="kb:NVDA+3" + ) def script_toggleSpeakTypedWords(self,gesture): if config.conf["keyboard"]["speakTypedWords"]: # Translators: The message announced when toggling the speak typed words keyboard setting. @@ -301,10 +362,13 @@ def script_toggleSpeakTypedWords(self,gesture): state = _("speak typed words on") config.conf["keyboard"]["speakTypedWords"]=True ui.message(state) - # Translators: Input help mode message for toggle speak typed words command. - script_toggleSpeakTypedWords.__doc__=_("Toggles on and off the speaking of typed words") - script_toggleSpeakTypedWords.category=SCRCAT_SPEECH + @script( + # Translators: Input help mode message for toggle speak command keys command. + description=_("Toggles on and off the speaking of typed keys, that are not specifically characters"), + category=SCRCAT_SPEECH, + gesture="kb:NVDA+4" + ) def script_toggleSpeakCommandKeys(self,gesture): if config.conf["keyboard"]["speakCommandKeys"]: # Translators: The message announced when toggling the speak typed command keyboard setting. @@ -315,10 +379,12 @@ def script_toggleSpeakCommandKeys(self,gesture): state = _("speak command keys on") config.conf["keyboard"]["speakCommandKeys"]=True ui.message(state) - # Translators: Input help mode message for toggle speak command keys command. - script_toggleSpeakCommandKeys.__doc__=_("Toggles on and off the speaking of typed keys, that are not specifically characters") - script_toggleSpeakCommandKeys.category=SCRCAT_SPEECH + @script( + # Translators: Input help mode message for toggle report font name command. + description=_("Toggles on and off the reporting of font changes"), + category=SCRCAT_DOCUMENTFORMATTING + ) def script_toggleReportFontName(self,gesture): if config.conf["documentFormatting"]["reportFontName"]: # Translators: The message announced when toggling the report font name document formatting setting. @@ -329,10 +395,12 @@ def script_toggleReportFontName(self,gesture): state = _("report font name on") config.conf["documentFormatting"]["reportFontName"]=True ui.message(state) - # Translators: Input help mode message for toggle report font name command. - script_toggleReportFontName.__doc__=_("Toggles on and off the reporting of font changes") - script_toggleReportFontName.category=SCRCAT_DOCUMENTFORMATTING + @script( + # Translators: Input help mode message for toggle report font size command. + description=_("Toggles on and off the reporting of font size changes"), + category=SCRCAT_DOCUMENTFORMATTING + ) def script_toggleReportFontSize(self,gesture): if config.conf["documentFormatting"]["reportFontSize"]: # Translators: The message announced when toggling the report font size document formatting setting. @@ -343,10 +411,12 @@ def script_toggleReportFontSize(self,gesture): state = _("report font size on") config.conf["documentFormatting"]["reportFontSize"]=True ui.message(state) - # Translators: Input help mode message for toggle report font size command. - script_toggleReportFontSize.__doc__=_("Toggles on and off the reporting of font size changes") - script_toggleReportFontSize.category=SCRCAT_DOCUMENTFORMATTING + @script( + # Translators: Input help mode message for toggle report font attributes command. + description=_("Toggles on and off the reporting of font attributes"), + category=SCRCAT_DOCUMENTFORMATTING + ) def script_toggleReportFontAttributes(self,gesture): if config.conf["documentFormatting"]["reportFontAttributes"]: # Translators: The message announced when toggling the report font attributes document formatting setting. @@ -357,14 +427,11 @@ def script_toggleReportFontAttributes(self,gesture): state = _("report font attributes on") config.conf["documentFormatting"]["reportFontAttributes"]=True ui.message(state) - # Translators: Input help mode message for toggle report font attributes command. - script_toggleReportFontAttributes.__doc__=_("Toggles on and off the reporting of font attributes") - script_toggleReportFontAttributes.category=SCRCAT_DOCUMENTFORMATTING @script( # Translators: Input help mode message for toggle superscripts and subscripts command. description=_("Toggles on and off the reporting of superscripts and subscripts"), - category=SCRCAT_DOCUMENTFORMATTING, + category=SCRCAT_DOCUMENTFORMATTING ) def script_toggleReportSuperscriptsAndSubscripts(self, gesture): shouldReport: bool = not config.conf["documentFormatting"]["reportSuperscriptsAndSubscripts"] @@ -378,7 +445,12 @@ def script_toggleReportSuperscriptsAndSubscripts(self, gesture): # document formatting setting. state = _("report superscripts and subscripts off") ui.message(state) - + + @script( + # Translators: Input help mode message for toggle report revisions command. + description=_("Toggles on and off the reporting of revisions"), + category=SCRCAT_DOCUMENTFORMATTING + ) def script_toggleReportRevisions(self,gesture): if config.conf["documentFormatting"]["reportRevisions"]: # Translators: The message announced when toggling the report revisions document formatting setting. @@ -389,10 +461,12 @@ def script_toggleReportRevisions(self,gesture): state = _("report revisions on") config.conf["documentFormatting"]["reportRevisions"]=True ui.message(state) - # Translators: Input help mode message for toggle report revisions command. - script_toggleReportRevisions.__doc__=_("Toggles on and off the reporting of revisions") - script_toggleReportRevisions.category=SCRCAT_DOCUMENTFORMATTING + @script( + # Translators: Input help mode message for toggle report emphasis command. + description=_("Toggles on and off the reporting of emphasis"), + category=SCRCAT_DOCUMENTFORMATTING + ) def script_toggleReportEmphasis(self,gesture): if config.conf["documentFormatting"]["reportEmphasis"]: # Translators: The message announced when toggling the report emphasis document formatting setting. @@ -403,14 +477,11 @@ def script_toggleReportEmphasis(self,gesture): state = _("report emphasis on") config.conf["documentFormatting"]["reportEmphasis"]=True ui.message(state) - # Translators: Input help mode message for toggle report emphasis command. - script_toggleReportEmphasis.__doc__=_("Toggles on and off the reporting of emphasis") - script_toggleReportEmphasis.category=SCRCAT_DOCUMENTFORMATTING - + @script( # Translators: Input help mode message for toggle report marked (highlighted) content command. description=_("Toggles on and off the reporting of marked text"), - category=SCRCAT_DOCUMENTFORMATTING, + category=SCRCAT_DOCUMENTFORMATTING ) def script_toggleReportHighlightedText(self, gesture): shouldReport: bool = not config.conf["documentFormatting"]["reportHighlight"] @@ -423,6 +494,11 @@ def script_toggleReportHighlightedText(self, gesture): state = _("report marked off") ui.message(state) + @script( + # Translators: Input help mode message for toggle report colors command. + description=_("Toggles on and off the reporting of colors"), + category=SCRCAT_DOCUMENTFORMATTING + ) def script_toggleReportColor(self,gesture): if config.conf["documentFormatting"]["reportColor"]: # Translators: The message announced when toggling the report colors document formatting setting. @@ -433,10 +509,12 @@ def script_toggleReportColor(self,gesture): state = _("report colors on") config.conf["documentFormatting"]["reportColor"]=True ui.message(state) - # Translators: Input help mode message for toggle report colors command. - script_toggleReportColor.__doc__=_("Toggles on and off the reporting of colors") - script_toggleReportColor.category=SCRCAT_DOCUMENTFORMATTING + @script( + # Translators: Input help mode message for toggle report alignment command. + description=_("Toggles on and off the reporting of text alignment"), + category=SCRCAT_DOCUMENTFORMATTING + ) def script_toggleReportAlignment(self,gesture): if config.conf["documentFormatting"]["reportAlignment"]: # Translators: The message announced when toggling the report alignment document formatting setting. @@ -447,10 +525,12 @@ def script_toggleReportAlignment(self,gesture): state = _("report alignment on") config.conf["documentFormatting"]["reportAlignment"]=True ui.message(state) - # Translators: Input help mode message for toggle report alignment command. - script_toggleReportAlignment.__doc__=_("Toggles on and off the reporting of text alignment") - script_toggleReportAlignment.category=SCRCAT_DOCUMENTFORMATTING + @script( + # Translators: Input help mode message for toggle report style command. + description=_("Toggles on and off the reporting of style changes"), + category=SCRCAT_DOCUMENTFORMATTING + ) def script_toggleReportStyle(self,gesture): if config.conf["documentFormatting"]["reportStyle"]: # Translators: The message announced when toggling the report style document formatting setting. @@ -461,10 +541,12 @@ def script_toggleReportStyle(self,gesture): state = _("report style on") config.conf["documentFormatting"]["reportStyle"]=True ui.message(state) - # Translators: Input help mode message for toggle report style command. - script_toggleReportStyle.__doc__=_("Toggles on and off the reporting of style changes") - script_toggleReportStyle.category=SCRCAT_DOCUMENTFORMATTING + @script( + # Translators: Input help mode message for toggle report spelling errors command. + description=_("Toggles on and off the reporting of spelling errors"), + category=SCRCAT_DOCUMENTFORMATTING + ) def script_toggleReportSpellingErrors(self,gesture): if config.conf["documentFormatting"]["reportSpellingErrors"]: # Translators: The message announced when toggling the report spelling errors document formatting setting. @@ -475,10 +557,12 @@ def script_toggleReportSpellingErrors(self,gesture): state = _("report spelling errors on") config.conf["documentFormatting"]["reportSpellingErrors"]=True ui.message(state) - # Translators: Input help mode message for toggle report spelling errors command. - script_toggleReportSpellingErrors.__doc__=_("Toggles on and off the reporting of spelling errors") - script_toggleReportSpellingErrors.category=SCRCAT_DOCUMENTFORMATTING + @script( + # Translators: Input help mode message for toggle report pages command. + description=_("Toggles on and off the reporting of pages"), + category=SCRCAT_DOCUMENTFORMATTING + ) def script_toggleReportPage(self,gesture): if config.conf["documentFormatting"]["reportPage"]: # Translators: The message announced when toggling the report pages document formatting setting. @@ -489,10 +573,12 @@ def script_toggleReportPage(self,gesture): state = _("report pages on") config.conf["documentFormatting"]["reportPage"]=True ui.message(state) - # Translators: Input help mode message for toggle report pages command. - script_toggleReportPage.__doc__=_("Toggles on and off the reporting of pages") - script_toggleReportPage.category=SCRCAT_DOCUMENTFORMATTING + @script( + # Translators: Input help mode message for toggle report line numbers command. + description=_("Toggles on and off the reporting of line numbers"), + category=SCRCAT_DOCUMENTFORMATTING + ) def script_toggleReportLineNumber(self,gesture): if config.conf["documentFormatting"]["reportLineNumber"]: # Translators: The message announced when toggling the report line numbers document formatting setting. @@ -503,10 +589,12 @@ def script_toggleReportLineNumber(self,gesture): state = _("report line numbers on") config.conf["documentFormatting"]["reportLineNumber"]=True ui.message(state) - # Translators: Input help mode message for toggle report line numbers command. - script_toggleReportLineNumber.__doc__=_("Toggles on and off the reporting of line numbers") - script_toggleReportLineNumber.category=SCRCAT_DOCUMENTFORMATTING + @script( + # Translators: Input help mode message for toggle report line indentation command. + description=_("Cycles through line indentation settings"), + category=SCRCAT_DOCUMENTFORMATTING + ) def script_toggleReportLineIndentation(self,gesture): lineIndentationSpeech = config.conf["documentFormatting"]["reportLineIndentation"] lineIndentationTones = config.conf["documentFormatting"]["reportLineIndentationWithTones"] @@ -530,10 +618,12 @@ def script_toggleReportLineIndentation(self,gesture): lineIndentationTones = False config.conf["documentFormatting"]["reportLineIndentation"] = lineIndentationSpeech config.conf["documentFormatting"]["reportLineIndentationWithTones"] = lineIndentationTones - # Translators: Input help mode message for toggle report line indentation command. - script_toggleReportLineIndentation.__doc__=_("Cycles through line indentation settings") - script_toggleReportLineIndentation.category=SCRCAT_DOCUMENTFORMATTING + @script( + # Translators: Input help mode message for toggle report paragraph indentation command. + description=_("Toggles on and off the reporting of paragraph indentation"), + category=SCRCAT_DOCUMENTFORMATTING + ) def script_toggleReportParagraphIndentation(self,gesture): if config.conf["documentFormatting"]["reportParagraphIndentation"]: # Translators: The message announced when toggling the report paragraph indentation document formatting setting. @@ -544,10 +634,12 @@ def script_toggleReportParagraphIndentation(self,gesture): state = _("report paragraph indentation on") config.conf["documentFormatting"]["reportParagraphIndentation"]=True ui.message(state) - # Translators: Input help mode message for toggle report paragraph indentation command. - script_toggleReportParagraphIndentation.__doc__=_("Toggles on and off the reporting of paragraph indentation") - script_toggleReportParagraphIndentation.category=SCRCAT_DOCUMENTFORMATTING + @script( + # Translators: Input help mode message for toggle report line spacing command. + description=_("Toggles on and off the reporting of line spacing"), + category=SCRCAT_DOCUMENTFORMATTING + ) def script_toggleReportLineSpacing(self,gesture): if config.conf["documentFormatting"]["reportLineSpacing"]: # Translators: The message announced when toggling the report line spacing document formatting setting. @@ -558,10 +650,12 @@ def script_toggleReportLineSpacing(self,gesture): state = _("report line spacing on") config.conf["documentFormatting"]["reportLineSpacing"]=True ui.message(state) - # Translators: Input help mode message for toggle report line spacing command. - script_toggleReportLineSpacing.__doc__=_("Toggles on and off the reporting of line spacing") - script_toggleReportLineSpacing.category=SCRCAT_DOCUMENTFORMATTING + @script( + # Translators: Input help mode message for toggle report tables command. + description=_("Toggles on and off the reporting of tables"), + category=SCRCAT_DOCUMENTFORMATTING + ) def script_toggleReportTables(self,gesture): if config.conf["documentFormatting"]["reportTables"]: # Translators: The message announced when toggling the report tables document formatting setting. @@ -572,10 +666,12 @@ def script_toggleReportTables(self,gesture): state = _("report tables on") config.conf["documentFormatting"]["reportTables"]=True ui.message(state) - # Translators: Input help mode message for toggle report tables command. - script_toggleReportTables.__doc__=_("Toggles on and off the reporting of tables") - script_toggleReportTables.category=SCRCAT_DOCUMENTFORMATTING + @script( + # Translators: Input help mode message for toggle report table row/column headers command. + description=_("Toggles on and off the reporting of table row and column headers"), + category=SCRCAT_DOCUMENTFORMATTING + ) def script_toggleReportTableHeaders(self,gesture): if config.conf["documentFormatting"]["reportTableHeaders"]: # Translators: The message announced when toggling the report table row/column headers document formatting setting. @@ -586,10 +682,12 @@ def script_toggleReportTableHeaders(self,gesture): state = _("report table row and column headers on") config.conf["documentFormatting"]["reportTableHeaders"]=True ui.message(state) - # Translators: Input help mode message for toggle report table row/column headers command. - script_toggleReportTableHeaders.__doc__=_("Toggles on and off the reporting of table row and column headers") - script_toggleReportTableHeaders.category=SCRCAT_DOCUMENTFORMATTING + @script( + # Translators: Input help mode message for toggle report table cell coordinates command. + description=_("Toggles on and off the reporting of table cell coordinates"), + category=SCRCAT_DOCUMENTFORMATTING + ) def script_toggleReportTableCellCoords(self,gesture): if config.conf["documentFormatting"]["reportTableCellCoords"]: # Translators: The message announced when toggling the report table cell coordinates document formatting setting. @@ -600,10 +698,12 @@ def script_toggleReportTableCellCoords(self,gesture): state = _("report table cell coordinates on") config.conf["documentFormatting"]["reportTableCellCoords"]=True ui.message(state) - # Translators: Input help mode message for toggle report table cell coordinates command. - script_toggleReportTableCellCoords.__doc__=_("Toggles on and off the reporting of table cell coordinates") - script_toggleReportTableCellCoords.category=SCRCAT_DOCUMENTFORMATTING + @script( + # Translators: Input help mode message for toggle report links command. + description=_("Toggles on and off the reporting of links"), + category=SCRCAT_DOCUMENTFORMATTING + ) def script_toggleReportLinks(self,gesture): if config.conf["documentFormatting"]["reportLinks"]: # Translators: The message announced when toggling the report links document formatting setting. @@ -614,11 +714,8 @@ def script_toggleReportLinks(self,gesture): state = _("report links on") config.conf["documentFormatting"]["reportLinks"]=True ui.message(state) - # Translators: Input help mode message for toggle report links command. - script_toggleReportLinks.__doc__=_("Toggles on and off the reporting of links") - script_toggleReportLinks.category=SCRCAT_DOCUMENTFORMATTING - @scriptHandler.script( + @script( # Translators: Input help mode message for toggle report graphics command. description=_("Toggles on and off the reporting of graphics"), category=SCRCAT_DOCUMENTFORMATTING @@ -634,6 +731,11 @@ def script_toggleReportGraphics(self, gesture): config.conf["documentFormatting"]["reportGraphics"] = True ui.message(state) + @script( + # Translators: Input help mode message for toggle report comments command. + description=_("Toggles on and off the reporting of comments"), + category=SCRCAT_DOCUMENTFORMATTING + ) def script_toggleReportComments(self,gesture): if config.conf["documentFormatting"]["reportComments"]: # Translators: The message announced when toggling the report comments document formatting setting. @@ -644,10 +746,12 @@ def script_toggleReportComments(self,gesture): state = _("report comments on") config.conf["documentFormatting"]["reportComments"]=True ui.message(state) - # Translators: Input help mode message for toggle report comments command. - script_toggleReportComments.__doc__=_("Toggles on and off the reporting of comments") - script_toggleReportComments.category=SCRCAT_DOCUMENTFORMATTING + @script( + # Translators: Input help mode message for toggle report lists command. + description=_("Toggles on and off the reporting of lists"), + category=SCRCAT_DOCUMENTFORMATTING + ) def script_toggleReportLists(self,gesture): if config.conf["documentFormatting"]["reportLists"]: # Translators: The message announced when toggling the report lists document formatting setting. @@ -658,10 +762,12 @@ def script_toggleReportLists(self,gesture): state = _("report lists on") config.conf["documentFormatting"]["reportLists"]=True ui.message(state) - # Translators: Input help mode message for toggle report lists command. - script_toggleReportLists.__doc__=_("Toggles on and off the reporting of lists") - script_toggleReportLists.category=SCRCAT_DOCUMENTFORMATTING + @script( + # Translators: Input help mode message for toggle report headings command. + description=_("Toggles on and off the reporting of headings"), + category=SCRCAT_DOCUMENTFORMATTING + ) def script_toggleReportHeadings(self,gesture): if config.conf["documentFormatting"]["reportHeadings"]: # Translators: The message announced when toggling the report headings document formatting setting. @@ -672,9 +778,6 @@ def script_toggleReportHeadings(self,gesture): state = _("report headings on") config.conf["documentFormatting"]["reportHeadings"]=True ui.message(state) - # Translators: Input help mode message for toggle report headings command. - script_toggleReportHeadings.__doc__=_("Toggles on and off the reporting of headings") - script_toggleReportHeadings.category=SCRCAT_DOCUMENTFORMATTING @script( # Translators: Input help mode message for toggle report groupings command. @@ -692,6 +795,11 @@ def script_toggleReportGroupings(self, gesture): config.conf["documentFormatting"]["reportGroupings"] = True ui.message(state) + @script( + # Translators: Input help mode message for toggle report block quotes command. + description=_("Toggles on and off the reporting of block quotes"), + category=SCRCAT_DOCUMENTFORMATTING + ) def script_toggleReportBlockQuotes(self,gesture): if config.conf["documentFormatting"]["reportBlockQuotes"]: # Translators: The message announced when toggling the report block quotes document formatting setting. @@ -702,10 +810,12 @@ def script_toggleReportBlockQuotes(self,gesture): state = _("report block quotes on") config.conf["documentFormatting"]["reportBlockQuotes"]=True ui.message(state) - # Translators: Input help mode message for toggle report block quotes command. - script_toggleReportBlockQuotes.__doc__=_("Toggles on and off the reporting of block quotes") - script_toggleReportBlockQuotes.category=SCRCAT_DOCUMENTFORMATTING + @script( + # Translators: Input help mode message for toggle report landmarks command. + description=_("Toggles on and off the reporting of landmarks"), + category=SCRCAT_DOCUMENTFORMATTING + ) def script_toggleReportLandmarks(self,gesture): if config.conf["documentFormatting"]["reportLandmarks"]: # Translators: The message announced when toggling the report landmarks document formatting setting. @@ -716,9 +826,6 @@ def script_toggleReportLandmarks(self,gesture): state = _("report landmarks and regions on") config.conf["documentFormatting"]["reportLandmarks"]=True ui.message(state) - # Translators: Input help mode message for toggle report landmarks command. - script_toggleReportLandmarks.__doc__=_("Toggles on and off the reporting of landmarks") - script_toggleReportLandmarks.category=SCRCAT_DOCUMENTFORMATTING @script( # Translators: Input help mode message for toggle report articles command. @@ -736,6 +843,11 @@ def script_toggleReportArticles(self, gesture): config.conf["documentFormatting"]["reportArticles"] = True ui.message(state) + @script( + # Translators: Input help mode message for toggle report frames command. + description=_("Toggles on and off the reporting of frames"), + category=SCRCAT_DOCUMENTFORMATTING + ) def script_toggleReportFrames(self,gesture): if config.conf["documentFormatting"]["reportFrames"]: # Translators: The message announced when toggling the report frames document formatting setting. @@ -746,10 +858,12 @@ def script_toggleReportFrames(self,gesture): state = _("report frames on") config.conf["documentFormatting"]["reportFrames"]=True ui.message(state) - # Translators: Input help mode message for toggle report frames command. - script_toggleReportFrames.__doc__=_("Toggles on and off the reporting of frames") - script_toggleReportFrames.category=SCRCAT_DOCUMENTFORMATTING + @script( + # Translators: Input help mode message for toggle report if clickable command. + description=_("Toggles on and off reporting if clickable"), + category=SCRCAT_DOCUMENTFORMATTING + ) def script_toggleReportClickable(self,gesture): if config.conf["documentFormatting"]["reportClickable"]: # Translators: The message announced when toggling the report if clickable document formatting setting. @@ -760,10 +874,13 @@ def script_toggleReportClickable(self,gesture): state = _("report if clickable on") config.conf["documentFormatting"]["reportClickable"]=True ui.message(state) - # Translators: Input help mode message for toggle report if clickable command. - script_toggleReportClickable.__doc__=_("Toggles on and off reporting if clickable") - script_toggleReportClickable.category=SCRCAT_DOCUMENTFORMATTING + @script( + # Translators: Input help mode message for cycle speech symbol level command. + description=_("Cycles through speech symbol levels which determine what symbols are spoken"), + category=SCRCAT_SPEECH, + gesture="kb:NVDA+p" + ) def script_cycleSpeechSymbolLevel(self,gesture): curLevel = config.conf["speech"]["symbolLevel"] for level in characterProcessing.CONFIGURABLE_SPEECH_SYMBOL_LEVELS: @@ -777,10 +894,13 @@ def script_cycleSpeechSymbolLevel(self,gesture): # which determine what symbols are spoken. # %s will be replaced with the symbol level; e.g. none, some, most and all. ui.message(_("Symbol level %s") % name) - # Translators: Input help mode message for cycle speech symbol level command. - script_cycleSpeechSymbolLevel.__doc__=_("Cycles through speech symbol levels which determine what symbols are spoken") - script_cycleSpeechSymbolLevel.category=SCRCAT_SPEECH + @script( + # Translators: Input help mode message for move mouse to navigator object command. + description=_("Moves the mouse pointer to the current navigator object"), + category=SCRCAT_MOUSE, + gestures=("kb:NVDA+numpadDivide", "kb(laptop):NVDA+shift+m") + ) def script_moveMouseToNavigatorObject(self,gesture): try: p=api.getReviewPosition().pointAtStart @@ -800,20 +920,29 @@ def script_moveMouseToNavigatorObject(self,gesture): y=top+(height//2) winUser.setCursorPos(x,y) mouseHandler.executeMouseMoveEvent(x,y) - # Translators: Input help mode message for move mouse to navigator object command. - script_moveMouseToNavigatorObject.__doc__=_("Moves the mouse pointer to the current navigator object") - script_moveMouseToNavigatorObject.category=SCRCAT_MOUSE + @script( + # Translators: Input help mode message for move navigator object to mouse command. + description=_("Sets the navigator object to the current object under the mouse pointer and speaks it"), + category=SCRCAT_MOUSE, + gestures=("kb:NVDA+numpadMultiply", "kb(laptop):NVDA+shift+n") + ) def script_moveNavigatorObjectToMouse(self,gesture): # Translators: Reported when attempting to move the navigator object to the object under mouse pointer. ui.message(_("Move navigator object to mouse")) obj=api.getMouseObject() api.setNavigatorObject(obj) speech.speakObject(obj) - # Translators: Input help mode message for move navigator object to mouse command. - script_moveNavigatorObjectToMouse.__doc__=_("Sets the navigator object to the current object under the mouse pointer and speaks it") - script_moveNavigatorObjectToMouse.category=SCRCAT_MOUSE + @script( + description=_( + # Translators: Script help message for next review mode command. + "Switches to the next review mode (e.g. object, document or screen) " + "and positions the review position at the point of the navigator object" + ), + category=SCRCAT_TEXTREVIEW, + gestures=("kb:NVDA+numpad7", "kb(laptop):NVDA+pageUp", "ts(object):2finger_flickUp") + ) def script_reviewMode_next(self,gesture): label=review.nextMode() if label: @@ -825,10 +954,16 @@ def script_reviewMode_next(self,gesture): else: # Translators: reported when there are no other available review modes for this object ui.reviewMessage(_("No next review mode")) - # Translators: Script help message for next review mode command. - script_reviewMode_next.__doc__=_("Switches to the next review mode (e.g. object, document or screen) and positions the review position at the point of the navigator object") - script_reviewMode_next.category=SCRCAT_TEXTREVIEW + @script( + description=_( + # Translators: Script help message for previous review mode command. + "Switches to the previous review mode (e.g. object, document or screen) " + "and positions the review position at the point of the navigator object" + ), + category=SCRCAT_TEXTREVIEW, + gestures=("kb:NVDA+numpad1", "kb(laptop):NVDA+pageDown", "ts(object):2finger_flickDown") + ) def script_reviewMode_previous(self,gesture): label=review.nextMode(prev=True) if label: @@ -840,10 +975,12 @@ def script_reviewMode_previous(self,gesture): else: # Translators: reported when there are no other available review modes for this object ui.reviewMessage(_("No previous review mode")) - # Translators: Script help message for previous review mode command. - script_reviewMode_previous.__doc__=_("Switches to the previous review mode (e.g. object, document or screen) and positions the review position at the point of the navigator object") - script_reviewMode_previous.category=SCRCAT_TEXTREVIEW + @script( + # Translators: Input help mode message for toggle simple review mode command. + description=_("Toggles simple review mode on and off"), + category=SCRCAT_OBJECTNAVIGATION + ) def script_toggleSimpleReviewMode(self,gesture): if config.conf["reviewCursor"]["simpleReviewMode"]: # Translators: The message announced when toggling simple review mode. @@ -854,10 +991,17 @@ def script_toggleSimpleReviewMode(self,gesture): state = _("Simple review mode on") config.conf["reviewCursor"]["simpleReviewMode"]=True ui.message(state) - # Translators: Input help mode message for toggle simple review mode command. - script_toggleSimpleReviewMode.__doc__=_("Toggles simple review mode on and off") - script_toggleSimpleReviewMode.category=SCRCAT_OBJECTNAVIGATION + @script( + description=_( + # Translators: Input help mode message for report current navigator object command. + "Reports the current navigator object. " + "Pressing twice spells this information, " + "and pressing three times Copies name and value of this object to the clipboard" + ), + category=SCRCAT_OBJECTNAVIGATION, + gestures=("kb:NVDA+numpad5", "kb(laptop):NVDA+shift+o") + ) def script_navigatorObject_current(self,gesture): curObject=api.getNavigatorObject() if not isinstance(curObject,NVDAObject): @@ -894,11 +1038,17 @@ def script_navigatorObject_current(self,gesture): else: api.copyToClip(text, notify=True) else: - speech.speakObject(curObject,reason=controlTypes.REASON_QUERY) - # Translators: Input help mode message for report current navigator object command. - script_navigatorObject_current.__doc__=_("Reports the current navigator object. Pressing twice spells this information, and pressing three times Copies name and value of this object to the clipboard") - script_navigatorObject_current.category=SCRCAT_OBJECTNAVIGATION + speech.speakObject(curObject, reason=controlTypes.OutputReason.QUERY) + @script( + description=_( + # Translators: Description for report review cursor location command. + "Reports information about the location of the text or object at the review cursor. " + "Pressing twice may provide further detail." + ), + category=SCRCAT_OBJECTNAVIGATION, + gestures=("kb:NVDA+numpadDelete", "kb(laptop):NVDA+delete") + ) def script_navigatorObject_currentDimensions(self,gesture): count=scriptHandler.getLastScriptRepeatCount() locationText=api.getReviewPosition().locationText if count==0 else None @@ -909,9 +1059,6 @@ def script_navigatorObject_currentDimensions(self,gesture): ui.message(_("No location information")) return ui.message(locationText) - # Translators: Description for report review cursor location command. - script_navigatorObject_currentDimensions.__doc__=_("Reports information about the location of the text or object at the review cursor. Pressing twice may provide further detail.") - script_navigatorObject_currentDimensions.category=SCRCAT_OBJECTNAVIGATION @script( description=_( @@ -920,7 +1067,7 @@ def script_navigatorObject_currentDimensions(self,gesture): "and the review cursor to the position of the caret inside it, if possible." ), category=SCRCAT_OBJECTNAVIGATION, - gestures=("kb:NVDA+numpadMinus", "kb(laptop):NVDA+backspace"), + gestures=("kb:NVDA+numpadMinus", "kb(laptop):NVDA+backspace") ) def script_navigatorObject_toFocus(self,gesture): tIAtCaret = self._getTIAtCaret(True) @@ -931,6 +1078,15 @@ def script_navigatorObject_toFocus(self,gesture): speech.speakMessage(_("Move to focus")) speech.speakObject(api.getNavigatorObject(), reason=controlTypes.OutputReason.FOCUS) + @script( + description=_( + # Translators: Input help mode message for move focus to current navigator object command. + "Pressed once sets the keyboard focus to the navigator object, " + "pressed twice sets the system caret to the position of the review cursor" + ), + category=SCRCAT_OBJECTNAVIGATION, + gestures=("kb:NVDA+shift+numpadMinus", "kb(laptop):NVDA+shift+backspace") + ) def script_navigatorObject_moveFocus(self,gesture): obj=api.getNavigatorObject() if not isinstance(obj,NVDAObject): @@ -952,11 +1108,14 @@ def script_navigatorObject_moveFocus(self,gesture): return info=review.copy() info.expand(textInfos.UNIT_LINE) - speech.speakTextInfo(info,reason=controlTypes.REASON_CARET) - # Translators: Input help mode message for move focus to current navigator object command. - script_navigatorObject_moveFocus.__doc__=_("Pressed once sets the keyboard focus to the navigator object, pressed twice sets the system caret to the position of the review cursor") - script_navigatorObject_moveFocus.category=SCRCAT_OBJECTNAVIGATION + speech.speakTextInfo(info, reason=controlTypes.OutputReason.CARET) + @script( + # Translators: Input help mode message for move to parent object command. + description=_("Moves the navigator object to the object containing it"), + category=SCRCAT_OBJECTNAVIGATION, + gestures=("kb:NVDA+numpad8", "kb(laptop):NVDA+shift+upArrow", "ts(object):flickup") + ) def script_navigatorObject_parent(self,gesture): curObject=api.getNavigatorObject() if not isinstance(curObject,NVDAObject): @@ -968,14 +1127,17 @@ def script_navigatorObject_parent(self,gesture): curObject=curObject.simpleParent if simpleReviewMode else curObject.parent if curObject is not None: api.setNavigatorObject(curObject) - speech.speakObject(curObject,reason=controlTypes.REASON_FOCUS) + speech.speakObject(curObject, reason=controlTypes.OutputReason.FOCUS) else: # Translators: Reported when there is no containing (parent) object such as when focused on desktop. ui.reviewMessage(_("No containing object")) - # Translators: Input help mode message for move to parent object command. - script_navigatorObject_parent.__doc__=_("Moves the navigator object to the object containing it") - script_navigatorObject_parent.category=SCRCAT_OBJECTNAVIGATION + @script( + # Translators: Input help mode message for move to next object command. + description=_("Moves the navigator object to the next object"), + category=SCRCAT_OBJECTNAVIGATION, + gestures=("kb:NVDA+numpad6", "kb(laptop):NVDA+shift+rightArrow", "ts(object):2finger_flickright") + ) def script_navigatorObject_next(self,gesture): curObject=api.getNavigatorObject() if not isinstance(curObject,NVDAObject): @@ -987,14 +1149,17 @@ def script_navigatorObject_next(self,gesture): curObject=curObject.simpleNext if simpleReviewMode else curObject.next if curObject is not None: api.setNavigatorObject(curObject) - speech.speakObject(curObject,reason=controlTypes.REASON_FOCUS) + speech.speakObject(curObject, reason=controlTypes.OutputReason.FOCUS) else: # Translators: Reported when there is no next object (current object is the last object). ui.reviewMessage(_("No next")) - # Translators: Input help mode message for move to next object command. - script_navigatorObject_next.__doc__=_("Moves the navigator object to the next object") - script_navigatorObject_next.category=SCRCAT_OBJECTNAVIGATION + @script( + # Translators: Input help mode message for move to previous object command. + description=_("Moves the navigator object to the previous object"), + category=SCRCAT_OBJECTNAVIGATION, + gestures=("kb:NVDA+numpad4", "kb(laptop):NVDA+shift+leftArrow", "ts(object):2finger_flickleft") + ) def script_navigatorObject_previous(self,gesture): curObject=api.getNavigatorObject() if not isinstance(curObject,NVDAObject): @@ -1006,14 +1171,17 @@ def script_navigatorObject_previous(self,gesture): curObject=curObject.simplePrevious if simpleReviewMode else curObject.previous if curObject is not None: api.setNavigatorObject(curObject) - speech.speakObject(curObject,reason=controlTypes.REASON_FOCUS) + speech.speakObject(curObject, reason=controlTypes.OutputReason.FOCUS) else: # Translators: Reported when there is no previous object (current object is the first object). ui.reviewMessage(_("No previous")) - # Translators: Input help mode message for move to previous object command. - script_navigatorObject_previous.__doc__=_("Moves the navigator object to the previous object") - script_navigatorObject_previous.category=SCRCAT_OBJECTNAVIGATION + @script( + # Translators: Input help mode message for move to first child object command. + description=_("Moves the navigator object to the first object inside it"), + category=SCRCAT_OBJECTNAVIGATION, + gestures=("kb:NVDA+numpad2", "kb(laptop):NVDA+shift+downArrow", "ts(object):flickdown") + ) def script_navigatorObject_firstChild(self,gesture): curObject=api.getNavigatorObject() if not isinstance(curObject,NVDAObject): @@ -1025,14 +1193,20 @@ def script_navigatorObject_firstChild(self,gesture): curObject=curObject.simpleFirstChild if simpleReviewMode else curObject.firstChild if curObject is not None: api.setNavigatorObject(curObject) - speech.speakObject(curObject,reason=controlTypes.REASON_FOCUS) + speech.speakObject(curObject, reason=controlTypes.OutputReason.FOCUS) else: # Translators: Reported when there is no contained (first child) object such as inside a document. ui.reviewMessage(_("No objects inside")) - # Translators: Input help mode message for move to first child object command. - script_navigatorObject_firstChild.__doc__=_("Moves the navigator object to the first object inside it") - script_navigatorObject_firstChild.category=SCRCAT_OBJECTNAVIGATION + @script( + description=_( + # Translators: Input help mode message for activate current object command. + "Performs the default action on the current navigator object " + "(example: presses it if it is a button)." + ), + category=SCRCAT_OBJECTNAVIGATION, + gestures=("kb:NVDA+numpadEnter", "kb(laptop):NVDA+enter", "ts:double_tap") + ) def script_review_activate(self,gesture): # Translators: a message reported when the action at the position of the review cursor or navigator object is performed. actionName=_("Activate") @@ -1063,19 +1237,26 @@ def script_review_activate(self,gesture): obj=obj.parent # Translators: the message reported when there is no action to perform on the review position or navigator object. ui.message(_("No action")) - # Translators: Input help mode message for activate current object command. - script_review_activate.__doc__=_("Performs the default action on the current navigator object (example: presses it if it is a button).") - script_review_activate.category=SCRCAT_OBJECTNAVIGATION + @script( + # Translators: Input help mode message for move review cursor to top line command. + description=_("Moves the review cursor to the top line of the current navigator object and speaks it"), + category=SCRCAT_TEXTREVIEW, + gestures=("kb:shift+numpad7", "kb(laptop):NVDA+control+home") + ) def script_review_top(self,gesture): info=api.getReviewPosition().obj.makeTextInfo(textInfos.POSITION_FIRST) api.setReviewPosition(info) info.expand(textInfos.UNIT_LINE) - speech.speakTextInfo(info,unit=textInfos.UNIT_LINE,reason=controlTypes.REASON_CARET) - # Translators: Input help mode message for move review cursor to top line command. - script_review_top.__doc__=_("Moves the review cursor to the top line of the current navigator object and speaks it") - script_review_top.category=SCRCAT_TEXTREVIEW + speech.speakTextInfo(info, unit=textInfos.UNIT_LINE, reason=controlTypes.OutputReason.CARET) + @script( + # Translators: Input help mode message for move review cursor to previous line command. + description=_("Moves the review cursor to the previous line of the current navigator object and speaks it"), + resumeSayAllMode=sayAllHandler.CURSOR_REVIEW, + category=SCRCAT_TEXTREVIEW, + gestures=("kb:numpad7", "kb(laptop):NVDA+upArrow", "ts(text):flickUp") + ) def script_review_previousLine(self,gesture): info=api.getReviewPosition().copy() info.expand(textInfos.UNIT_LINE) @@ -1087,12 +1268,18 @@ def script_review_previousLine(self,gesture): else: api.setReviewPosition(info) info.expand(textInfos.UNIT_LINE) - speech.speakTextInfo(info,unit=textInfos.UNIT_LINE,reason=controlTypes.REASON_CARET) - # Translators: Input help mode message for move review cursor to previous line command. - script_review_previousLine.__doc__=_("Moves the review cursor to the previous line of the current navigator object and speaks it") - script_review_previousLine.resumeSayAllMode=sayAllHandler.CURSOR_REVIEW - script_review_previousLine.category=SCRCAT_TEXTREVIEW + speech.speakTextInfo(info, unit=textInfos.UNIT_LINE, reason=controlTypes.OutputReason.CARET) + @script( + description=_( + # Translators: Input help mode message for read current line under review cursor command. + "Reports the line of the current navigator object where the review cursor is situated. " + "If this key is pressed twice, the current line will be spelled. " + "Pressing three times will spell the line using character descriptions." + ), + category=SCRCAT_TEXTREVIEW, + gestures=("kb:numpad8", "kb(laptop):NVDA+shift+.") + ) def script_review_currentLine(self,gesture): info=api.getReviewPosition().copy() info.expand(textInfos.UNIT_LINE) @@ -1100,13 +1287,17 @@ def script_review_currentLine(self,gesture): braille.handler.setTether(braille.handler.TETHER_REVIEW, auto=True) scriptCount=scriptHandler.getLastScriptRepeatCount() if scriptCount==0: - speech.speakTextInfo(info,unit=textInfos.UNIT_LINE,reason=controlTypes.REASON_CARET) + speech.speakTextInfo(info, unit=textInfos.UNIT_LINE, reason=controlTypes.OutputReason.CARET) else: speech.spellTextInfo(info,useCharacterDescriptions=scriptCount>1) - # Translators: Input help mode message for read current line under review cursor command. - script_review_currentLine.__doc__=_("Reports the line of the current navigator object where the review cursor is situated. If this key is pressed twice, the current line will be spelled. Pressing three times will spell the line using character descriptions.") - script_review_currentLine.category=SCRCAT_TEXTREVIEW + @script( + # Translators: Input help mode message for move review cursor to next line command. + description=_("Moves the review cursor to the next line of the current navigator object and speaks it"), + resumeSayAllMode=sayAllHandler.CURSOR_REVIEW, + category=SCRCAT_TEXTREVIEW, + gestures=("kb:numpad9", "kb(laptop):NVDA+downArrow", "ts(text):flickDown") + ) def script_review_nextLine(self,gesture): info=api.getReviewPosition().copy() info.expand(textInfos.UNIT_LINE) @@ -1118,21 +1309,26 @@ def script_review_nextLine(self,gesture): else: api.setReviewPosition(info) info.expand(textInfos.UNIT_LINE) - speech.speakTextInfo(info,unit=textInfos.UNIT_LINE,reason=controlTypes.REASON_CARET) - # Translators: Input help mode message for move review cursor to next line command. - script_review_nextLine.__doc__=_("Moves the review cursor to the next line of the current navigator object and speaks it") - script_review_nextLine.resumeSayAllMode=sayAllHandler.CURSOR_REVIEW - script_review_nextLine.category=SCRCAT_TEXTREVIEW + speech.speakTextInfo(info, unit=textInfos.UNIT_LINE, reason=controlTypes.OutputReason.CARET) + @script( + # Translators: Input help mode message for move review cursor to bottom line command. + description=_("Moves the review cursor to the bottom line of the current navigator object and speaks it"), + category=SCRCAT_TEXTREVIEW, + gestures=("kb:shift+numpad9", "kb(laptop):NVDA+control+end") + ) def script_review_bottom(self,gesture): info=api.getReviewPosition().obj.makeTextInfo(textInfos.POSITION_LAST) api.setReviewPosition(info) info.expand(textInfos.UNIT_LINE) - speech.speakTextInfo(info,unit=textInfos.UNIT_LINE,reason=controlTypes.REASON_CARET) - # Translators: Input help mode message for move review cursor to bottom line command. - script_review_bottom.__doc__=_("Moves the review cursor to the bottom line of the current navigator object and speaks it") - script_review_bottom.category=SCRCAT_TEXTREVIEW + speech.speakTextInfo(info, unit=textInfos.UNIT_LINE, reason=controlTypes.OutputReason.CARET) + @script( + # Translators: Input help mode message for move review cursor to previous word command. + description=_("Moves the review cursor to the previous word of the current navigator object and speaks it"), + category=SCRCAT_TEXTREVIEW, + gestures=("kb:numpad4", "kb(laptop):NVDA+control+leftArrow", "ts(text):2finger_flickLeft") + ) def script_review_previousWord(self,gesture): info=api.getReviewPosition().copy() info.expand(textInfos.UNIT_WORD) @@ -1144,11 +1340,18 @@ def script_review_previousWord(self,gesture): else: api.setReviewPosition(info) info.expand(textInfos.UNIT_WORD) - speech.speakTextInfo(info,reason=controlTypes.REASON_CARET,unit=textInfos.UNIT_WORD) - # Translators: Input help mode message for move review cursor to previous word command. - script_review_previousWord.__doc__=_("Moves the review cursor to the previous word of the current navigator object and speaks it") - script_review_previousWord.category=SCRCAT_TEXTREVIEW + speech.speakTextInfo(info, reason=controlTypes.OutputReason.CARET, unit=textInfos.UNIT_WORD) + @script( + description=_( + # Translators: Input help mode message for report current word under review cursor command. + "Speaks the word of the current navigator object where the review cursor is situated. " + "Pressing twice spells the word. " + "Pressing three times spells the word using character descriptions" + ), + category=SCRCAT_TEXTREVIEW, + gestures=("kb:numpad5", "kb(laptop):NVDA+control+.", "ts(text):hoverUp") + ) def script_review_currentWord(self,gesture): info=api.getReviewPosition().copy() info.expand(textInfos.UNIT_WORD) @@ -1156,13 +1359,16 @@ def script_review_currentWord(self,gesture): braille.handler.setTether(braille.handler.TETHER_REVIEW, auto=True) scriptCount=scriptHandler.getLastScriptRepeatCount() if scriptCount==0: - speech.speakTextInfo(info,reason=controlTypes.REASON_CARET,unit=textInfos.UNIT_WORD) + speech.speakTextInfo(info, reason=controlTypes.OutputReason.CARET, unit=textInfos.UNIT_WORD) else: speech.spellTextInfo(info,useCharacterDescriptions=scriptCount>1) - # Translators: Input help mode message for report current word under review cursor command. - script_review_currentWord.__doc__=_("Speaks the word of the current navigator object where the review cursor is situated. Pressing twice spells the word. Pressing three times spells the word using character descriptions") - script_review_currentWord.category=SCRCAT_TEXTREVIEW + @script( + # Translators: Input help mode message for move review cursor to next word command. + description=_("Moves the review cursor to the next word of the current navigator object and speaks it"), + category=SCRCAT_TEXTREVIEW, + gestures=("kb:numpad6", "kb(laptop):NVDA+control+rightArrow", "ts(text):2finger_flickRight") + ) def script_review_nextWord(self,gesture): info=api.getReviewPosition().copy() info.expand(textInfos.UNIT_WORD) @@ -1174,22 +1380,33 @@ def script_review_nextWord(self,gesture): else: api.setReviewPosition(info) info.expand(textInfos.UNIT_WORD) - speech.speakTextInfo(info,reason=controlTypes.REASON_CARET,unit=textInfos.UNIT_WORD) - # Translators: Input help mode message for move review cursor to next word command. - script_review_nextWord.__doc__=_("Moves the review cursor to the next word of the current navigator object and speaks it") - script_review_nextWord.category=SCRCAT_TEXTREVIEW + speech.speakTextInfo(info, reason=controlTypes.OutputReason.CARET, unit=textInfos.UNIT_WORD) + @script( + description=_( + # Translators: Input help mode message for move review cursor to start of current line command. + "Moves the review cursor to the first character of the line " + "where it is situated in the current navigator object and speaks it" + ), + category=SCRCAT_TEXTREVIEW, + gestures=("kb:shift+numpad1", "kb(laptop):NVDA+home") + ) def script_review_startOfLine(self,gesture): info=api.getReviewPosition().copy() info.expand(textInfos.UNIT_LINE) info.collapse() api.setReviewPosition(info) info.expand(textInfos.UNIT_CHARACTER) - speech.speakTextInfo(info,unit=textInfos.UNIT_CHARACTER,reason=controlTypes.REASON_CARET) - # Translators: Input help mode message for move review cursor to start of current line command. - script_review_startOfLine.__doc__=_("Moves the review cursor to the first character of the line where it is situated in the current navigator object and speaks it") - script_review_startOfLine.category=SCRCAT_TEXTREVIEW + speech.speakTextInfo(info, unit=textInfos.UNIT_CHARACTER, reason=controlTypes.OutputReason.CARET) + @script( + description=_( + # Translators: Input help mode message for move review cursor to previous character command. + "Moves the review cursor to the previous character of the current navigator object and speaks it" + ), + category=SCRCAT_TEXTREVIEW, + gestures=("kb:numpad1", "kb(laptop):NVDA+leftArrow", "ts(text):flickLeft") + ) def script_review_previousCharacter(self,gesture): lineInfo=api.getReviewPosition().copy() lineInfo.expand(textInfos.UNIT_LINE) @@ -1202,15 +1419,22 @@ def script_review_previousCharacter(self,gesture): ui.reviewMessage(_("Left")) reviewInfo=api.getReviewPosition().copy() reviewInfo.expand(textInfos.UNIT_CHARACTER) - speech.speakTextInfo(reviewInfo,unit=textInfos.UNIT_CHARACTER,reason=controlTypes.REASON_CARET) + speech.speakTextInfo(reviewInfo, unit=textInfos.UNIT_CHARACTER, reason=controlTypes.OutputReason.CARET) else: api.setReviewPosition(charInfo) charInfo.expand(textInfos.UNIT_CHARACTER) - speech.speakTextInfo(charInfo,unit=textInfos.UNIT_CHARACTER,reason=controlTypes.REASON_CARET) - # Translators: Input help mode message for move review cursor to previous character command. - script_review_previousCharacter.__doc__=_("Moves the review cursor to the previous character of the current navigator object and speaks it") - script_review_previousCharacter.category=SCRCAT_TEXTREVIEW + speech.speakTextInfo(charInfo, unit=textInfos.UNIT_CHARACTER, reason=controlTypes.OutputReason.CARET) + @script( + description=_( + # Translators: Input help mode message for report current character under review cursor command. + "Reports the character of the current navigator object where the review cursor is situated. " + "Pressing twice reports a description or example of that character. " + "Pressing three times reports the numeric value of the character in decimal and hexadecimal" + ), + category=SCRCAT_TEXTREVIEW, + gestures=("kb:numpad2", "kb(laptop):NVDA+.") + ) def script_review_currentCharacter(self,gesture): info=api.getReviewPosition().copy() info.expand(textInfos.UNIT_CHARACTER) @@ -1218,7 +1442,7 @@ def script_review_currentCharacter(self,gesture): braille.handler.setTether(braille.handler.TETHER_REVIEW, auto=True) scriptCount=scriptHandler.getLastScriptRepeatCount() if scriptCount==0: - speech.speakTextInfo(info,unit=textInfos.UNIT_CHARACTER,reason=controlTypes.REASON_CARET) + speech.speakTextInfo(info, unit=textInfos.UNIT_CHARACTER, reason=controlTypes.OutputReason.CARET) elif scriptCount==1: speech.spellTextInfo(info,useCharacterDescriptions=True) else: @@ -1231,11 +1455,16 @@ def script_review_currentCharacter(self,gesture): speech.speakSpelling(hex(c)) else: log.debugWarning("Couldn't calculate ordinal for character %r" % info.text) - speech.speakTextInfo(info,unit=textInfos.UNIT_CHARACTER,reason=controlTypes.REASON_CARET) - # Translators: Input help mode message for report current character under review cursor command. - script_review_currentCharacter.__doc__=_("Reports the character of the current navigator object where the review cursor is situated. Pressing twice reports a description or example of that character. Pressing three times reports the numeric value of the character in decimal and hexadecimal") - script_review_currentCharacter.category=SCRCAT_TEXTREVIEW + speech.speakTextInfo(info, unit=textInfos.UNIT_CHARACTER, reason=controlTypes.OutputReason.CARET) + @script( + description=_( + # Translators: Input help mode message for move review cursor to next character command. + "Moves the review cursor to the next character of the current navigator object and speaks it" + ), + category=SCRCAT_TEXTREVIEW, + gestures=("kb:numpad3", "kb(laptop):NVDA+rightArrow", "ts(text):flickRight") + ) def script_review_nextCharacter(self,gesture): lineInfo=api.getReviewPosition().copy() lineInfo.expand(textInfos.UNIT_LINE) @@ -1248,15 +1477,21 @@ def script_review_nextCharacter(self,gesture): ui.reviewMessage(_("Right")) reviewInfo=api.getReviewPosition().copy() reviewInfo.expand(textInfos.UNIT_CHARACTER) - speech.speakTextInfo(reviewInfo,unit=textInfos.UNIT_CHARACTER,reason=controlTypes.REASON_CARET) + speech.speakTextInfo(reviewInfo, unit=textInfos.UNIT_CHARACTER, reason=controlTypes.OutputReason.CARET) else: api.setReviewPosition(charInfo) charInfo.expand(textInfos.UNIT_CHARACTER) - speech.speakTextInfo(charInfo,unit=textInfos.UNIT_CHARACTER,reason=controlTypes.REASON_CARET) - # Translators: Input help mode message for move review cursor to next character command. - script_review_nextCharacter.__doc__=_("Moves the review cursor to the next character of the current navigator object and speaks it") - script_review_nextCharacter.category=SCRCAT_TEXTREVIEW + speech.speakTextInfo(charInfo, unit=textInfos.UNIT_CHARACTER, reason=controlTypes.OutputReason.CARET) + @script( + description=_( + # Translators: Input help mode message for move review cursor to end of current line command. + "Moves the review cursor to the last character of the line " + "where it is situated in the current navigator object and speaks it" + ), + category=SCRCAT_TEXTREVIEW, + gestures=("kb:shift+numpad3", "kb(laptop):NVDA+end") + ) def script_review_endOfLine(self,gesture): info=api.getReviewPosition().copy() info.expand(textInfos.UNIT_LINE) @@ -1264,10 +1499,7 @@ def script_review_endOfLine(self,gesture): info.move(textInfos.UNIT_CHARACTER,-1) api.setReviewPosition(info) info.expand(textInfos.UNIT_CHARACTER) - speech.speakTextInfo(info,unit=textInfos.UNIT_CHARACTER,reason=controlTypes.REASON_CARET) - # Translators: Input help mode message for move review cursor to end of current line command. - script_review_endOfLine.__doc__=_("Moves the review cursor to the last character of the line where it is situated in the current navigator object and speaks it") - script_review_endOfLine.category=SCRCAT_TEXTREVIEW + speech.speakTextInfo(info, unit=textInfos.UNIT_CHARACTER, reason=controlTypes.OutputReason.CARET) def _getCurrentLanguageForTextInfo(self, info): curLanguage = None @@ -1282,7 +1514,7 @@ def _getCurrentLanguageForTextInfo(self, info): @script( # Translators: Input help mode message for Review Current Symbol command. description=_("Reports the symbol where the review cursor is positioned. Pressed twice, shows the symbol and the text used to speak it in browse mode"), - category=SCRCAT_TEXTREVIEW, + category=SCRCAT_TEXTREVIEW ) def script_review_currentSymbol(self,gesture): info=api.getReviewPosition().copy() @@ -1305,6 +1537,17 @@ def script_review_currentSymbol(self,gesture): title = _("Expanded symbol ({})").format(languageDescription) ui.browseableMessage(message, title) + @script( + description=_( + # Translators: Input help mode message for toggle speech mode command. + "Toggles between the speech modes of off, beep and talk. " + "When set to off NVDA will not speak anything. " + "If beeps then NVDA will simply beep each time it its supposed to speak something. " + "If talk then NVDA will just speak normally." + ), + category=SCRCAT_SPEECH, + gesture="kb:NVDA+s" + ) def script_speechMode(self,gesture): curMode=speech.speechMode speech.speechMode=speech.speechMode_talk @@ -1321,10 +1564,16 @@ def script_speechMode(self,gesture): speech.cancelSpeech() ui.message(name) speech.speechMode=newMode - # Translators: Input help mode message for toggle speech mode command. - script_speechMode.__doc__=_("Toggles between the speech modes of off, beep and talk. When set to off NVDA will not speak anything. If beeps then NVDA will simply beep each time it its supposed to speak something. If talk then NVDA wil just speak normally.") - script_speechMode.category=SCRCAT_SPEECH + @script( + description=_( + # Translators: Input help mode message for move to next document with focus command, + # mostly used in web browsing to move from embedded object to the webpage document. + "Moves the focus to the next closest document that contains the focus" + ), + category=SCRCAT_FOCUS, + gesture="kb:NVDA+control+space" + ) def script_moveToParentTreeInterceptor(self,gesture): obj=api.getFocusObject() parent=obj.parent @@ -1340,10 +1589,19 @@ def script_moveToParentTreeInterceptor(self,gesture): # We must use core.callLater rather than wx.CallLater to ensure that the callback runs within NVDA's core pump. # If it didn't, and it directly or indirectly called wx.Yield, it could start executing NVDA's core pump from within the yield, causing recursion. core.callLater(50,eventHandler.executeEvent,"gainFocus",parent.treeInterceptor.rootNVDAObject) - # Translators: Input help mode message for move to next document with focus command, mostly used in web browsing to move from embedded object to the webpage document. - script_moveToParentTreeInterceptor.__doc__=_("Moves the focus to the next closest document that contains the focus") - script_moveToParentTreeInterceptor.category=SCRCAT_FOCUS + @script( + description=_( + # Translators: Input help mode message for toggle focus and browse mode command + # in web browsing and other situations. + "Toggles between browse mode and focus mode. " + "When in focus mode, keys will pass straight through to the application, " + "allowing you to interact directly with a control. " + "When in browse mode, you can navigate the document with the cursor, quick navigation keys, etc." + ), + category=inputCore.SCRCAT_BROWSEMODE, + gesture="kb:NVDA+space" + ) def script_toggleVirtualBufferPassThrough(self,gesture): focus = api.getFocusObject() vbuf = focus.treeInterceptor @@ -1383,39 +1641,50 @@ def script_toggleVirtualBufferPassThrough(self,gesture): # If we're disabling pass-through, re-enable auto-pass-through. vbuf.disableAutoPassThrough = vbuf.passThrough browseMode.reportPassThrough(vbuf) - # Translators: Input help mode message for toggle focus and browse mode command in web browsing and other situations. - script_toggleVirtualBufferPassThrough.__doc__=_("Toggles between browse mode and focus mode. When in focus mode, keys will pass straight through to the application, allowing you to interact directly with a control. When in browse mode, you can navigate the document with the cursor, quick navigation keys, etc.") - script_toggleVirtualBufferPassThrough.category=inputCore.SCRCAT_BROWSEMODE + @script( + # Translators: Input help mode message for quit NVDA command. + description=_("Quits NVDA!"), + gesture="kb:NVDA+q" + ) def script_quit(self,gesture): gui.quit() - # Translators: Input help mode message for quit NVDA command. - script_quit.__doc__=_("Quits NVDA!") + @script( + # Translators: Input help mode message for restart NVDA command. + description=_("Restarts NVDA!") + ) def script_restart(self,gesture): core.restart() - # Translators: Input help mode message for restart NVDA command. - script_restart.__doc__=_("Restarts NVDA!") + @script( + # Translators: Input help mode message for show NVDA menu command. + description=_("Shows the NVDA menu"), + gestures=("kb:NVDA+n", "ts:2finger_double_tap") + ) def script_showGui(self,gesture): gui.showGui() - # Translators: Input help mode message for show NVDA menu command. - script_showGui.__doc__=_("Shows the NVDA menu") + @script( + description=_( + # Translators: Input help mode message for say all in review cursor command. + "Reads from the review cursor up to the end of the current text," + " moving the review cursor as it goes" + ), + category=SCRCAT_TEXTREVIEW, + gestures=("kb:numpadPlus", "kb(laptop):NVDA+shift+a", "ts(text):3finger_flickDown") + ) def script_review_sayAll(self,gesture): sayAllHandler.readText(sayAllHandler.CURSOR_REVIEW) - script_review_sayAll.__doc__ = _( - # Translators: Input help mode message for say all in review cursor command. - "Reads from the review cursor up to the end of the current text," - " moving the review cursor as it goes" - ) - script_review_sayAll.category=SCRCAT_TEXTREVIEW + @script( + # Translators: Input help mode message for say all with system caret command. + description=_("Reads from the system caret up to the end of the text, moving the caret as it goes"), + category=SCRCAT_SYSTEMCARET, + gestures=("kb(desktop):NVDA+downArrow", "kb(laptop):NVDA+a") + ) def script_sayAll(self,gesture): sayAllHandler.readText(sayAllHandler.CURSOR_CARET) - # Translators: Input help mode message for say all with system caret command. - script_sayAll.__doc__ = _("Reads from the system caret up to the end of the text, moving the caret as it goes") - script_sayAll.category=SCRCAT_SYSTEMCARET def _reportFormattingHelper(self, info, browseable=False): # Report all formatting-related changes regardless of user settings @@ -1517,7 +1786,7 @@ def _getTIAtCaret(fallbackToPOSITION_FIRST=False): @script( # Translators: Input help mode message for report formatting command. description=_("Reports formatting info for the current review cursor position."), - category=SCRCAT_TEXTREVIEW, + category=SCRCAT_TEXTREVIEW ) def script_reportFormattingAtReview(self, gesture): self._reportFormattingHelper(api.getReviewPosition(), False) @@ -1525,7 +1794,7 @@ def script_reportFormattingAtReview(self, gesture): @script( # Translators: Input help mode message for show formatting at review cursor command. description=_("Presents, in browse mode, formatting info for the current review cursor position."), - category=SCRCAT_TEXTREVIEW, + category=SCRCAT_TEXTREVIEW ) def script_showFormattingAtReview(self, gesture): self._reportFormattingHelper(api.getReviewPosition(), True) @@ -1537,7 +1806,7 @@ def script_showFormattingAtReview(self, gesture): " If pressed twice, presents the information in browse mode" ), category=SCRCAT_TEXTREVIEW, - gesture="kb:NVDA+shift+f", + gesture="kb:NVDA+shift+f" ) def script_reportFormatting(self, gesture): repeats = scriptHandler.getLastScriptRepeatCount() @@ -1549,7 +1818,7 @@ def script_reportFormatting(self, gesture): @script( # Translators: Input help mode message for report formatting at caret command. description=_("Reports formatting info for the text under the caret."), - category=SCRCAT_SYSTEMCARET, + category=SCRCAT_SYSTEMCARET ) def script_reportFormattingAtCaret(self, gesture): self._reportFormattingHelper(self._getTIAtCaret(True), False) @@ -1557,7 +1826,7 @@ def script_reportFormattingAtCaret(self, gesture): @script( # Translators: Input help mode message for show formatting at caret position command. description=_("Presents, in browse mode, formatting info for the text under the caret."), - category=SCRCAT_SYSTEMCARET, + category=SCRCAT_SYSTEMCARET ) def script_showFormattingAtCaret(self, gesture): self._reportFormattingHelper(self._getTIAtCaret(True), True) @@ -1569,7 +1838,7 @@ def script_showFormattingAtCaret(self, gesture): " If pressed twice, presents the information in browse mode" ), category=SCRCAT_SYSTEMCARET, - gesture="kb:NVDA+f", + gesture="kb:NVDA+f" ) def script_reportOrShowFormattingAtCaret(self, gesture): repeats = scriptHandler.getLastScriptRepeatCount() @@ -1578,19 +1847,32 @@ def script_reportOrShowFormattingAtCaret(self, gesture): elif repeats == 1: self.script_showFormattingAtCaret(gesture) + @script( + # Translators: Input help mode message for report current focus command. + description=_("Reports the object with focus. If pressed twice, spells the information"), + category=SCRCAT_FOCUS, + gesture="kb:NVDA+tab" + ) def script_reportCurrentFocus(self,gesture): focusObject=api.getFocusObject() if isinstance(focusObject,NVDAObject): if scriptHandler.getLastScriptRepeatCount()==0: - speech.speakObject(focusObject, reason=controlTypes.REASON_QUERY) + speech.speakObject(focusObject, reason=controlTypes.OutputReason.QUERY) else: speech.speakSpelling(focusObject.name) else: ui.message(_("No focus")) - # Translators: Input help mode message for report current focus command. - script_reportCurrentFocus.__doc__ = _("Reports the object with focus. If pressed twice, spells the information") - script_reportCurrentFocus.category=SCRCAT_FOCUS + @script( + description=_( + # Translators: Input help mode message for report status line text command. + "Reads the current application status bar and moves the navigator to it. " + "If pressed twice, spells the information. " + "If pressed three times, copies the status bar to the clipboard" + ), + category=SCRCAT_FOCUS, + gestures=("kb(desktop):NVDA+end", "kb(laptop):NVDA+shift+end") + ) def script_reportStatusLine(self,gesture): obj = api.getStatusBar() found=False @@ -1635,10 +1917,13 @@ def script_reportStatusLine(self,gesture): ui.message(_("unable to copy status bar content to clipboard")) else: api.copyToClip(text, notify=True) - # Translators: Input help mode message for report status line text command. - script_reportStatusLine.__doc__ = _("Reads the current application status bar and moves the navigator to it. If pressed twice, spells the information. If pressed three times, copies the status bar to the clipboard") - script_reportStatusLine.category=SCRCAT_FOCUS + @script( + # Translators: Input help mode message for toggle mouse tracking command. + description=_("Toggles the reporting of information as the mouse moves"), + category=SCRCAT_MOUSE, + gesture="kb:NVDA+m" + ) def script_toggleMouseTracking(self,gesture): if config.conf["mouse"]["enableMouseTracking"]: # Translators: presented when the mouse tracking is toggled. @@ -1649,10 +1934,12 @@ def script_toggleMouseTracking(self,gesture): state = _("Mouse tracking on") config.conf["mouse"]["enableMouseTracking"]=True ui.message(state) - # Translators: Input help mode message for toggle mouse tracking command. - script_toggleMouseTracking.__doc__=_("Toggles the reporting of information as the mouse moves") - script_toggleMouseTracking.category=SCRCAT_MOUSE + @script( + # Translators: Input help mode message for toggle mouse text unit resolution command. + description=_("Toggles how much text will be spoken when the mouse moves"), + category=SCRCAT_MOUSE + ) def script_toggleMouseTextResolution(self,gesture): values = textInfos.MOUSE_TEXT_RESOLUTION_UNITS labels = [textInfos.unitLabels[x] for x in values] @@ -1668,10 +1955,17 @@ def script_toggleMouseTextResolution(self,gesture): # %s will be replaced with the new label. # For example, the full message might be "Mouse text unit resolution character" ui.message(_("Mouse text unit resolution %s")%labels[newIndex]) - # Translators: Input help mode message for toggle mouse text unit resolution command. - script_toggleMouseTextResolution.__doc__=_("Toggles how much text will be spoken when the mouse moves") - script_toggleMouseTextResolution.category=SCRCAT_MOUSE + @script( + description=_( + # Translators: Input help mode message for report title bar command. + "Reports the title of the current application or foreground window. " + "If pressed twice, spells the title. " + "If pressed three times, copies the title to the clipboard" + ), + category=SCRCAT_FOCUS, + gesture="kb:NVDA+t" + ) def script_title(self,gesture): obj=api.getForegroundObject() title=obj.name @@ -1687,42 +1981,55 @@ def script_title(self,gesture): speech.speakSpelling(title) else: api.copyToClip(title, notify=True) - # Translators: Input help mode message for report title bar command. - script_title.__doc__=_("Reports the title of the current application or foreground window. If pressed twice, spells the title. If pressed three times, copies the title to the clipboard") - script_title.category=SCRCAT_FOCUS + @script( + # Translators: Input help mode message for read foreground object command (usually the foreground window). + description=_("Reads all controls in the active window"), + category=SCRCAT_FOCUS, + gesture="kb:NVDA+b" + ) def script_speakForeground(self,gesture): obj=api.getForegroundObject() if obj: sayAllHandler.readObjects(obj) - # Translators: Input help mode message for read foreground object command (usually the foreground window). - script_speakForeground.__doc__ = _("Reads all controls in the active window") - script_speakForeground.category=SCRCAT_FOCUS + @script( + gesture="kb(desktop):NVDA+control+f2" + ) def script_test_navigatorDisplayModelText(self,gesture): obj=api.getNavigatorObject() text=obj.displayText speech.speakMessage(text) log.info(text) + @script( + description=_( + # Translators: GUI development tool, to get information about the components used in the NVDA GUI + "Opens the WX GUI inspection tool. Used to get more information about the state of GUI components." + ), + category=SCRCAT_TOOLS + ) def script_startWxInspectionTool(self, gesture): import wx.lib.inspection wx.lib.inspection.InspectionTool().Show() - script_startWxInspectionTool.__doc__ = _( - # Translators: GUI development tool, to get information about the components used in the NVDA GUI - "Opens the WX GUI inspection tool. Used to get more information about the state of GUI components." - ) - script_startWxInspectionTool.category = SCRCAT_TOOLS + @script( + description=_( + # Translators: Input help mode message for developer info for current navigator object command, + # used by developers to examine technical info on navigator object. + # This command also serves as a shortcut to open NVDA log viewer. + "Logs information about the current navigator object which is useful to developers " + "and activates the log viewer so the information can be examined." + ), + category=SCRCAT_TOOLS, + gesture="kb:NVDA+f1" + ) def script_navigatorObject_devInfo(self,gesture): obj=api.getNavigatorObject() if hasattr(obj, "devInfo"): log.info("Developer info for navigator object:\n%s" % "\n".join(obj.devInfo), activateLogViewer=True) else: log.info("No developer info for navigator object", activateLogViewer=True) - # Translators: Input help mode message for developer info for current navigator object command, used by developers to examine technical info on navigator object. This command also serves as a shortcut to open NVDA log viewer. - script_navigatorObject_devInfo.__doc__ = _("Logs information about the current navigator object which is useful to developers and activates the log viewer so the information can be examined.") - script_navigatorObject_devInfo.category=SCRCAT_TOOLS @script( description=_( @@ -1771,6 +2078,14 @@ def script_openUserConfigurationDirectory(self, gesture): import systemUtils systemUtils.openUserConfigurationDirectory() + @script( + description=_( + # Translators: Input help mode message for toggle progress bar output command. + "Toggles between beeps, speech, beeps and speech, and off, for reporting progress bar updates" + ), + category=SCRCAT_SPEECH, + gesture="kb:NVDA+u" + ) def script_toggleProgressBarOutput(self,gesture): outputMode=config.conf["presentation"]["progressBarUpdates"]["progressBarOutputMode"] if outputMode=="both": @@ -1790,10 +2105,16 @@ def script_toggleProgressBarOutput(self,gesture): # Translators: A mode where both speech and beeps will indicate progress bar updates. ui.message(_("Beep and speak progress bar updates")) config.conf["presentation"]["progressBarUpdates"]["progressBarOutputMode"]=outputMode - # Translators: Input help mode message for toggle progress bar output command. - script_toggleProgressBarOutput.__doc__=_("Toggles between beeps, speech, beeps and speech, and off, for reporting progress bar updates") - script_toggleProgressBarOutput.category=SCRCAT_SPEECH + @script( + description=_( + # Translators: Input help mode message for toggle dynamic content changes command. + "Toggles on and off the reporting of dynamic content changes, " + "such as new text in dos console windows" + ), + category=SCRCAT_SPEECH, + gesture="kb:NVDA+5" + ) def script_toggleReportDynamicContentChanges(self,gesture): if config.conf["presentation"]["reportDynamicContentChanges"]: # Translators: presented when the present dynamic changes is toggled. @@ -1804,10 +2125,13 @@ def script_toggleReportDynamicContentChanges(self,gesture): state = _("report dynamic content changes on") config.conf["presentation"]["reportDynamicContentChanges"]=True ui.message(state) - # Translators: Input help mode message for toggle dynamic content changes command. - script_toggleReportDynamicContentChanges.__doc__=_("Toggles on and off the reporting of dynamic content changes, such as new text in dos console windows") - script_toggleReportDynamicContentChanges.category=SCRCAT_SPEECH + @script( + # Translators: Input help mode message for toggle caret moves review cursor command. + description=_("Toggles on and off the movement of the review cursor due to the caret moving."), + category=SCRCAT_TEXTREVIEW, + gesture="kb:NVDA+6" + ) def script_toggleCaretMovesReviewCursor(self,gesture): if config.conf["reviewCursor"]["followCaret"]: # Translators: presented when toggled. @@ -1818,10 +2142,13 @@ def script_toggleCaretMovesReviewCursor(self,gesture): state = _("caret moves review cursor on") config.conf["reviewCursor"]["followCaret"]=True ui.message(state) - # Translators: Input help mode message for toggle caret moves review cursor command. - script_toggleCaretMovesReviewCursor.__doc__=_("Toggles on and off the movement of the review cursor due to the caret moving.") - script_toggleCaretMovesReviewCursor.category=SCRCAT_TEXTREVIEW + @script( + # Translators: Input help mode message for toggle focus moves navigator object command. + description=_("Toggles on and off the movement of the navigator object due to focus changes"), + category=SCRCAT_OBJECTNAVIGATION, + gesture="kb:NVDA+7" + ) def script_toggleFocusMovesNavigatorObject(self,gesture): if config.conf["reviewCursor"]["followFocus"]: # Translators: presented when toggled. @@ -1832,10 +2159,13 @@ def script_toggleFocusMovesNavigatorObject(self,gesture): state = _("focus moves navigator object on") config.conf["reviewCursor"]["followFocus"]=True ui.message(state) - # Translators: Input help mode message for toggle focus moves navigator object command. - script_toggleFocusMovesNavigatorObject.__doc__=_("Toggles on and off the movement of the navigator object due to focus changes") - script_toggleFocusMovesNavigatorObject.category=SCRCAT_OBJECTNAVIGATION + @script( + # Translators: Input help mode message for toggle auto focus focusable elements command. + description=_("Toggles on and off automatic movement of the system focus due to browse mode commands"), + category=inputCore.SCRCAT_BROWSEMODE, + gesture="kb:NVDA+8" + ) def script_toggleAutoFocusFocusableElements(self,gesture): if config.conf["virtualBuffers"]["autoFocusFocusableElements"]: # Translators: presented when toggled. @@ -1846,11 +2176,14 @@ def script_toggleAutoFocusFocusableElements(self,gesture): state = _("Automatically set system focus to focusable elements on") config.conf["virtualBuffers"]["autoFocusFocusableElements"]=True ui.message(state) - # Translators: Input help mode message for toggle auto focus focusable elements command. - script_toggleAutoFocusFocusableElements.__doc__=_("Toggles on and off automatic movement of the system focus due to browse mode commands") - script_toggleAutoFocusFocusableElements.category=inputCore.SCRCAT_BROWSEMODE - #added by Rui Batista to implement a battery status script + # added by Rui Batista to implement a battery status script + @script( + # Translators: Input help mode message for report battery status command. + description=_("Reports battery status and time remaining if AC is not plugged in"), + category=SCRCAT_SYSTEM, + gesture="kb:NVDA+shift+b" + ) def script_say_battery_status(self,gesture): UNKNOWN_BATTERY_STATUS = 0xFF AC_ONLINE = 0X1 @@ -1871,18 +2204,29 @@ def script_say_battery_status(self,gesture): # Translators: This is the estimated remaining runtime of the laptop battery. text += _("{hours:d} hours and {minutes:d} minutes remaining") .format(hours=sps.BatteryLifeTime // 3600, minutes=(sps.BatteryLifeTime % 3600) // 60) ui.message(text) - # Translators: Input help mode message for report battery status command. - script_say_battery_status.__doc__ = _("Reports battery status and time remaining if AC is not plugged in") - script_say_battery_status.category=SCRCAT_SYSTEM + @script( + description=_( + # Translators: Input help mode message for pass next key through command. + "The next key that is pressed will not be handled at all by NVDA, " + "it will be passed directly through to Windows." + ), + category=SCRCAT_INPUT, + gesture="kb:NVDA+f2" + ) def script_passNextKeyThrough(self,gesture): keyboardHandler.passNextKeyThrough() # Translators: Spoken to indicate that the next key press will be sent straight to the current program as though NVDA is not running. ui.message(_("Pass next key through")) - # Translators: Input help mode message for pass next key through command. - script_passNextKeyThrough.__doc__=_("The next key that is pressed will not be handled at all by NVDA, it will be passed directly through to Windows.") - script_passNextKeyThrough.category=SCRCAT_INPUT + @script( + description=_( + # Translators: Input help mode message for report current program name and app module name command. + "Speaks the filename of the active application along with the name of the currently loaded appModule" + ), + category=SCRCAT_TOOLS, + gesture="kb:NVDA+control+f1" + ) def script_reportAppModuleInfo(self,gesture): focus=api.getFocusObject() message = '' @@ -1897,116 +2241,156 @@ def script_reportAppModuleInfo(self,gesture): # For example, the complete message for Windows explorer is: "explorer module is loaded. Explorer.exe is currenty running." message +=_(" %s is currently running.") % appName ui.message(message) - # Translators: Input help mode message for report current program name and app module name command. - script_reportAppModuleInfo.__doc__ = _("Speaks the filename of the active application along with the name of the currently loaded appModule") - script_reportAppModuleInfo.category=SCRCAT_TOOLS + @script( + # Translators: Input help mode message for go to general settings command. + description=_("Shows NVDA's general settings"), + category=SCRCAT_CONFIG, + gesture="kb:NVDA+control+g" + ) def script_activateGeneralSettingsDialog(self, gesture): wx.CallAfter(gui.mainFrame.onGeneralSettingsCommand, None) - # Translators: Input help mode message for go to general settings command. - script_activateGeneralSettingsDialog.__doc__ = _("Shows NVDA's general settings") - script_activateGeneralSettingsDialog.category=SCRCAT_CONFIG + @script( + # Translators: Input help mode message for go to select synthesizer command. + description=_("Shows the NVDA synthesizer selection dialog"), + category=SCRCAT_CONFIG, + gesture="kb:NVDA+control+s" + ) def script_activateSynthesizerDialog(self, gesture): wx.CallAfter(gui.mainFrame.onSelectSynthesizerCommand, None) - # Translators: Input help mode message for go to select synthesizer command. - script_activateSynthesizerDialog.__doc__ = _("Shows the NVDA synthesizer selection dialog") - script_activateSynthesizerDialog.category=SCRCAT_CONFIG + @script( + # Translators: Input help mode message for go to speech settings command. + description=_("Shows NVDA's speech settings"), + category=SCRCAT_CONFIG, + gesture="kb:NVDA+control+v" + ) def script_activateVoiceDialog(self, gesture): wx.CallAfter(gui.mainFrame.onSpeechSettingsCommand, None) - # Translators: Input help mode message for go to speech settings command. - script_activateVoiceDialog.__doc__ = _("Shows NVDA's speech settings") - script_activateVoiceDialog.category=SCRCAT_CONFIG + @script( + # Translators: Input help mode message for go to select braille display command. + description=_("Shows the NVDA braille display selection dialog"), + category=SCRCAT_CONFIG, + gesture="kb:NVDA+control+a" + ) def script_activateBrailleDisplayDialog(self, gesture): wx.CallAfter(gui.mainFrame.onSelectBrailleDisplayCommand, None) - # Translators: Input help mode message for go to select braille display command. - script_activateBrailleDisplayDialog.__doc__ = _("Shows the NVDA braille display selection dialog") - script_activateBrailleDisplayDialog.category=SCRCAT_CONFIG + @script( + # Translators: Input help mode message for go to braille settings command. + description=_("Shows NVDA's braille settings"), + category=SCRCAT_CONFIG + ) def script_activateBrailleSettingsDialog(self, gesture): wx.CallAfter(gui.mainFrame.onBrailleSettingsCommand, None) - # Translators: Input help mode message for go to braille settings command. - script_activateBrailleSettingsDialog.__doc__ = _("Shows NVDA's braille settings") - script_activateBrailleSettingsDialog.category=SCRCAT_CONFIG + @script( + # Translators: Input help mode message for go to keyboard settings command. + description=_("Shows NVDA's keyboard settings"), + category=SCRCAT_CONFIG, + gesture="kb:NVDA+control+k" + ) def script_activateKeyboardSettingsDialog(self, gesture): wx.CallAfter(gui.mainFrame.onKeyboardSettingsCommand, None) - # Translators: Input help mode message for go to keyboard settings command. - script_activateKeyboardSettingsDialog.__doc__ = _("Shows NVDA's keyboard settings") - script_activateKeyboardSettingsDialog.category=SCRCAT_CONFIG + @script( + # Translators: Input help mode message for go to mouse settings command. + description=_("Shows NVDA's mouse settings"), + category=SCRCAT_CONFIG, + gesture="kb:NVDA+control+m" + ) def script_activateMouseSettingsDialog(self, gesture): wx.CallAfter(gui.mainFrame.onMouseSettingsCommand, None) - # Translators: Input help mode message for go to mouse settings command. - script_activateMouseSettingsDialog.__doc__ = _("Shows NVDA's mouse settings") - script_activateMouseSettingsDialog.category=SCRCAT_CONFIG + @script( + # Translators: Input help mode message for go to review cursor settings command. + description=_("Shows NVDA's review cursor settings"), + category=SCRCAT_CONFIG + ) def script_activateReviewCursorDialog(self, gesture): wx.CallAfter(gui.mainFrame.onReviewCursorCommand, None) - # Translators: Input help mode message for go to review cursor settings command. - script_activateReviewCursorDialog.__doc__ = _("Shows NVDA's review cursor settings") - script_activateReviewCursorDialog.category=SCRCAT_CONFIG + @script( + # Translators: Input help mode message for go to input composition settings command. + description=_("Shows NVDA's input composition settings"), + category=SCRCAT_CONFIG + ) def script_activateInputCompositionDialog(self, gesture): wx.CallAfter(gui.mainFrame.onInputCompositionCommand, None) - # Translators: Input help mode message for go to input composition settings command. - script_activateInputCompositionDialog.__doc__ = _("Shows NVDA's input composition settings") - script_activateInputCompositionDialog.category=SCRCAT_CONFIG + @script( + # Translators: Input help mode message for go to object presentation settings command. + description=_("Shows NVDA's object presentation settings"), + category=SCRCAT_CONFIG, + gesture="kb:NVDA+control+o" + ) def script_activateObjectPresentationDialog(self, gesture): wx.CallAfter(gui.mainFrame. onObjectPresentationCommand, None) - # Translators: Input help mode message for go to object presentation settings command. - script_activateObjectPresentationDialog.__doc__ = _("Shows NVDA's object presentation settings") - script_activateObjectPresentationDialog.category=SCRCAT_CONFIG + @script( + # Translators: Input help mode message for go to browse mode settings command. + description=_("Shows NVDA's browse mode settings"), + category=SCRCAT_CONFIG, + gesture="kb:NVDA+control+b" + ) def script_activateBrowseModeDialog(self, gesture): wx.CallAfter(gui.mainFrame.onBrowseModeCommand, None) - # Translators: Input help mode message for go to browse mode settings command. - script_activateBrowseModeDialog.__doc__ = _("Shows NVDA's browse mode settings") - script_activateBrowseModeDialog.category=SCRCAT_CONFIG + @script( + # Translators: Input help mode message for go to document formatting settings command. + description=_("Shows NVDA's document formatting settings"), + category=SCRCAT_CONFIG, + gesture="kb:NVDA+control+d" + ) def script_activateDocumentFormattingDialog(self, gesture): wx.CallAfter(gui.mainFrame.onDocumentFormattingCommand, None) - # Translators: Input help mode message for go to document formatting settings command. - script_activateDocumentFormattingDialog.__doc__ = _("Shows NVDA's document formatting settings") - script_activateDocumentFormattingDialog.category=SCRCAT_CONFIG + @script( + # Translators: Input help mode message for opening default dictionary dialog. + description=_("Shows the NVDA default dictionary dialog"), + category=SCRCAT_CONFIG + ) def script_activateDefaultDictionaryDialog(self, gesture): wx.CallAfter(gui.mainFrame.onDefaultDictionaryCommand, None) - # Translators: Input help mode message for opening default dictionary dialog. - script_activateDefaultDictionaryDialog.__doc__ = _("Shows the NVDA default dictionary dialog") - script_activateDefaultDictionaryDialog.category=SCRCAT_CONFIG + @script( + # Translators: Input help mode message for opening voice-specific dictionary dialog. + description=_("Shows the NVDA voice-specific dictionary dialog"), + category=SCRCAT_CONFIG + ) def script_activateVoiceDictionaryDialog(self, gesture): wx.CallAfter(gui.mainFrame.onVoiceDictionaryCommand, None) - # Translators: Input help mode message for opening voice-specific dictionary dialog. - script_activateVoiceDictionaryDialog.__doc__ = _("Shows the NVDA voice-specific dictionary dialog") - script_activateVoiceDictionaryDialog.category=SCRCAT_CONFIG + @script( + # Translators: Input help mode message for opening temporary dictionary. + description=_("Shows the NVDA temporary dictionary dialog"), + category=SCRCAT_CONFIG + ) def script_activateTemporaryDictionaryDialog(self, gesture): wx.CallAfter(gui.mainFrame.onTemporaryDictionaryCommand, None) - # Translators: Input help mode message for opening temporary dictionary. - script_activateTemporaryDictionaryDialog.__doc__ = _("Shows the NVDA temporary dictionary dialog") - script_activateTemporaryDictionaryDialog.category=SCRCAT_CONFIG + @script( + # Translators: Input help mode message for go to punctuation/symbol pronunciation dialog. + description=_("Shows the NVDA symbol pronunciation dialog"), + category=SCRCAT_CONFIG + ) def script_activateSpeechSymbolsDialog(self, gesture): wx.CallAfter(gui.mainFrame.onSpeechSymbolsCommand, None) - # Translators: Input help mode message for go to punctuation/symbol pronunciation dialog. - script_activateSpeechSymbolsDialog.__doc__ = _("Shows the NVDA symbol pronunciation dialog") - script_activateSpeechSymbolsDialog.category=SCRCAT_CONFIG + @script( + # Translators: Input help mode message for go to input gestures dialog command. + description=_("Shows the NVDA input gestures dialog"), + category=SCRCAT_CONFIG + ) def script_activateInputGesturesDialog(self, gesture): wx.CallAfter(gui.mainFrame.onInputGesturesCommand, None) - # Translators: Input help mode message for go to input gestures dialog command. - script_activateInputGesturesDialog.__doc__ = _("Shows the NVDA input gestures dialog") - script_activateInputGesturesDialog.category=SCRCAT_CONFIG @script( # Translators: Input help mode message for the report current configuration profile command. description=_("Reports the name of the current NVDA configuration profile"), - category=SCRCAT_CONFIG, + category=SCRCAT_CONFIG ) def script_reportActiveConfigurationProfile(self, gesture): activeProfileName = config.conf.profiles[-1].name @@ -2023,25 +2407,37 @@ def script_reportActiveConfigurationProfile(self, gesture): ) ui.message(activeProfileMessage) + @script( + # Translators: Input help mode message for save current configuration command. + description=_("Saves the current NVDA configuration"), + category=SCRCAT_CONFIG, + gesture="kb:NVDA+control+c" + ) def script_saveConfiguration(self,gesture): wx.CallAfter(gui.mainFrame.onSaveConfigurationCommand, None) - # Translators: Input help mode message for save current configuration command. - script_saveConfiguration.__doc__ = _("Saves the current NVDA configuration") - script_saveConfiguration.category=SCRCAT_CONFIG + @script( + description=_( + # Translators: Input help mode message for apply last saved or default settings command. + "Pressing once reverts the current configuration to the most recently saved state." + " Pressing three times resets to factory defaults." + ), + category=SCRCAT_CONFIG, + gesture="kb:NVDA+control+r" + ) def script_revertConfiguration(self,gesture): scriptCount=scriptHandler.getLastScriptRepeatCount() if scriptCount==0: gui.mainFrame.onRevertToSavedConfigurationCommand(None) elif scriptCount==2: gui.mainFrame.onRevertToDefaultConfigurationCommand(None) - script_revertConfiguration.__doc__ = _( - # Translators: Input help mode message for apply last saved or default settings command. - "Pressing once reverts the current configuration to the most recently saved state." - " Pressing three times resets to factory defaults." - ) - script_revertConfiguration.category=SCRCAT_CONFIG + @script( + # Translators: Input help mode message for activate python console command. + description=_("Activates the NVDA Python Console, primarily useful for development"), + category=SCRCAT_TOOLS, + gesture="kb:NVDA+control+z" + ) def script_activatePythonConsole(self,gesture): if globalVars.appArgs.secure or config.isAppX: return @@ -2050,16 +2446,23 @@ def script_activatePythonConsole(self,gesture): pythonConsole.initialize() pythonConsole.consoleUI.console.updateNamespaceSnapshotVars() pythonConsole.activate() - # Translators: Input help mode message for activate python console command. - script_activatePythonConsole.__doc__ = _("Activates the NVDA Python Console, primarily useful for development") - script_activatePythonConsole.category=SCRCAT_TOOLS + @script( + # Translators: Input help mode message for activate manage add-ons command. + description=_("Activates the NVDA Add-ons Manager to install and uninstall add-on packages for NVDA"), + category=SCRCAT_TOOLS + ) def script_activateAddonsManager(self,gesture): wx.CallAfter(gui.mainFrame.onAddonsManagerCommand, None) - # Translators: Input help mode message for activate manage add-ons command. - script_activateAddonsManager.__doc__ = _("Activates the NVDA Add-ons Manager to install and uninstall add-on packages for NVDA") - script_activateAddonsManager.category=SCRCAT_TOOLS + @script( + description=_( + # Translators: Input help mode message for toggle speech viewer command. + "Toggles the NVDA Speech viewer, " + "a floating window that allows you to view all the text that NVDA is currently speaking" + ), + category=SCRCAT_TOOLS + ) def script_toggleSpeechViewer(self,gesture): if gui.speechViewer.isActive: # Translators: The message announced when disabling speech viewer. @@ -2072,10 +2475,14 @@ def script_toggleSpeechViewer(self,gesture): gui.speechViewer.activate() gui.mainFrame.sysTrayIcon.menu_tools_toggleSpeechViewer.Check(True) ui.message(state) - # Translators: Input help mode message for toggle speech viewer command. - script_toggleSpeechViewer.__doc__ = _("Toggles the NVDA Speech viewer, a floating window that allows you to view all the text that NVDA is currently speaking") - script_toggleSpeechViewer.category=SCRCAT_TOOLS + @script( + # Translators: Input help mode message for toggle braille tether to command + # (tethered means connected to or follows). + description=_("Toggle tethering of braille between the focus and the review position"), + category=SCRCAT_BRAILLE, + gesture="kb:NVDA+control+t" + ) def script_braille_toggleTether(self, gesture): values = [x[0] for x in braille.handler.tetherValues] labels = [x[1] for x in braille.handler.tetherValues] @@ -2100,10 +2507,12 @@ def script_braille_toggleTether(self, gesture): # Translators: Reports which position braille is tethered to # (braille can be tethered automatically or to either focus or review position). ui.message(_("Braille tethered %s") % labels[newIndex]) - # Translators: Input help mode message for toggle braille tether to command (tethered means connected to or follows). - script_braille_toggleTether.__doc__ = _("Toggle tethering of braille between the focus and the review position") - script_braille_toggleTether.category=SCRCAT_BRAILLE + @script( + # Translators: Input help mode message for toggle braille focus context presentation command. + description=_("Toggle the way context information is presented in braille"), + category=SCRCAT_BRAILLE + ) def script_braille_toggleFocusContextPresentation(self, gesture): values = [x[0] for x in braille.focusContextPresentations] labels = [x[1] for x in braille.focusContextPresentations] @@ -2119,10 +2528,12 @@ def script_braille_toggleFocusContextPresentation(self, gesture): # %s will be replaced with the context presentation setting. # For example, the full message might be "Braille focus context presentation: fill display for context changes" ui.message(_("Braille focus context presentation: %s")%labels[newIndex].lower()) - # Translators: Input help mode message for toggle braille focus context presentation command. - script_braille_toggleFocusContextPresentation.__doc__ = _("Toggle the way context information is presented in braille") - script_braille_toggleFocusContextPresentation.category=SCRCAT_BRAILLE + @script( + # Translators: Input help mode message for toggle braille cursor command. + description=_("Toggle the braille cursor on and off"), + category=SCRCAT_BRAILLE + ) def script_braille_toggleShowCursor(self, gesture): if config.conf["braille"]["showCursor"]: # Translators: The message announced when toggling the braille cursor. @@ -2133,10 +2544,12 @@ def script_braille_toggleShowCursor(self, gesture): state = _("Braille cursor on") config.conf["braille"]["showCursor"]=True ui.message(state) - # Translators: Input help mode message for toggle braille cursor command. - script_braille_toggleShowCursor.__doc__ = _("Toggle the braille cursor on and off") - script_braille_toggleShowCursor.category=SCRCAT_BRAILLE + @script( + # Translators: Input help mode message for cycle braille cursor shape command. + description=_("Cycle through the braille cursor shapes"), + category=SCRCAT_BRAILLE + ) def script_braille_cycleCursorShape(self, gesture): if not config.conf["braille"]["showCursor"]: # Translators: A message reported when changing the braille cursor shape when the braille cursor is turned off. @@ -2157,10 +2570,13 @@ def script_braille_cycleCursorShape(self, gesture): shapeMsg = braille.CURSOR_SHAPES[index][1] # Translators: Reports which braille cursor shape is activated. ui.message(_("Braille cursor %s") % shapeMsg) - # Translators: Input help mode message for cycle braille cursor shape command. - script_braille_cycleCursorShape.__doc__ = _("Cycle through the braille cursor shapes") - script_braille_cycleCursorShape.category=SCRCAT_BRAILLE + @script( + # Translators: Input help mode message for report clipboard text command. + description=_("Reports the text on the Windows clipboard"), + category=SCRCAT_SYSTEM, + gesture="kb:NVDA+c" + ) def script_reportClipboardText(self,gesture): try: text = api.getClipData() @@ -2176,10 +2592,16 @@ def script_reportClipboardText(self,gesture): # Translators: If the number of characters on the clipboard is greater than about 1000, it reports this message and gives number of characters on the clipboard. # Example output: The clipboard contains a large portion of text. It is 2300 characters long. ui.message(_("The clipboard contains a large portion of text. It is %s characters long") % len(text)) - # Translators: Input help mode message for report clipboard text command. - script_reportClipboardText.__doc__ = _("Reports the text on the Windows clipboard") - script_reportClipboardText.category=SCRCAT_SYSTEM + @script( + description=_( + # Translators: Input help mode message for mark review cursor position for a select or copy command + # (that is, marks the current review cursor position as the starting point for text to be selected). + "Marks the current position of the review cursor as the start of text to be selected or copied" + ), + category=SCRCAT_TEXTREVIEW, + gesture="kb:NVDA+f9" + ) def script_review_markStartForCopy(self, gesture): reviewPos = api.getReviewPosition() # attach the marker to obj so that the marker is cleaned up when obj is cleaned up. @@ -2187,9 +2609,6 @@ def script_review_markStartForCopy(self, gesture): reviewPos.obj._selectThenCopyRange = None # we may be part way through a select, reset the copy range. # Translators: Indicates start of review cursor text to be copied to clipboard. ui.message(_("Start marked")) - # Translators: Input help mode message for mark review cursor position for a select or copy command (that is, marks the current review cursor position as the starting point for text to be selected). - script_review_markStartForCopy.__doc__ = _("Marks the current position of the review cursor as the start of text to be selected or copied") - script_review_markStartForCopy.category=SCRCAT_TEXTREVIEW @script( description=_( @@ -2198,7 +2617,7 @@ def script_review_markStartForCopy(self, gesture): "Move the review cursor to the position marked as the start of text to be selected or copied" ), category=SCRCAT_TEXTREVIEW, - gesture="kb:NVDA+shift+F9", + gesture="kb:NVDA+shift+F9" ) def script_review_moveToStartMarkedForCopy(self, gesture): pos = api.getReviewPosition() @@ -2210,8 +2629,18 @@ def script_review_moveToStartMarkedForCopy(self, gesture): api.setReviewPosition(startMarker) startMarker.collapse() startMarker.expand(textInfos.UNIT_CHARACTER) - speech.speakTextInfo(startMarker, unit=textInfos.UNIT_CHARACTER, reason=controlTypes.REASON_CARET) + speech.speakTextInfo(startMarker, unit=textInfos.UNIT_CHARACTER, reason=controlTypes.OutputReason.CARET) + @script( + description=_( + # Translators: Input help mode message for the select then copy command. + # The select then copy command first selects the review cursor text, then copies it to the clipboard. + "If pressed once, the text from the previously set start marker up to and including the current " + "position of the review cursor is selected. If pressed twice, the text is copied to the clipboard" + ), + category=SCRCAT_TEXTREVIEW, + gesture="kb:NVDA+f10" + ) def script_review_copy(self, gesture): pos = api.getReviewPosition().copy() if not getattr(pos.obj, "_copyStartMarker", None): @@ -2275,30 +2704,38 @@ def script_review_copy(self, gesture): api.getReviewPosition().obj._selectThenCopyRange = None api.getReviewPosition().obj._copyStartMarker = None return - # Translators: Input help mode message for the select then copy command. The select then copy command first selects the review cursor text, then copies it to the clipboard. - script_review_copy.__doc__ = _("If pressed once, the text from the previously set start marker up to and including the current position of the review cursor is selected. If pressed twice, the text is copied to the clipboard") - script_review_copy.category=SCRCAT_TEXTREVIEW + @script( + # Translators: Input help mode message for a braille command. + description=_("Scrolls the braille display back"), + category=SCRCAT_BRAILLE, + bypassInputHelp=True + ) def script_braille_scrollBack(self, gesture): braille.handler.scrollBack() - # Translators: Input help mode message for a braille command. - script_braille_scrollBack.__doc__ = _("Scrolls the braille display back") - script_braille_scrollBack.bypassInputHelp = True - script_braille_scrollBack.category=SCRCAT_BRAILLE + @script( + # Translators: Input help mode message for a braille command. + description=_("Scrolls the braille display forward"), + category=SCRCAT_BRAILLE, + bypassInputHelp=True + ) def script_braille_scrollForward(self, gesture): braille.handler.scrollForward() - # Translators: Input help mode message for a braille command. - script_braille_scrollForward.__doc__ = _("Scrolls the braille display forward") - script_braille_scrollForward.bypassInputHelp = True - script_braille_scrollForward.category=SCRCAT_BRAILLE + @script( + # Translators: Input help mode message for a braille command. + description=_("Routes the cursor to or activates the object under this braille cell"), + category=SCRCAT_BRAILLE + ) def script_braille_routeTo(self, gesture): braille.handler.routeTo(gesture.routingIndex) - # Translators: Input help mode message for a braille command. - script_braille_routeTo.__doc__ = _("Routes the cursor to or activates the object under this braille cell") - script_braille_routeTo.category=SCRCAT_BRAILLE + @script( + # Translators: Input help mode message for Braille report formatting command. + description=_("Reports formatting info for the text under this braille cell"), + category=SCRCAT_BRAILLE + ) def script_braille_reportFormatting(self, gesture): info = braille.handler.getTextInfoForWindowPos(gesture.routingIndex) if info is None: @@ -2306,30 +2743,39 @@ def script_braille_reportFormatting(self, gesture): ui.message(_("No formatting information")) return self._reportFormattingHelper(info, False) - # Translators: Input help mode message for Braille report formatting command. - script_braille_reportFormatting.__doc__ = _("Reports formatting info for the text under this braille cell") - script_braille_reportFormatting.category=SCRCAT_BRAILLE + @script( + # Translators: Input help mode message for a braille command. + description=_("Moves the braille display to the previous line"), + category=SCRCAT_BRAILLE + ) def script_braille_previousLine(self, gesture): if braille.handler.buffer.regions: braille.handler.buffer.regions[-1].previousLine(start=True) - # Translators: Input help mode message for a braille command. - script_braille_previousLine.__doc__ = _("Moves the braille display to the previous line") - script_braille_previousLine.category=SCRCAT_BRAILLE + @script( + # Translators: Input help mode message for a braille command. + description=_("Moves the braille display to the next line"), + category=SCRCAT_BRAILLE + ) def script_braille_nextLine(self, gesture): if braille.handler.buffer.regions: braille.handler.buffer.regions[-1].nextLine() - # Translators: Input help mode message for a braille command. - script_braille_nextLine.__doc__ = _("Moves the braille display to the next line") - script_braille_nextLine.category=SCRCAT_BRAILLE + @script( + # Translators: Input help mode message for a braille command. + description=_("Inputs braille dots via the braille keyboard"), + category=SCRCAT_BRAILLE, + gesture="bk:dots" + ) def script_braille_dots(self, gesture): brailleInput.handler.input(gesture.dots) - # Translators: Input help mode message for a braille command. - script_braille_dots.__doc__= _("Inputs braille dots via the braille keyboard") - script_braille_dots.category=SCRCAT_BRAILLE + @script( + # Translators: Input help mode message for a braille command. + description=_("Moves the braille display to the current focus"), + category=SCRCAT_BRAILLE + ) def script_braille_toFocus(self, gesture): braille.handler.setTether(braille.handler.TETHER_FOCUS, auto=True) if braille.handler.getTether() == braille.handler.TETHER_REVIEW: @@ -2346,63 +2792,87 @@ def script_braille_toFocus(self, gesture): braille.handler.mainBuffer.updateDisplay() else: braille.handler.handleGainFocus(obj,shouldAutoTether=False) - # Translators: Input help mode message for a braille command. - script_braille_toFocus.__doc__= _("Moves the braille display to the current focus") - script_braille_toFocus.category=SCRCAT_BRAILLE + @script( + # Translators: Input help mode message for a braille command. + description=_("Erases the last entered braille cell or character"), + category=SCRCAT_BRAILLE, + gesture="bk:dot7" + ) def script_braille_eraseLastCell(self, gesture): brailleInput.handler.eraseLastCell() - # Translators: Input help mode message for a braille command. - script_braille_eraseLastCell.__doc__= _("Erases the last entered braille cell or character") - script_braille_eraseLastCell.category=SCRCAT_BRAILLE + @script( + # Translators: Input help mode message for a braille command. + description=_("Translates any braille input and presses the enter key"), + category=SCRCAT_BRAILLE, + gesture="bk:dot8" + ) def script_braille_enter(self, gesture): brailleInput.handler.enter() - # Translators: Input help mode message for a braille command. - script_braille_enter.__doc__= _("Translates any braille input and presses the enter key") - script_braille_enter.category=SCRCAT_BRAILLE + @script( + # Translators: Input help mode message for a braille command. + description=_("Translates any braille input"), + category=SCRCAT_BRAILLE, + gesture="bk:dot7+dot8" + ) def script_braille_translate(self, gesture): brailleInput.handler.translate() - # Translators: Input help mode message for a braille command. - script_braille_translate.__doc__= _("Translates any braille input") - script_braille_translate.category=SCRCAT_BRAILLE + @script( + # Translators: Input help mode message for a braille command. + description=_("Virtually toggles the shift key to emulate a keyboard shortcut with braille input"), + category=inputCore.SCRCAT_KBEMU, + bypassInputHelp=True + ) def script_braille_toggleShift(self, gesture): brailleInput.handler.toggleModifier("shift") - # Translators: Input help mode message for a braille command. - script_braille_toggleShift.__doc__= _("Virtually toggles the shift key to emulate a keyboard shortcut with braille input") - script_braille_toggleShift.category=inputCore.SCRCAT_KBEMU - script_braille_toggleShift.bypassInputHelp = True + @script( + # Translators: Input help mode message for a braille command. + description=_("Virtually toggles the control key to emulate a keyboard shortcut with braille input"), + category=inputCore.SCRCAT_KBEMU, + bypassInputHelp=True + ) def script_braille_toggleControl(self, gesture): brailleInput.handler.toggleModifier("control") - # Translators: Input help mode message for a braille command. - script_braille_toggleControl.__doc__= _("Virtually toggles the control key to emulate a keyboard shortcut with braille input") - script_braille_toggleControl.category=inputCore.SCRCAT_KBEMU - script_braille_toggleControl.bypassInputHelp = True + @script( + # Translators: Input help mode message for a braille command. + description=_("Virtually toggles the alt key to emulate a keyboard shortcut with braille input"), + category=inputCore.SCRCAT_KBEMU, + bypassInputHelp=True + ) def script_braille_toggleAlt(self, gesture): brailleInput.handler.toggleModifier("alt") - # Translators: Input help mode message for a braille command. - script_braille_toggleAlt.__doc__= _("Virtually toggles the alt key to emulate a keyboard shortcut with braille input") - script_braille_toggleAlt.category=inputCore.SCRCAT_KBEMU - script_braille_toggleAlt.bypassInputHelp = True + @script( + # Translators: Input help mode message for a braille command. + description=_("Virtually toggles the left windows key to emulate a keyboard shortcut with braille input"), + category=inputCore.SCRCAT_KBEMU, + bypassInputHelp=True + ) def script_braille_toggleWindows(self, gesture): brailleInput.handler.toggleModifier("leftWindows") - # Translators: Input help mode message for a braille command. - script_braille_toggleWindows.__doc__= _("Virtually toggles the left windows key to emulate a keyboard shortcut with braille input") - script_braille_toggleWindows.category=inputCore.SCRCAT_KBEMU - script_braille_toggleAlt.bypassInputHelp = True + @script( + # Translators: Input help mode message for a braille command. + description=_("Virtually toggles the NVDA key to emulate a keyboard shortcut with braille input"), + category=inputCore.SCRCAT_KBEMU, + bypassInputHelp=True + ) def script_braille_toggleNVDAKey(self, gesture): brailleInput.handler.toggleModifier("NVDA") - # Translators: Input help mode message for a braille command. - script_braille_toggleNVDAKey.__doc__= _("Virtually toggles the NVDA key to emulate a keyboard shortcut with braille input") - script_braille_toggleNVDAKey.category=inputCore.SCRCAT_KBEMU - script_braille_toggleNVDAKey.bypassInputHelp = True + @script( + description=_( + # Translators: Input help mode message for reload plugins command. + "Reloads app modules and global plugins without restarting NVDA, which can be Useful for developers" + ), + category=SCRCAT_TOOLS, + gesture="kb:NVDA+control+f3" + ) def script_reloadPlugins(self, gesture): import globalPluginHandler appModuleHandler.reloadAppModules() @@ -2410,10 +2880,13 @@ def script_reloadPlugins(self, gesture): NVDAObject.clearDynamicClassCache() # Translators: Presented when plugins (app modules and global plugins) are reloaded. ui.message(_("Plugins reloaded")) - # Translators: Input help mode message for reload plugins command. - script_reloadPlugins.__doc__=_("Reloads app modules and global plugins without restarting NVDA, which can be Useful for developers") - script_reloadPlugins.category=SCRCAT_TOOLS + @script( + # Translators: Input help mode message for a touchscreen gesture. + description=_("Moves to the next object in a flattened view of the object navigation hierarchy"), + category=SCRCAT_OBJECTNAVIGATION, + gesture="ts(object):flickright" + ) def script_navigatorObject_nextInFlow(self,gesture): curObject=api.getNavigatorObject() newObject=None @@ -2429,14 +2902,17 @@ def script_navigatorObject_nextInFlow(self,gesture): newObject=parent.simpleNext if newObject: api.setNavigatorObject(newObject) - speech.speakObject(newObject,reason=controlTypes.REASON_FOCUS) + speech.speakObject(newObject, reason=controlTypes.OutputReason.FOCUS) else: # Translators: a message when there is no next object when navigating ui.reviewMessage(_("No next")) - # Translators: Input help mode message for a touchscreen gesture. - script_navigatorObject_nextInFlow.__doc__=_("Moves to the next object in a flattened view of the object navigation hierarchy") - script_navigatorObject_nextInFlow.category=SCRCAT_OBJECTNAVIGATION + @script( + # Translators: Input help mode message for a touchscreen gesture. + description=_("Moves to the previous object in a flattened view of the object navigation hierarchy"), + category=SCRCAT_OBJECTNAVIGATION, + gesture="ts(object):flickleft" + ) def script_navigatorObject_previousInFlow(self,gesture): curObject=api.getNavigatorObject() newObject=curObject.simplePrevious @@ -2447,19 +2923,16 @@ def script_navigatorObject_previousInFlow(self,gesture): newObject=curObject.simpleParent if newObject: api.setNavigatorObject(newObject) - speech.speakObject(newObject,reason=controlTypes.REASON_FOCUS) + speech.speakObject(newObject, reason=controlTypes.OutputReason.FOCUS) else: # Translators: a message when there is no previous object when navigating ui.reviewMessage(_("No previous")) - # Translators: Input help mode message for a touchscreen gesture. - script_navigatorObject_previousInFlow.__doc__=_("Moves to the previous object in a flattened view of the object navigation hierarchy") - script_navigatorObject_previousInFlow.category=SCRCAT_OBJECTNAVIGATION @script( # Translators: Describes a command. description=_("Toggles the support of touch interaction"), category=SCRCAT_TOUCH, - gesture="kb:NVDA+control+alt+t", + gesture="kb:NVDA+control+alt+t" ) def script_toggleTouchSupport(self, gesture): enabled = not bool(config.conf["touch"]["enabled"]) @@ -2478,6 +2951,12 @@ def script_toggleTouchSupport(self, gesture): # Translators: Presented when support of touch interaction has been disabled ui.message(_("Touch interaction disabled")) + @script( + # Translators: Input help mode message for a touchscreen gesture. + description=_("Cycles between available touch modes"), + category=SCRCAT_TOUCH, + gesture="ts:3finger_tap" + ) def script_touch_changeMode(self,gesture): mode=touchHandler.handler._curTouchMode index=touchHandler.availableTouchModes.index(mode) @@ -2490,33 +2969,51 @@ def script_touch_changeMode(self,gesture): # Translators: Cycles through available touch modes (a group of related touch gestures; example output: "object mode"; see the user guide for more information on touch modes). newModeLabel=_("%s mode")%newMode ui.message(newModeLabel) - # Translators: Input help mode message for a touchscreen gesture. - script_touch_changeMode.__doc__=_("Cycles between available touch modes") - script_touch_changeMode.category=SCRCAT_TOUCH - + @script( + # Translators: Input help mode message for a touchscreen gesture. + description=_("Reports the object and content directly under your finger"), + category=SCRCAT_TOUCH, + gestures=("ts:tap", "ts:hoverDown") + ) def script_touch_newExplore(self,gesture): touchHandler.handler.screenExplorer.moveTo(gesture.x,gesture.y,new=True) - # Translators: Input help mode message for a touchscreen gesture. - script_touch_newExplore.__doc__=_("Reports the object and content directly under your finger") - script_touch_newExplore.category=SCRCAT_TOUCH + @script( + description=_( + # Translators: Input help mode message for a touchscreen gesture. + "Reports the new object or content under your finger " + "if different to where your finger was last" + ), + category=SCRCAT_TOUCH, + gesture="ts:hover" + ) def script_touch_explore(self,gesture): touchHandler.handler.screenExplorer.moveTo(gesture.x,gesture.y) - # Translators: Input help mode message for a touchscreen gesture. - script_touch_explore.__doc__=_("Reports the new object or content under your finger if different to where your finger was last") - script_touch_explore.category=SCRCAT_TOUCH + @script( + category=SCRCAT_TOUCH, + gesture="ts:hoverUp" + ) def script_touch_hoverUp(self,gesture): #Specifically for touch typing with onscreen keyboard keys - # #7309: by default, one mustdouble tap the touch key. To restore old behavior, go to Touch Interaction dialog and change touch typing option. + # #7309: by default, one must double-tap the touch key. + # To restore old behavior, go to Touch Interaction dialog and change touch typing option. if config.conf["touch"]["touchTyping"]: obj=api.getNavigatorObject() import NVDAObjects.UIA if isinstance(obj,NVDAObjects.UIA.UIA) and obj.UIAElement.cachedClassName=="CRootKey": obj.doAction() - script_touch_hoverUp.category=SCRCAT_TOUCH + @script( + description=_( + # Translators: Input help mode message for touch right click command. + "Clicks the right mouse button at the current touch position. " + "This is generally used to activate a context menu." + ), + category=SCRCAT_TOUCH, + gesture="ts:tapAndHold" + ) def script_touch_rightClick(self, gesture): obj = api.getNavigatorObject() # Ignore invisible or offscreen objects as they cannot even be navigated with touch gestures. @@ -2544,16 +3041,24 @@ def script_touch_rightClick(self, gesture): y = top + (height // 2) winUser.setCursorPos(x, y) self.script_rightMouseClick(gesture) - # Translators: Input help mode message for touch right click command. - script_touch_rightClick.__doc__ = _("Clicks the right mouse button at the current touch position. This is generally used to activate a context menu.") # noqa Flake8/E501 - script_touch_rightClick.category = SCRCAT_TOUCH + @script( + # Translators: Describes the command to open the Configuration Profiles dialog. + description=_("Shows the NVDA Configuration Profiles dialog"), + category=SCRCAT_CONFIG_PROFILES, + gesture="kb:NVDA+control+p" + ) def script_activateConfigProfilesDialog(self, gesture): wx.CallAfter(gui.mainFrame.onConfigProfilesCommand, None) - # Translators: Describes the command to open the Configuration Profiles dialog. - script_activateConfigProfilesDialog.__doc__ = _("Shows the NVDA Configuration Profiles dialog") - script_activateConfigProfilesDialog.category=SCRCAT_CONFIG_PROFILES + @script( + description=_( + # Translators: Input help mode message for toggle configuration profile triggers command. + "Toggles disabling of all configuration profile triggers. " + "Disabling remains in effect until NVDA is restarted" + ), + category=SCRCAT_CONFIG + ) def script_toggleConfigProfileTriggers(self,gesture): if config.conf.profileTriggersEnabled: config.conf.disableProfileTriggers() @@ -2568,10 +3073,12 @@ def script_toggleConfigProfileTriggers(self,gesture): # Translators: The message announced when re-enabling all configuration profile triggers. state = _("Configuration profile triggers enabled") ui.message(state) - # Translators: Input help mode message for toggle configuration profile triggers command. - script_toggleConfigProfileTriggers.__doc__=_("Toggles disabling of all configuration profile triggers. Disabling remains in effect until NVDA is restarted") - script_toggleConfigProfileTriggers.category=SCRCAT_CONFIG + @script( + # Translators: Describes a command. + description=_("Begins interaction with math content"), + gesture="kb:NVDA+alt+m" + ) def script_interactWithMath(self, gesture): import mathPres mathMl = mathPres.getMathMlFromTextInfo(api.getReviewPosition()) @@ -2588,19 +3095,28 @@ def script_interactWithMath(self, gesture): ui.message(_("Not math")) return mathPres.interactWithMathMl(mathMl) - # Translators: Describes a command. - script_interactWithMath.__doc__ = _("Begins interaction with math content") + @script( + # Translators: Describes a command. + description=_("Recognizes the content of the current navigator object with Windows 10 OCR"), + gesture="kb:NVDA+r" + ) def script_recognizeWithUwpOcr(self, gesture): if not winVersion.isUwpOcrAvailable(): # Translators: Reported when Windows 10 OCR is not available. ui.message(_("Windows 10 OCR not available")) return + from visionEnhancementProviders.screenCurtain import ScreenCurtainProvider + screenCurtainId = ScreenCurtainProvider.getSettings().getId() + screenCurtainProviderInfo = vision.handler.getProviderInfo(screenCurtainId) + isScreenCurtainRunning = bool(vision.handler.getProviderInstance(screenCurtainProviderInfo)) + if isScreenCurtainRunning: + # Translators: Reported when screen curtain is enabled. + ui.message(_("Please disable screen curtain before using Windows 10 OCR.")) + return from contentRecog import uwpOcr, recogUi recog = uwpOcr.UwpOcr() recogUi.recognizeNavigatorObject(recog) - # Translators: Describes a command. - script_recognizeWithUwpOcr.__doc__ = _("Recognizes the content of the current navigator object with Windows 10 OCR") _tempEnableScreenCurtain = True _waitingOnScreenCurtainWarningDialog: Optional[wx.Dialog] = None @@ -2608,7 +3124,7 @@ def script_recognizeWithUwpOcr(self, gesture): @script( # Translators: Input help mode message for toggle report CLDR command. description=_("Toggles on and off the reporting of CLDR characters, such as emojis"), - category=SCRCAT_SPEECH, + category=SCRCAT_SPEECH ) def script_toggleReportCLDR(self, gesture): if config.conf["speech"]["includeCLDR"]: @@ -2657,11 +3173,11 @@ def script_toggleScreenCurtain(self, gesture): speech.cancelSpeech() speech.speakObject( api.getForegroundObject(), - reason=controlTypes.REASON_FOCUS + reason=controlTypes.OutputReason.FOCUS ) speech.speakObject( api.getFocusObject(), - reason=controlTypes.REASON_FOCUS + reason=controlTypes.OutputReason.FOCUS ) return @@ -2755,190 +3271,6 @@ def _enableScreenCurtain(doEnable: bool = True): else: _enableScreenCurtain() - __gestures = { - # Basic - "kb:NVDA+n": "showGui", - "kb:NVDA+1": "toggleInputHelp", - "kb:NVDA+q": "quit", - "kb:NVDA+f2": "passNextKeyThrough", - "kb(desktop):NVDA+shift+s":"toggleCurrentAppSleepMode", - "kb(laptop):NVDA+shift+z":"toggleCurrentAppSleepMode", - - # System status - "kb:NVDA+f12": "dateTime", - "kb:NVDA+shift+b": "say_battery_status", - "kb:NVDA+c": "reportClipboardText", - - # System focus - "kb:NVDA+tab": "reportCurrentFocus", - "kb:NVDA+t": "title", - "kb:NVDA+b": "speakForeground", - "kb(desktop):NVDA+end": "reportStatusLine", - "kb(laptop):NVDA+shift+end": "reportStatusLine", - - # System caret - "kb(desktop):NVDA+downArrow": "sayAll", - "kb(laptop):NVDA+a": "sayAll", - "kb(desktop):NVDA+upArrow": "reportCurrentLine", - "kb(laptop):NVDA+l": "reportCurrentLine", - "kb(desktop):NVDA+shift+upArrow": "reportCurrentSelection", - "kb(laptop):NVDA+shift+s": "reportCurrentSelection", - # Object navigation - "kb:NVDA+numpad5": "navigatorObject_current", - "kb(laptop):NVDA+shift+o": "navigatorObject_current", - "kb:NVDA+numpad8": "navigatorObject_parent", - "kb(laptop):NVDA+shift+upArrow": "navigatorObject_parent", - "ts(object):flickup":"navigatorObject_parent", - "kb:NVDA+numpad4": "navigatorObject_previous", - "kb(laptop):NVDA+shift+leftArrow": "navigatorObject_previous", - "ts(object):flickleft":"navigatorObject_previousInFlow", - "ts(object):2finger_flickleft":"navigatorObject_previous", - "kb:NVDA+numpad6": "navigatorObject_next", - "kb(laptop):NVDA+shift+rightArrow": "navigatorObject_next", - "ts(object):flickright":"navigatorObject_nextInFlow", - "ts(object):2finger_flickright":"navigatorObject_next", - "kb:NVDA+numpad2": "navigatorObject_firstChild", - "kb(laptop):NVDA+shift+downArrow": "navigatorObject_firstChild", - "ts(object):flickdown":"navigatorObject_firstChild", - "kb:NVDA+numpadEnter": "review_activate", - "kb(laptop):NVDA+enter": "review_activate", - "ts:double_tap": "review_activate", - "kb:NVDA+shift+numpadMinus": "navigatorObject_moveFocus", - "kb(laptop):NVDA+shift+backspace": "navigatorObject_moveFocus", - "kb:NVDA+numpadDelete": "navigatorObject_currentDimensions", - "kb(laptop):NVDA+delete": "navigatorObject_currentDimensions", - - #Touch-specific commands - "ts:tap":"touch_newExplore", - "ts:hoverDown":"touch_newExplore", - "ts:hover":"touch_explore", - "ts:3finger_tap":"touch_changeMode", - "ts:2finger_double_tap":"showGui", - "ts:hoverUp":"touch_hoverUp", - "ts:tapAndHold": "touch_rightClick", # noqa (Flake8/ET121) - - # Review cursor - "kb:shift+numpad7": "review_top", - "kb(laptop):NVDA+control+home": "review_top", - "kb:numpad7": "review_previousLine", - "ts(text):flickUp":"review_previousLine", - "kb(laptop):NVDA+upArrow": "review_previousLine", - "kb:numpad8": "review_currentLine", - "kb(laptop):NVDA+shift+.": "review_currentLine", - "kb:numpad9": "review_nextLine", - "kb(laptop):NVDA+downArrow": "review_nextLine", - "ts(text):flickDown":"review_nextLine", - "kb:shift+numpad9": "review_bottom", - "kb(laptop):NVDA+control+end": "review_bottom", - "kb:numpad4": "review_previousWord", - "kb(laptop):NVDA+control+leftArrow": "review_previousWord", - "ts(text):2finger_flickLeft":"review_previousWord", - "kb:numpad5": "review_currentWord", - "kb(laptop):NVDA+control+.": "review_currentWord", - "ts(text):hoverUp":"review_currentWord", - "kb:numpad6": "review_nextWord", - "kb(laptop):NVDA+control+rightArrow": "review_nextWord", - "ts(text):2finger_flickRight":"review_nextWord", - "kb:shift+numpad1": "review_startOfLine", - "kb(laptop):NVDA+home": "review_startOfLine", - "kb:numpad1": "review_previousCharacter", - "kb(laptop):NVDA+leftArrow": "review_previousCharacter", - "ts(text):flickLeft":"review_previousCharacter", - "kb:numpad2": "review_currentCharacter", - "kb(laptop):NVDA+.": "review_currentCharacter", - "kb:numpad3": "review_nextCharacter", - "kb(laptop):NVDA+rightArrow": "review_nextCharacter", - "ts(text):flickRight":"review_nextCharacter", - "kb:shift+numpad3": "review_endOfLine", - "kb(laptop):NVDA+end": "review_endOfLine", - "kb:numpadPlus": "review_sayAll", - "kb(laptop):NVDA+shift+a": "review_sayAll", - "ts(text):3finger_flickDown":"review_sayAll", - "kb:NVDA+f9": "review_markStartForCopy", - "kb:NVDA+f10": "review_copy", - - # Flat review - "kb:NVDA+numpad7": "reviewMode_next", - "kb(laptop):NVDA+pageUp": "reviewMode_next", - "ts(object):2finger_flickUp": "reviewMode_next", - "kb:NVDA+numpad1": "reviewMode_previous", - "kb(laptop):NVDA+pageDown": "reviewMode_previous", - "ts(object):2finger_flickDown": "reviewMode_previous", - - # Mouse - "kb:numpadDivide": "leftMouseClick", - "kb(laptop):NVDA+[": "leftMouseClick", - "kb:shift+numpadDivide": "toggleLeftMouseButton", - "kb(laptop):NVDA+control+[": "toggleLeftMouseButton", - "kb:numpadMultiply": "rightMouseClick", - "kb(laptop):NVDA+]": "rightMouseClick", - "kb:shift+numpadMultiply": "toggleRightMouseButton", - "kb(laptop):NVDA+control+]": "toggleRightMouseButton", - "kb:NVDA+numpadDivide": "moveMouseToNavigatorObject", - "kb(laptop):NVDA+shift+m": "moveMouseToNavigatorObject", - "kb:NVDA+numpadMultiply": "moveNavigatorObjectToMouse", - "kb(laptop):NVDA+shift+n": "moveNavigatorObjectToMouse", - - # Tree interceptors - "kb:NVDA+space": "toggleVirtualBufferPassThrough", - "kb:NVDA+control+space": "moveToParentTreeInterceptor", - - # Preferences dialogs and panels - "kb:NVDA+control+g": "activateGeneralSettingsDialog", - "kb:NVDA+control+s": "activateSynthesizerDialog", - "kb:NVDA+control+v": "activateVoiceDialog", - "kb:NVDA+control+a": "activateBrailleDisplayDialog", - "kb:NVDA+control+k": "activateKeyboardSettingsDialog", - "kb:NVDA+control+m": "activateMouseSettingsDialog", - "kb:NVDA+control+o": "activateObjectPresentationDialog", - "kb:NVDA+control+b": "activateBrowseModeDialog", - "kb:NVDA+control+d": "activateDocumentFormattingDialog", - - # Configuration management - "kb:NVDA+control+c": "saveConfiguration", - "kb:NVDA+control+r": "revertConfiguration", - "kb:NVDA+control+p": "activateConfigProfilesDialog", - - # Settings - "kb:NVDA+shift+d":"cycleAudioDuckingMode", - "kb:NVDA+2": "toggleSpeakTypedCharacters", - "kb:NVDA+3": "toggleSpeakTypedWords", - "kb:NVDA+4": "toggleSpeakCommandKeys", - "kb:NVDA+p": "cycleSpeechSymbolLevel", - "kb:NVDA+s": "speechMode", - "kb:NVDA+m": "toggleMouseTracking", - "kb:NVDA+u": "toggleProgressBarOutput", - "kb:NVDA+5": "toggleReportDynamicContentChanges", - "kb:NVDA+6": "toggleCaretMovesReviewCursor", - "kb:NVDA+7": "toggleFocusMovesNavigatorObject", - "kb:NVDA+8": "toggleAutoFocusFocusableElements", - "kb:NVDA+control+t": "braille_toggleTether", - - # Synth settings ring - "kb(desktop):NVDA+control+leftArrow": "previousSynthSetting", - "kb(laptop):NVDA+shift+control+leftArrow": "previousSynthSetting", - "kb(desktop):NVDA+control+rightArrow": "nextSynthSetting", - "kb(laptop):NVDA+shift+control+rightArrow": "nextSynthSetting", - "kb(desktop):NVDA+control+upArrow": "increaseSynthSetting", - "kb(laptop):NVDA+shift+control+upArrow": "increaseSynthSetting", - "kb(desktop):NVDA+control+downArrow": "decreaseSynthSetting", - "kb(laptop):NVDA+control+shift+downArrow": "decreaseSynthSetting", - - # Braille keyboard - "bk:dots" : "braille_dots", - "bk:dot7" : "braille_eraseLastCell", - "bk:dot8" : "braille_enter", - "bk:dot7+dot8" : "braille_translate", - - # Tools - "kb:NVDA+f1": "navigatorObject_devInfo", - "kb:NVDA+control+f1": "reportAppModuleInfo", - "kb:NVDA+control+z": "activatePythonConsole", - "kb:NVDA+control+f3": "reloadPlugins", - "kb(desktop):NVDA+control+f2": "test_navigatorDisplayModelText", - "kb:NVDA+alt+m": "interactWithMath", - "kb:NVDA+r": "recognizeWithUwpOcr", - } #: The single global commands instance. #: @type: L{GlobalCommands} diff --git a/source/gui/__init__.py b/source/gui/__init__.py index 7a4e46e7851..9aaab0b7a87 100644 --- a/source/gui/__init__.py +++ b/source/gui/__init__.py @@ -1,21 +1,14 @@ # -*- coding: UTF-8 -*- # A part of NonVisual Desktop Access (NVDA) -# Copyright (C) 2006-2020 NV Access Limited, Peter Vágner, Aleksey Sadovoy, Mesar Hameed, Joseph Lee, -# Thomas Stivers, Babbage B.V. +# Copyright (C) 2006-2021 NV Access Limited, Peter Vágner, Aleksey Sadovoy, Mesar Hameed, Joseph Lee, +# Thomas Stivers, Babbage B.V., Accessolutions, Julien Cochuyt # This file is covered by the GNU General Public License. # See the file COPYING for more details. -from .contextHelp import ( - # several other submodules depend on ContextHelpMixin - # ensure early that it can be imported successfully. - ContextHelpMixin as _ContextHelpMixin, # don't expose from gui, import submodule directly. -) - import time import os import sys import threading -import codecs import ctypes import weakref import wx @@ -23,25 +16,22 @@ import globalVars import tones import ui +from documentationUtils import getDocFilePath from logHandler import log import config import versionInfo -import addonAPIVersion import speech import queueHandler import core from . import guiHelper +from .settingsDialogs import SettingsDialog from .settingsDialogs import * from .inputGestures import InputGesturesDialog import speechDictHandler -import languageHandler -import keyboardHandler from . import logViewer import speechViewer import winUser import api -from . import guiHelper -import winVersion try: import updateCheck @@ -57,43 +47,6 @@ mainFrame = None isInMessageBox = False -def getDocFilePath(fileName, localized=True): - if not getDocFilePath.rootPath: - if hasattr(sys, "frozen"): - getDocFilePath.rootPath = os.path.join(NVDA_PATH, "documentation") - else: - getDocFilePath.rootPath = os.path.join(NVDA_PATH, "..", "user_docs") - - if localized: - lang = languageHandler.getLanguage() - tryLangs = [lang] - if "_" in lang: - # This locale has a sub-locale, but documentation might not exist for the sub-locale, so try stripping it. - tryLangs.append(lang.split("_")[0]) - # If all else fails, use English. - tryLangs.append("en") - - fileName, fileExt = os.path.splitext(fileName) - for tryLang in tryLangs: - tryDir = os.path.join(getDocFilePath.rootPath, tryLang) - if not os.path.isdir(tryDir): - continue - - # Some out of date translations might include .txt files which are now .html files in newer translations. - # Therefore, ignore the extension and try both .html and .txt. - for tryExt in ("html", "txt"): - tryPath = os.path.join(tryDir, "%s.%s" % (fileName, tryExt)) - if os.path.isfile(tryPath): - return tryPath - return None - else: - # Not localized. - if not hasattr(sys, "frozen") and fileName in ("copying.txt", "contributors.txt"): - # If running from source, these two files are in the root dir. - return os.path.join(NVDA_PATH, "..", fileName) - else: - return os.path.join(getDocFilePath.rootPath, fileName) -getDocFilePath.rootPath = None class MainFrame(wx.Frame): @@ -123,10 +76,6 @@ def __init__(self): self.Show() self.Hide() - def Destroy(self): - self.sysTrayIcon.Destroy() - super(MainFrame, self).Destroy() - def prePopup(self): """Prepare for a popup. This should be called before any dialog or menu which should pop up for the user. @@ -135,7 +84,12 @@ def prePopup(self): """ nvdaPid = os.getpid() focus = api.getFocusObject() - if focus.processID != nvdaPid: + # Do not set prevFocus if the focus is on a control rendered by NVDA itself, such as the NVDA menu. + # This allows to refer to the control that had focus before opening the menu while still using NVDA + # on its own controls. The L{nvdaPid} check can be bypassed by setting the optional attribute + # L{isPrevFocusOnNvdaPopup} to L{True} when a NVDA dialog offers customizable bound gestures, + # eg. the NVDA Python Console. + if focus.processID != nvdaPid or getattr(focus, "isPrevFocusOnNvdaPopup", False): self.prevFocus = focus self.prevFocusAncestors = api.getFocusAncestors() if winUser.getWindowThreadProcessID(winUser.getForegroundWindow())[0] != nvdaPid: @@ -247,7 +201,7 @@ def onExitCommand(self, evt): d.Show() self.postPopup() else: - wx.GetApp().ExitMainLoop() + core.triggerNVDAExit() def onNVDASettingsCommand(self,evt): self._popupSettingsDialog(NVDASettingsDialog) @@ -470,7 +424,8 @@ def __init__(self, frame): item = menu_tools.Append(wx.ID_ANY, _("View log")) self.Bind(wx.EVT_MENU, frame.onViewLogCommand, item) # Translators: The label for the menu item to toggle Speech Viewer. - item=self.menu_tools_toggleSpeechViewer = menu_tools.AppendCheckItem(wx.ID_ANY, _("Speech viewer")) + item = self.menu_tools_toggleSpeechViewer = menu_tools.AppendCheckItem(wx.ID_ANY, _("Speech viewer")) + item.Check(speechViewer.isActive) self.Bind(wx.EVT_MENU, frame.onToggleSpeechViewerCommand, item) self.menu_tools_toggleBrailleViewer: wx.MenuItem = menu_tools.AppendCheckItem( @@ -530,6 +485,7 @@ def __init__(self, frame): self.Bind(wx.EVT_MENU, lambda evt: os.startfile(getDocFilePath("contributors.txt", False)), item) # Translators: The label for the menu item to open NVDA Welcome Dialog. item = menu_help.Append(wx.ID_ANY, _("We&lcome dialog...")) + from .startupDialogs import WelcomeDialog self.Bind(wx.EVT_MENU, lambda evt: WelcomeDialog.run(), item) menu_help.AppendSeparator() if updateCheck: @@ -574,10 +530,6 @@ def __init__(self, frame): self.Bind(wx.adv.EVT_TASKBAR_LEFT_DOWN, self.onActivate) self.Bind(wx.adv.EVT_TASKBAR_RIGHT_DOWN, self.onActivate) - def Destroy(self): - self.menu.Destroy() - super(SysTrayIcon, self).Destroy() - def onActivate(self, evt): mainFrame.prePopup() import appModules.nvda @@ -591,35 +543,36 @@ def onActivate(self, evt): appModules.nvda.nvdaMenuIaIdentity = None mainFrame.postPopup() + def initialize(): global mainFrame if mainFrame: raise RuntimeError("GUI already initialized") mainFrame = MainFrame() + wxLang = core.getWxLangOrNone() + if wxLang: + # otherwise the system default will be used + mainFrame.SetLayoutDirection(wxLang.LayoutDirection) wx.GetApp().SetTopWindow(mainFrame) + # In wxPython >= 4.1, + # wx.CallAfter no longer executes callbacks while NVDA's main thread is within apopup menu or message box. + # To work around this, + # Monkeypatch wx.CallAfter to + # post a WM_NULL message to our top-level window after calling the original CallAfter, + # which causes wx's event loop to wake up enough to execute the callback. + old_wx_CallAfter = wx.CallAfter + + def wx_CallAfter_wrapper(func, *args, **kwargs): + old_wx_CallAfter(func, *args, **kwargs) + # mainFrame may be None as NVDA could be terminating. + topHandle = mainFrame.Handle if mainFrame else None + if topHandle: + winUser.PostMessage(topHandle, winUser.WM_NULL, 0, 0) + wx.CallAfter = wx_CallAfter_wrapper -def terminate(): - import brailleViewer - brailleViewer.destroyBrailleViewer() - for instance, state in gui.SettingsDialog._instances.items(): - if state is gui.SettingsDialog._DIALOG_DESTROYED_STATE: - log.error( - "Destroyed but not deleted instance of settings dialog exists: {!r}".format(instance) - ) - else: - log.debug("Exiting NVDA with an open settings dialog: {!r}".format(instance)) +def terminate(): global mainFrame - # This is called after the main loop exits because WM_QUIT exits the main loop - # without destroying all objects correctly and we need to support WM_QUIT. - # Therefore, any request to exit should exit the main loop. - wx.CallAfter(mainFrame.Destroy) - # #4460: We need another iteration of the main loop - # so that everything (especially the TaskBarIcon) is cleaned up properly. - # ProcessPendingEvents doesn't seem to work, but MainLoop does. - # Because the top window gets destroyed, - # MainLoop thankfully returns pretty quickly. - wx.GetApp().MainLoop() mainFrame = None def showGui(): @@ -675,191 +628,6 @@ def run(): wx.CallAfter(run) -class WelcomeDialog( - _ContextHelpMixin, - wx.Dialog # wxPython does not seem to call base class initializer, put last in MRO -): - """The NVDA welcome dialog. - This provides essential information for new users, such as a description of the NVDA key and instructions on how to activate the NVDA menu. - It also provides quick access to some important configuration options. - This dialog is displayed the first time NVDA is started with a new configuration. - """ - helpId = "WelcomeDialog" - WELCOME_MESSAGE_DETAIL = _( - # Translators: The main message for the Welcome dialog when the user starts NVDA for the first time. - "Most commands for controlling NVDA require you to hold down" - " the NVDA key while pressing other keys.\n" - "By default, the numpad Insert and main Insert keys may both be used as the NVDA key.\n" - "You can also configure NVDA to use the CapsLock as the NVDA key.\n" - "Press NVDA+n at any time to activate the NVDA menu.\n" - "From this menu, you can configure NVDA, get help and access other NVDA functions." - ) - - def __init__(self, parent): - # Translators: The title of the Welcome dialog when user starts NVDA for the first time. - super(WelcomeDialog, self).__init__(parent, wx.ID_ANY, _("Welcome to NVDA")) - - mainSizer=wx.BoxSizer(wx.VERTICAL) - # Translators: The header for the Welcome dialog when user starts NVDA for the first time. This is in larger, - # bold lettering - welcomeTextHeader = wx.StaticText(self, label=_("Welcome to NVDA!")) - welcomeTextHeader.SetFont(wx.Font(18, wx.FONTFAMILY_DEFAULT, wx.NORMAL, wx.BOLD)) - mainSizer.AddSpacer(guiHelper.SPACE_BETWEEN_VERTICAL_DIALOG_ITEMS) - mainSizer.Add(welcomeTextHeader,border=20,flag=wx.EXPAND|wx.LEFT|wx.RIGHT) - mainSizer.AddSpacer(guiHelper.SPACE_BETWEEN_VERTICAL_DIALOG_ITEMS) - welcomeTextDetail = wx.StaticText(self, wx.ID_ANY, self.WELCOME_MESSAGE_DETAIL) - mainSizer.Add(welcomeTextDetail,border=20,flag=wx.EXPAND|wx.LEFT|wx.RIGHT) - - optionsSizer = wx.StaticBoxSizer( - wx.StaticBox( - self, - # Translators: The label for a group box containing the NVDA welcome dialog options. - label=_("Options") - ), - wx.VERTICAL - ) - sHelper = guiHelper.BoxSizerHelper(self, sizer=optionsSizer) - # Translators: The label of a combobox in the Welcome dialog. - kbdLabelText = _("&Keyboard layout:") - layouts = keyboardHandler.KeyboardInputGesture.LAYOUTS - self.kbdNames = sorted(layouts) - kbdChoices = [layouts[layout] for layout in self.kbdNames] - self.kbdList = sHelper.addLabeledControl(kbdLabelText, wx.Choice, choices=kbdChoices) - try: - index = self.kbdNames.index(config.conf["keyboard"]["keyboardLayout"]) - self.kbdList.SetSelection(index) - except: - log.error("Could not set Keyboard layout list to current layout",exc_info=True) - # Translators: The label of a checkbox in the Welcome dialog. - capsAsNVDAModifierText = _("&Use CapsLock as an NVDA modifier key") - self.capsAsNVDAModifierCheckBox = sHelper.addItem(wx.CheckBox(self, label=capsAsNVDAModifierText)) - self.capsAsNVDAModifierCheckBox.SetValue(config.conf["keyboard"]["useCapsLockAsNVDAModifierKey"]) - # Translators: The label of a checkbox in the Welcome dialog. - startAfterLogonText = _("St&art NVDA after I sign in") - self.startAfterLogonCheckBox = sHelper.addItem(wx.CheckBox(self, label=startAfterLogonText)) - self.startAfterLogonCheckBox.Value = config.getStartAfterLogon() - if globalVars.appArgs.secure or config.isAppX or not config.isInstalledCopy(): - self.startAfterLogonCheckBox.Disable() - # Translators: The label of a checkbox in the Welcome dialog. - showWelcomeDialogAtStartupText = _("&Show this dialog when NVDA starts") - self.showWelcomeDialogAtStartupCheckBox = sHelper.addItem(wx.CheckBox(self, label=showWelcomeDialogAtStartupText)) - self.showWelcomeDialogAtStartupCheckBox.SetValue(config.conf["general"]["showWelcomeDialogAtStartup"]) - mainSizer.Add(optionsSizer, border=guiHelper.BORDER_FOR_DIALOGS, flag=wx.ALL) - mainSizer.Add(self.CreateButtonSizer(wx.OK), border=guiHelper.BORDER_FOR_DIALOGS, flag=wx.ALL|wx.ALIGN_RIGHT) - self.Bind(wx.EVT_BUTTON, self.onOk, id=wx.ID_OK) - - mainSizer.Fit(self) - self.SetSizer(mainSizer) - self.kbdList.SetFocus() - self.CentreOnScreen() - - def onOk(self, evt): - layout = self.kbdNames[self.kbdList.GetSelection()] - config.conf["keyboard"]["keyboardLayout"] = layout - config.conf["keyboard"]["useCapsLockAsNVDAModifierKey"] = self.capsAsNVDAModifierCheckBox.IsChecked() - if self.startAfterLogonCheckBox.Enabled: - config.setStartAfterLogon(self.startAfterLogonCheckBox.Value) - config.conf["general"]["showWelcomeDialogAtStartup"] = self.showWelcomeDialogAtStartupCheckBox.IsChecked() - try: - config.conf.save() - except: - log.debugWarning("Could not save",exc_info=True) - self.EndModal(wx.ID_OK) - - @classmethod - def run(cls): - """Prepare and display an instance of this dialog. - This does not require the dialog to be instantiated. - """ - mainFrame.prePopup() - d = cls(mainFrame) - d.ShowModal() - d.Destroy() - mainFrame.postPopup() - - -class LauncherDialog( - _ContextHelpMixin, - wx.Dialog # wxPython does not seem to call base class initializer, put last in MRO -): - """The dialog that is displayed when NVDA is started from the launcher. - This displays the license and allows the user to install or create a portable copy of NVDA. - """ - helpId = "InstallingNVDA" - - def __init__(self, parent): - super(LauncherDialog, self).__init__(parent, title=versionInfo.name) - - mainSizer = wx.BoxSizer(wx.VERTICAL) - sHelper = guiHelper.BoxSizerHelper(self, orientation=wx.VERTICAL) - - # Translators: The label of the license text which will be shown when NVDA installation program starts. - groupLabel = _("License Agreement") - sizer = sHelper.addItem(wx.StaticBoxSizer(wx.StaticBox(self, label=groupLabel), wx.VERTICAL)) - licenseTextCtrl = wx.TextCtrl(self, size=(500, 400), style=wx.TE_MULTILINE | wx.TE_READONLY | wx.TE_RICH) - licenseTextCtrl.Value = codecs.open(getDocFilePath("copying.txt", False), "r", encoding="UTF-8").read() - sizer.Add(licenseTextCtrl) - - # Translators: The label for a checkbox in NvDA installation program to agree to the license agreement. - agreeText = _("I &agree") - self.licenseAgreeCheckbox = sHelper.addItem(wx.CheckBox(self, label=agreeText)) - self.licenseAgreeCheckbox.Value = False - self.licenseAgreeCheckbox.Bind(wx.EVT_CHECKBOX, self.onLicenseAgree) - - sizer = sHelper.addItem(wx.GridSizer(2, 2, 0, 0)) - self.actionButtons = [] - # Translators: The label of the button in NVDA installation program to install NvDA on the user's computer. - ctrl = wx.Button(self, label=_("&Install NVDA on this computer")) - sizer.Add(ctrl) - ctrl.Bind(wx.EVT_BUTTON, lambda evt: self.onAction(evt, mainFrame.onInstallCommand)) - self.actionButtons.append(ctrl) - # Translators: The label of the button in NVDA installation program to create a portable version of NVDA. - ctrl = wx.Button(self, label=_("Create &portable copy")) - sizer.Add(ctrl) - ctrl.Bind(wx.EVT_BUTTON, lambda evt: self.onAction(evt, mainFrame.onCreatePortableCopyCommand)) - self.actionButtons.append(ctrl) - # Translators: The label of the button in NVDA installation program to continue using the installation program as a temporary copy of NVDA. - ctrl = wx.Button(self, label=_("&Continue running")) - sizer.Add(ctrl) - ctrl.Bind(wx.EVT_BUTTON, self.onContinueRunning) - self.actionButtons.append(ctrl) - sizer.Add(wx.Button(self, label=_("E&xit"), id=wx.ID_CANCEL)) - # If we bind this on the button, it fails to trigger when the dialog is closed. - self.Bind(wx.EVT_BUTTON, self.onExit, id=wx.ID_CANCEL) - - for ctrl in self.actionButtons: - ctrl.Disable() - - mainSizer.Add(sHelper.sizer, border = guiHelper.BORDER_FOR_DIALOGS, flag=wx.ALL) - self.Sizer = mainSizer - mainSizer.Fit(self) - self.CentreOnScreen() - - def onLicenseAgree(self, evt): - for ctrl in self.actionButtons: - ctrl.Enable(evt.IsChecked()) - - def onAction(self, evt, func): - self.Destroy() - func(evt) - - def onContinueRunning(self, evt): - self.Destroy() - core.doStartupDialogs() - - def onExit(self, evt): - wx.GetApp().ExitMainLoop() - - @classmethod - def run(cls): - """Prepare and display an instance of this dialog. - This does not require the dialog to be instantiated. - """ - mainFrame.prePopup() - d = cls(mainFrame) - d.Show() - mainFrame.postPopup() - class ExitDialog(wx.Dialog): _instance = None @@ -925,7 +693,7 @@ def onOk(self, evt): if action >= 2 and config.isAppX: action += 1 if action == 0: - wx.GetApp().ExitMainLoop() + core.triggerNVDAExit() elif action == 1: queueHandler.queueFunction(queueHandler.eventQueue,core.restart) elif action == 2: @@ -947,7 +715,7 @@ def onOk(self, evt): confirmUpdateDialog.ShowModal() else: updateCheck.executePendingUpdate() - self.Destroy() + wx.CallAfter(self.Destroy) def onCancel(self, evt): self.Destroy() @@ -1070,67 +838,3 @@ def Notify(self): def _isDebug(): return config.conf["debugLog"]["gui"] - - -class AskAllowUsageStatsDialog( - _ContextHelpMixin, - wx.Dialog # wxPython does not seem to call base class initializer, put last in MRO -): - """A dialog asking if the user wishes to allow NVDA usage stats to be collected by NV Access.""" - - helpId = "UsageStatsDialog" - - def __init__(self, parent): - # Translators: The title of the dialog asking if usage data can be collected - super().__init__(parent, title=_("NVDA Usage Data Collection")) - mainSizer = wx.BoxSizer(wx.VERTICAL) - sHelper = guiHelper.BoxSizerHelper(self, orientation=wx.VERTICAL) - - # Translators: A message asking the user if they want to allow usage stats gathering - message=_("In order to improve NVDA in the future, NV Access wishes to collect usage data from running copies of NVDA.\n\n" - "Data includes Operating System version, NVDA version, language, country of origin, plus certain NVDA configuration such as current synthesizer, braille display and braille table. " - "No spoken or braille content will be ever sent to NV Access. Please refer to the User Guide for a current list of all data collected.\n\n" - "Do you wish to allow NV Access to periodically collect this data in order to improve NVDA?") - sText=sHelper.addItem(wx.StaticText(self, label=message)) - # the wx.Window must be constructed before we can get the handle. - import windowUtils - self.scaleFactor = windowUtils.getWindowScalingFactor(self.GetHandle()) - sText.Wrap(self.scaleFactor*600) # 600 was fairly arbitrarily chosen by a visual user to look acceptable on their machine. - - bHelper = sHelper.addDialogDismissButtons(guiHelper.ButtonHelper(wx.HORIZONTAL)) - - # Translators: The label of a Yes button in a dialog - yesButton = bHelper.addButton(self, wx.ID_YES, label=_("&Yes")) - yesButton.Bind(wx.EVT_BUTTON, self.onYesButton) - - # Translators: The label of a No button in a dialog - noButton = bHelper.addButton(self, wx.ID_NO, label=_("&No")) - noButton.Bind(wx.EVT_BUTTON, self.onNoButton) - - # Translators: The label of a button to remind the user later about performing some action. - remindMeButton = bHelper.addButton(self, wx.ID_CANCEL, label=_("Remind me &later")) - remindMeButton.Bind(wx.EVT_BUTTON, self.onLaterButton) - remindMeButton.SetFocus() - - mainSizer.Add(sHelper.sizer, border=guiHelper.BORDER_FOR_DIALOGS, flag=wx.ALL) - self.Sizer = mainSizer - mainSizer.Fit(self) - self.Center(wx.BOTH | wx.CENTER_ON_SCREEN) - - def onYesButton(self,evt): - log.debug("Usage stats gathering has been allowed") - config.conf['update']['askedAllowUsageStats']=True - config.conf['update']['allowUsageStats']=True - self.EndModal(wx.ID_YES) - - def onNoButton(self,evt): - log.debug("Usage stats gathering has been disallowed") - config.conf['update']['askedAllowUsageStats']=True - config.conf['update']['allowUsageStats']=False - self.EndModal(wx.ID_NO) - - def onLaterButton(self,evt): - log.debug("Usage stats gathering question has been deferred") - # evt.Skip() is called since wx.ID_CANCEL is used as the ID for the Ask Later button, - # wx automatically ends the modal itself. - evt.Skip() diff --git a/source/gui/accPropServer.py b/source/gui/accPropServer.py deleted file mode 100644 index aabef6e367b..00000000000 --- a/source/gui/accPropServer.py +++ /dev/null @@ -1,178 +0,0 @@ -#accPropServer.py -#A part of NonVisual Desktop Access (NVDA) -#Copyright (C) 2017-2018 NV Access Limited, Derek Riemer, Babbage B.V. -#This file is covered by the GNU General Public License. -#See the file COPYING for more details. - -"""Implementation of IAccProcServer, so that customization of a wx control can be done very fast.""" -from ctypes.wintypes import BOOL -from typing import Optional, Tuple, Any, Union, Callable - -from logHandler import log -from comtypes.automation import S_OK, VARIANT, POINTER, c_int, c_double, _oleaut32 -from comtypes import COMObject, GUID -from comInterfaces.Accessibility import IAccPropServer, ANNO_CONTAINER, ANNO_THIS -from abc import ABCMeta, abstractmethod -import weakref -import winUser -import wx - -_VariantInit: Callable[[POINTER(VARIANT),], None] = _oleaut32.VariantInit -_VariantInit.argtypes = (POINTER(VARIANT),) - -AcceptedGetPropTypes = Union[ - bool, - int, c_int, - float, c_double, - str, - VARIANT, - # And others: L{comtpyes.automation.tagVariant._set_value} - ] - -class IAccPropServer_Impl(COMObject, metaclass=ABCMeta): - """Base class for implementing a COM interface for a hwnd based AccPropServer\ - to annotate a WX control. - The AccPropServer registers itself using the window handle of the WX control. - When the WX control is destroyed, the instance is automatically unregistered. - This should eventually be dropped in favor of WX' own annotation support, - blocked by wxWidgets/Phoenix#1129. - Please override the L{_GetPropValue} method, not L{GetPropValue}. - L{GetPropValue} wraps L{_getPropValue} to catch and log exceptions (Which for some reason NVDA's logger misses when they occur in GetPropValue). - You must also provide the L{properties} property. - """ - - _com_interfaces_ = [ - IAccPropServer - ] - - # Constants used with `IAccPropServer::GetPropValue` method see - # https://msdn.microsoft.com/en-us/library/windows/desktop/dd318495(v=vs.85).aspx - HAS_PROP = 1 # TRUE - Constant for `BOOL* pfHasProp` out param of `IAccPropServer::GetPropValue` method - DOES_NOT_HAVE_PROP = 0 # FALSE - Constant for `BOOL* pfHasProp` out param of `IAccPropServer::GetPropValue` method - - # An array with the GUIDs of the properties that an AccPropServer should override - properties_GUIDPTR = [] - properties = [] - - def __init__(self, control, annotateProperties, annotateChildren=False): - """Initialize the instance of AccPropServer. - @param control: the WX control instance, so you can look up things in the _getPropValue method. - It's available on self.control. - @Type control: Subclass of wx.Window - @param annotateProperties The properties that should be annotated, see oleacc.py for constants. - @type annotateProperties List of oleacc constants. Internally these are converted to GUID pointers for the server. - @param annotateChildren: whether the WX control is a container which children should be annotated. - @type annotateChildren: bool - """ - self.properties = annotateProperties - self.properties_GUIDPTR = convertToGUIDPointerList(annotateProperties) - self.control = weakref.ref(control) - self.hwnd = control.GetHandle() - super(IAccPropServer_Impl, self).__init__() - # Import late to avoid circular import - from IAccessibleHandler import accPropServices - accPropServices.SetHwndPropServer( - hwnd=self.hwnd, - idObject=winUser.OBJID_CLIENT, - idChild=0, - paProps=self.properties_GUIDPTR, - cProps=len(self.properties_GUIDPTR), - pServer=self, - AnnoScope=ANNO_CONTAINER if annotateChildren else ANNO_THIS - ) - # clean up of accPropServices needs to happen when the control is destroyed. We can't rely on - # pythons `__del__` method to be called, and the wx framework does not call Destroy on child controls, - # automatically. Instead we can bind to the "window destroy" event for the control to do necessary - # cleanup (wxWidgets/Phoenix/#630). Not performing this cleanup results in a reference to the parent - # window, keeping it from being deleted correctly. This can cause a freeze on exit of NVDA. - control.Bind(wx.EVT_WINDOW_DESTROY, self._onDestroyControl, source=control) - - @abstractmethod - def _getPropValue( - self, - pIDString: str, - dwIDStringLen: int, - idProp: GUID - ) -> Optional[Tuple[BOOL, AcceptedGetPropTypes]]: - """ Use this method to implement GetPropValue. - It is wrapped by the callback GetPropValue to handle exceptions, and ensure valid return types. - For instructions on implementing accPropServers, see https://msdn.microsoft.com/en-us/library/windows/desktop/dd373681(v=vs.85).aspx . - For instructions specifically about this method, see https://msdn.microsoft.com/en-us/library/windows/desktop/dd318495(v=vs.85).aspx . - @param pIDString: Contains a string that identifies the property being requested. - If a single callback object is registered for annotating multiple accessible elements, - the identity string can be used to determine which element the request refers to. - If the accessible element is HWND-based, - IAccessibleHandler.accPropServices.DecomposeHwndIdentityString can be used - to extract the HWND/idObject/idChild from the identity string. - Note that, while one IAccPropServer implementation can annotate - multiple accessible elements, it is still bound to one wx.Control. - @param dwIDStringLen: Specifies the length of the identity string specified by the pIDString parameter. - @param idProp: Specifies a GUID indicating the desired property. One of the values from oleacc.PROPID_* - @return Use L{self._hasProp} to return correct values or return None if unable to supply the property. - """ - raise NotImplementedError - - def _hasProp( - self, - value: AcceptedGetPropTypes - ) -> Optional[Tuple[BOOL, AcceptedGetPropTypes]]: - """Constructs a tuple for the `IAccPropServer::GetPropValue` method, two elements: - 1. `VARIANT pvarValue` - 2. `BOOL pfHasProp` (either self.HAS_PROP or self.DOES_NOT_HAVE_PROP)""" - return value, self.HAS_PROP - - def GetPropValue( - self, this, # unused "this" used to indicate to comTypes we want a low level implementation - pIDString: str, - dwIDStringLen: int, - idProp: GUID, - pvarValue: POINTER(VARIANT), - pfGotProp: POINTER(BOOL) - ) -> int: - """ Exposed method to get a prop value. - see L{_getPropValue} for more details of args. - Uses a low-level approach, because comtypes tries to clear the VARIANT even though it is an out param. - When the pfHasProp part is FALSE / self.DOES_NOT_HAVE_PROP, then the pvarValue.vt part must be VT_EMPTY. - """ - # ensure exceptions don't leave this function. They will get get swallowed by the caller. - # instead catch and log exceptions. - try: - # Preset values for "no prop value", in case we return early. - pfGotProp.contents.value = self.DOES_NOT_HAVE_PROP - _VariantInit(pvarValue) - - ret = self._getPropValue(pIDString, dwIDStringLen, idProp) - if ret is None: - # We don't have the prop value, return early. - return S_OK - elif len(ret) != 2: - # We don't have the prop value, internal error. - raise RuntimeError("_getPropValue implementation must return None or two element tuple") - elif ret[1] != self.HAS_PROP: - # We don't have the prop value, return early. - return S_OK - - # we do have the prop value - pfGotProp.contents.value = self.HAS_PROP - pvarValue.contents.value = ret[0] - except Exception as e: # catch and log all exceptions so they are not swallowed by caller. - log.exception() - return S_OK - - def _onDestroyControl(self, evt): - evt.Skip() # Allow other handlers to process this event. - self._cleanup() - - def _cleanup(self): - # Import late to avoid circular import - from IAccessibleHandler import accPropServices - accPropServices.ClearHwndProps( - hwnd=self.hwnd, - idObject=winUser.OBJID_CLIENT, - idChild=0, - paProps=self.properties_GUIDPTR, - cProps=len(self.properties_GUIDPTR) - ) - -def convertToGUIDPointerList(propList): - return (GUID * len(propList))(*propList) diff --git a/source/gui/configProfiles.py b/source/gui/configProfiles.py index ceb20ae0b3c..63417fad3c3 100644 --- a/source/gui/configProfiles.py +++ b/source/gui/configProfiles.py @@ -42,13 +42,16 @@ def __init__(self, parent): mainSizer = wx.BoxSizer(wx.VERTICAL) sHelper = guiHelper.BoxSizerHelper(self,orientation=wx.VERTICAL) - profilesListGroupSizer = wx.StaticBoxSizer(wx.StaticBox(self), wx.HORIZONTAL) + profilesListGroupSizer = wx.StaticBoxSizer(wx.HORIZONTAL, self) + profilesListBox = profilesListGroupSizer.GetStaticBox() profilesListGroupContents = wx.BoxSizer(wx.HORIZONTAL) #contains the profile list and activation button in vertical arrangement. changeProfilesSizer = wx.BoxSizer(wx.VERTICAL) - item = self.profileList = wx.ListBox(self, - choices=[self.getProfileDisplay(name, includeStates=True) for name in self.profileNames]) + item = self.profileList = wx.ListBox( + profilesListBox, + choices=[self.getProfileDisplay(name, includeStates=True) for name in self.profileNames] + ) self.bindHelpEvent("ProfilesBasicManagement", self.profileList) item.Bind(wx.EVT_LISTBOX, self.onProfileListChoice) item.Selection = self.profileNames.index(config.conf.profiles[-1].name) @@ -56,7 +59,7 @@ def __init__(self, parent): changeProfilesSizer.AddSpacer(guiHelper.SPACE_BETWEEN_BUTTONS_VERTICAL) - self.changeStateButton = wx.Button(self) + self.changeStateButton = wx.Button(profilesListBox) self.bindHelpEvent("ConfigProfileManual", self.changeStateButton) self.changeStateButton.Bind(wx.EVT_BUTTON, self.onChangeState) self.AffirmativeId = self.changeStateButton.Id @@ -68,17 +71,17 @@ def __init__(self, parent): buttonHelper = guiHelper.ButtonHelper(wx.VERTICAL) # Translators: The label of a button to create a new configuration profile. - newButton = buttonHelper.addButton(self, label=_("&New")) + newButton = buttonHelper.addButton(profilesListBox, label=_("&New")) self.bindHelpEvent("ProfilesCreating", newButton) newButton.Bind(wx.EVT_BUTTON, self.onNew) # Translators: The label of a button to rename a configuration profile. - self.renameButton = buttonHelper.addButton(self, label=_("&Rename")) + self.renameButton = buttonHelper.addButton(profilesListBox, label=_("&Rename")) self.bindHelpEvent("ProfilesBasicManagement", self.renameButton) self.renameButton.Bind(wx.EVT_BUTTON, self.onRename) # Translators: The label of a button to delete a configuration profile. - self.deleteButton = buttonHelper.addButton(self, label=_("&Delete")) + self.deleteButton = buttonHelper.addButton(profilesListBox, label=_("&Delete")) self.bindHelpEvent("ProfilesBasicManagement", self.deleteButton) self.deleteButton.Bind(wx.EVT_BUTTON, self.onDelete) diff --git a/source/gui/contextHelp.py b/source/gui/contextHelp.py index 5c562d25f7a..b9b2a7d54bb 100644 --- a/source/gui/contextHelp.py +++ b/source/gui/contextHelp.py @@ -9,6 +9,7 @@ import wx from logHandler import log +import documentationUtils def writeRedirect(helpId: str, helpFilePath: str, contextHelpPath: str): @@ -34,9 +35,7 @@ def showHelp(helpId: str): noHelpMessage = _("No help available here.") queueHandler.queueFunction(queueHandler.eventQueue, ui.message, noHelpMessage) return - - import gui - helpFile = gui.getDocFilePath("userGuide.html") + helpFile = documentationUtils.getDocFilePath("userGuide.html") if helpFile is None: # Translators: Message shown when trying to display context sensitive help, # indicating that the user guide could not be found. diff --git a/source/gui/guiHelper.py b/source/gui/guiHelper.py index b33edad2875..7531d682300 100644 --- a/source/gui/guiHelper.py +++ b/source/gui/guiHelper.py @@ -324,7 +324,10 @@ def addLabeledControl(self, labelText, wxCtrlClass, **kwargs): Relies on guiHelper.LabeledControlHelper and thus guiHelper.associateElements, and therefore inherits any limitations from there. """ - labeledControl = LabeledControlHelper(self._parent, labelText, wxCtrlClass, **kwargs) + parent = self._parent + if isinstance(self.sizer, wx.StaticBoxSizer): + parent = self.sizer.GetStaticBox() + labeledControl = LabeledControlHelper(parent, labelText, wxCtrlClass, **kwargs) if(isinstance(labeledControl.control, (wx.ListCtrl,wx.ListBox,wx.TreeCtrl))): self.addItem(labeledControl.sizer, flag=wx.EXPAND, proportion=1) else: @@ -345,6 +348,9 @@ def addDialogDismissButtons(self, buttons, separated=False): Should be set to L{False} for message or single input dialogs, L{True} otherwise. @type separated: L{bool} """ + parent = self._parent + if isinstance(self.sizer, wx.StaticBoxSizer): + parent = self.sizer.GetStaticBox() if self.sizer.GetOrientation() != wx.VERTICAL: raise NotImplementedError( "Adding dialog dismiss buttons to a horizontal BoxSizerHelper is not implemented." @@ -354,11 +360,11 @@ def addDialogDismissButtons(self, buttons, separated=False): elif isinstance(buttons, (wx.Sizer, wx.Button)): toAdd = buttons elif isinstance(buttons, int): - toAdd = self._parent.CreateButtonSizer(buttons) + toAdd = parent.CreateButtonSizer(buttons) else: raise NotImplementedError("Unknown type: {}".format(buttons)) if separated: - self.addItem(wx.StaticLine(self._parent), flag=wx.EXPAND) + self.addItem(wx.StaticLine(parent), flag=wx.EXPAND) self.addItem(toAdd, flag=wx.ALIGN_RIGHT) self.dialogDismissButtonsAdded = True return buttons @@ -366,4 +372,3 @@ def addDialogDismissButtons(self, buttons, separated=False): class SIPABCMeta(wx.siplib.wrappertype, ABCMeta): """Meta class to be used for wx subclasses with abstract methods.""" pass - diff --git a/source/gui/inputGestures.py b/source/gui/inputGestures.py index 8920275ed82..82dbb9808a9 100644 --- a/source/gui/inputGestures.py +++ b/source/gui/inputGestures.py @@ -608,7 +608,7 @@ def makeSettings(self, settingsSizer): bHelper.sizer.AddStretchSpacer() # Translators: The label of a button to reset all gestures in the Input Gestures dialog. resetButton = wx.Button(self, label=_("Reset to factory &defaults")) - bHelper.sizer.Add(resetButton, flag=wx.ALIGN_RIGHT) + bHelper.sizer.Add(resetButton) resetButton.Bind(wx.EVT_BUTTON, self.onReset) settingsSizer.Add(bHelper.sizer, flag=wx.EXPAND) diff --git a/source/gui/installerGui.py b/source/gui/installerGui.py index 8c7fd3e6377..7846ff58072 100644 --- a/source/gui/installerGui.py +++ b/source/gui/installerGui.py @@ -1,29 +1,46 @@ -#gui/installerGui.py -#A part of NonVisual Desktop Access (NVDA) -#This file is covered by the GNU General Public License. -#See the file COPYING for more details. -#Copyright (C) 2011-2018 NV Access Limited, Babbage B.v. +# A part of NonVisual Desktop Access (NVDA) +# This file is covered by the GNU General Public License. +# See the file COPYING for more details. +# Copyright (C) 2011-2021 NV Access Limited, Babbage B.v., Cyrille Bougot, Julien Cochuyt, Accessolutions, +# Bill Dengler, Joseph Lee, Takuya Nishimoto import os -import ctypes -import buildVersion import shellapi import winUser import wx import config +import core import globalVars -import versionInfo import installer from logHandler import log import gui from gui import guiHelper import gui.contextHelp from gui.dpiScalingHelper import DpiScalingHelperMixinWithoutInit -import tones import systemUtils +def _canPortableConfigBeCopied() -> bool: + # In some cases even though user requested to copy config from the portable copy during installation + # it should not be done. + if globalVars.appArgs.launcher: + # Normally when running from the launcher + # and configPath is not overridden by the user copying config during installation is rather pointless + # as we would copy it into itself. + # However, if a user wants to run the launcher with a custom configPath, + # it is likely that he wants to copy that configuration when installing. + return globalVars.appArgs.configPath != config.getUserDefaultConfigPath(useInstalledPathIfExists=True) + else: + # For portable copies we want to avoid copying the configuration to itself, + # so return True only if the configPath + # does not point to the config of the installed copy in appdata. + confPath = config.getInstalledUserConfigPath() + if confPath and confPath == globalVars.appArgs.configPath: + return False + return True + + def doInstall( createDesktopShortcut=True, startOnLogon=True, @@ -52,7 +69,8 @@ def doInstall( if copyPortableConfig: installedUserConfigPath=config.getInstalledUserConfigPath() if installedUserConfigPath: - gui.ExecAndPump(installer.copyUserConfig,installedUserConfigPath) + if _canPortableConfigBeCopied(): + gui.ExecAndPump(installer.copyUserConfig, installedUserConfigPath) except Exception as e: res=e log.error("Failed to execute installer",exc_info=True) @@ -102,7 +120,7 @@ def doInstall( winUser.SW_SHOWNORMAL ) else: - wx.GetApp().ExitMainLoop() + core.triggerNVDAExit() def doSilentInstall( @@ -175,18 +193,15 @@ def __init__(self, parent, isUpdate): self.bindHelpEvent("InstallWithIncompatibleAddons", self.confirmationCheckbox) self.confirmationCheckbox.SetFocus() - optionsSizer = guiHelper.BoxSizerHelper(self, sizer=sHelper.addItem(wx.StaticBoxSizer( - wx.StaticBox( - self, - # Translators: The label for a group box containing the NVDA installation dialog options. - label=_("Options") - ), - wx.VERTICAL - ))) + # Translators: The label for a group box containing the NVDA installation dialog options. + optionsLabel = _("Options") + optionsSizer = sHelper.addItem(wx.StaticBoxSizer(wx.VERTICAL, self, label=optionsLabel)) + optionsHelper = guiHelper.BoxSizerHelper(self, sizer=optionsSizer) + optionsBox = optionsSizer.GetStaticBox() # Translators: The label of a checkbox option in the Install NVDA dialog. startOnLogonText = _("Use NVDA during sign-in") - self.startOnLogonCheckbox = optionsSizer.addItem(wx.CheckBox(self, label=startOnLogonText)) + self.startOnLogonCheckbox = optionsHelper.addItem(wx.CheckBox(optionsBox, label=startOnLogonText)) self.bindHelpEvent("StartAtWindowsLogon", self.startOnLogonCheckbox) if globalVars.appArgs.enableStartOnLogon is not None: self.startOnLogonCheckbox.Value = globalVars.appArgs.enableStartOnLogon @@ -197,25 +212,27 @@ def __init__(self, parent, isUpdate): if self.isUpdate and shortcutIsPrevInstalled: # Translators: The label of a checkbox option in the Install NVDA dialog. keepShortCutText = _("&Keep existing desktop shortcut") - self.createDesktopShortcutCheckbox = optionsSizer.addItem(wx.CheckBox(self, label=keepShortCutText)) + keepShortCutBox = wx.CheckBox(optionsBox, label=keepShortCutText) + self.createDesktopShortcutCheckbox = optionsHelper.addItem(keepShortCutBox) else: # Translators: The label of the option to create a desktop shortcut in the Install NVDA dialog. # If the shortcut key has been changed for this locale, # this change must also be reflected here. createShortcutText = _("Create &desktop icon and shortcut key (control+alt+n)") - self.createDesktopShortcutCheckbox = optionsSizer.addItem(wx.CheckBox(self, label=createShortcutText)) + createShortcutBox = wx.CheckBox(optionsBox, label=createShortcutText) + self.createDesktopShortcutCheckbox = optionsHelper.addItem(createShortcutBox) self.bindHelpEvent("CreateDesktopShortcut", self.createDesktopShortcutCheckbox) self.createDesktopShortcutCheckbox.Value = shortcutIsPrevInstalled if self.isUpdate else True # Translators: The label of a checkbox option in the Install NVDA dialog. createPortableText = _("Copy &portable configuration to current user account") - self.copyPortableConfigCheckbox = optionsSizer.addItem(wx.CheckBox(self, label=createPortableText)) + createPortableBox = wx.CheckBox(optionsBox, label=createPortableText) + self.copyPortableConfigCheckbox = optionsHelper.addItem(createPortableBox) self.bindHelpEvent("CopyPortableConfigurationToCurrentUserAccount", self.copyPortableConfigCheckbox) - self.copyPortableConfigCheckbox.Value = bool(globalVars.appArgs.copyPortableConfig) - if globalVars.appArgs.copyPortableConfig is None: - # copyPortableConfig is set to C{None} in the main loop, - # when copying the portable configuration should be disabled at all costs. - self.copyPortableConfigCheckbox.Disable() + self.copyPortableConfigCheckbox.Value = ( + bool(globalVars.appArgs.copyPortableConfig) and _canPortableConfigBeCopied() + ) + self.copyPortableConfigCheckbox.Enable(_canPortableConfigBeCopied()) bHelper = sHelper.addDialogDismissButtons(guiHelper.ButtonHelper(wx.HORIZONTAL)) if shouldAskAboutAddons: @@ -349,13 +366,16 @@ def __init__(self, parent): # Translators: The label of a grouping containing controls to select the destination directory # in the Create Portable NVDA dialog. directoryGroupText = _("Portable &directory:") - groupHelper = sHelper.addItem(gui.guiHelper.BoxSizerHelper(self, sizer=wx.StaticBoxSizer(wx.StaticBox(self, label=directoryGroupText), wx.VERTICAL))) + groupSizer = wx.StaticBoxSizer(wx.VERTICAL, self, label=directoryGroupText) + groupHelper = sHelper.addItem(gui.guiHelper.BoxSizerHelper(self, sizer=groupSizer)) + groupBox = groupSizer.GetStaticBox() # Translators: The label of a button to browse for a directory. browseText = _("Browse...") # Translators: The title of the dialog presented when browsing for the # destination directory when creating a portable copy of NVDA. dirDialogTitle = _("Select portable directory") - directoryEntryControl = groupHelper.addItem(gui.guiHelper.PathSelectionHelper(self, browseText, dirDialogTitle)) + directoryPathHelper = gui.guiHelper.PathSelectionHelper(groupBox, browseText, dirDialogTitle) + directoryEntryControl = groupHelper.addItem(directoryPathHelper) self.portableDirectoryEdit = directoryEntryControl.pathControl if globalVars.appArgs.portablePath: self.portableDirectoryEdit.Value = globalVars.appArgs.portablePath @@ -447,7 +467,7 @@ def doCreatePortable(portableDirectory,copyUserConfig=False,silent=False,startAf return d.done() if silent: - wx.GetApp().ExitMainLoop() + core.triggerNVDAExit() else: # Translators: The message displayed when a portable copy of NVDA has been successfully created. # %s will be replaced with the destination directory. diff --git a/source/gui/nvdaControls.py b/source/gui/nvdaControls.py index a0a08dcf326..4bb34341927 100644 --- a/source/gui/nvdaControls.py +++ b/source/gui/nvdaControls.py @@ -1,22 +1,20 @@ # -*- coding: UTF-8 -*- -#A part of NonVisual Desktop Access (NVDA) -#Copyright (C) 2016-2018 NV Access Limited, Derek Riemer -#This file is covered by the GNU General Public License. -#See the file COPYING for more details. +# A part of NonVisual Desktop Access (NVDA) +# Copyright (C) 2016-2021 NV Access Limited, Derek Riemer +# This file is covered by the GNU General Public License. +# See the file COPYING for more details. -from ctypes.wintypes import BOOL -from typing import Any, Tuple, Optional import wx -from comtypes import GUID +from wx.lib import scrolledpanel from wx.lib.mixins import listctrl as listmix -from . import accPropServer from .dpiScalingHelper import DpiScalingHelperMixin from . import guiHelper -import oleacc import winUser import winsound + from collections.abc import Callable + class AutoWidthColumnListCtrl(wx.ListCtrl, listmix.ListCtrlAutoWidthMixin): """ A list control that allows you to specify a column to resize to take up the remaining width of a wx.ListCtrl. @@ -86,85 +84,35 @@ def OnSetFocus(self, evt): self.SetSelection(0, numChars) evt.Skip() -class AccPropertyOverride(accPropServer.IAccPropServer_Impl): - def __init__(self, control, propertyAnnotations): - """ - A simple class for overriding specific values on a control - :type propertyAnnotations: dict - """ - super(AccPropertyOverride, self).__init__( - control, - annotateProperties=list(propertyAnnotations.keys()), - annotateChildren=False - ) - self.propertyAnnotations = propertyAnnotations +class ListCtrlAccessible(wx.Accessible): + """WX Accessible implementation for checkable lists which aren't fully accessible.""" - def _getPropValue(self, pIDString, dwIDStringLen, idProp): - control = self.control() # self.control held as a weak ref, ensure it stays alive for the duration of this method - if control is None or not self.propertyAnnotations: - return None + def GetRole(self, childId): + if childId == winUser.CHILDID_SELF: + return super().GetRole(childId) + return (wx.ACC_OK, wx.ROLE_SYSTEM_CHECKBUTTON) - try: - val = self.propertyAnnotations[idProp] - if callable(val): - val = val() - return self._hasProp(val) - except KeyError: - pass - - return None - - def _cleanup(self): - # could contain references (via lambda) of our owner, set it to None to avoid a circular reference which - # would block destruction. - self.propertyAnnotations = None - super(AccPropertyOverride, self)._cleanup() - -class ListCtrlAccPropServer(accPropServer.IAccPropServer_Impl): - """AccPropServer for wx checkable lists which aren't fully accessible.""" - - def __init__(self, control): - super(ListCtrlAccPropServer, self).__init__( - control, - annotateProperties=[ - oleacc.PROPID_ACC_ROLE, # supposed to be checkbox, rather than list item - oleacc.PROPID_ACC_STATE # should include the checkable state and checked state if the item is checked. - ], - annotateChildren=True - ) + def GetState(self, childId): + if childId == winUser.CHILDID_SELF: + return super().GetState(childId) + states = wx.ACC_STATE_SYSTEM_SELECTABLE | wx.ACC_STATE_SYSTEM_FOCUSABLE + if self.Window.IsChecked(childId - 1): + states |= wx.ACC_STATE_SYSTEM_CHECKED + if self.Window.IsSelected(childId - 1): + # wx doesn't seem to have a method to check whether a list item is focused. + # Therefore, assume that a selected item is focused,which is the case in single select list boxes. + states |= wx.ACC_STATE_SYSTEM_SELECTED | wx.ACC_STATE_SYSTEM_FOCUSED + return (wx.ACC_OK, states) - def _getPropValue(self, pIDString: str, dwIDStringLen: int, idProp: GUID) -> Optional[Tuple[BOOL, Any]]: - control = self.control() # self.control held as a weak ref, ensure it stays alive for the duration of this method - if control is None: - return None - - # Import late to prevent circular import. - from IAccessibleHandler import accPropServices - handle, objid, childid = accPropServices.DecomposeHwndIdentityString(pIDString, dwIDStringLen) - if childid == winUser.CHILDID_SELF: - return None - - if idProp == oleacc.PROPID_ACC_ROLE: - return self._hasProp(oleacc.ROLE_SYSTEM_CHECKBUTTON) - - if idProp == oleacc.PROPID_ACC_STATE: - states = oleacc.STATE_SYSTEM_SELECTABLE|oleacc.STATE_SYSTEM_FOCUSABLE - if control.IsChecked(childid-1): - states |= oleacc.STATE_SYSTEM_CHECKED - if control.IsSelected(childid-1): - # wx doesn't seem to have a method to check whether a list item is focused. - # Therefore, assume that a selected item is focused,which is the case in single select list boxes. - states |= oleacc.STATE_SYSTEM_SELECTED | oleacc.STATE_SYSTEM_FOCUSED - return self._hasProp(states) class CustomCheckListBox(wx.CheckListBox): """Custom checkable list to fix a11y bugs in the standard wx checkable list box.""" def __init__(self, *args, **kwargs): super(CustomCheckListBox, self).__init__(*args, **kwargs) - # Register object with COM to fix accessibility bugs in wx. - self.server = ListCtrlAccPropServer(self) + # Register a custom wx.Accessible implementation to fix accessibility incompleties + self.SetAccessible(ListCtrlAccessible(self)) # Register ourself with ourself's selected event, so that we can notify winEvent of the state change. self.Bind(wx.EVT_CHECKLISTBOX, self.notifyIAccessible) @@ -189,8 +137,8 @@ def __init__(self, parent, id=wx.ID_ANY, autoSizeColumn="LAST", pos=wx.DefaultPo ): AutoWidthColumnListCtrl.__init__(self, parent, id=id, pos=pos, size=size, style=style, autoSizeColumn=autoSizeColumn) listmix.CheckListCtrlMixin.__init__(self, check_image, uncheck_image, imgsz) - # Register object with COM to fix accessibility bugs in wx. - self.server = ListCtrlAccPropServer(self) + # Register a custom wx.Accessible implementation to fix accessibility incompleties + self.SetAccessible(ListCtrlAccessible(self)) # Register our hook to check/uncheck items with space. # Use wx.EVT_CHAR_HOOK, because EVT_LIST_KEY_DOWN isn't triggered for space. self.Bind(wx.EVT_CHAR_HOOK, self.onCharHook) @@ -406,3 +354,40 @@ def onSliderChar(self, evt): evt.Skip() return self.SetValue(newValue) + + +class TabbableScrolledPanel(scrolledpanel.ScrolledPanel): + """ + This class was created to ensure a ScrolledPanel scrolls to nested children of the panel when navigating + with tabs (#12224). A PR to wxPython implementing this fix can be tracked on + https://github.com/wxWidgets/Phoenix/pull/1950 + """ + def GetChildRectRelativeToSelf(self, child: wx.Window) -> wx.Rect: + """ + window.GetRect returns the size of a window, and its position relative to its parent. + When calculating ScrollChildIntoView, the position relative to its parent is not relevant unless the + parent is the ScrolledPanel itself. Instead, calculate the position relative to scrolledPanel + """ + childRectRelativeToScreen = child.GetScreenRect() + scrolledPanelScreenPosition = self.GetScreenPosition() + return wx.Rect( + childRectRelativeToScreen.x - scrolledPanelScreenPosition.x, + childRectRelativeToScreen.y - scrolledPanelScreenPosition.y, + childRectRelativeToScreen.width, + childRectRelativeToScreen.height + ) + + def ScrollChildIntoView(self, child: wx.Window) -> None: + """ + Overrides child.GetRect with `GetChildRectRelativeToSelf` before calling + `super().ScrollChildIntoView`. `super().ScrollChildIntoView` incorrectly uses child.GetRect to + navigate scrolling, which is relative to the parent, where it should instead be relative to this + ScrolledPanel. + """ + oldChildGetRectFunction = child.GetRect + child.GetRect = lambda: self.GetChildRectRelativeToSelf(child) + try: + super().ScrollChildIntoView(child) + finally: + # ensure child.GetRect is reset properly even if super().ScrollChildIntoView throws an exception + child.GetRect = oldChildGetRectFunction diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index b2647c7dfbb..18321a33176 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -6,19 +6,20 @@ # This file is covered by the GNU General Public License. # See the file COPYING for more details. import logging -from abc import ABCMeta +from abc import ABCMeta, abstractmethod import copy +import os +from enum import IntEnum +import typing import wx from vision.providerBase import VisionEnhancementProviderSettings -from wx.lib import scrolledpanel from wx.lib.expando import ExpandoTextCtrl import wx.lib.newevent import winUser import logHandler import installer -from synthDriverHandler import * -from synthDriverHandler import SynthDriver, getSynth +from synthDriverHandler import changeVoice, getSynth, getSynthList, setSynth, SynthDriver import config import languageHandler import speech @@ -80,10 +81,12 @@ class SettingsDialog( class MultiInstanceError(RuntimeError): pass - _DIALOG_CREATED_STATE = 0 - _DIALOG_DESTROYED_STATE = 1 + class DialogState(IntEnum): + CREATED = 0 + DESTROYED = 1 + # holds instances of SettingsDialogs as keys, and state as the value - _instances=weakref.WeakKeyDictionary() + _instances = weakref.WeakKeyDictionary() title = "" helpId = "NVDASettings" shouldSuspendConfigProfileTriggers = True @@ -102,25 +105,39 @@ def __new__(cls, *args, **kwargs): "Creating new settings dialog (multiInstanceAllowed:{}). " "State of _instances {!r}".format(multiInstanceAllowed, instancesState) ) - if state is cls._DIALOG_CREATED_STATE and not multiInstanceAllowed: + if state is cls.DialogState.CREATED and not multiInstanceAllowed: raise SettingsDialog.MultiInstanceError("Only one instance of SettingsDialog can exist at a time") - if state is cls._DIALOG_DESTROYED_STATE and not multiInstanceAllowed: + if state is cls.DialogState.DESTROYED and not multiInstanceAllowed: # the dialog has been destroyed by wx, but the instance is still available. This indicates there is something # keeping it alive. log.error("Opening new settings dialog while instance still exists: {!r}".format(firstMatchingInstance)) obj = super(SettingsDialog, cls).__new__(cls, *args, **kwargs) - SettingsDialog._instances[obj] = cls._DIALOG_CREATED_STATE + SettingsDialog._instances[obj] = cls.DialogState.CREATED return obj def _setInstanceDestroyedState(self): - if log.isEnabledFor(log.DEBUG): - instancesState = dict(SettingsDialog._instances) - log.debug( - "Setting state to destroyed for instance: {!r}\n" - "Current _instances {!r}".format(self, instancesState) - ) - if self in SettingsDialog._instances: - SettingsDialog._instances[self] = self._DIALOG_DESTROYED_STATE + # prevent race condition with object deletion + # prevent deletion of the object while we work on it. + nonWeak: typing.Dict[SettingsDialog, SettingsDialog.DialogState] = dict(SettingsDialog._instances) + + if ( + self in SettingsDialog._instances + # Because destroy handlers are use evt.skip, _setInstanceDestroyedState may be called many times + # prevent noisy logging. + and self.DialogState.DESTROYED != SettingsDialog._instances[self] + ): + if log.isEnabledFor(log.DEBUG): + instanceStatesGen = ( + f"{instance.title} - {state.name}" + for instance, state in nonWeak.items() + ) + instancesList = list(instanceStatesGen) + log.debug( + f"Setting state to destroyed for instance: {self.title} - {self.__class__.__qualname__} - {self}\n" + f"Current _instances {instancesList}" + ) + SettingsDialog._instances[self] = self.DialogState.DESTROYED + def __init__( self, parent, @@ -220,8 +237,7 @@ def onOk(self, evt): Sub-classes may extend this method. This base method should always be called to clean up the dialog. """ - self.DestroyChildren() - self.Destroy() + self.DestroyLater() self.SetReturnCode(wx.ID_OK) def onCancel(self, evt): @@ -229,8 +245,7 @@ def onCancel(self, evt): Sub-classes may extend this method. This base method should always be called to clean up the dialog. """ - self.DestroyChildren() - self.Destroy() + self.DestroyLater() self.SetReturnCode(wx.ID_CANCEL) def onApply(self, evt): @@ -357,6 +372,22 @@ def _sendLayoutUpdatedEvent(self): event.SetEventObject(self) self.GetEventHandler().ProcessEvent(event) + +class SettingsPanelAccessible(wx.Accessible): + """ + WX Accessible implementation to set the role of a settings panel to property page, + as well as to set the accessible description based on the panel's description. + """ + + Window: SettingsPanel + + def GetRole(self, childId): + return (wx.ACC_OK, wx.ROLE_SYSTEM_PROPERTYPAGE) + + def GetDescription(self, childId): + return (wx.ACC_OK, self.Window.panelDescription) + + class MultiCategorySettingsDialog(SettingsDialog): """A settings dialog with multiple settings categories. A multi category settings dialog consists of a list view with settings categories on the left side, @@ -371,7 +402,7 @@ class MultiCategorySettingsDialog(SettingsDialog): """ title="" - categoryClasses=[] + categoryClasses: typing.List[typing.Type[SettingsPanel]] = [] class CategoryUnavailableError(RuntimeError): pass @@ -395,8 +426,9 @@ def __init__(self, parent, initialCategory=None): self.initialCategory = initialCategory self.currentCategory = None self.setPostInitFocus = None - # dictionary key is index of category in self.catList, value is the instance. Partially filled, check for KeyError - self.catIdToInstanceMap = {} + # dictionary key is index of category in self.catList, value is the instance. + # Partially filled, check for KeyError + self.catIdToInstanceMap: typing.Dict[int, SettingsPanel] = {} super(MultiCategorySettingsDialog, self).__init__( parent, @@ -449,7 +481,7 @@ def makeSettings(self, settingsSizer): # The provided column header is just a placeholder, as it is hidden due to the wx.LC_NO_HEADER style flag. self.catListCtrl.InsertColumn(0,categoriesLabelText) - self.container = scrolledpanel.ScrolledPanel( + self.container = nvdaControls.TabbableScrolledPanel( parent = self, style = wx.TAB_TRAVERSAL | wx.BORDER_THEME, size=containerDim @@ -524,14 +556,7 @@ def _getCategoryPanel(self, catId): ).format(cls, panel.Size[0]) ) panel.SetLabel(panel.title) - import oleacc - panel.server = nvdaControls.AccPropertyOverride( - panel, - propertyAnnotations={ - oleacc.PROPID_ACC_ROLE: oleacc.ROLE_SYSTEM_PROPERTYPAGE, # change the role from pane to property page - oleacc.PROPID_ACC_DESCRIPTION: panel.panelDescription, # set a description - } - ) + panel.SetAccessible(SettingsPanelAccessible(panel)) return panel def postInit(self): @@ -609,37 +634,42 @@ def onCategoryChange(self, evt): else: evt.Skip() - def _doSave(self): + def _validateAllPanels(self): + """Check if all panels are valid, and can be saved + @note: raises ValueError if a panel is not valid. See c{SettingsPanel.isValid} + """ for panel in self.catIdToInstanceMap.values(): if panel.isValid() is False: raise ValueError("Validation for %s blocked saving settings" % panel.__class__.__name__) + + def _saveAllPanels(self): for panel in self.catIdToInstanceMap.values(): panel.onSave() + + def _notifyAllPanelsSaveOccurred(self): for panel in self.catIdToInstanceMap.values(): panel.postSave() - def onOk(self,evt): + def _doSave(self): try: - self._doSave() + self._validateAllPanels() + self._saveAllPanels() + self._notifyAllPanelsSaveOccurred() except ValueError: - log.debugWarning("", exc_info=True) + log.debugWarning("Error while saving settings:", exc_info=True) return - for panel in self.catIdToInstanceMap.values(): - panel.Destroy() + + def onOk(self, evt): + self._doSave() super(MultiCategorySettingsDialog,self).onOk(evt) def onCancel(self,evt): for panel in self.catIdToInstanceMap.values(): panel.onDiscard() - panel.Destroy() super(MultiCategorySettingsDialog,self).onCancel(evt) def onApply(self,evt): - try: - self._doSave() - except ValueError: - log.debugWarning("", exc_info=True) - return + self._doSave() super(MultiCategorySettingsDialog,self).onApply(evt) @@ -737,7 +767,7 @@ def makeSettings(self, settingsSizer): ) self.bindHelpEvent("GeneralSettingsStartOnLogOnScreen", self.startOnLogonScreenCheckBox) self.startOnLogonScreenCheckBox.SetValue(config.getStartOnLogonScreen()) - if globalVars.appArgs.secure or not config.canStartOnSecureScreens(): + if globalVars.appArgs.secure or not config.isInstalledCopy(): self.startOnLogonScreenCheckBox.Disable() settingsSizerHelper.addItem(self.startOnLogonScreenCheckBox) @@ -754,7 +784,7 @@ def makeSettings(self, settingsSizer): ) self.bindHelpEvent("GeneralSettingsCopySettings", self.copySettingsButton) self.copySettingsButton.Bind(wx.EVT_BUTTON,self.onCopySettings) - if globalVars.appArgs.secure or not config.canStartOnSecureScreens(): + if globalVars.appArgs.secure or not config.isInstalledCopy(): self.copySettingsButton.Disable() settingsSizerHelper.addItem(self.copySettingsButton) if updateCheck: @@ -909,8 +939,9 @@ def makeSettings(self, settingsSizer): settingsSizerHelper = guiHelper.BoxSizerHelper(self, sizer=settingsSizer) # Translators: A label for the synthesizer on the speech panel. synthLabel = _("&Synthesizer") - synthBox = wx.StaticBox(self, label=synthLabel) - synthGroup = guiHelper.BoxSizerHelper(self, sizer=wx.StaticBoxSizer(synthBox, wx.HORIZONTAL)) + synthBoxSizer = wx.StaticBoxSizer(wx.HORIZONTAL, self, label=synthLabel) + synthBox = synthBoxSizer.GetStaticBox() + synthGroup = guiHelper.BoxSizerHelper(self, sizer=synthBoxSizer) settingsSizerHelper.addItem(synthGroup) # Use a ExpandoTextCtrl because even when readonly it accepts focus from keyboard, which @@ -919,12 +950,17 @@ def makeSettings(self, settingsSizer): # and a vertical scroll bar. This is not neccessary for the single line of text we wish to # display here. synthDesc = getSynth().description - self.synthNameCtrl = ExpandoTextCtrl(self, size=(self.scaleSize(250), -1), value=synthDesc, style=wx.TE_READONLY) + self.synthNameCtrl = ExpandoTextCtrl( + synthBox, + size=(self.scaleSize(250), -1), + value=synthDesc, + style=wx.TE_READONLY, + ) self.synthNameCtrl.Bind(wx.EVT_CHAR_HOOK, self._enterTriggersOnChangeSynth) # Translators: This is the label for the button used to change synthesizer, # it appears in the context of a synthesizer group on the speech settings panel. - changeSynthBtn = wx.Button(self, label=_("C&hange...")) + changeSynthBtn = wx.Button(synthBox, label=_("C&hange...")) self.bindHelpEvent("SpeechSettingsChange", self.synthNameCtrl) self.bindHelpEvent("SpeechSettingsChange", changeSynthBtn) synthGroup.addItem( @@ -1389,11 +1425,6 @@ def onPanelActivated(self): super().onPanelActivated() -#: DriverSettingsMixin name is provided or backwards compatibility. -# The name DriverSettingsMixin should be considered deprecated, use AutoSettingsMixin instead. -DriverSettingsMixin = AutoSettingsMixin - - class VoiceSettingsPanel(AutoSettingsMixin, SettingsPanel): # Translators: This is the label for the voice settings panel. title = _("Voice") @@ -2121,32 +2152,34 @@ def makeSettings(self, settingsSizer): # Translators: This is the label for a group of document formatting options in the # document formatting settings panel fontGroupText = _("Font") - fontGroup = guiHelper.BoxSizerHelper(self, sizer=wx.StaticBoxSizer(wx.StaticBox(self, label=fontGroupText), wx.VERTICAL)) + fontGroupSizer = wx.StaticBoxSizer(wx.VERTICAL, self, label=fontGroupText) + fontGroupBox = fontGroupSizer.GetStaticBox() + fontGroup = guiHelper.BoxSizerHelper(self, sizer=fontGroupSizer) sHelper.addItem(fontGroup) # Translators: This is the label for a checkbox in the # document formatting settings panel. fontNameText = _("&Font name") - self.fontNameCheckBox=fontGroup.addItem(wx.CheckBox(self, label=fontNameText)) + self.fontNameCheckBox = fontGroup.addItem(wx.CheckBox(fontGroupBox, label=fontNameText)) self.fontNameCheckBox.SetValue(config.conf["documentFormatting"]["reportFontName"]) # Translators: This is the label for a checkbox in the # document formatting settings panel. fontSizeText = _("Font &size") - self.fontSizeCheckBox=fontGroup.addItem(wx.CheckBox(self,label=fontSizeText)) + self.fontSizeCheckBox = fontGroup.addItem(wx.CheckBox(fontGroupBox, label=fontSizeText)) self.fontSizeCheckBox.SetValue(config.conf["documentFormatting"]["reportFontSize"]) # Translators: This is the label for a checkbox in the # document formatting settings panel. fontAttributesText = _("Font attrib&utes") - self.fontAttrsCheckBox=fontGroup.addItem(wx.CheckBox(self,label=fontAttributesText)) + self.fontAttrsCheckBox = fontGroup.addItem(wx.CheckBox(fontGroupBox, label=fontAttributesText)) self.fontAttrsCheckBox.SetValue(config.conf["documentFormatting"]["reportFontAttributes"]) # Translators: This is the label for a checkbox in the # document formatting settings panel. superscriptsAndSubscriptsText = _("Su&perscripts and subscripts") self.superscriptsAndSubscriptsCheckBox = fontGroup.addItem( - wx.CheckBox(self, label=superscriptsAndSubscriptsText) + wx.CheckBox(fontGroupBox, label=superscriptsAndSubscriptsText) ) self.superscriptsAndSubscriptsCheckBox.SetValue( config.conf["documentFormatting"]["reportSuperscriptsAndSubscripts"] @@ -2155,14 +2188,14 @@ def makeSettings(self, settingsSizer): # Translators: This is the label for a checkbox in the # document formatting settings panel. emphasisText=_("E&mphasis") - self.emphasisCheckBox=fontGroup.addItem(wx.CheckBox(self,label=emphasisText)) + self.emphasisCheckBox = fontGroup.addItem(wx.CheckBox(fontGroupBox, label=emphasisText)) self.emphasisCheckBox.SetValue(config.conf["documentFormatting"]["reportEmphasis"]) # Translators: This is the label for a checkbox in the # document formatting settings panel. highlightText = _("Mar&ked (highlighted text)") self.highlightCheckBox = fontGroup.addItem( - wx.CheckBox(self, label=highlightText) + wx.CheckBox(fontGroupBox, label=highlightText) ) self.highlightCheckBox.SetValue( config.conf["documentFormatting"]["reportHighlight"] @@ -2171,55 +2204,59 @@ def makeSettings(self, settingsSizer): # Translators: This is the label for a checkbox in the # document formatting settings panel. styleText =_("St&yle") - self.styleCheckBox=fontGroup.addItem(wx.CheckBox(self,label=styleText)) + self.styleCheckBox = fontGroup.addItem(wx.CheckBox(fontGroupBox, label=styleText)) self.styleCheckBox.SetValue(config.conf["documentFormatting"]["reportStyle"]) # Translators: This is the label for a checkbox in the # document formatting settings panel. colorsText = _("&Colors") - self.colorCheckBox=fontGroup.addItem(wx.CheckBox(self,label=colorsText)) + self.colorCheckBox = fontGroup.addItem(wx.CheckBox(fontGroupBox, label=colorsText)) self.colorCheckBox.SetValue(config.conf["documentFormatting"]["reportColor"]) # Translators: This is the label for a group of document formatting options in the # document formatting settings panel documentInfoGroupText = _("Document information") - docInfoGroup = guiHelper.BoxSizerHelper(self, sizer=wx.StaticBoxSizer(wx.StaticBox(self, label=documentInfoGroupText), wx.VERTICAL)) + docInfoSizer = wx.StaticBoxSizer(wx.VERTICAL, self, label=documentInfoGroupText) + docInfoBox = docInfoSizer.GetStaticBox() + docInfoGroup = guiHelper.BoxSizerHelper(self, sizer=docInfoSizer) sHelper.addItem(docInfoGroup) # Translators: This is the label for a checkbox in the # document formatting settings panel. commentsText = _("No&tes and comments") - self.commentsCheckBox=docInfoGroup.addItem(wx.CheckBox(self,label=commentsText)) + self.commentsCheckBox = docInfoGroup.addItem(wx.CheckBox(docInfoBox, label=commentsText)) self.commentsCheckBox.SetValue(config.conf["documentFormatting"]["reportComments"]) # Translators: This is the label for a checkbox in the # document formatting settings panel. revisionsText = _("&Editor revisions") - self.revisionsCheckBox=docInfoGroup.addItem(wx.CheckBox(self,label=revisionsText)) + self.revisionsCheckBox = docInfoGroup.addItem(wx.CheckBox(docInfoBox, label=revisionsText)) self.revisionsCheckBox.SetValue(config.conf["documentFormatting"]["reportRevisions"]) # Translators: This is the label for a checkbox in the # document formatting settings panel. spellingErrorText = _("Spelling e&rrors") - self.spellingErrorsCheckBox=docInfoGroup.addItem(wx.CheckBox(self,label=spellingErrorText)) + self.spellingErrorsCheckBox = docInfoGroup.addItem(wx.CheckBox(docInfoBox, label=spellingErrorText)) self.spellingErrorsCheckBox.SetValue(config.conf["documentFormatting"]["reportSpellingErrors"]) # Translators: This is the label for a group of document formatting options in the # document formatting settings panel pageAndSpaceGroupText = _("Pages and spacing") - pageAndSpaceGroup = guiHelper.BoxSizerHelper(self, sizer=wx.StaticBoxSizer(wx.StaticBox(self, label=pageAndSpaceGroupText), wx.VERTICAL)) + pageAndSpaceSizer = wx.StaticBoxSizer(wx.VERTICAL, self, label=pageAndSpaceGroupText) + pageAndSpaceBox = pageAndSpaceSizer.GetStaticBox() + pageAndSpaceGroup = guiHelper.BoxSizerHelper(self, sizer=pageAndSpaceSizer) sHelper.addItem(pageAndSpaceGroup) # Translators: This is the label for a checkbox in the # document formatting settings panel. pageText = _("&Pages") - self.pageCheckBox=pageAndSpaceGroup.addItem(wx.CheckBox(self,label=pageText)) + self.pageCheckBox = pageAndSpaceGroup.addItem(wx.CheckBox(pageAndSpaceBox, label=pageText)) self.pageCheckBox.SetValue(config.conf["documentFormatting"]["reportPage"]) # Translators: This is the label for a checkbox in the # document formatting settings panel. lineText = _("Line &numbers") - self.lineNumberCheckBox=pageAndSpaceGroup.addItem(wx.CheckBox(self,label=lineText)) + self.lineNumberCheckBox = pageAndSpaceGroup.addItem(wx.CheckBox(pageAndSpaceBox, label=lineText)) self.lineNumberCheckBox.SetValue(config.conf["documentFormatting"]["reportLineNumber"]) # Translators: This is the label for a combobox controlling the reporting of line indentation in the @@ -2247,40 +2284,46 @@ def makeSettings(self, settingsSizer): # Translators: This message is presented in the document formatting settings panelue # If this option is selected, NVDA will report paragraph indentation if available. paragraphIndentationText = _("&Paragraph indentation") - self.paragraphIndentationCheckBox=pageAndSpaceGroup.addItem(wx.CheckBox(self,label=paragraphIndentationText)) + _paragraphIndentationCheckBox = wx.CheckBox(pageAndSpaceBox, label=paragraphIndentationText) + self.paragraphIndentationCheckBox = pageAndSpaceGroup.addItem(_paragraphIndentationCheckBox) self.paragraphIndentationCheckBox.SetValue(config.conf["documentFormatting"]["reportParagraphIndentation"]) # Translators: This message is presented in the document formatting settings panelue # If this option is selected, NVDA will report line spacing if available. lineSpacingText=_("&Line spacing") - self.lineSpacingCheckBox=pageAndSpaceGroup.addItem(wx.CheckBox(self,label=lineSpacingText)) + _lineSpacingCheckBox = wx.CheckBox(pageAndSpaceBox, label=lineSpacingText) + self.lineSpacingCheckBox = pageAndSpaceGroup.addItem(_lineSpacingCheckBox) self.lineSpacingCheckBox.SetValue(config.conf["documentFormatting"]["reportLineSpacing"]) # Translators: This is the label for a checkbox in the # document formatting settings panel. alignmentText = _("&Alignment") - self.alignmentCheckBox=pageAndSpaceGroup.addItem(wx.CheckBox(self,label=alignmentText)) + self.alignmentCheckBox = pageAndSpaceGroup.addItem(wx.CheckBox(pageAndSpaceBox, label=alignmentText)) self.alignmentCheckBox.SetValue(config.conf["documentFormatting"]["reportAlignment"]) # Translators: This is the label for a group of document formatting options in the # document formatting settings panel tablesGroupText = _("Table information") - tablesGroup = guiHelper.BoxSizerHelper(self, sizer=wx.StaticBoxSizer(wx.StaticBox(self, label=tablesGroupText), wx.VERTICAL)) + tablesGroupSizer = wx.StaticBoxSizer(wx.VERTICAL, self, label=tablesGroupText) + tablesGroupBox = tablesGroupSizer.GetStaticBox() + tablesGroup = guiHelper.BoxSizerHelper(self, sizer=tablesGroupSizer) sHelper.addItem(tablesGroup) # Translators: This is the label for a checkbox in the # document formatting settings panel. - self.tablesCheckBox=tablesGroup.addItem(wx.CheckBox(self,label=_("&Tables"))) + self.tablesCheckBox = tablesGroup.addItem(wx.CheckBox(tablesGroupBox, label=_("&Tables"))) self.tablesCheckBox.SetValue(config.conf["documentFormatting"]["reportTables"]) # Translators: This is the label for a checkbox in the # document formatting settings panel. - self.tableHeadersCheckBox=tablesGroup.addItem(wx.CheckBox(self,label=_("Row/column h&eaders"))) + _tableHeadersCheckBox = wx.CheckBox(tablesGroupBox, label=_("Row/column h&eaders")) + self.tableHeadersCheckBox = tablesGroup.addItem(_tableHeadersCheckBox) self.tableHeadersCheckBox.SetValue(config.conf["documentFormatting"]["reportTableHeaders"]) # Translators: This is the label for a checkbox in the # document formatting settings panel. - self.tableCellCoordsCheckBox=tablesGroup.addItem(wx.CheckBox(self,label=_("Cell c&oordinates"))) + _tableCellCoordsCheckBox = wx.CheckBox(tablesGroupBox, label=_("Cell c&oordinates")) + self.tableCellCoordsCheckBox = tablesGroup.addItem(_tableCellCoordsCheckBox) self.tableCellCoordsCheckBox.SetValue(config.conf["documentFormatting"]["reportTableCellCoords"]) borderChoices=[ @@ -2312,65 +2355,68 @@ def makeSettings(self, settingsSizer): # Translators: This is the label for a group of document formatting options in the # document formatting settings panel elementsGroupText = _("Elements") - elementsGroup = guiHelper.BoxSizerHelper(self, sizer=wx.StaticBoxSizer(wx.StaticBox(self, label=elementsGroupText), wx.VERTICAL)) + elementsGroupSizer = wx.StaticBoxSizer(wx.VERTICAL, self, label=elementsGroupText) + elementsGroupBox = elementsGroupSizer.GetStaticBox() + elementsGroup = guiHelper.BoxSizerHelper(self, sizer=elementsGroupSizer) sHelper.addItem(elementsGroup, flag=wx.EXPAND, proportion=1) # Translators: This is the label for a checkbox in the # document formatting settings panel. - self.headingsCheckBox=elementsGroup.addItem(wx.CheckBox(self,label=_("&Headings"))) + self.headingsCheckBox = elementsGroup.addItem(wx.CheckBox(elementsGroupBox, label=_("&Headings"))) self.headingsCheckBox.SetValue(config.conf["documentFormatting"]["reportHeadings"]) # Translators: This is the label for a checkbox in the # document formatting settings panel. - self.linksCheckBox=elementsGroup.addItem(wx.CheckBox(self,label=_("Lin&ks"))) + self.linksCheckBox = elementsGroup.addItem(wx.CheckBox(elementsGroupBox, label=_("Lin&ks"))) self.linksCheckBox.SetValue(config.conf["documentFormatting"]["reportLinks"]) # Translators: This is the label for a checkbox in the # document formatting settings panel. - self.graphicsCheckBox = elementsGroup.addItem(wx.CheckBox(self, label=_("&Graphics"))) + self.graphicsCheckBox = elementsGroup.addItem(wx.CheckBox(elementsGroupBox, label=_("&Graphics"))) self.graphicsCheckBox.SetValue(config.conf["documentFormatting"]["reportGraphics"]) # Translators: This is the label for a checkbox in the # document formatting settings panel. - self.listsCheckBox=elementsGroup.addItem(wx.CheckBox(self,label=_("&Lists"))) + self.listsCheckBox = elementsGroup.addItem(wx.CheckBox(elementsGroupBox, label=_("&Lists"))) self.listsCheckBox.SetValue(config.conf["documentFormatting"]["reportLists"]) # Translators: This is the label for a checkbox in the # document formatting settings panel. - self.blockQuotesCheckBox=elementsGroup.addItem(wx.CheckBox(self,label=_("Block "es"))) + _blockQuotesCheckBox = wx.CheckBox(elementsGroupBox, label=_("Block "es")) + self.blockQuotesCheckBox = elementsGroup.addItem(_blockQuotesCheckBox) self.blockQuotesCheckBox.SetValue(config.conf["documentFormatting"]["reportBlockQuotes"]) # Translators: This is the label for a checkbox in the # document formatting settings panel. groupingsText = _("&Groupings") - self.groupingsCheckBox = elementsGroup.addItem(wx.CheckBox(self, label=groupingsText)) + self.groupingsCheckBox = elementsGroup.addItem(wx.CheckBox(elementsGroupBox, label=groupingsText)) self.groupingsCheckBox.SetValue(config.conf["documentFormatting"]["reportGroupings"]) # Translators: This is the label for a checkbox in the # document formatting settings panel. landmarksText = _("Lan&dmarks and regions") - self.landmarksCheckBox = elementsGroup.addItem(wx.CheckBox(self, label=landmarksText)) + self.landmarksCheckBox = elementsGroup.addItem(wx.CheckBox(elementsGroupBox, label=landmarksText)) self.landmarksCheckBox.SetValue(config.conf["documentFormatting"]["reportLandmarks"]) # Translators: This is the label for a checkbox in the # document formatting settings panel. - self.articlesCheckBox = elementsGroup.addItem(wx.CheckBox(self, label=_("Arti&cles"))) + self.articlesCheckBox = elementsGroup.addItem(wx.CheckBox(elementsGroupBox, label=_("Arti&cles"))) self.articlesCheckBox.SetValue(config.conf["documentFormatting"]["reportArticles"]) # Translators: This is the label for a checkbox in the # document formatting settings panel. - self.framesCheckBox=elementsGroup.addItem(wx.CheckBox(self,label=_("Fra&mes"))) + self.framesCheckBox = elementsGroup.addItem(wx.CheckBox(elementsGroupBox, label=_("Fra&mes"))) self.framesCheckBox.Value=config.conf["documentFormatting"]["reportFrames"] # Translators: This is the label for a checkbox in the # document formatting settings panel. - self.clickableCheckBox=elementsGroup.addItem(wx.CheckBox(self,label=_("&Clickable"))) + self.clickableCheckBox = elementsGroup.addItem(wx.CheckBox(elementsGroupBox, label=_("&Clickable"))) self.clickableCheckBox.Value=config.conf["documentFormatting"]["reportClickable"] # Translators: This is the label for a checkbox in the # document formatting settings panel. detectFormatAfterCursorText = _("Report formatting chan&ges after the cursor (can cause a lag)") - self.detectFormatAfterCursorCheckBox=wx.CheckBox(self, label=detectFormatAfterCursorText) + self.detectFormatAfterCursorCheckBox = wx.CheckBox(self, label=detectFormatAfterCursorText) self.bindHelpEvent( "DocumentFormattingDetectFormatAfterCursor", self.detectFormatAfterCursorCheckBox @@ -2478,6 +2524,9 @@ class AdvancedPanelControls( """Holds the actual controls for the Advanced Settings panel, this allows the state of the controls to be more easily managed. """ + + helpId = "AdvancedSettings" + def __init__(self, parent): super().__init__(parent) self._defaultsRestored = False @@ -2487,16 +2536,15 @@ def __init__(self, parent): # Translators: This is the label for a group of advanced options in the # Advanced settings panel groupText = _("NVDA Development") - devGroup = guiHelper.BoxSizerHelper( - parent=self, - sizer=wx.StaticBoxSizer(parent=self, label=groupText, orient=wx.VERTICAL) - ) + devGroupSizer = wx.StaticBoxSizer(wx.VERTICAL, self, label=groupText) + devGroupBox = devGroupSizer.GetStaticBox() + devGroup = guiHelper.BoxSizerHelper(self, sizer=devGroupSizer) sHelper.addItem(devGroup) # Translators: This is the label for a checkbox in the # Advanced settings panel. label = _("Enable loading custom code from Developer Scratchpad directory") - self.scratchpadCheckBox=devGroup.addItem(wx.CheckBox(self, label=label)) + self.scratchpadCheckBox = devGroup.addItem(wx.CheckBox(devGroupBox, label=label)) self.bindHelpEvent("AdvancedSettingsEnableScratchpad", self.scratchpadCheckBox) self.scratchpadCheckBox.SetValue(config.conf["development"]["enableScratchpadDir"]) self.scratchpadCheckBox.defaultValue = self._getDefaultValue(["development", "enableScratchpadDir"]) @@ -2509,7 +2557,7 @@ def __init__(self, parent): # Translators: the label for a button in the Advanced settings category label=_("Open developer scratchpad directory") - self.openScratchpadButton=devGroup.addItem(wx.Button(self, label=label)) + self.openScratchpadButton = devGroup.addItem(wx.Button(devGroupBox, label=label)) self.bindHelpEvent("AdvancedSettingsOpenScratchpadDir", self.openScratchpadButton) self.openScratchpadButton.Enable(config.conf["development"]["enableScratchpadDir"]) self.openScratchpadButton.Bind(wx.EVT_BUTTON,self.onOpenScratchpadDir) @@ -2519,16 +2567,15 @@ def __init__(self, parent): # Translators: This is the label for a group of advanced options in the # Advanced settings panel label = _("Microsoft UI Automation") - UIAGroup = guiHelper.BoxSizerHelper( - parent=self, - sizer=wx.StaticBoxSizer(parent=self, label=label, orient=wx.VERTICAL) - ) + UIASizer = wx.StaticBoxSizer(wx.VERTICAL, self, label=label) + UIABox = UIASizer.GetStaticBox() + UIAGroup = guiHelper.BoxSizerHelper(self, sizer=UIASizer) sHelper.addItem(UIAGroup) # Translators: This is the label for a checkbox in the # Advanced settings panel. label = _("Enable &selective registration for UI Automation events and property changes") - self.selectiveUIAEventRegistrationCheckBox = UIAGroup.addItem(wx.CheckBox(self, label=label)) + self.selectiveUIAEventRegistrationCheckBox = UIAGroup.addItem(wx.CheckBox(UIABox, label=label)) self.bindHelpEvent( "AdvancedSettingsSelectiveUIAEventRegistration", self.selectiveUIAEventRegistrationCheckBox @@ -2541,16 +2588,24 @@ def __init__(self, parent): # Translators: This is the label for a checkbox in the # Advanced settings panel. label = _("Use UI Automation to access Microsoft &Word document controls when available") - self.UIAInMSWordCheckBox=UIAGroup.addItem(wx.CheckBox(self, label=label)) + self.UIAInMSWordCheckBox = UIAGroup.addItem(wx.CheckBox(UIABox, label=label)) self.bindHelpEvent("AdvancedSettingsUseUiaForWord", self.UIAInMSWordCheckBox) self.UIAInMSWordCheckBox.SetValue(config.conf["UIA"]["useInMSWordWhenAvailable"]) self.UIAInMSWordCheckBox.defaultValue = self._getDefaultValue(["UIA", "useInMSWordWhenAvailable"]) + # Translators: This is the label for a checkbox in the + # Advanced settings panel. + label = _("Use UI Automation to access Microsoft &Excel spreadsheet controls when available") + self.UIAInMSExcelCheckBox = UIAGroup.addItem(wx.CheckBox(UIABox, label=label)) + self.bindHelpEvent("UseUiaForExcel", self.UIAInMSExcelCheckBox) + self.UIAInMSExcelCheckBox.SetValue(config.conf["UIA"]["useInMSExcelWhenAvailable"]) + self.UIAInMSExcelCheckBox.defaultValue = self._getDefaultValue(["UIA", "useInMSExcelWhenAvailable"]) + # Translators: This is the label for a checkbox in the # Advanced settings panel. label = _("Use UI Automation to access the Windows C&onsole when available") consoleUIADevMap = True if config.conf['UIA']['winConsoleImplementation'] == 'UIA' else False - self.ConsoleUIACheckBox = UIAGroup.addItem(wx.CheckBox(self, label=label)) + self.ConsoleUIACheckBox = UIAGroup.addItem(wx.CheckBox(UIABox, label=label)) self.bindHelpEvent("AdvancedSettingsConsoleUIA", self.ConsoleUIACheckBox) self.ConsoleUIACheckBox.SetValue(consoleUIADevMap) self.ConsoleUIACheckBox.defaultValue = self._getDefaultValue(["UIA", "winConsoleImplementation"]) @@ -2558,41 +2613,96 @@ def __init__(self, parent): # Translators: This is the label for a checkbox in the # Advanced settings panel. label = _("Speak &passwords in UIA consoles (may improve performance)") - self.winConsoleSpeakPasswordsCheckBox = UIAGroup.addItem(wx.CheckBox(self, label=label)) + self.winConsoleSpeakPasswordsCheckBox = UIAGroup.addItem(wx.CheckBox(UIABox, label=label)) self.bindHelpEvent("AdvancedSettingsWinConsoleSpeakPasswords", self.winConsoleSpeakPasswordsCheckBox) self.winConsoleSpeakPasswordsCheckBox.SetValue(config.conf["terminals"]["speakPasswords"]) self.winConsoleSpeakPasswordsCheckBox.defaultValue = self._getDefaultValue(["terminals", "speakPasswords"]) + label = pgettext( + "advanced.uiaWithChromium", + # Translators: Label for the Use UIA with Chromium combobox, in the Advanced settings panel. + # Note the '\n' is used to split this long label approximately in half. + "Use UIA with Microsoft Edge and other \n&Chromium based browsers when available:" + ) + chromiumChoices = ( + # Translators: Label for the default value of the Use UIA with Chromium combobox, + # in the Advanced settings panel. + pgettext("advanced.uiaWithChromium", "Default (Only when necessary)"), + # Translators: Label for a value in the Use UIA with Chromium combobox, in the Advanced settings panel. + pgettext("advanced.uiaWithChromium", "Only when necessary"), + # Translators: Label for a value in the Use UIA with Chromium combobox, in the Advanced settings panel. + pgettext("advanced.uiaWithChromium", "Yes"), + # Translators: Label for a value in the Use UIA with Chromium combobox, in the Advanced settings panel. + pgettext("advanced.uiaWithChromium", "No"), + ) + self.UIAInChromiumCombo = UIAGroup.addLabeledControl(label, wx.Choice, choices=chromiumChoices) + self.bindHelpEvent("ChromiumUIA", self.UIAInChromiumCombo) + self.UIAInChromiumCombo.SetSelection(config.conf["UIA"]["allowInChromium"]) + self.UIAInChromiumCombo.defaultValue = self._getDefaultValue(["UIA", "allowInChromium"]) + # Translators: This is the label for a group of advanced options in the # Advanced settings panel label = _("Terminal programs") - terminalsGroup = guiHelper.BoxSizerHelper( - parent=self, - sizer=wx.StaticBoxSizer(parent=self, label=label, orient=wx.VERTICAL) - ) + terminalsSizer = wx.StaticBoxSizer(wx.VERTICAL, self, label=label) + terminalsBox = terminalsSizer.GetStaticBox() + terminalsGroup = guiHelper.BoxSizerHelper(self, sizer=terminalsSizer) sHelper.addItem(terminalsGroup) # Translators: This is the label for a checkbox in the # Advanced settings panel. label = _("Use the new t&yped character support in Windows Console when available") - self.keyboardSupportInLegacyCheckBox=terminalsGroup.addItem(wx.CheckBox(self, label=label)) + self.keyboardSupportInLegacyCheckBox = terminalsGroup.addItem(wx.CheckBox(terminalsBox, label=label)) self.bindHelpEvent("AdvancedSettingsKeyboardSupportInLegacy", self.keyboardSupportInLegacyCheckBox) self.keyboardSupportInLegacyCheckBox.SetValue(config.conf["terminals"]["keyboardSupportInLegacy"]) self.keyboardSupportInLegacyCheckBox.defaultValue = self._getDefaultValue(["terminals", "keyboardSupportInLegacy"]) - self.keyboardSupportInLegacyCheckBox.Enable(winVersion.isWin10(1607)) + self.keyboardSupportInLegacyCheckBox.Enable(winVersion.getWinVer() >= winVersion.WIN10_1607) + + # Translators: This is the label for a combo box for selecting a + # method of detecting changed content in terminals in the advanced + # settings panel. + # Choices are automatic, allow Diff Match Patch, and force Difflib. + diffAlgoComboText = _("&Diff algorithm:") + diffAlgoChoices = [ + # Translators: A choice in a combo box in the advanced settings + # panel to have NVDA determine the method of detecting changed + # content in terminals automatically. + _("Automatic (Difflib)"), + # Translators: A choice in a combo box in the advanced settings + # panel to have NVDA detect changes in terminals + # by character when supported, using the diff match patch algorithm. + _("allow Diff Match Patch"), + # Translators: A choice in a combo box in the advanced settings + # panel to have NVDA detect changes in terminals + # by line, using the difflib algorithm. + _("force Difflib") + ] + #: The possible diffAlgo config values, in the order they appear + #: in the combo box. + self.diffAlgoVals = ( + "auto", + "dmp", + "difflib" + ) + self.diffAlgoCombo = terminalsGroup.addLabeledControl(diffAlgoComboText, wx.Choice, choices=diffAlgoChoices) + self.bindHelpEvent("DiffAlgo", self.diffAlgoCombo) + curChoice = self.diffAlgoVals.index( + config.conf['terminals']['diffAlgo'] + ) + self.diffAlgoCombo.SetSelection(curChoice) + self.diffAlgoCombo.defaultValue = self.diffAlgoVals.index( + self._getDefaultValue(["terminals", "diffAlgo"]) + ) # Translators: This is the label for a group of advanced options in the # Advanced settings panel label = _("Speech") - speechGroup = guiHelper.BoxSizerHelper( - parent=self, - sizer=wx.StaticBoxSizer(parent=self, label=label, orient=wx.VERTICAL) - ) + speechSizer = wx.StaticBoxSizer(wx.VERTICAL, self, label=label) + speechGroup = guiHelper.BoxSizerHelper(speechSizer, sizer=speechSizer) sHelper.addItem(speechGroup) expiredFocusSpeechChoices = [ # Translators: Label for the 'Cancel speech for expired &focus events' combobox # in the Advanced settings panel. - _("Default (No)"), + _("Default (Yes)"), # Translators: Label for the 'Cancel speech for expired &focus events' combobox # in the Advanced settings panel. _("Yes"), @@ -2619,10 +2729,8 @@ def __init__(self, parent): # Translators: This is the label for a group of advanced options in the # Advanced settings panel label = _("Editable Text") - editableTextGroup = guiHelper.BoxSizerHelper( - self, - sizer=wx.StaticBoxSizer(parent=self, label=label, orient=wx.VERTICAL) - ) + editableSizer = wx.StaticBoxSizer(wx.VERTICAL, self, label=label) + editableTextGroup = guiHelper.BoxSizerHelper(editableSizer, sizer=editableSizer) sHelper.addItem(editableTextGroup) # Translators: This is the label for a numeric control in the @@ -2641,10 +2749,8 @@ def __init__(self, parent): # Translators: This is the label for a group of advanced options in the # Advanced settings panel label = _("Debug logging") - debugLogGroup = guiHelper.BoxSizerHelper( - self, - sizer=wx.StaticBoxSizer(parent=self, label=label, orient=wx.VERTICAL) - ) + debugLogSizer = wx.StaticBoxSizer(wx.VERTICAL, self, label=label) + debugLogGroup = guiHelper.BoxSizerHelper(self, sizer=debugLogSizer) sHelper.addItem(debugLogGroup) self.logCategories=[ @@ -2697,10 +2803,13 @@ def haveConfigDefaultsBeenRestored(self): == self.selectiveUIAEventRegistrationCheckBox.defaultValue ) and self.UIAInMSWordCheckBox.IsChecked() == self.UIAInMSWordCheckBox.defaultValue + and self.UIAInMSExcelCheckBox.IsChecked() == self.UIAInMSExcelCheckBox.defaultValue and self.ConsoleUIACheckBox.IsChecked() == (self.ConsoleUIACheckBox.defaultValue == 'UIA') and self.winConsoleSpeakPasswordsCheckBox.IsChecked() == self.winConsoleSpeakPasswordsCheckBox.defaultValue and self.cancelExpiredFocusSpeechCombo.GetSelection() == self.cancelExpiredFocusSpeechCombo.defaultValue + and self.UIAInChromiumCombo.GetSelection() == self.UIAInChromiumCombo.defaultValue and self.keyboardSupportInLegacyCheckBox.IsChecked() == self.keyboardSupportInLegacyCheckBox.defaultValue + and self.diffAlgoCombo.GetSelection() == self.diffAlgoCombo.defaultValue and self.caretMoveTimeoutSpinControl.GetValue() == self.caretMoveTimeoutSpinControl.defaultValue and set(self.logCategoriesList.CheckedItems) == set(self.logCategoriesList.defaultCheckedItems) and True # reduce noise in diff when the list is extended. @@ -2710,10 +2819,13 @@ def restoreToDefaults(self): self.scratchpadCheckBox.SetValue(self.scratchpadCheckBox.defaultValue) self.selectiveUIAEventRegistrationCheckBox.SetValue(self.selectiveUIAEventRegistrationCheckBox.defaultValue) self.UIAInMSWordCheckBox.SetValue(self.UIAInMSWordCheckBox.defaultValue) + self.UIAInMSExcelCheckBox.SetValue(self.UIAInMSExcelCheckBox.defaultValue) self.ConsoleUIACheckBox.SetValue(self.ConsoleUIACheckBox.defaultValue == 'UIA') + self.UIAInChromiumCombo.SetSelection(self.UIAInChromiumCombo.defaultValue) self.winConsoleSpeakPasswordsCheckBox.SetValue(self.winConsoleSpeakPasswordsCheckBox.defaultValue) self.cancelExpiredFocusSpeechCombo.SetSelection(self.cancelExpiredFocusSpeechCombo.defaultValue) self.keyboardSupportInLegacyCheckBox.SetValue(self.keyboardSupportInLegacyCheckBox.defaultValue) + self.diffAlgoCombo.SetSelection(self.diffAlgoCombo.defaultValue == 'auto') self.caretMoveTimeoutSpinControl.SetValue(self.caretMoveTimeoutSpinControl.defaultValue) self.logCategoriesList.CheckedItems = self.logCategoriesList.defaultCheckedItems self._defaultsRestored = True @@ -2723,13 +2835,19 @@ def onSave(self): config.conf["development"]["enableScratchpadDir"]=self.scratchpadCheckBox.IsChecked() config.conf["UIA"]["selectiveEventRegistration"] = self.selectiveUIAEventRegistrationCheckBox.IsChecked() config.conf["UIA"]["useInMSWordWhenAvailable"]=self.UIAInMSWordCheckBox.IsChecked() + config.conf["UIA"]["useInMSExcelWhenAvailable"] = self.UIAInMSExcelCheckBox.IsChecked() if self.ConsoleUIACheckBox.IsChecked(): config.conf['UIA']['winConsoleImplementation'] = "UIA" else: config.conf['UIA']['winConsoleImplementation'] = "auto" config.conf["terminals"]["speakPasswords"] = self.winConsoleSpeakPasswordsCheckBox.IsChecked() config.conf["featureFlag"]["cancelExpiredFocusSpeech"] = self.cancelExpiredFocusSpeechCombo.GetSelection() + config.conf["UIA"]["allowInChromium"] = self.UIAInChromiumCombo.GetSelection() config.conf["terminals"]["keyboardSupportInLegacy"]=self.keyboardSupportInLegacyCheckBox.IsChecked() + diffAlgoChoice = self.diffAlgoCombo.GetSelection() + config.conf['terminals']['diffAlgo'] = ( + self.diffAlgoVals[diffAlgoChoice] + ) config.conf["editableText"]["caretMoveTimeoutMs"]=self.caretMoveTimeoutSpinControl.GetValue() for index,key in enumerate(self.logCategories): config.conf['debugLog'][key]=self.logCategoriesList.IsChecked(index) @@ -2759,12 +2877,10 @@ def makeSettings(self, settingsSizer): :type settingsSizer: wx.BoxSizer """ sHelper = guiHelper.BoxSizerHelper(self, sizer=settingsSizer) - warningGroup = guiHelper.BoxSizerHelper( - self, - sizer=wx.StaticBoxSizer(wx.StaticBox(self), wx.VERTICAL) - ) - sHelper.addItem(warningGroup) + warningSizer = wx.StaticBoxSizer(wx.VERTICAL, self) + warningGroup = guiHelper.BoxSizerHelper(self, sizer=warningSizer) warningBox = warningGroup.sizer.GetStaticBox() # type: wx.StaticBox + sHelper.addItem(warningGroup) warningText = wx.StaticText(warningBox, label=self.warningHeader) warningText.SetFont(wx.Font(18, wx.FONTFAMILY_DEFAULT, wx.NORMAL, wx.BOLD)) @@ -2786,7 +2902,7 @@ def makeSettings(self, settingsSizer): restoreDefaultsButton = warningGroup.addItem( # Translators: This is the label for a button in the Advanced settings panel - wx.Button(self, label=_("Restore defaults")) + wx.Button(warningBox, label=_("Restore defaults")) ) self.bindHelpEvent("AdvancedSettingsRestoringDefaults", restoreDefaultsButton) restoreDefaultsButton.Bind(wx.EVT_BUTTON, lambda evt: self.advancedControls.restoreToDefaults()) @@ -3037,15 +3153,20 @@ def makeSettings(self, settingsSizer): # Translators: A label for the braille display on the braille panel. displayLabel = _("Braille &display") - displayBox = wx.StaticBox(self, label=displayLabel) - displayGroup = guiHelper.BoxSizerHelper(self, sizer=wx.StaticBoxSizer(displayBox, wx.HORIZONTAL)) + displaySizer = wx.StaticBoxSizer(wx.HORIZONTAL, self, label=displayLabel) + displayBox = displaySizer.GetStaticBox() + displayGroup = guiHelper.BoxSizerHelper(self, sizer=displaySizer) settingsSizerHelper.addItem(displayGroup) - self.displayNameCtrl = ExpandoTextCtrl(self, size=(self.scaleSize(250), -1), style=wx.TE_READONLY) + self.displayNameCtrl = ExpandoTextCtrl( + displayBox, + size=(self.scaleSize(250), -1), + style=wx.TE_READONLY + ) self.bindHelpEvent("BrailleSettingsChange", self.displayNameCtrl) self.updateCurrentDisplay() # Translators: This is the label for the button used to change braille display, # it appears in the context of a braille display group on the braille settings panel. - changeDisplayBtn = wx.Button(self, label=_("C&hange...")) + changeDisplayBtn = wx.Button(displayBox, label=_("C&hange...")) self.bindHelpEvent("BrailleSettingsChange", changeDisplayBtn) displayGroup.addItem( guiHelper.associateElements( @@ -3647,7 +3768,7 @@ def makeSettings(self, settingsSizer: wx.BoxSizer): for providerInfo in vision.handler.getProviderList(reloadFromSystem=True): providerSizer = self.settingsSizerHelper.addItem( - wx.StaticBoxSizer(wx.StaticBox(self, label=providerInfo.displayName), wx.VERTICAL), + wx.StaticBoxSizer(wx.VERTICAL, self, label=providerInfo.displayName), flag=wx.EXPAND ) if len(self.providerPanelInstances) > 0: @@ -4024,14 +4145,9 @@ def makeSettings(self, settingsSizer): # Translators: The label for the group of controls in symbol pronunciation dialog to change the pronunciation of a symbol. changeSymbolText = _("Change selected symbol") - changeSymbolHelper = sHelper.addItem(guiHelper.BoxSizerHelper( - parent=self, - sizer=wx.StaticBoxSizer( - parent=self, - label=changeSymbolText, - orient=wx.VERTICAL, - ) - )) + changeSymbolSizer = wx.StaticBoxSizer(wx.VERTICAL, self, label=changeSymbolText) + changeSymbolGroup = guiHelper.BoxSizerHelper(self, sizer=changeSymbolSizer) + changeSymbolHelper = sHelper.addItem(changeSymbolGroup) # Used to ensure that event handlers call Skip(). Not calling skip can cause focus problems for controls. More # generally the advice on the wx documentation is: "In general, it is recommended to skip all non-command events diff --git a/source/gui/startupDialogs.py b/source/gui/startupDialogs.py new file mode 100644 index 00000000000..ad6e60a26a0 --- /dev/null +++ b/source/gui/startupDialogs.py @@ -0,0 +1,278 @@ +# -*- coding: UTF-8 -*- +# A part of NonVisual Desktop Access (NVDA) +# Copyright (C) 2006-2021 NV Access Limited, Łukasz Golonka +# This file may be used under the terms of the GNU General Public License, version 2 or later. +# For more details see: https://www.gnu.org/licenses/gpl-2.0.html + +import wx + +import config +import core +from documentationUtils import getDocFilePath +import globalVars +import gui +import keyboardHandler +from logHandler import log +import versionInfo + + +class WelcomeDialog( + gui.contextHelp.ContextHelpMixin, + wx.Dialog # wxPython does not seem to call base class initializer, put last in MRO +): + """The NVDA welcome dialog. + This provides essential information for new users, + such as a description of the NVDA key and instructions on how to activate the NVDA menu. + It also provides quick access to some important configuration options. + This dialog is displayed the first time NVDA is started with a new configuration. + """ + helpId = "WelcomeDialog" + WELCOME_MESSAGE_DETAIL = _( + # Translators: The main message for the Welcome dialog when the user starts NVDA for the first time. + "Most commands for controlling NVDA require you to hold down" + " the NVDA key while pressing other keys.\n" + "By default, the numpad Insert and main Insert keys may both be used as the NVDA key.\n" + "You can also configure NVDA to use the CapsLock as the NVDA key.\n" + "Press NVDA+n at any time to activate the NVDA menu.\n" + "From this menu, you can configure NVDA, get help and access other NVDA functions." + ) + + def __init__(self, parent): + # Translators: The title of the Welcome dialog when user starts NVDA for the first time. + super().__init__(parent, wx.ID_ANY, _("Welcome to NVDA")) + + mainSizer = wx.BoxSizer(wx.VERTICAL) + # Translators: The header for the Welcome dialog when user starts NVDA for the first time. + # This is in larger, bold lettering + welcomeTextHeader = wx.StaticText(self, label=_("Welcome to NVDA!")) + welcomeTextHeader.SetFont(wx.Font(18, wx.FONTFAMILY_DEFAULT, wx.NORMAL, wx.BOLD)) + mainSizer.AddSpacer(gui.guiHelper.SPACE_BETWEEN_VERTICAL_DIALOG_ITEMS) + mainSizer.Add(welcomeTextHeader, border=20, flag=wx.EXPAND | wx.LEFT | wx.RIGHT) + mainSizer.AddSpacer(gui.guiHelper.SPACE_BETWEEN_VERTICAL_DIALOG_ITEMS) + welcomeTextDetail = wx.StaticText(self, wx.ID_ANY, self.WELCOME_MESSAGE_DETAIL) + mainSizer.Add(welcomeTextDetail, border=20, flag=wx.EXPAND | wx.LEFT | wx.RIGHT) + + # Translators: The label for a group box containing the NVDA welcome dialog options. + optionsLabel = _("Options") + optionsSizer = wx.StaticBoxSizer(wx.VERTICAL, self, label=optionsLabel) + optionsBox = optionsSizer.GetStaticBox() + sHelper = gui.guiHelper.BoxSizerHelper(self, sizer=optionsSizer) + # Translators: The label of a combobox in the Welcome dialog. + kbdLabelText = _("&Keyboard layout:") + layouts = keyboardHandler.KeyboardInputGesture.LAYOUTS + self.kbdNames = sorted(layouts) + kbdChoices = [layouts[layout] for layout in self.kbdNames] + self.kbdList = sHelper.addLabeledControl(kbdLabelText, wx.Choice, choices=kbdChoices) + try: + index = self.kbdNames.index(config.conf["keyboard"]["keyboardLayout"]) + self.kbdList.SetSelection(index) + except (ValueError, KeyError): + log.error("Could not set Keyboard layout list to current layout", exc_info=True) + # Translators: The label of a checkbox in the Welcome dialog. + capsAsNVDAModifierText = _("&Use CapsLock as an NVDA modifier key") + self.capsAsNVDAModifierCheckBox = sHelper.addItem(wx.CheckBox(optionsBox, label=capsAsNVDAModifierText)) + self.capsAsNVDAModifierCheckBox.SetValue(config.conf["keyboard"]["useCapsLockAsNVDAModifierKey"]) + # Translators: The label of a checkbox in the Welcome dialog. + startAfterLogonText = _("St&art NVDA after I sign in") + self.startAfterLogonCheckBox = sHelper.addItem(wx.CheckBox(optionsBox, label=startAfterLogonText)) + self.startAfterLogonCheckBox.Value = config.getStartAfterLogon() + if globalVars.appArgs.secure or config.isAppX or not config.isInstalledCopy(): + self.startAfterLogonCheckBox.Disable() + # Translators: The label of a checkbox in the Welcome dialog. + showWelcomeDialogAtStartupText = _("&Show this dialog when NVDA starts") + _showWelcomeDialogAtStartupCheckBox = wx.CheckBox(optionsBox, label=showWelcomeDialogAtStartupText) + self.showWelcomeDialogAtStartupCheckBox = sHelper.addItem(_showWelcomeDialogAtStartupCheckBox) + self.showWelcomeDialogAtStartupCheckBox.SetValue(config.conf["general"]["showWelcomeDialogAtStartup"]) + mainSizer.Add(optionsSizer, border=gui.guiHelper.BORDER_FOR_DIALOGS, flag=wx.ALL) + mainSizer.Add( + self.CreateButtonSizer(wx.OK), + border=gui.guiHelper.BORDER_FOR_DIALOGS, + flag=wx.ALL | wx.ALIGN_RIGHT + ) + self.Bind(wx.EVT_BUTTON, self.onOk, id=wx.ID_OK) + + mainSizer.Fit(self) + self.SetSizer(mainSizer) + self.kbdList.SetFocus() + self.CentreOnScreen() + + def onOk(self, evt): + layout = self.kbdNames[self.kbdList.GetSelection()] + config.conf["keyboard"]["keyboardLayout"] = layout + config.conf["keyboard"]["useCapsLockAsNVDAModifierKey"] = self.capsAsNVDAModifierCheckBox.IsChecked() + if self.startAfterLogonCheckBox.Enabled: + config.setStartAfterLogon(self.startAfterLogonCheckBox.Value) + config.conf["general"]["showWelcomeDialogAtStartup"] = self.showWelcomeDialogAtStartupCheckBox.IsChecked() + try: + config.conf.save() + except Exception: + log.debugWarning("Could not save", exc_info=True) + self.EndModal(wx.ID_OK) + + @classmethod + def run(cls): + """Prepare and display an instance of this dialog. + This does not require the dialog to be instantiated. + """ + gui.mainFrame.prePopup() + d = cls(gui.mainFrame) + d.ShowModal() + wx.CallAfter(d.Destroy) + gui.mainFrame.postPopup() + + +class LauncherDialog( + gui.contextHelp.ContextHelpMixin, + wx.Dialog # wxPython does not seem to call base class initializer, put last in MRO +): + """The dialog that is displayed when NVDA is started from the launcher. + This displays the license and allows the user to install or create a portable copy of NVDA. + """ + helpId = "InstallingNVDA" + + def __init__(self, parent): + super().__init__(parent, title=f"{versionInfo.name} {_('Launcher')}") + + mainSizer = wx.BoxSizer(wx.VERTICAL) + sHelper = gui.guiHelper.BoxSizerHelper(self, orientation=wx.VERTICAL) + + # Translators: The label of the license text which will be shown when NVDA installation program starts. + groupLabel = _("License Agreement") + sizer = wx.StaticBoxSizer(wx.VERTICAL, self, label=groupLabel) + sHelper.addItem(sizer) + licenseTextCtrl = wx.TextCtrl(self, size=(500, 400), style=wx.TE_MULTILINE | wx.TE_READONLY | wx.TE_RICH) + licenseTextCtrl.Value = open(getDocFilePath("copying.txt", False), "r", encoding="UTF-8").read() + sizer.Add(licenseTextCtrl) + + # Translators: The label for a checkbox in NvDA installation program to agree to the license agreement. + agreeText = _("I &agree") + self.licenseAgreeCheckbox = sHelper.addItem(wx.CheckBox(self, label=agreeText)) + self.licenseAgreeCheckbox.Value = False + self.licenseAgreeCheckbox.Bind(wx.EVT_CHECKBOX, self.onLicenseAgree) + + sizer = sHelper.addItem(wx.GridSizer(2, 2, 0, 0)) + self.actionButtons = [] + # Translators: The label of the button in NVDA installation program to install NvDA on the user's computer. + ctrl = wx.Button(self, label=_("&Install NVDA on this computer")) + sizer.Add(ctrl) + ctrl.Bind(wx.EVT_BUTTON, lambda evt: self.onAction(evt, gui.mainFrame.onInstallCommand)) + self.actionButtons.append(ctrl) + # Translators: The label of the button in NVDA installation program to create a portable version of NVDA. + ctrl = wx.Button(self, label=_("Create &portable copy")) + sizer.Add(ctrl) + ctrl.Bind(wx.EVT_BUTTON, lambda evt: self.onAction(evt, gui.mainFrame.onCreatePortableCopyCommand)) + self.actionButtons.append(ctrl) + # Translators: The label of the button in NVDA installation program + # to continue using the installation program as a temporary copy of NVDA. + ctrl = wx.Button(self, label=_("&Continue running")) + sizer.Add(ctrl) + ctrl.Bind(wx.EVT_BUTTON, self.onContinueRunning) + self.actionButtons.append(ctrl) + sizer.Add(wx.Button(self, label=_("E&xit"), id=wx.ID_CANCEL)) + # If we bind this on the button, it fails to trigger when the dialog is closed. + self.Bind(wx.EVT_BUTTON, self.onExit, id=wx.ID_CANCEL) + + for ctrl in self.actionButtons: + ctrl.Disable() + + mainSizer.Add(sHelper.sizer, border=gui.guiHelper.BORDER_FOR_DIALOGS, flag=wx.ALL) + self.Sizer = mainSizer + mainSizer.Fit(self) + self.CentreOnScreen() + + def onLicenseAgree(self, evt): + for ctrl in self.actionButtons: + ctrl.Enable(evt.IsChecked()) + + def onAction(self, evt, func): + self.Destroy() + func(evt) + + def onContinueRunning(self, evt): + self.Destroy() + core.doStartupDialogs() + + def onExit(self, evt): + core.triggerNVDAExit() + + @classmethod + def run(cls): + """Prepare and display an instance of this dialog. + This does not require the dialog to be instantiated. + """ + gui.mainFrame.prePopup() + d = cls(gui.mainFrame) + d.Show() + gui.mainFrame.postPopup() + + +class AskAllowUsageStatsDialog( + gui.contextHelp.ContextHelpMixin, + wx.Dialog # wxPython does not seem to call base class initializer, put last in MRO +): + """A dialog asking if the user wishes to allow NVDA usage stats to be collected by NV Access.""" + + helpId = "UsageStatsDialog" + + def __init__(self, parent): + # Translators: The title of the dialog asking if usage data can be collected + super().__init__(parent, title=_("NVDA Usage Data Collection")) + mainSizer = wx.BoxSizer(wx.VERTICAL) + sHelper = gui.guiHelper.BoxSizerHelper(self, orientation=wx.VERTICAL) + + message = _( + # Translators: A message asking the user if they want to allow usage stats gathering + "In order to improve NVDA in the future, " + "NV Access wishes to collect usage data from running copies of NVDA.\n\n" + "Data includes Operating System version, NVDA version, language, country of origin, plus " + "certain NVDA configuration such as current synthesizer, braille display and braille table. " + "No spoken or braille content will be ever sent to NV Access. " + "Please refer to the User Guide for a current list of all data collected.\n\n" + "Do you wish to allow NV Access to periodically collect this data in order to improve NVDA?" + ) + sText = sHelper.addItem(wx.StaticText(self, label=message)) + # the wx.Window must be constructed before we can get the handle. + import windowUtils + self.scaleFactor = windowUtils.getWindowScalingFactor(self.GetHandle()) + sText.Wrap( + # 600 was fairly arbitrarily chosen by a visual user to look acceptable on their machine. + self.scaleFactor * 600 + ) + + bHelper = sHelper.addDialogDismissButtons(gui.guiHelper.ButtonHelper(wx.HORIZONTAL)) + + # Translators: The label of a Yes button in a dialog + yesButton = bHelper.addButton(self, wx.ID_YES, label=_("&Yes")) + yesButton.Bind(wx.EVT_BUTTON, self.onYesButton) + + # Translators: The label of a No button in a dialog + noButton = bHelper.addButton(self, wx.ID_NO, label=_("&No")) + noButton.Bind(wx.EVT_BUTTON, self.onNoButton) + + # Translators: The label of a button to remind the user later about performing some action. + remindMeButton = bHelper.addButton(self, wx.ID_CANCEL, label=_("Remind me &later")) + remindMeButton.Bind(wx.EVT_BUTTON, self.onLaterButton) + remindMeButton.SetFocus() + + mainSizer.Add(sHelper.sizer, border=gui.guiHelper.BORDER_FOR_DIALOGS, flag=wx.ALL) + self.Sizer = mainSizer + mainSizer.Fit(self) + self.CentreOnScreen() + + def onYesButton(self, evt): + log.debug("Usage stats gathering has been allowed") + config.conf['update']['askedAllowUsageStats'] = True + config.conf['update']['allowUsageStats'] = True + self.EndModal(wx.ID_YES) + + def onNoButton(self, evt): + log.debug("Usage stats gathering has been disallowed") + config.conf['update']['askedAllowUsageStats'] = True + config.conf['update']['allowUsageStats'] = False + self.EndModal(wx.ID_NO) + + def onLaterButton(self, evt): + log.debug("Usage stats gathering question has been deferred") + # evt.Skip() is called since wx.ID_CANCEL is used as the ID for the Ask Later button, + # wx automatically ends the modal itself. + evt.Skip() diff --git a/source/inputCore.py b/source/inputCore.py index e35d44db062..948b00d5bba 100644 --- a/source/inputCore.py +++ b/source/inputCore.py @@ -550,7 +550,11 @@ def _handleInputHelp(self, gesture, onlyLog=False): import braille braille.handler.message("\t\t".join(textList)) # Punctuation must be spoken for the gesture name (the first chunk) so that punctuation keys are spoken. - speech.speakText(textList[0], reason=controlTypes.REASON_MESSAGE, symbolLevel=characterProcessing.SYMLVL_ALL) + speech.speakText( + textList[0], + reason=controlTypes.OutputReason.MESSAGE, + symbolLevel=characterProcessing.SYMLVL_ALL + ) for text in textList[1:]: speech.speakMessage(text) diff --git a/source/installer.py b/source/installer.py index f1ca65e4277..b8e0ea2f44e 100644 --- a/source/installer.py +++ b/source/installer.py @@ -4,10 +4,8 @@ # See the file COPYING for more details. # Copyright (C) 2011-2019 NV Access Limited, Joseph Lee, Babbage B.V., Łukasz Golonka -from ctypes import * -from ctypes.wintypes import * +import ctypes import winreg -import threading import time import os import tempfile @@ -168,11 +166,16 @@ def removeOldLibFiles(destPath, rebootOK=False): continue for d in subdirs: path = os.path.join(parent, d) - log.debug("Removing old lib directory: %r"%path) - try: - os.rmdir(path) - except OSError: - log.warning("Failed to remove a directory no longer needed. This can be manually removed after a reboot or the installer will try removing it again next time. Directory: %r"%path) + if path != currentLibPath: + log.debug(f"Removing old lib directory: {repr(path)}") + try: + os.rmdir(path) + except OSError: + log.warning( + "Failed to remove a directory no longer needed. " + "This can be manually removed after a reboot or the installer will try" + f" removing it again next time. Directory: {repr(path)}" + ) for f in files: path = os.path.join(parent, f) log.debug("Removing old lib file: %r"%path) @@ -531,8 +534,8 @@ def tryCopyFile(sourceFilePath,destFilePath): sourceFilePath=u"\\\\?\\"+sourceFilePath if not destFilePath.startswith('\\\\'): destFilePath=u"\\\\?\\"+destFilePath - if windll.kernel32.CopyFileW(sourceFilePath,destFilePath,False)==0: - errorCode=GetLastError() + if ctypes.windll.kernel32.CopyFileW(sourceFilePath, destFilePath, False) == 0: + errorCode = ctypes.GetLastError() log.debugWarning("Unable to copy %s, error %d"%(sourceFilePath,errorCode)) if not os.path.exists(destFilePath): raise OSError("error %d copying %s to %s"%(errorCode,sourceFilePath,destFilePath)) @@ -543,8 +546,8 @@ def tryCopyFile(sourceFilePath,destFilePath): log.error("Failed to rename %s after failed overwrite"%destFilePath,exc_info=True) raise RetriableFailure("Failed to rename %s after failed overwrite"%destFilePath) winKernel.moveFileEx(tempPath,None,winKernel.MOVEFILE_DELAY_UNTIL_REBOOT) - if windll.kernel32.CopyFileW(sourceFilePath,destFilePath,False)==0: - errorCode=GetLastError() + if ctypes.windll.kernel32.CopyFileW(sourceFilePath, destFilePath, False) == 0: + errorCode = ctypes.GetLastError() raise OSError("Unable to copy file %s to %s, error %d"%(sourceFilePath,destFilePath,errorCode)) def install(shouldCreateDesktopShortcut=True,shouldRunAtLogon=True): diff --git a/source/keyboardHandler.py b/source/keyboardHandler.py index 5d685d32fab..89de9ae0348 100644 --- a/source/keyboardHandler.py +++ b/source/keyboardHandler.py @@ -107,7 +107,7 @@ def shouldUseToUnicodeEx(focus=None): from NVDAObjects.behaviors import KeyboardHandlerBasedTypedCharSupport return ( # This is only possible in Windows 10 1607 and above - winVersion.isWin10(1607) + winVersion.getWinVer() >= winVersion.WIN10_1607 and ( # Either of # We couldn't inject in-process, and its not a legacy console window without keyboard support. # console windows have their own specific typed character support. diff --git a/source/languageHandler.py b/source/languageHandler.py index 7699701ec0e..a32fdf73dc9 100644 --- a/source/languageHandler.py +++ b/source/languageHandler.py @@ -1,8 +1,8 @@ -#languageHandler.py -#A part of NonVisual Desktop Access (NVDA) -#Copyright (C) 2007-2018 NV access Limited, Joseph Lee -#This file is covered by the GNU General Public License. -#See the file COPYING for more details. +# languageHandler.py +# A part of NonVisual Desktop Access (NVDA) +# Copyright (C) 2007-2018 NV access Limited, Joseph Lee +# This file is covered by the GNU General Public License. +# See the file COPYING for more details. """Language and localization support. This module assists in NVDA going global through language services such as converting Windows locale ID's to friendly names and presenting available languages. @@ -15,10 +15,13 @@ import locale import gettext import globalVars +from logHandler import log #a few Windows locale constants LOCALE_SLANGUAGE=0x2 +LOCALE_SLIST = 0xC LOCALE_SLANGDISPLAYNAME=0x6f +LOCALE_USER_DEFAULT = 0x400 LOCALE_CUSTOM_UNSPECIFIED = 0x1000 #: Returned from L{localeNameToWindowsLCID} when the locale name cannot be mapped to a locale identifier. #: This might be because Windows doesn't know about the locale (e.g. "an"), @@ -123,6 +126,7 @@ def getAvailableLanguages(presentational=False): ) return langs + def makePgettext(translations): """Obtaina pgettext function for use with a gettext translations instance. pgettext is used to support message contexts, @@ -141,7 +145,7 @@ def pgettext(context, message): def pgettext(context, message): return message else: - raise ValueError("%s is Not a GNUTranslations or NullTranslations object"%translations) + raise ValueError("%s is Not a GNUTranslations or NullTranslations object" % translations) return pgettext def getWindowsLanguage(): @@ -169,41 +173,122 @@ def getWindowsLanguage(): localeName="en" return localeName -def setLanguage(lang): + +def setLanguage(lang: str) -> None: + ''' + Sets the following using `lang` such as "en", "ru_RU", or "es-ES". Use "Windows" to use the system locale + - the windows locale for the thread (fallback to system locale) + - the translation service (fallback to English) + - languageHandler.curLang (match the translation service) + - the python locale for the thread (match the translation service, fallback to system default) + ''' global curLang - try: - if lang=="Windows": - localeName=getWindowsLanguage() - trans=gettext.translation('nvda',localedir='locale',languages=[localeName]) - curLang=localeName - else: - trans=gettext.translation("nvda", localedir="locale", languages=[lang]) - curLang=lang - localeChanged=False - #Try setting Python's locale to lang - try: - locale.setlocale(locale.LC_ALL,lang) - localeChanged=True - except: - pass - if not localeChanged and '_' in lang: - #Python couldn'tsupport the language_country locale, just try language. - try: - locale.setlocale(locale.LC_ALL,lang.split('_')[0]) - except: - pass - #Set the windows locale for this thread (NVDA core) to this locale. - LCID=localeNameToWindowsLCID(lang) + if lang == "Windows": + localeName = getWindowsLanguage() + else: + localeName = lang + # Set the windows locale for this thread (NVDA core) to this locale. + try: + LCID = localeNameToWindowsLCID(lang) ctypes.windll.kernel32.SetThreadLocale(LCID) + except IOError: + log.debugWarning(f"couldn't set windows thread locale to {lang}") + + try: + trans = gettext.translation("nvda", localedir="locale", languages=[localeName]) + curLang = localeName except IOError: - trans=gettext.translation("nvda",fallback=True) - curLang="en" + log.debugWarning(f"couldn't set the translation service locale to {localeName}") + trans = gettext.translation("nvda", fallback=True) + curLang = "en" + trans.install() + setLocale(curLang) # Install our pgettext function. - import builtins builtins.pgettext = makePgettext(trans) -def getLanguage(): +def setLocale(localeName: str) -> None: + ''' + Set python's locale using a `localeName` such as "en", "ru_RU", or "es-ES". + Will fallback on `curLang` if it cannot be set and finally fallback to the system locale. + ''' + + r''' + Python 3.8's locale system allows you to set locales that you cannot get + so we must test for both ValueErrors and locale.Errors + + >>> import locale + >>> locale.setlocale(locale.LC_ALL, 'foobar') + Traceback (most recent call last): + File "", line 1, in + File "Python38-32\lib\locale.py", line 608, in setlocale + return _setlocale(category, locale) + locale.Error: unsupported locale setting + >>> locale.setlocale(locale.LC_ALL, 'en-GB') + 'en-GB' + >>> locale.getlocale() + Traceback (most recent call last): + File "", line 1, in + File "Python38-32\lib\locale.py", line 591, in getlocale + return _parse_localename(localename) + File "Python38-32\lib\locale.py", line 499, in _parse_localename + raise ValueError('unknown locale: %s' % localename) + ValueError: unknown locale: en-GB + ''' + originalLocaleName = localeName + # Try setting Python's locale to localeName + try: + locale.setlocale(locale.LC_ALL, localeName) + locale.getlocale() + log.debug(f"set python locale to {localeName}") + return + except locale.Error: + log.debugWarning(f"python locale {localeName} could not be set") + except ValueError: + log.debugWarning(f"python locale {localeName} could not be retrieved with getlocale") + + if '-' in localeName: + # Python couldn't support the language-country locale, try language_country. + try: + localeName = localeName.replace('-', '_') + locale.setlocale(locale.LC_ALL, localeName) + locale.getlocale() + log.debug(f"set python locale to {localeName}") + return + except locale.Error: + log.debugWarning(f"python locale {localeName} could not be set") + except ValueError: + log.debugWarning(f"python locale {localeName} could not be retrieved with getlocale") + + if '_' in localeName: + # Python couldn't support the language_country locale, just try language. + try: + localeName = localeName.split('_')[0] + locale.setlocale(locale.LC_ALL, localeName) + locale.getlocale() + log.debug(f"set python locale to {localeName}") + return + except locale.Error: + log.debugWarning(f"python locale {localeName} could not be set") + except ValueError: + log.debugWarning(f"python locale {localeName} could not be retrieved with getlocale") + + try: + locale.getlocale() + except ValueError: + # as the locale may have been changed to something that getlocale() couldn't retrieve + # reset to default locale + if originalLocaleName == curLang: + # reset to system locale default if we can't set the current lang's locale + locale.setlocale(locale.LC_ALL, "") + log.debugWarning(f"set python locale to system default") + else: + log.debugWarning(f"setting python locale to the current language {curLang}") + # fallback and try to reset the locale to the current lang + setLocale(curLang) + + +def getLanguage() -> str: return curLang def normalizeLanguage(lang): @@ -338,5 +423,9 @@ def normalizeLanguage(lang): 134:'qut', 135:'rw', 136:'wo', - 140:'gbz' + 140: 'gbz', + 1170: 'ckb', + 1109: 'my', + 1143: 'so', + 9242: 'sr', } diff --git a/source/locale/en/symbols.dic b/source/locale/en/symbols.dic index 5bffe0d72c9..ab8af85da46 100644 --- a/source/locale/en/symbols.dic +++ b/source/locale/en/symbols.dic @@ -1,253 +1,358 @@ -#locale/en/symbols.dic -#A part of NonVisual Desktop Access (NVDA) -#Copyright (c) 2011-2017 NVDA Contributors -#This file is covered by the GNU General Public License. - -complexSymbols: -# identifier regexp -# Sentence endings. -. sentence ending (?<=[^\s.])\.(?=[\"'”’)\s]|$) -! sentence ending (?<=[^\s!])\!(?=[\"'”’)\s]|$) -? sentence ending (?<=[^\s?])\?(?=[\"'”’)\s]|$) -# Phrase endings. -; phrase ending (?<=[^\s;]);(?=\s|$) -: phrase ending (?<=[^\s:]):(?=\s|$) -# Others -decimal point (? greater some -= equals some -? question all -@ at some -[ left bracket most -] right bracket most -\\ backslash most -^ caret most -_ line most -` graav most -{ left brace most -} right brace most -| bar most -¦ broken bar most -~ tilda most -¡ inverted exclamation point some -¿ inverted question mark some -· middle dot most -‚ single low quote most -„ double low quote most -′ prime some -″ double prime some -‴ triple prime some - -# Other characters -• bullet some -… dot dot dot all always -... dot dot dot all always -“ left quote most -” right quote most -‘ left tick most -’ right tick most -– en dash most always -— em dash most -­ soft hyphen most -⁃ hyphen bullet none -● circle most -○ white circle most -¨ diaeresis most -¯ macron most -´ acute most -¸ cedilla most -‎ left to right mark char -‏ right to left mark char -¶ paragraph marker most -■ black square some -▪ black square some -◾ black square some -□ white square some -◦ white bullet some -⇒ right double arrow some -⇨ right white arrow some -➔ right-pointing arrow some -➢ right arrowhead some -⮚ right arrowhead some -❖ black diamond minus white X some -♣ black club some -♦ black diamond some -◆ black diamond some -§ section all -° degrees some -« double left pointing angle bracket -» double right pointing angle bracket -µ micro some -º ordinal some -ª superscript a some -⁰ superscript 0 some -¹ superscript 1 some -² superscript 2 some -³ superscript 3 some -⁴ superscript 4 some -⁵ superscript 5 some -⁶ superscript 6 some -⁷ superscript 7 some -⁸ superscript 8 some -⁹ superscript 9 some -⁺ superscript plus some -⁻ superscript minus some -⁼ superscript equals some -⁽ superscript left paren some -⁾ superscript right paren some -ⁿ superscript n some -₀ subscript 0 some -₁ subscript 1 some -₂ subscript 2 some -₃ subscript 3 some -₄ subscript 4 some -₅ subscript 5 some -₆ subscript 6 some -₇ subscript 7 some -₈ subscript 8 some -₉ subscript 9 some -₊ subscript plus some -₋ subscript minus some -₌ subscript equals some -₍ subscript left paren some -₎ subscript right paren some -® registered some -™ Trademark some -© Copyright some -℠ ServiceMark some -± Plus or Minus some -× times some -÷ divide by some -← left arrow some -↑ up arrow some -→ right arrow some -↓ down arrow some -✓ check some -✔ check some -🡺 right arrow some -† dagger some -‡ double dagger some -‣ triangular bullet none -✗ x-shaped bullet none - -#Mathematical Operators U+2200 to U+220F -∀ for all none -∁ complement none -∂ partial derivative none -∃ there exists none -∄ there does not exist none -∅ empty set none -∆ increment none -∇ nabla none -∈ element of none -∉ not an element of none -∊ small element of none -∋ contains as member none -∌ does not contain as member none -∍ small contains as member none -∎ end of proof none -∏ n-ary product none - -# Miscellaneous Mathematical Operators -∑ n-ary summation none -√ square root none -∛ cube root none -∜ fourth root none -∝ proportional to none -∞ infinity none -∟ right angle none -∠ angle none -∥ parallel to none -∦ not parallel to none -∧ logical and none -∨ logical or none -¬ logical not none -∩ intersection none -∪ union none -∫ integral none -∴ therefore none -∵ because none -∶ ratio none -∷ proportion none -≤ less- than or equal to none -≥ greater-than or equal to none -⊂ subset of none -⊃ superset of none -⊆ subset of or equal to none -⊇ superset of or equal to none - -# Vulgur Fractions U+2150 to U+215E -¼ one quarter none -½ one half none -¾ three quarters none -⅐ one seventh none -⅑ one ninth none -⅒ one tenth none -⅓ one third none -⅔ two thirds none -⅕ one fifth none -⅖ two fifths none -⅗ three fifths none -⅘ four fifths none -⅙ one sixth none -⅚ five sixths none -⅛ one eighth none -⅜ three eights none -⅝ five eighths none -⅞ seven eighths none - -# Miscellaneous Technical -⌘ Mac Command key none +#locale/en/symbols.dic +#A part of NonVisual Desktop Access (NVDA) +#Copyright (c) 2011-2017 NVDA Contributors +#This file is covered by the GNU General Public License. + +complexSymbols: +# identifier regexp +# Sentence endings. +. sentence ending (?<=[^\s.])\.(?=[\"'”’)\s]|$) +! sentence ending (?<=[^\s!])\!(?=[\"'”’)\s]|$) +? sentence ending (?<=[^\s?])\?(?=[\"'”’)\s]|$) +# Phrase endings. +; phrase ending (?<=[^\s;]);(?=\s|$) +: phrase ending (?<=[^\s:]):(?=\s|$) +# Others +decimal point (? greater some +≤ less- than or equal to none +≦ less- than or equal to none +≪ much smaller than none +≥ greater-than or equal to none +≧ greater-than or equal to none +≫ much bigger than none +≶ less than or greater than none +≷ greater than or less than none +≮ not less than none +≯ not greater than none + +#Functions +⁻ inverse some +∘ ring Operator none +∂ partial derivative none +∇ gradient of none + +#Geometry and linear Algebra +⃗ vector between none +△ triangle none +▭ rectangle none +∟ right angle none +∠ angle none +∥ parallel to none +∦ not parallel to none +⊥ perpendicular to none +⟂ ortogonal to none +‖ norm of vector none +̂ normalizes none +∿ sine wave none +∡ measured Angle none +∢ spherical Angle none + +#Logical operators +∀ for all none +∃ there exists none +∄ there does not exist none +⇏ does not imply none +⇐ is implied by none + +#Other mathematical Operators +∈ element of none +∉ not an element of none +∊ small element of none +∋ contains as member none +∌ does not contain as member none +∍ small contains as member none +∎ end of proof none +∏ n-ary product none +∐ n-ary coproduct none +∑ n-ary summation none +√ square root none +∛ cube root none +∜ fourth root none +∝ proportional to none +∞ infinity none +∧ and none +∨ or none +¬ not none +∩ intersection none +∪ union none +∫ integral none +∬ double Integral none +∭ tripple Integral none +∮ contour Integral none +∯ surface Integral none +∰ volume Integral none +∱ clockwise Integral none +∲ clockwise contour Integral none +∳ anticlockwise Contour Integral none +∴ therefore none +∵ because none +∶ ratio none +∷ proportion none +∹ excess none +∺ geometric proportion none +≀ wreath product none +≏ difference between none +≐ approaches the limit none +∙ bullet Operator none +∣ divides none +∤ does not divide none +≔ colon equals none +≕ equals colon none +≺ precedes none +≻ succeeds none +⊀ does not precede none +⊁ does not succeed none + +# Vulgur Fractions U+2150 to U+215E +¼ one quarter none +½ one half none +¾ three quarters none +⅐ one seventh none +⅑ one ninth none +⅒ one tenth none +⅓ one third none +⅔ two thirds none +⅕ one fifth none +⅖ two fifths none +⅗ three fifths none +⅘ four fifths none +⅙ one sixth none +⅚ five sixths none +⅛ one eighth none +⅜ three eights none +⅝ five eighths none +⅞ seven eighths none + +#Number sets +𝔸 algebraic numbers none +ℂ complex numbers none +ℑ imaginary part of complex number none +ℍ quaternions none +ℕ natural numbers none +𝕁 nonnegative (whole) numbers none +ℚ rational numbers none +ℝ real numbers none +ℜ real part of complex number none +ℤ integers none +ℵ aleph number none +ℶ beth number none + +# Miscellaneous Technical +⌘ mac Command key none diff --git a/source/locationHelper.py b/source/locationHelper.py index 4eb2028ea97..dc5d4ee154b 100644 --- a/source/locationHelper.py +++ b/source/locationHelper.py @@ -1,8 +1,7 @@ -#locationHelper.py -#A part of NonVisual Desktop Access (NVDA) -#This file is covered by the GNU General Public License. -#See the file COPYING for more details. -#Copyright (C) 2017-2018 NV Access Limited, Babbage B.V. +# A part of NonVisual Desktop Access (NVDA) +# This file is covered by the GNU General Public License. +# See the file COPYING for more details. +# Copyright (C) 2017-2021 NV Access Limited, Babbage B.V. """Classes and helper functions for working with rectangles and coordinates.""" @@ -39,8 +38,8 @@ def fromCompatibleType(cls, point): def fromDWORD(cls, dwPoint): if isinstance(dwPoint,DWORD): dwPoint = dwPoint.value - if not isinstance(dwPoint,(int,long)): - raise TypeError("dwPoint should be one of int, long or ctypes.wintypes.DWORD (ctypes.ulong)") + if not isinstance(dwPoint, int): + raise TypeError("dwPoint should be either int or ctypes.wintypes.DWORD (ctypes.ulong)") return Point(winUser.GET_X_LPARAM(dwPoint),winUser.GET_Y_LPARAM(dwPoint)) def __add__(self,other): diff --git a/source/logHandler.py b/source/logHandler.py index 6fa34b52a6f..56f4ed98e7d 100755 --- a/source/logHandler.py +++ b/source/logHandler.py @@ -10,13 +10,13 @@ import ctypes import sys import warnings -from encodings import utf_8 import logging import inspect import winsound import traceback -from types import MethodType, FunctionType +from types import FunctionType import globalVars +import winKernel import buildVersion from typing import Optional @@ -288,6 +288,19 @@ class Formatter(logging.Formatter): def formatException(self, ex): return stripBasePathFromTracebackText(super(Formatter, self).formatException(ex)) + def formatTime(self, record: logging.LogRecord, datefmt: Optional[str] = None) -> str: + """Custom implementation of `formatTime` which avoids `time.localtime` + since it causes a crash under some versions of Universal CRT ( #12160, Python issue 36792) + """ + timeAsFileTime = winKernel.time_tToFileTime(record.created) + timeAsSystemTime = winKernel.SYSTEMTIME() + winKernel.FileTimeToSystemTime(timeAsFileTime, timeAsSystemTime) + timeAsLocalTime = winKernel.SYSTEMTIME() + winKernel.SystemTimeToTzSpecificLocalTime(None, timeAsSystemTime, timeAsLocalTime) + res = f"{timeAsLocalTime.wHour:02d}:{timeAsLocalTime.wMinute:02d}:{timeAsLocalTime.wSecond:02d}" + return self.default_msec_format % (res, record.msecs) + + class StreamRedirector(object): """Redirects an output stream to a logger. """ diff --git a/source/louisHelper.py b/source/louisHelper.py index c89064a03cc..7a3bdc72631 100644 --- a/source/louisHelper.py +++ b/source/louisHelper.py @@ -1,8 +1,8 @@ -#louisHelper.py -#A part of NonVisual Desktop Access (NVDA) -#This file is covered by the GNU General Public License. -#See the file COPYING for more details. -#Copyright (C) 2018 NV Access Limited, Babbage B.V. +# louisHelper.py +# A part of NonVisual Desktop Access (NVDA) +# This file is covered by the GNU General Public License. +# See the file COPYING for more details. +# Copyright (C) 2018 NV Access Limited, Babbage B.V. """Helper module to ease communication to and from liblouis.""" diff --git a/source/mathPres/__init__.py b/source/mathPres/__init__.py index ae96e180206..a7a927606ae 100644 --- a/source/mathPres/__init__.py +++ b/source/mathPres/__init__.py @@ -33,7 +33,7 @@ def getSpeechForMathMl(self, mathMl): @param mathMl: The MathML markup. @type mathMl: str @return: A speech sequence. - @rtype: List[str, speech.SpeechCommand] + @rtype: List[str, SpeechCommand] """ raise NotImplementedError diff --git a/source/mathPres/mathPlayer.py b/source/mathPres/mathPlayer.py index 42432083973..128bb8fffb1 100644 --- a/source/mathPres/mathPlayer.py +++ b/source/mathPres/mathPlayer.py @@ -18,6 +18,16 @@ import braille import mathPres +from speech.commands import ( + PitchCommand, + VolumeCommand, + RateCommand, + LangChangeCommand, + BreakCommand, + CharacterModeCommand, + PhonemeCommand, +) + RE_MP_SPEECH = re.compile( # Break. r" ?" @@ -35,9 +45,9 @@ # Actual content. r"|(?P[^<,]+)") PROSODY_COMMANDS = { - "pitch": speech.PitchCommand, - "volume": speech.VolumeCommand, - "rate": speech.RateCommand, + "pitch": PitchCommand, + "volume": VolumeCommand, + "rate": RateCommand, } def _processMpSpeech(text, language): # MathPlayer's default rate is 180 wpm. @@ -47,16 +57,15 @@ def _processMpSpeech(text, language): breakMulti = 180.0 / wpm out = [] if language: - out.append(speech.LangChangeCommand(language)) + out.append(LangChangeCommand(language)) resetProsody = set() for m in RE_MP_SPEECH.finditer(text): if m.lastgroup == "break": - out.append(speech.BreakCommand(time=int(m.group("break")) * breakMulti)) + out.append(BreakCommand(time=int(m.group("break")) * breakMulti)) elif m.lastgroup == "char": - out.extend((speech.CharacterModeCommand(True), - m.group("char"), speech.CharacterModeCommand(False))) + out.extend((CharacterModeCommand(True), m.group("char"), CharacterModeCommand(False))) elif m.lastgroup == "comma": - out.append(speech.BreakCommand(time=100)) + out.append(BreakCommand(time=100)) elif m.lastgroup in PROSODY_COMMANDS: command = PROSODY_COMMANDS[m.lastgroup] out.append(command(multiplier=int(m.group(m.lastgroup)) / 100.0)) @@ -66,12 +75,11 @@ def _processMpSpeech(text, language): out.append(command(multiplier=1)) resetProsody.clear() elif m.lastgroup == "phonemeText": - out.append(speech.PhonemeCommand(m.group("ipa"), - text=m.group("phonemeText"))) + out.append(PhonemeCommand(m.group("ipa"), text=m.group("phonemeText"))) elif m.lastgroup == "content": out.append(m.group(0)) if language: - out.append(speech.LangChangeCommand(None)) + out.append(LangChangeCommand(None)) return out class MathPlayerInteraction(mathPres.MathInteractionNVDAObject): diff --git a/source/message.html b/source/message.html index 6af6aea2890..130b7af0f00 100644 --- a/source/message.html +++ b/source/message.html @@ -15,16 +15,11 @@ function windowOnLoad() { // #5875: string.prototype.split strips the tail when a limit is supplied, // so use a regexp instead. - var args = window.dialogArguments.match(/^(.*?);(.*?);([\s\S]*)$/); + var args = window.dialogArguments.match(/^(.*?);([\s\S]*)$/); // args[0] is the whole string. - if (args && args.length == 4){ - document.title= args[2]; - if ( args[1] == "true") { - messageID.innerHTML= args[3]; - } - else if ( args[1] == "false") { - textID.innerText= args[3]; - } + if (args && args.length == 3){ + document.title= args[1]; + messageID.innerHTML= args[2]; } } //--> @@ -32,6 +27,5 @@
-

 
 
diff --git a/source/nvda.pyw b/source/nvda.pyw
index f2e07dfdc38..eddf757e0ec 100755
--- a/source/nvda.pyw
+++ b/source/nvda.pyw
@@ -10,14 +10,29 @@
 import sys
 import os
 import globalVars
+import ctypes
 
-
+customVenvDetected = False
 if getattr(sys, "frozen", None):
 	# We are running as an executable.
 	# Append the path of the executable to sys so we can import modules from the dist dir.
 	sys.path.append(sys.prefix)
 	appDir = sys.prefix
 else:
+	# we are running from source
+	# Ensure we are inside the NVDA build system's Python virtual environment.
+	nvdaVenv = os.getenv("NVDA_VENV")
+	virtualEnv = os.getenv("VIRTUAL_ENV")
+	if not virtualEnv or not os.path.isdir(virtualEnv):
+		ctypes.windll.user32.MessageBoxW(
+			0,
+			"NVDA cannot  detect the Python virtual environment. "
+			"To run NVDA from source, please use runnvda.bat in the root of this repository.",
+			"Error",
+			0,
+		)
+		sys.exit(1)
+	customVenvDetected = nvdaVenv != virtualEnv
 	import sourceEnv
 	#We should always change directory to the location of this module (nvda.pyw), don't rely on sys.path[0]
 	appDir = os.path.normpath(os.path.dirname(__file__))
@@ -25,7 +40,7 @@ appDir = os.path.abspath(appDir)
 os.chdir(appDir)
 globalVars.appDir = appDir
 
-import ctypes
+
 import locale
 import gettext
 
@@ -183,8 +198,8 @@ if oldAppWindowHandle and not globalVars.appArgs.easeOfAccess:
 		sys.exit(0)
 	try:
 		terminateRunningNVDA(oldAppWindowHandle)
-	except:
-		sys.exit(1)
+	except Exception as e:
+		parser.error(f"Couldn't terminate existing NVDA process, abandoning start:\nException: {e}")
 if globalVars.appArgs.quit or (oldAppWindowHandle and globalVars.appArgs.easeOfAccess):
 	sys.exit(0)
 elif globalVars.appArgs.check_running:
@@ -232,11 +247,15 @@ if logHandler.log.getEffectiveLevel() is log.DEBUG:
 import buildVersion
 log.info("Starting NVDA version %s" % buildVersion.version)
 log.debug("Debug level logging enabled")
+if customVenvDetected:
+	log.warning("NVDA launched using a custom Python virtual environment.")
 if globalVars.appArgs.changeScreenReaderFlag:
 	winUser.setSystemScreenReaderFlag(True)
-#Accept wm_quit from other processes, even if running with higher privilages
-if not ctypes.windll.user32.ChangeWindowMessageFilter(winUser.WM_QUIT,1):
-	raise WinError()
+
+# Accept WM_QUIT from other processes, even if running with higher privileges
+if not ctypes.windll.user32.ChangeWindowMessageFilter(winUser.WM_QUIT, winUser.MSGFLT.ALLOW):
+	log.error("Unable to set the NVDA process to receive WM_QUIT messages from other processes")
+	raise winUser.WinError()
 # Make this the last application to be shut down and don't display a retry dialog box.
 winKernel.SetProcessShutdownParameters(0x100, winKernel.SHUTDOWN_NORETRY)
 if not isSecureDesktop and not config.isAppX:
diff --git a/source/nvwave.py b/source/nvwave.py
index b84bce674a7..cf6dc706d8d 100644
--- a/source/nvwave.py
+++ b/source/nvwave.py
@@ -1,8 +1,7 @@
-#nvwave.py
-#A part of NonVisual Desktop Access (NVDA)
-#Copyright (C) 2007-2017 NV Access Limited, Aleksey Sadovoy
-#This file is covered by the GNU General Public License.
-#See the file COPYING for more details.
+# A part of NonVisual Desktop Access (NVDA)
+# Copyright (C) 2007-2021 NV Access Limited, Aleksey Sadovoy, Cyrille Bougot, Peter Vágner
+# This file is covered by the GNU General Public License.
+# See the file COPYING for more details.
 
 """Provides a simple Python interface to playing audio using the Windows multimedia waveOut functions, as well as other useful utilities.
 """
@@ -31,11 +30,7 @@
 	UINT,
 	LPUINT
 )
-from ctypes import *
-from ctypes.wintypes import *
-import time
 import atexit
-import wx
 import garbageHandler
 import winKernel
 import wave
diff --git a/source/objidl.py b/source/objidl.py
index a5a5f127f79..884bb37d1d4 100644
--- a/source/objidl.py
+++ b/source/objidl.py
@@ -1,12 +1,14 @@
-#objidl.py
-#A part of NonVisual Desktop Access (NVDA)
-#This file is covered by the GNU General Public License.
-#See the file COPYING for more details.
+# A part of NonVisual Desktop Access (NVDA)
+# Copyright (C) 2010-2021 NV Access Limited, Leonard de Ruijter, Joseph Lee
+# This file may be used under the terms of the GNU General Public License, version 2 or later.
+# For more details see: https://www.gnu.org/licenses/gpl-2.0.html
 
-from ctypes import *
+from ctypes import c_int, c_longlong, c_ubyte, c_ulong, c_ulonglong, c_wchar_p, POINTER, Structure, windll
 from ctypes.wintypes import HWND, BOOL
 from comtypes import HRESULT, GUID, COMMETHOD, IUnknown, tagBIND_OPTS2
 from comtypes.persist import IPersist
+import winKernel
+
 WSTRING = c_wchar_p
 
 class IOleWindow(IUnknown):
@@ -30,20 +32,15 @@ class _ULARGE_INTEGER(Structure):
 		('QuadPart', c_ulonglong),
 	]
 
-class _FILETIME(Structure):
-	_fields_ = [
-		('dwLowDateTime', c_ulong),
-		('dwHighDateTime', c_ulong),
-	]
 
 class tagSTATSTG(Structure):
 	_fields_ = [
 		('pwcsName', WSTRING),
 		('type', c_ulong),
 		('cbSize', _ULARGE_INTEGER),
-		('mtime', _FILETIME),
-		('ctime', _FILETIME),
-		('atime', _FILETIME),
+		('mtime', winKernel.FILETIME),
+		('ctime', winKernel.FILETIME),
+		('atime', winKernel.FILETIME),
 		('grfMode', c_ulong),
 		('grfLocksSupported', c_ulong),
 		('clsid', GUID),
@@ -238,10 +235,14 @@ def __next__(self):
 		( ['in'], POINTER(IBindCtx), 'pbc' ),
 		( ['in'], POINTER(IMoniker), 'pmkToLeft' ),
 		( ['in'], POINTER(IMoniker), 'pmkNewlyRunning' )),
-	COMMETHOD([], HRESULT, 'GetTimeOfLastChange',
+	COMMETHOD(
+		[],
+		HRESULT,
+		'GetTimeOfLastChange',
 		( ['in'], POINTER(IBindCtx), 'pbc' ),
 		( ['in'], POINTER(IMoniker), 'pmkToLeft' ),
-		( ['out'], POINTER(_FILETIME), 'pfiletime' )),
+		(['out'], POINTER(winKernel.FILETIME), 'pfiletime')
+	),
 	COMMETHOD([], HRESULT, 'Inverse',
 		( ['out'], POINTER(POINTER(IMoniker)), 'ppmk' )),
 	COMMETHOD([], HRESULT, 'CommonPrefixWith',
@@ -277,12 +278,20 @@ def __next__(self):
 	COMMETHOD([], HRESULT, 'GetObject',
 		( ['in'], POINTER(IMoniker), 'pmkObjectName' ),
 		( ['out'], POINTER(POINTER(IUnknown)), 'ppunkObject' )),
-	COMMETHOD([], HRESULT, 'NoteChangeTime',
+	COMMETHOD(
+		[],
+		HRESULT,
+		'NoteChangeTime',
 		( ['in'], c_ulong, 'dwRegister' ),
-		( ['in'], POINTER(_FILETIME), 'pfiletime' )),
-	COMMETHOD([], HRESULT, 'GetTimeOfLastChange',
+		(['in'], POINTER(winKernel.FILETIME), 'pfiletime')
+	),
+	COMMETHOD(
+		[],
+		HRESULT,
+		'GetTimeOfLastChange',
 		( ['in'], POINTER(IMoniker), 'pmkObjectName' ),
-		( ['out'], POINTER(_FILETIME), 'pfiletime' )),
+		(['out'], POINTER(winKernel.FILETIME), 'pfiletime')
+	),
 	COMMETHOD([], HRESULT, 'EnumRunning',
 		( ['out'], POINTER(POINTER(IEnumMoniker)), 'ppenumMoniker' )),
 ]
diff --git a/source/pythonConsole.py b/source/pythonConsole.py
index 3612dfdd8b3..4dc8a7590ec 100755
--- a/source/pythonConsole.py
+++ b/source/pythonConsole.py
@@ -1,8 +1,8 @@
-#pythonConsole.py
-#A part of NonVisual Desktop Access (NVDA)
-#This file is covered by the GNU General Public License.
-#See the file COPYING for more details.
-#Copyright (C) 2008-2019 NV Access Limited, Leonard de Ruijter
+# pythonConsole.py
+# A part of NonVisual Desktop Access (NVDA)
+# This file is covered by the GNU General Public License.
+# See the file COPYING for more details.
+# Copyright (C) 2008-2020 NV Access Limited, Leonard de Ruijter, Julien Cochuyt
 
 import watchdog
 
@@ -12,6 +12,7 @@
 
 import builtins
 import os
+from typing import Sequence
 import code
 import codeop
 import sys
@@ -253,6 +254,7 @@ def __init__(self, parent):
 		# Even the most recent line has a position in the history, so initialise with one blank line.
 		self.inputHistory = [""]
 		self.inputHistoryPos = 0
+		self.outputPositions: Sequence[int] = [0]
 
 	def onActivate(self, evt):
 		if evt.GetActive():
@@ -268,6 +270,12 @@ def output(self, data):
 		if data and not data.isspace():
 			queueHandler.queueFunction(queueHandler.eventQueue, speech.speakText, data)
 
+	def clear(self):
+		"""Clear the output.
+		"""
+		self.outputCtrl.Clear()
+		self.outputPositions[:] = [0]
+
 	def echo(self, data):
 		self.outputCtrl.write(data)
 
@@ -292,6 +300,8 @@ def execute(self):
 			self.inputHistory.append("")
 		self.inputHistoryPos = len(self.inputHistory) - 1
 		self.inputCtrl.ChangeValue("")
+		if self.console.prompt != "...":
+			self.outputPositions.append(self.outputCtrl.GetInsertionPoint())
 
 	def historyMove(self, movement):
 		newIndex = self.inputHistoryPos + movement
@@ -306,9 +316,11 @@ def historyMove(self, movement):
 		return True
 
 	RE_COMPLETE_UNIT = re.compile(r"[\w.]*$")
+
 	def complete(self):
+		textBeforeCursor = self.inputCtrl.GetRange(0, self.inputCtrl.GetSelection()[0])
 		try:
-			original = self.RE_COMPLETE_UNIT.search(self.inputCtrl.GetValue()).group(0)
+			original = self.RE_COMPLETE_UNIT.search(textBeforeCursor).group(0)
 		except AttributeError:
 			return False
 
@@ -373,16 +385,20 @@ def _insertCompletion(self, original, completed):
 		insert = completed[len(original):]
 		if not insert:
 			return
-		self.inputCtrl.SetValue(self.inputCtrl.GetValue() + insert)
+		inputCtrl = self.inputCtrl
+		selStartPos, selEndPos = inputCtrl.GetSelection()
+		prefix = inputCtrl.GetRange(0, selStartPos)
+		suffix = inputCtrl.GetRange(selEndPos, inputCtrl.GetLastPosition())
+		inputCtrl.SetValue(prefix + insert + suffix)
 		queueHandler.queueFunction(queueHandler.eventQueue, speech.speakText, insert)
-		self.inputCtrl.SetInsertionPointEnd()
+		inputCtrl.SetInsertionPoint(selStartPos + len(insert))
 
 	def onInputChar(self, evt):
 		key = evt.GetKeyCode()
 
 		if key == wx.WXK_TAB:
-			line = self.inputCtrl.GetValue()
-			if line and not line.isspace():
+			textBeforeCursor = self.inputCtrl.GetRange(0, self.inputCtrl.GetSelection()[0])
+			if textBeforeCursor and not textBeforeCursor.isspace():
 				if not self.complete():
 					wx.Bell()
 				return
diff --git a/source/sayAllHandler.py b/source/sayAllHandler.py
index bf9784d9e81..ae49cfccc22 100644
--- a/source/sayAllHandler.py
+++ b/source/sayAllHandler.py
@@ -3,10 +3,10 @@
 # This file may be used under the terms of the GNU General Public License, version 2 or later.
 # For more details see: https://www.gnu.org/licenses/gpl-2.0.html
 
+from typing import Optional
 import weakref
 import garbageHandler
 import speech
-import synthDriverHandler
 from logHandler import log
 import config
 import controlTypes
@@ -15,6 +15,9 @@
 import queueHandler
 import winKernel
 
+from speech.commands import CallbackCommand, EndUtteranceCommand
+
+
 CURSOR_CARET = 0
 CURSOR_REVIEW = 1
 
@@ -23,6 +26,18 @@
 #: This is a weakref because the manager should be allowed to die once say all is complete.
 _activeSayAll = lambda: None # Return None when called like a dead weakref.
 
+
+def getSpeechWithoutPauses() -> "speech.SpeechWithoutPauses":
+	"""Returns an instance of `speech.SpeechWithoutPauses` which should be used for say all
+	creating it if necessary."""
+	if getSpeechWithoutPauses.speechWithoutPausesInstance is None:
+		getSpeechWithoutPauses.speechWithoutPausesInstance = speech.SpeechWithoutPauses(speakFunc=speech.speak)
+	return getSpeechWithoutPauses.speechWithoutPausesInstance
+
+
+getSpeechWithoutPauses.speechWithoutPausesInstance: Optional["speech.SpeechWithoutPauses"] = None
+
+
 def stop():
 	active = _activeSayAll()
 	if active:
@@ -69,8 +84,8 @@ def next(self):
 		if not obj:
 			return
 		# Call this method again when we start speaking this object.
-		callbackCommand = speech.CallbackCommand(self.next, name="say-all:next")
-		speech.speakObject(obj, reason=controlTypes.REASON_SAYALL, _prefixSpeechCommand=callbackCommand)
+		callbackCommand = CallbackCommand(self.next, name="say-all:next")
+		speech.speakObject(obj, reason=controlTypes.OutputReason.SAYALL, _prefixSpeechCommand=callbackCommand)
 
 	def stop(self):
 		self.walker = None
@@ -148,8 +163,8 @@ def nextLine(self):
 			# No more text.
 			if isinstance(self.reader.obj, textInfos.DocumentWithPageTurns):
 				# Once the last line finishes reading, try turning the page.
-				cb = speech.CallbackCommand(self.turnPage, name="say-all:turnPage")
-				speech.speakWithoutPauses([cb, speech.EndUtteranceCommand()])
+				cb = CallbackCommand(self.turnPage, name="say-all:turnPage")
+				getSpeechWithoutPauses().speakWithoutPauses([cb, EndUtteranceCommand()])
 			else:
 				self.finish()
 			return
@@ -163,7 +178,7 @@ def nextLine(self):
 		def _onLineReached(obj=self.reader.obj, state=state):
 			self.lineReached(obj, bookmark, state)
 
-		cb = speech.CallbackCommand(
+		cb = CallbackCommand(
 			_onLineReached,
 			name="say-all:lineReached"
 		)
@@ -175,13 +190,13 @@ def _onLineReached(obj=self.reader.obj, state=state):
 		speechGen = speech.getTextInfoSpeech(
 			self.reader,
 			unit=textInfos.UNIT_READINGCHUNK,
-			reason=controlTypes.REASON_SAYALL,
+			reason=controlTypes.OutputReason.SAYALL,
 			useCache=state
 		)
 		seq = list(speech._flattenNestedSequences(speechGen))
 		seq.insert(0, cb)
 		# Speak the speech sequence.
-		spoke = speech.speakWithoutPauses(seq)
+		spoke = getSpeechWithoutPauses().speakWithoutPauses(seq)
 		# Update the textInfo state ready for when speaking the next line.
 		self.speakTextInfoState = state.copy()
 
@@ -203,7 +218,7 @@ def _onLineReached(obj=self.reader.obj, state=state):
 			else:
 				# We don't want to buffer too much.
 				# Force speech. lineReached will resume things when speech catches up.
-				speech.speakWithoutPauses(None)
+				getSpeechWithoutPauses().speakWithoutPauses(None)
 				# The first buffered line has now started speaking.
 				self.numBufferedLines -= 1
 
@@ -239,11 +254,11 @@ def finish(self):
 		# Otherwise, if a different synth is being used for say all,
 		# we might switch synths too early and truncate the final speech.
 		# We do this by putting a CallbackCommand at the start of a new utterance.
-		cb = speech.CallbackCommand(self.stop, name="say-all:stop")
-		speech.speakWithoutPauses([
-			speech.EndUtteranceCommand(),
+		cb = CallbackCommand(self.stop, name="say-all:stop")
+		getSpeechWithoutPauses().speakWithoutPauses([
+			EndUtteranceCommand(),
 			cb,
-			speech.EndUtteranceCommand()
+			EndUtteranceCommand()
 		])
 
 	def stop(self):
diff --git a/source/screenExplorer.py b/source/screenExplorer.py
index 53e3b81319b..d62d15bfbcc 100644
--- a/source/screenExplorer.py
+++ b/source/screenExplorer.py
@@ -58,4 +58,4 @@ def moveTo(self,x,y,new=False,unit=textInfos.UNIT_LINE):
 				self._pos=pos
 				if not speechCanceled:
 					speech.cancelSpeech()
-				speech.speakTextInfo(pos,reason=controlTypes.REASON_CARET)
+				speech.speakTextInfo(pos, reason=controlTypes.OutputReason.CARET)
diff --git a/source/scriptHandler.py b/source/scriptHandler.py
index 6c3422f6fe1..cebbf17464b 100644
--- a/source/scriptHandler.py
+++ b/source/scriptHandler.py
@@ -1,9 +1,9 @@
-# scriptHandler.py
 # A part of NonVisual Desktop Access (NVDA)
 # Copyright (C) 2007-2020 NV Access Limited, Babbage B.V., Julien Cochuyt
 # This file is covered by the GNU General Public License.
 # See the file COPYING for more details.
 
+from typing import List, Optional
 import time
 import weakref
 import inspect
@@ -241,32 +241,27 @@ def isScriptWaiting():
 	return bool(_numScriptsQueued)
 
 def script(
-	description="",
-	category=None,
-	gesture=None,
-	gestures=None,
-	canPropagate=False,
-	bypassInputHelp=False,
-	resumeSayAllMode=None
+		description: str = "",
+		category: Optional[str] = None,
+		gesture: Optional[str] = None,
+		gestures: Optional[List[str]] = None,
+		canPropagate: bool = False,
+		bypassInputHelp: bool = False,
+		allowInSleepMode: bool = False,
+		resumeSayAllMode: Optional[int] = None
 ):
 	"""Define metadata for a script.
 	This function is to be used as a decorator to set metadata used by the scripting system and gesture editor.
 	It can only decorate methods which name start swith "script_"
 	@param description: A short translatable description of the script to be used in the gesture editor, etc.
-	@type description: string 
 	@param category: The category of the script displayed in the gesture editor.
-	@type category: string
 	@param gesture: A gesture associated with this script.
-	@type gesture: string
 	@param gestures: A list of gestures associated with this script
-	@type gestures: list(string)
 	@param canPropagate: Whether this script should also apply when it belongs to a  focus ancestor object.
-	@type canPropagate: bool
 	@param bypassInputHelp: Whether this script should run when input help is active.
-	@type bypassInputHelp: bool
+	@param allowInSleepMode: Whether this script should run when NVDA is in sleep mode.
 	@param resumeSayAllMode: The say all mode that should be resumed when active before executing this script.
-		One of the C{sayAllHandler.CURSOR_*} constants.
-	@type resumeSayAllMode: int
+	One of the C{sayAllHandler.CURSOR_*} constants.
 	"""
 	if gestures is None:
 		gestures = []
@@ -295,5 +290,6 @@ def script_decorator(decoratedScript):
 		decoratedScript.bypassInputHelp = bypassInputHelp
 		if resumeSayAllMode is not None:
 			decoratedScript.resumeSayAllMode = resumeSayAllMode
+		decoratedScript.allowInSleepMode = allowInSleepMode
 		return decoratedScript
 	return script_decorator
diff --git a/source/setup.py b/source/setup.py
index ba6b0639e8c..387294c15e8 100755
--- a/source/setup.py
+++ b/source/setup.py
@@ -6,6 +6,7 @@
 #See the file COPYING for more details.
 
 import os
+import sys
 import copy
 import gettext
 gettext.install("nvda")
@@ -13,12 +14,21 @@
 import py2exe as py2exeModule
 from glob import glob
 import fnmatch
+# versionInfo names must be imported after Gettext
+# Suppress E402 (module level import not at top of file)
+from versionInfo import (
+	formatBuildVersionString,
+	name,
+	version,
+	publisher
+)  # noqa: E402
 from versionInfo import *
 from py2exe import distutils_buildexe
 from py2exe.dllfinder import DllFinder
 import wx
 import importlib.machinery
-
+# Explicitly put the nvda_dmp dir on the build path so the DMP library is included
+sys.path.append(os.path.join("..", "include", "nvda_dmp"))
 RT_MANIFEST = 24
 manifest_template = """\
 
@@ -187,14 +197,29 @@ def getRecursiveDataFiles(dest,source,excludes=()):
 			"company_name": publisher,
 		},
 	],
+	console=[
+		{
+			"script": os.path.join("..", "include", "nvda_dmp", "nvda_dmp.py"),
+			"uiAccess": False,
+			"icon_resources": [(1, "images/nvda.ico")],
+			"other_resources": [],  # Populated at runtime
+			"version":formatBuildVersionString(),
+			"description": "NVDA Diff-match-patch proxy",
+			"product_name": name,
+			"product_version": version,
+			"copyright": f"{copyright}, Bill Dengler",
+			"company_name": f"Bill Dengler, {publisher}",
+		},
+	],
 	options = {"py2exe": {
 		"bundle_files": 3,
-		"excludes": ["tkinter",
-			"serial.loopback_connection", 
-			"serial.rfc2217", 
-			"serial.serialcli", 
-			"serial.serialjava", 
-			"serial.serialposix", 
+		"excludes": [
+			"tkinter",
+			"serial.loopback_connection",
+			"serial.rfc2217",
+			"serial.serialcli",
+			"serial.serialjava",
+			"serial.serialposix",
 			"serial.socket_connection",
 			# netbios (from pywin32) is optionally used by Python3's uuid module.
 			# This is not needed.
@@ -204,6 +229,8 @@ def getRecursiveDataFiles(dest,source,excludes=()):
 			# winxptheme is optionally used by wx.lib.agw.aui.
 			# We don't need this.
 			"winxptheme",
+			# numpy is an optional dependency of comtypes but we don't require it.
+			"numpy",
 		],
 		"packages": [
 			"NVDAObjects",
@@ -218,6 +245,8 @@ def getRecursiveDataFiles(dest,source,excludes=()):
 			"nvdaBuiltin",
 			# #3368: bisect was implicitly included with Python 2.7.3, but isn't with 2.7.5.
 			"bisect",
+			# robotremoteserver (for system tests) depends on xmlrpc.server
+			"xmlrpc.server",
 		],
 	}},
 	data_files=[
diff --git a/source/sourceEnv.py b/source/sourceEnv.py
index dfd7a6b9297..decfc2e5d1d 100644
--- a/source/sourceEnv.py
+++ b/source/sourceEnv.py
@@ -11,13 +11,9 @@
 
 # Get the path to the top of the repo; i.e. where include and miscDeps are.
 TOP_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
+
 # Directories containing Python modules included in git submodules.
 PYTHON_DIRS = (
-	os.path.join(TOP_DIR, "include", "pyserial"),
-	os.path.join(TOP_DIR, "include", "comtypes"),
-	os.path.join(TOP_DIR, "include", "configobj", "src"),
-	os.path.join(TOP_DIR, "include", "wxPython"),
-	os.path.join(TOP_DIR, "include", "py2exe"),
 	os.path.join(TOP_DIR, "miscDeps", "python"),
 )
 
diff --git a/source/speech/__init__.py b/source/speech/__init__.py
index b8348cad53c..d7a9b72943c 100755
--- a/source/speech/__init__.py
+++ b/source/speech/__init__.py
@@ -34,25 +34,7 @@
 	EndUtteranceCommand,
 	CharacterModeCommand,
 )
-from .commands import (  # noqa: F401
-	# F401 imported but unused:
-	# The following are imported here because other files that speech.py
-	# previously relied on "import * from .commands"
-	# New commands added to commands.py should be directly imported only where needed.
-	# Usage of these imports is deprecated and will be removed in 2021.1
-	SynthCommand,
-	IndexCommand,
-	SynthParamCommand,
-	BreakCommand,
-	BaseProsodyCommand,
-	VolumeCommand,
-	RateCommand,
-	PhonemeCommand,
-	BaseCallbackCommand,
-	CallbackCommand,
-	WaveFileCommand,
-	ConfigProfileTriggerCommand,
-)
+
 from . import types
 from .types import (
 	SpeechSequence,
@@ -69,7 +51,6 @@
 	Generator,
 	Union,
 	Callable,
-	Iterator,
 	Tuple,
 )
 from logHandler import log
@@ -136,7 +117,7 @@ def cancelSpeech():
 	# Import only for this function to avoid circular import.
 	import sayAllHandler
 	sayAllHandler.stop()
-	_speakWithoutPauses.reset()
+	sayAllHandler.getSpeechWithoutPauses().reset()
 	if beenCanceled:
 		return
 	elif speechMode==speechMode_off:
@@ -230,14 +211,70 @@ def speakSpelling(
 	speak(seq, priority=priority)
 
 
-# C901 'getSpellingSpeech' is too complex
-# Note: when working on getSpellingSpeech, look for opportunities to simplify
-# and move logic out into smaller helper functions.
-def getSpellingSpeech(  # noqa: C901
+def _getSpellingSpeechAddCharMode(
+		seq: Generator[SequenceItemT, None, None],
+) -> Generator[SequenceItemT, None, None]:
+	"""Inserts CharacterMode commands in a speech sequence generator to ensure any single character
+	is spelt by the synthesizer.
+	@param seq: The speech sequence to be spelt.
+	"""
+	charMode = False
+	for item in seq:
+		if isinstance(item, str):
+			if len(item) == 1:
+				if not charMode:
+					yield CharacterModeCommand(True)
+					charMode = True
+			elif charMode:
+				yield CharacterModeCommand(False)
+				charMode = False
+		yield item
+
+
+def _getSpellingCharAddCapNotification(
+		speakCharAs: str,
+		sayCapForCapitals: bool,
+		capPitchChange: int,
+		beepForCapitals: bool,
+) -> Generator[SequenceItemT, None, None]:
+	"""This function produces a speech sequence containing a character to be spelt as well as commands
+	to indicate that this character is uppercase if applicable.
+	@param speakCharAs: The character as it will be spoken by the synthesizer.
+	@param sayCapForCapitals: indicates if 'cap' should be reported along with the currently spelt character.
+	@param capPitchChange: pitch offset to apply while spelling the currently spelt character.
+	@param beepForCapitals: indicates if a cap notification beep should be produced while spelling the currently
+	spellt character.
+	"""
+	if sayCapForCapitals:
+		# Translators: cap will be spoken before the given letter when it is capitalized.
+		capMsg = _("cap %s")
+		(capMsgBefore, capMsgAfter) = capMsg.split('%s')
+	else:
+		capMsgBefore = ''
+		capMsgAfter = ''
+	
+	if capPitchChange:
+		yield PitchCommand(offset=capPitchChange)
+	if beepForCapitals:
+		yield BeepCommand(2000, 50)
+	if capMsgBefore:
+		yield capMsgBefore
+	yield speakCharAs
+	if capMsgAfter:
+		yield capMsgAfter
+	if capPitchChange:
+		yield PitchCommand()
+
+
+def _getSpellingSpeechWithoutCharMode(
 		text: str,
-		locale: Optional[str] = None,
-		useCharacterDescriptions: bool = False
+		locale: str,
+		useCharacterDescriptions: bool,
+		sayCapForCapitals: bool,
+		capPitchChange: int,
+		beepForCapitals: bool,
 ) -> Generator[SequenceItemT, None, None]:
+	
 	defaultLanguage=getCurrentLanguage()
 	if not locale or (not config.conf['speech']['autoDialectSwitching'] and locale.split('_')[0]==defaultLanguage.split('_')[0]):
 		locale=defaultLanguage
@@ -249,9 +286,6 @@ def getSpellingSpeech(  # noqa: C901
 	if not text.isspace():
 		text=text.rstrip()
 
-	synth = getSynth()
-	synthConfig=config.conf["speech"][synth.name]
-	charMode = False
 	textLength=len(text)
 	count = 0
 	localeHasConjuncts = True if locale.split('_',1)[0] in LANGS_WITH_CONJUNCT_CHARS else False
@@ -272,33 +306,42 @@ def getSpellingSpeech(  # noqa: C901
 			speakCharAs=charDesc[0] if textLength>1 else IDEOGRAPHIC_COMMA.join(charDesc)
 		else:
 			speakCharAs=characterProcessing.processSpeechSymbol(locale,speakCharAs)
-		if uppercase and synthConfig["sayCapForCapitals"]:
-			# Translators: cap will be spoken before the given letter when it is capitalized.
-			speakCharAs=_("cap %s")%speakCharAs
-		if uppercase and synth.isSupported("pitch") and synthConfig["capPitchChange"]:
-			yield PitchCommand(offset=synthConfig["capPitchChange"])
 		if config.conf['speech']['autoLanguageSwitching']:
 			yield LangChangeCommand(locale)
-		if len(speakCharAs) == 1 and synthConfig["useSpellingFunctionality"]:
-			if not charMode:
-				yield CharacterModeCommand(True)
-				charMode = True
-		elif charMode:
-			yield CharacterModeCommand(False)
-			charMode = False
-		if uppercase and  synthConfig["beepForCapitals"]:
-			yield BeepCommand(2000, 50)
-		yield speakCharAs
-		if uppercase and synth.isSupported("pitch") and synthConfig["capPitchChange"]:
-			yield PitchCommand()
+		yield from _getSpellingCharAddCapNotification(
+			speakCharAs,
+			uppercase and sayCapForCapitals,
+			capPitchChange if uppercase else 0,
+			uppercase and beepForCapitals,
+		)
 		yield EndUtteranceCommand()
 
 
-# 'getSpeechForSpelling' should be considered deprecated, use getSpellingSpeech instead.
-# The name 'getSpeechForSpelling' was introduced during the 2019.3 release.
-# The decision was made to rename it to be consistent with the other functions (get*Speech) which create
-# speech sequences.
-getSpeechForSpelling = getSpellingSpeech
+def getSpellingSpeech(
+		text: str,
+		locale: Optional[str] = None,
+		useCharacterDescriptions: bool = False
+) -> Generator[SequenceItemT, None, None]:
+	
+	synth = getSynth()
+	synthConfig = config.conf["speech"][synth.name]
+	
+	if synth.isSupported("pitch"):
+		capPitchChange = synthConfig["capPitchChange"]
+	else:
+		capPitchChange = 0
+	seq = _getSpellingSpeechWithoutCharMode(
+		text,
+		locale,
+		useCharacterDescriptions,
+		sayCapForCapitals=synthConfig["sayCapForCapitals"],
+		capPitchChange=capPitchChange,
+		beepForCapitals=synthConfig["beepForCapitals"],
+	)
+	if synthConfig["useSpellingFunctionality"]:
+		seq = _getSpellingSpeechAddCharMode(seq)
+	yield from seq
+
 
 def getCharDescListFromText(text,locale):
 	"""This method prepares a list, which contains character and its description for all characters the text is made up of, by checking the presence of character descriptions in characterDescriptions.dic of that locale for all possible combination of consecutive characters in the text.
@@ -326,7 +369,7 @@ def getCharDescListFromText(text,locale):
 
 def speakObjectProperties(  # noqa: C901
 		obj,
-		reason: OutputReason = controlTypes.REASON_QUERY,
+		reason: OutputReason = OutputReason.QUERY,
 		_prefixSpeechCommand: Optional[SpeechCommand] = None,
 		priority: Optional[Spri] = None,
 		**allowedProperties
@@ -346,7 +389,7 @@ def speakObjectProperties(  # noqa: C901
 # and move logic out into smaller helper functions.
 def getObjectPropertiesSpeech(  # noqa: C901
 		obj,
-		reason: OutputReason = controlTypes.REASON_QUERY,
+		reason: OutputReason = OutputReason.QUERY,
 		_prefixSpeechCommand: Optional[SpeechCommand] = None,
 		**allowedProperties
 ) -> SpeechSequence:
@@ -362,7 +405,7 @@ def getObjectPropertiesSpeech(  # noqa: C901
 				positionInfo=obj.positionInfo
 		elif value and name == "current":
 			# getPropertiesSpeech names this "current", but the NVDAObject property is
-			# named "isCurrent".
+			# named "isCurrent", it's type should always be controltypes.IsCurrent
 			newPropertyValues['current'] = obj.isCurrent
 		elif value:
 			# Certain properties such as row and column numbers have presentational versions, which should be used for speech if they are available.
@@ -395,10 +438,10 @@ def getObjectPropertiesSpeech(  # noqa: C901
 	cachedPropertyValues.update(newPropertyValues)
 	obj._speakObjectPropertiesCache=cachedPropertyValues
 	#If we should only cache we can stop here
-	if reason==controlTypes.REASON_ONLYCACHE:
+	if reason == OutputReason.ONLYCACHE:
 		return []
 	#If only speaking change, then filter out all values that havn't changed
-	if reason==controlTypes.REASON_CHANGE:
+	if reason == OutputReason.CHANGE:
 		for name in set(newPropertyValues)&set(oldCachedPropertyValues):
 			if newPropertyValues[name]==oldCachedPropertyValues[name]:
 				del newPropertyValues[name]
@@ -423,7 +466,7 @@ def getObjectPropertiesSpeech(  # noqa: C901
 	# This is because that one item will be the focused object, and saying selected is redundant.
 	# Rather, 'unselected' will be spoken for an unselected object if 1 or more items are selected. 
 	states=newPropertyValues.get('states')
-	if states is not None and reason==controlTypes.REASON_FOCUS:
+	if states is not None and reason == OutputReason.FOCUS:
 		if (
 			controlTypes.STATE_SELECTABLE in states 
 			and controlTypes.STATE_FOCUSABLE in states
@@ -468,7 +511,7 @@ def _getPlaceholderSpeechIfTextEmpty(
 
 def speakObject(
 		obj,
-		reason: OutputReason = controlTypes.REASON_QUERY,
+		reason: OutputReason = OutputReason.QUERY,
 		_prefixSpeechCommand: Optional[SpeechCommand] = None,
 		priority: Optional[Spri] = None
 ):
@@ -488,27 +531,31 @@ def speakObject(
 # and move logic out into smaller helper functions.
 def getObjectSpeech(  # noqa: C901
 		obj,
-		reason: OutputReason = controlTypes.REASON_QUERY,
+		reason: OutputReason = OutputReason.QUERY,
 		_prefixSpeechCommand: Optional[SpeechCommand] = None,
 ):
-	from NVDAObjects import NVDAObjectTextInfo
 	role=obj.role
 	# Choose when we should report the content of this object's textInfo, rather than just the object's value
 	import browseMode
 	shouldReportTextContent=not (
 		# focusEntered should never present text content
-		(reason==controlTypes.REASON_FOCUSENTERED) or
-		# The rootNVDAObject of a browseMode document in browse mode (not passThrough) should never present text content
-		(isinstance(obj.treeInterceptor,browseMode.BrowseModeDocumentTreeInterceptor) and not obj.treeInterceptor.passThrough and obj==obj.treeInterceptor.rootNVDAObject) or
+		(reason == OutputReason.FOCUSENTERED)
+		# The rootNVDAObject of a browseMode document in browse mode (not passThrough)
+		# should never present text content
+		or (
+			isinstance(obj.treeInterceptor, browseMode.BrowseModeDocumentTreeInterceptor)
+			and not obj.treeInterceptor.passThrough
+			and obj == obj.treeInterceptor.rootNVDAObject
+		)
 		# objects that do not report as having navigableText should not report their text content either
-		not obj._hasNavigableText
+		or not obj._hasNavigableText
 	)
 
 	allowProperties = _objectSpeech_calculateAllowedProps(reason, shouldReportTextContent)
 
-	if reason == controlTypes.REASON_FOCUSENTERED:
+	if reason == OutputReason.FOCUSENTERED:
 		# Aside from excluding some properties, focus entered should be spoken like focus.
-		reason = controlTypes.REASON_FOCUS
+		reason = OutputReason.FOCUS
 
 	sequence = getObjectPropertiesSpeech(
 		obj,
@@ -516,36 +563,28 @@ def getObjectSpeech(  # noqa: C901
 		_prefixSpeechCommand=_prefixSpeechCommand,
 		**allowProperties
 	)
-	if reason == controlTypes.REASON_ONLYCACHE:
+	if reason == OutputReason.ONLYCACHE:
 		return sequence
 	if shouldReportTextContent:
 		try:
 			info = obj.makeTextInfo(textInfos.POSITION_SELECTION)
-			if not info.isCollapsed:
-				# if there is selected text, then there is a value and we do not report placeholder
-				sequence.extend(getPreselectedTextSpeech(info.text))
-			else:
-				info.expand(textInfos.UNIT_LINE)
-				textEmpty, placeholderSeq = _getPlaceholderSpeechIfTextEmpty(obj, reason)
-				sequence.extend(placeholderSeq)
-				speechGen = getTextInfoSpeech(
-					info,
-					unit=textInfos.UNIT_LINE,
-					reason=controlTypes.REASON_CARET
-				)
-				sequence.extend(_flattenNestedSequences(speechGen))
-		except:  # noqa E722 legacy bare except. Unknown what exceptions may be raised.
-			newInfo = obj.makeTextInfo(textInfos.POSITION_ALL)
+		except (NotImplementedError, RuntimeError):
+			info = None
+		if info and not info.isCollapsed:
+			# if there is selected text, then there is a value and we do not report placeholder
+			sequence.extend(getPreselectedTextSpeech(info.text))
+		else:
+			if not info:
+				info = obj.makeTextInfo(textInfos.POSITION_FIRST)
+			info.expand(textInfos.UNIT_LINE)
 			textEmpty, placeholderSeq = _getPlaceholderSpeechIfTextEmpty(obj, reason)
-			if textEmpty:
-				sequence.extend(placeholderSeq)
-			else:
-				speechGen = getTextInfoSpeech(
-					newInfo,
-					unit=textInfos.UNIT_PARAGRAPH,
-					reason=controlTypes.REASON_CARET,
-				)
-				sequence.extend(_flattenNestedSequences(speechGen))
+			sequence.extend(placeholderSeq)
+			speechGen = getTextInfoSpeech(
+				info,
+				unit=textInfos.UNIT_LINE,
+				reason=OutputReason.CARET,
+			)
+			sequence.extend(_flattenNestedSequences(speechGen))
 	elif role == controlTypes.ROLE_MATH:
 		import mathPres
 		mathPres.ensureInit()
@@ -583,7 +622,7 @@ def _objectSpeech_calculateAllowedProps(reason, shouldReportTextContent):
 		"columnSpan": True,
 		"current": True
 	}
-	if reason == controlTypes.REASON_FOCUSENTERED:
+	if reason == OutputReason.FOCUSENTERED:
 		allowProperties["value"] = False
 		allowProperties["keyboardShortcut"] = False
 		allowProperties["positionInfo_level"] = False
@@ -595,7 +634,7 @@ def _objectSpeech_calculateAllowedProps(reason, shouldReportTextContent):
 		allowProperties["positionInfo_level"] = False
 		allowProperties["positionInfo_indexInGroup"] = False
 		allowProperties["positionInfo_similarItemsInGroup"] = False
-	if reason != controlTypes.REASON_QUERY:
+	if reason != OutputReason.QUERY:
 		allowProperties["rowCount"] = False
 		allowProperties["columnCount"] = False
 	formatConf = config.conf["documentFormatting"]
@@ -625,7 +664,7 @@ def _objectSpeech_calculateAllowedProps(reason, shouldReportTextContent):
 
 def speakText(
 		text: str,
-		reason: OutputReason = controlTypes.REASON_MESSAGE,
+		reason: OutputReason = OutputReason.MESSAGE,
 		symbolLevel: Optional[int] = None,
 		priority: Optional[Spri] = None
 ):
@@ -1058,7 +1097,7 @@ def speakTextInfo(
 		useCache: Union[bool, SpeakTextInfoState] = True,
 		formatConfig: Dict[str, bool] = None,
 		unit: Optional[str] = None,
-		reason: OutputReason = controlTypes.REASON_QUERY,
+		reason: OutputReason = OutputReason.QUERY,
 		_prefixSpeechCommand: Optional[SpeechCommand] = None,
 		onlyInitialFields: bool = False,
 		suppressBlanks: bool = False,
@@ -1075,14 +1114,6 @@ def speakTextInfo(
 		suppressBlanks
 	)
 
-	if reason == controlTypes.REASON_SAYALL:
-		log.error(
-			"Deprecation warning: In 2021.1 speakTextInfo will no longer  send speech through "
-			"speakWithoutPauses if reason is sayAll, as sayAllhandler does this manually now."
-		)
-		flatSpeechGen = list(_flattenNestedSequences(speechGen))
-		return _speakWithoutPauses.speakWithoutPauses(flatSpeechGen)
-
 	speechGen = GeneratorWithReturn(speechGen)
 	for seq in speechGen:
 		speak(seq, priority=priority)
@@ -1097,12 +1128,12 @@ def getTextInfoSpeech(  # noqa: C901
 		useCache: Union[bool, SpeakTextInfoState] = True,
 		formatConfig: Dict[str, bool] = None,
 		unit: Optional[str] = None,
-		reason: OutputReason = controlTypes.REASON_QUERY,
+		reason: OutputReason = OutputReason.QUERY,
 		_prefixSpeechCommand: Optional[SpeechCommand] = None,
 		onlyInitialFields: bool = False,
 		suppressBlanks: bool = False
 ) -> Generator[SpeechSequence, None, bool]:
-	onlyCache=reason==controlTypes.REASON_ONLYCACHE
+	onlyCache = reason == OutputReason.ONLYCACHE
 	if isinstance(useCache,SpeakTextInfoState):
 		speakTextInfoState=useCache
 	elif useCache:
@@ -1118,7 +1149,7 @@ def getTextInfoSpeech(  # noqa: C901
 		formatConfig['extraDetail']=True
 	reportIndentation=unit==textInfos.UNIT_LINE and ( formatConfig["reportLineIndentation"] or formatConfig["reportLineIndentationWithTones"])
 	# For performance reasons, when navigating by paragraph or table cell, spelling errors will not be announced.
-	if unit in (textInfos.UNIT_PARAGRAPH,textInfos.UNIT_CELL) and reason is controlTypes.REASON_CARET:
+	if unit in (textInfos.UNIT_PARAGRAPH, textInfos.UNIT_CELL) and reason == OutputReason.CARET:
 		formatConfig['reportSpellingErrors']=False
 
 	#Fetch the last controlFieldStack, or make a blank one
@@ -1182,7 +1213,7 @@ def getTextInfoSpeech(  # noqa: C901
 	speechSequence: SpeechSequence = []
 	# #2591: Only if the reason is not focus, Speak the exit of any controlFields not in the new stack.
 	# We don't do this for focus because hearing "out of list", etc. isn't useful when tabbing or using quick navigation and makes navigation less efficient.
-	if reason!=controlTypes.REASON_FOCUS:
+	if reason != OutputReason.FOCUS:
 		endingBlock=False
 		for count in reversed(range(commonFieldCount,len(controlFieldStackCache))):
 			fieldSequence = info.getControlFieldSpeech(
@@ -1195,7 +1226,7 @@ def getTextInfoSpeech(  # noqa: C901
 			)
 			if fieldSequence:
 				speechSequence.extend(fieldSequence)
-			if not endingBlock and reason==controlTypes.REASON_SAYALL:
+			if not endingBlock and reason == OutputReason.SAYALL:
 				endingBlock=bool(int(controlFieldStackCache[count].get('isBlock',0)))
 		if endingBlock:
 			speechSequence.append(EndUtteranceCommand())
@@ -1440,7 +1471,7 @@ def isControlEndFieldCommand(x):
 
 	# If there is nothing that should cause the TextInfo to be considered
 	# non-blank, blank should be reported, unless we are doing a say all.
-	if not suppressBlanks and reason != controlTypes.REASON_SAYALL and shouldConsiderTextInfoBlank:
+	if not suppressBlanks and reason != OutputReason.SAYALL and shouldConsiderTextInfoBlank:
 		# Translators: This is spoken when the line is considered blank.
 		speechSequence.append(_("blank"))
 
@@ -1451,7 +1482,7 @@ def isControlEndFieldCommand(x):
 		if not isinstance(useCache,SpeakTextInfoState):
 			speakTextInfoState.updateObj()
 
-	if reason == controlTypes.REASON_ONLYCACHE or not speechSequence:
+	if reason == OutputReason.ONLYCACHE or not speechSequence:
 		return False
 
 	yield speechSequence
@@ -1462,7 +1493,7 @@ def isControlEndFieldCommand(x):
 # Note: when working on getPropertiesSpeech, look for opportunities to simplify
 # and move logic out into smaller helper functions.
 def getPropertiesSpeech(  # noqa: C901
-		reason: OutputReason = controlTypes.REASON_QUERY,
+		reason: OutputReason = OutputReason.QUERY,
 		**propertyValues
 ) -> SpeechSequence:
 	global oldTreeLevel, oldTableID, oldRowNumber, oldRowSpan, oldColumnNumber, oldColumnSpan
@@ -1493,9 +1524,9 @@ def getPropertiesSpeech(  # noqa: C901
 		and (
 			roleText
 			or reason not in (
-				controlTypes.REASON_SAYALL,
-				controlTypes.REASON_CARET,
-				controlTypes.REASON_FOCUS
+				OutputReason.SAYALL,
+				OutputReason.CARET,
+				OutputReason.FOCUS
 			)
 			or not (
 				name
@@ -1509,8 +1540,8 @@ def getPropertiesSpeech(  # noqa: C901
 		and (
 			role != controlTypes.ROLE_MATH
 			or reason not in (
-				controlTypes.REASON_CARET,
-				controlTypes.REASON_SAYALL
+				OutputReason.CARET,
+				OutputReason.SAYALL
 			)
 	)):
 		textList.append(roleText if roleText else controlTypes.roleLabels[role])
@@ -1606,15 +1637,12 @@ def getPropertiesSpeech(  # noqa: C901
 	if rowCount or columnCount:
 		# The caller is entering a table, so ensure that it is treated as a new table, even if the previous table was the same.
 		oldTableID = None
-	ariaCurrent = propertyValues.get('current', False)
-	if ariaCurrent:
-		try:
-			ariaCurrentLabel = controlTypes.isCurrentLabels[ariaCurrent]
-			textList.append(ariaCurrentLabel)
-		except KeyError:
-			log.debugWarning("Aria-current value not handled: %s"%ariaCurrent)
-			ariaCurrentLabel = controlTypes.isCurrentLabels[True]
-			textList.append(ariaCurrentLabel)
+
+	# speak isCurrent property EG aria-current
+	isCurrent = propertyValues.get('current', controlTypes.IsCurrent.NO)
+	if isCurrent != controlTypes.IsCurrent.NO:
+		textList.append(isCurrent.displayString)
+
 	placeholder: Optional[str] = propertyValues.get('placeholder', None)
 	if placeholder:
 		textList.append(placeholder)
@@ -1669,7 +1697,7 @@ def getControlFieldSpeech(  # noqa: C901
 	childControlCount=int(attrs.get('_childcontrolcount',"0"))
 	role = attrs.get('role', controlTypes.ROLE_UNKNOWN)
 	if (
-		reason == controlTypes.REASON_FOCUS
+		reason == OutputReason.FOCUS
 		or attrs.get('alwaysReportName', False)
 	):
 		name = attrs.get('name', "")
@@ -1677,10 +1705,10 @@ def getControlFieldSpeech(  # noqa: C901
 		name = ""
 	states=attrs.get('states',set())
 	keyboardShortcut=attrs.get('keyboardShortcut', "")
-	ariaCurrent=attrs.get('current', None)
+	isCurrent = attrs.get('current', controlTypes.IsCurrent.NO)
 	placeholderValue=attrs.get('placeholder', None)
 	value=attrs.get('value',"")
-	if reason==controlTypes.REASON_FOCUS or attrs.get('alwaysReportDescription',False):
+	if reason == OutputReason.FOCUS or attrs.get('alwaysReportDescription', False):
 		description=attrs.get('description',"")
 	else:
 		description=""
@@ -1707,7 +1735,7 @@ def getControlFieldSpeech(  # noqa: C901
 		keyboardShortcutSequence = getPropertiesSpeech(
 			reason=reason, keyboardShortcut=keyboardShortcut
 		)
-	ariaCurrentSequence = getPropertiesSpeech(reason=reason, current=ariaCurrent)
+	isCurrentSequence = getPropertiesSpeech(reason=reason, current=isCurrent)
 	placeholderSequence = getPropertiesSpeech(reason=reason, placeholder=placeholderValue)
 	nameSequence = getPropertiesSpeech(reason=reason, name=name)
 	valueSequence = getPropertiesSpeech(reason=reason, value=value)
@@ -1741,7 +1769,7 @@ def getControlFieldSpeech(  # noqa: C901
 	# Determine the order of speech.
 	# speakContentFirst: Speak the content before the control field info.
 	speakContentFirst = (
-		reason == controlTypes.REASON_FOCUS
+		reason == OutputReason.FOCUS
 		and presCat != attrs.PRESCAT_CONTAINER
 		and role not in (
 			controlTypes.ROLE_EDITABLETEXT,
@@ -1791,7 +1819,7 @@ def getControlFieldSpeech(  # noqa: C901
 		return tableSeq
 	elif (
 		nameSequence
-		and reason == controlTypes.REASON_FOCUS
+		and reason == OutputReason.FOCUS
 		and fieldType == "start_addedToControlFieldStack"
 		and role in (controlTypes.ROLE_GROUPING, controlTypes.ROLE_PROPERTYPAGE)
 	):
@@ -1824,7 +1852,7 @@ def getControlFieldSpeech(  # noqa: C901
 			getProps['columnHeaderText'] = attrs.get("table-columnheadertext")
 		tableCellSequence = getPropertiesSpeech(_tableID=tableID, **getProps)
 		tableCellSequence.extend(stateTextSequence)
-		tableCellSequence.extend(ariaCurrentSequence)
+		tableCellSequence.extend(isCurrentSequence)
 		types.logBadSequenceTypes(tableCellSequence)
 		return tableCellSequence
 
@@ -1873,7 +1901,7 @@ def getControlFieldSpeech(  # noqa: C901
 		out.extend(stateTextSequence if speakStatesFirst else roleTextSequence)
 		out.extend(roleTextSequence if speakStatesFirst else stateTextSequence)
 		out.append(containerContainsText)
-		out.extend(ariaCurrentSequence)
+		out.extend(isCurrentSequence)
 		out.extend(valueSequence)
 		out.extend(descriptionSequence)
 		out.extend(levelSequence)
@@ -1908,8 +1936,8 @@ def getControlFieldSpeech(  # noqa: C901
 	# Special cases
 	elif not speakEntry and fieldType in ("start_addedToControlFieldStack","start_relative"):
 		out = []
-		if ariaCurrent:
-			out.extend(ariaCurrentSequence)
+		if isCurrent != controlTypes.IsCurrent.NO:
+			out.extend(isCurrentSequence)
 		# Speak expanded / collapsed / level for treeview items (in ARIA treegrids)
 		if role == controlTypes.ROLE_TREEVIEWITEM:
 			if controlTypes.STATE_EXPANDED in states:
@@ -2016,7 +2044,14 @@ def getFormatFieldSpeech(  # noqa: C901
 		oldHeadingLevel=attrsCache.get("heading-level") if attrsCache is not None else None
 		# headings should be spoken not only if they change, but also when beginning to speak lines or paragraphs
 		# Ensuring a similar experience to if a heading was a controlField
-		if headingLevel and (initialFormat and (reason==controlTypes.REASON_FOCUS or unit in (textInfos.UNIT_LINE,textInfos.UNIT_PARAGRAPH)) or headingLevel!=oldHeadingLevel):
+		if(
+			headingLevel
+			and (
+				initialFormat
+				and (reason == OutputReason.FOCUS or unit in (textInfos.UNIT_LINE, textInfos.UNIT_PARAGRAPH))
+				or headingLevel != oldHeadingLevel
+			)
+		):
 			# Translators: Speaks the heading level (example output: heading level 2).
 			text=_("heading level %d")%headingLevel
 			textList.append(text)
@@ -2568,13 +2603,6 @@ def _getSpeech(
 		return finalSpeechSequence
 
 
-_speakWithoutPauses = SpeechWithoutPauses(speakFunc=speak)
-
-#: Alias for class SpeakWithoutPauses.speakWithoutPauses. Kept for backwards compatibility
-speakWithoutPauses = _speakWithoutPauses.speakWithoutPauses
-#: Kept for backwards compatibility.
-re_last_pause = _speakWithoutPauses.re_last_pause
-
 #: The singleton _SpeechManager instance used for speech functions.
 #: @type: L{manager.SpeechManager}
 _manager = manager.SpeechManager()
diff --git a/source/speech/manager.py b/source/speech/manager.py
index b74da7d081f..6e4fcd7c519 100644
--- a/source/speech/manager.py
+++ b/source/speech/manager.py
@@ -18,25 +18,7 @@
 	IndexCommand,
 	_CancellableSpeechCommand,
 )
-from .commands import (  # noqa: F401
-	# F401 imported but unused:
-	# These are imported explicitly to maintain backwards compatibility and will be removed in
-	# 2021.1. Rather than rely on these imports, import directly from the commands module.
-	# New commands added to commands.py should be directly imported only where needed.
-	SpeechCommand,
-	PitchCommand,
-	LangChangeCommand,
-	BeepCommand,
-	CharacterModeCommand,
-	SynthCommand,
-	BreakCommand,
-	BaseProsodyCommand,
-	VolumeCommand,
-	RateCommand,
-	PhonemeCommand,
-	CallbackCommand,
-	WaveFileCommand,
-)
+
 from .priorities import Spri, SPEECH_PRIORITIES
 from logHandler import log
 from synthDriverHandler import getSynth
@@ -52,8 +34,8 @@
 
 
 def _shouldCancelExpiredFocusEvents():
-	# 0: default (no), 1: yes, 2: no
-	return config.conf["featureFlag"]["cancelExpiredFocusSpeech"] == 1
+	# 0: default (yes), 1: yes, 2: no
+	return config.conf["featureFlag"]["cancelExpiredFocusSpeech"] != 2
 
 
 def _shouldDoSpeechManagerLogging():
diff --git a/source/speechViewer.py b/source/speechViewer.py
index 7109b55eead..7c293d6d466 100644
--- a/source/speechViewer.py
+++ b/source/speechViewer.py
@@ -1,5 +1,5 @@
 # A part of NonVisual Desktop Access (NVDA)
-# Copyright (C) 2006-2020 NV Access Limited, Thomas Stivers
+# Copyright (C) 2006-2021 NV Access Limited, Thomas Stivers, Accessolutions, Julien Cochuyt
 # This file is covered by the GNU General Public License.
 # See the file COPYING for more details.
 
@@ -38,7 +38,7 @@ def __init__(self, onDestroyCallBack):
 			title=_("NVDA Speech Viewer"),
 			size=dialogSize,
 			pos=dialogPos,
-			style=wx.CAPTION | wx.RESIZE_BORDER | wx.STAY_ON_TOP
+			style=wx.CAPTION | wx.CLOSE_BOX | wx.RESIZE_BORDER | wx.STAY_ON_TOP
 		)
 		self._isDestroyed = False
 		self.onDestroyCallBack = onDestroyCallBack
@@ -97,10 +97,8 @@ def _onDialogActivated(self, evt):
 			self.shouldShowOnStartupCheckBox.SetFocus()
 
 	def onClose(self, evt):
-		if not evt.CanVeto():
-			deactivate()
-			return
-		evt.Veto()
+		assert isActive, "Cannot close Speech Viewer as it is already inactive"
+		deactivate()
 
 	def onShouldShowOnStartupChanged(self, evt):
 		config.conf["speechViewer"]["showSpeechViewerAtStartup"] = self.shouldShowOnStartupCheckBox.IsChecked()
@@ -185,4 +183,3 @@ def deactivate():
 	# #7077: If the window is destroyed, text control will be gone, so save speech viewer position before destroying the window.
 	_guiFrame.savePositionInformation()
 	_guiFrame.Destroy()
-	isActive = False
diff --git a/source/speechXml.py b/source/speechXml.py
index 0abe0be00ac..7f308694a34 100644
--- a/source/speechXml.py
+++ b/source/speechXml.py
@@ -13,6 +13,7 @@
 from collections import namedtuple, OrderedDict
 import re
 import speech
+from speech.commands import SpeechCommand
 from logHandler import log
 
 XML_ESCAPES = {
@@ -196,7 +197,7 @@ class SpeechXmlConverter(object):
 	Subclasses implement specific XML schemas by implementing methods which convert each speech command.
 	The method for a speech command should be named with the prefix "convert" followed by the command's class name.
 	For example, the handler for C{IndexCommand} should be named C{convertIndexCommand}.
-	These methods receive the L{speech.SpeechCommand} instance as their only argument.
+	These methods receive the L{SpeechCommand} instance as their only argument.
 	They should return an appropriate XmlBalancer command.
 	Subclasses may wish to extend L{generateBalancerCommands}
 	to produce additional XmlBalancer commands at the start or end;
@@ -210,7 +211,7 @@ def generateBalancerCommands(self, speechSequence):
 		for item in speechSequence:
 			if isinstance(item, str):
 				yield item
-			elif isinstance(item, speech.SpeechCommand):
+			elif isinstance(item, SpeechCommand):
 				name = type(item).__name__
 				# For example: self.convertIndexCommand
 				func = getattr(self, "convert%s" % name, None)
diff --git a/source/synthDriverHandler.py b/source/synthDriverHandler.py
index a172e824c68..cf8f80023e6 100644
--- a/source/synthDriverHandler.py
+++ b/source/synthDriverHandler.py
@@ -1,19 +1,15 @@
-# -*- coding: UTF-8 -*-
-# synthDriverHandler.py
 # A part of NonVisual Desktop Access (NVDA)
 # This file is covered by the GNU General Public License.
 # See the file COPYING for more details.
 # Copyright (C) 2006-2019 NV Access Limited, Peter Vágner, Aleksey Sadovoy,
 # Joseph Lee, Arnold Loubriat, Leonard de Ruijter
 
-import os
 import pkgutil
 import importlib
 from typing import Optional
 from locale import strxfrm
 
 import config
-import baseObject
 import winVersion
 import globalVars
 from logHandler import log
@@ -23,11 +19,13 @@
 import extensionPoints
 import synthDrivers
 import driverHandler
-from driverHandler import StringParameterInfo  # noqa: F401 # Backwards compatibility
+from autoSettingsUtils.driverSetting import BooleanDriverSetting, DriverSetting, NumericDriverSetting
+from autoSettingsUtils.utils import StringParameterInfo
+
 from abc import abstractmethod
 
 
-class LanguageInfo(driverHandler.StringParameterInfo):
+class LanguageInfo(StringParameterInfo):
 	"""Holds information for a particular language"""
 
 	def __init__(self, id):
@@ -36,7 +34,7 @@ def __init__(self, id):
 		super(LanguageInfo, self).__init__(id, displayName)
 
 
-class VoiceInfo(driverHandler.StringParameterInfo):
+class VoiceInfo(StringParameterInfo):
 	"""Provides information about a single synthesizer voice.
 	"""
 
@@ -51,18 +49,23 @@ def __init__(self, id, displayName, language=None):
 
 
 class SynthDriver(driverHandler.Driver):
-	"""Abstract base synthesizer driver.
-	Each synthesizer driver should be a separate Python module in the root synthDrivers directory containing a SynthDriver class which inherits from this base class.
+	"""
+	Abstract base synthesizer driver.
+	Each synthesizer driver should be a separate Python module in the root synthDrivers directory
+	containing a SynthDriver class
+	which inherits from this base class.
 	
 	At a minimum, synth drivers must set L{name} and L{description} and override the L{check} method.
 	The methods L{speak}, L{cancel} and L{pause} should be overridden as appropriate.
 	L{supportedSettings} should be set as appropriate for the settings supported by the synthesiser.
-	There are factory functions to create L{driverHandler.DriverSetting} instances for common settings; e.g. L{VoiceSetting} and L{RateSetting}.
+	There are factory functions to create L{autoSettingsUtils.driverSetting.DriverSetting} instances
+	for common settings;
+	e.g. L{VoiceSetting} and L{RateSetting}.
 	Each setting is retrieved and set using attributes named after the setting;
 	e.g. the L{voice} attribute is used for the L{voice} setting.
 	These will usually be properties.
 	L{supportedCommands} should specify what synth commands the synthesizer supports.
-	At a minimum, L{speech.IndexCommand} must be supported.
+	At a minimum, L{IndexCommand} must be supported.
 	L{PitchCommand} must also be supported if you want capital pitch change to work;
 	support for the pitch setting is not sufficient.
 	L{supportedNotifications} should specify what notifications the synthesizer provides.
@@ -93,7 +96,7 @@ class SynthDriver(driverHandler.Driver):
 	#: @type: str
 	description = ""
 	#: The speech commands supported by the synth.
-	#: @type: set of L{speech.SynthCommand} subclasses.
+	#: @type: set of L{SynthCommand} subclasses.
 	supportedCommands = frozenset()
 	#: The notifications provided by the synth.
 	#: @type: set of L{extensionPoints.Action} instances
@@ -103,7 +106,7 @@ class SynthDriver(driverHandler.Driver):
 	@classmethod
 	def LanguageSetting(cls):
 		"""Factory function for creating a language setting."""
-		return driverHandler.DriverSetting(
+		return DriverSetting(
 			"language",
 			# Translators: Label for a setting in voice settings dialog.
 			_("&Language"),
@@ -115,7 +118,7 @@ def LanguageSetting(cls):
 	@classmethod
 	def VoiceSetting(cls):
 		"""Factory function for creating voice setting."""
-		return driverHandler.DriverSetting(
+		return DriverSetting(
 			"voice",
 			# Translators: Label for a setting in voice settings dialog.
 			_("&Voice"),
@@ -127,7 +130,7 @@ def VoiceSetting(cls):
 	@classmethod
 	def VariantSetting(cls):
 		"""Factory function for creating variant setting."""
-		return driverHandler.DriverSetting(
+		return DriverSetting(
 			"variant",
 			# Translators: Label for a setting in voice settings dialog.
 			_("V&ariant"),
@@ -139,7 +142,7 @@ def VariantSetting(cls):
 	@classmethod
 	def RateSetting(cls, minStep=1):
 		"""Factory function for creating rate setting."""
-		return driverHandler.NumericDriverSetting(
+		return NumericDriverSetting(
 			"rate",
 			# Translators: Label for a setting in voice settings dialog.
 			_("&Rate"),
@@ -152,7 +155,7 @@ def RateSetting(cls, minStep=1):
 	@classmethod
 	def RateBoostSetting(cls):
 		"""Factory function for creating rate boost setting."""
-		return driverHandler.BooleanDriverSetting(
+		return BooleanDriverSetting(
 			"rateBoost",
 			# Translators: This is the name of the rate boost voice toggle
 			# which further increases the speaking rate when enabled.
@@ -165,7 +168,7 @@ def RateBoostSetting(cls):
 	@classmethod
 	def VolumeSetting(cls, minStep=1):
 		"""Factory function for creating volume setting."""
-		return driverHandler.NumericDriverSetting(
+		return NumericDriverSetting(
 			"volume",
 			# Translators: Label for a setting in voice settings dialog.
 			_("V&olume"),
@@ -179,7 +182,7 @@ def VolumeSetting(cls, minStep=1):
 	@classmethod
 	def PitchSetting(cls, minStep=1):
 		"""Factory function for creating pitch setting."""
-		return driverHandler.NumericDriverSetting(
+		return NumericDriverSetting(
 			"pitch",
 			# Translators: Label for a setting in voice settings dialog.
 			_("&Pitch"),
@@ -192,7 +195,7 @@ def PitchSetting(cls, minStep=1):
 	@classmethod
 	def InflectionSetting(cls, minStep=1):
 		"""Factory function for creating inflection setting."""
-		return driverHandler.NumericDriverSetting(
+		return NumericDriverSetting(
 			"inflection",
 			# Translators: Label for a setting in voice settings dialog.
 			_("&Inflection"),
@@ -424,7 +427,7 @@ def getSynthInstance(name):
 # The synthDrivers that should be used by default.
 # The first that successfully initializes will be used when config is set to auto (I.e. new installs of NVDA).
 defaultSynthPriorityList = ['espeak', 'silence']
-if winVersion.winVersion.major >= 10:
+if winVersion.getWinVer() >= winVersion.WIN10:
 	# Default to OneCore on Windows 10 and above
 	defaultSynthPriorityList.insert(0, 'oneCore')
 
diff --git a/source/synthDrivers/_espeak.py b/source/synthDrivers/_espeak.py
index c2a5ee197df..a0ee6dc0175 100755
--- a/source/synthDrivers/_espeak.py
+++ b/source/synthDrivers/_espeak.py
@@ -1,9 +1,8 @@
 # -*- coding: UTF-8 -*-
-#synthDrivers/_espeak.py
-#A part of NonVisual Desktop Access (NVDA)
-#Copyright (C) 2007-2017 NV Access Limited, Peter Vágner
-#This file is covered by the GNU General Public License.
-#See the file COPYING for more details.
+# A part of NonVisual Desktop Access (NVDA)
+# Copyright (C) 2007-2020 NV Access Limited, Peter Vágner
+# This file is covered by the GNU General Public License.
+# See the file COPYING for more details.
 
 import time
 import nvwave
@@ -369,7 +368,8 @@ def terminate():
 	onIndexReached = None
 
 def info():
-	return espeakDLL.espeak_Info()
+	# Python 3.8: a path string must be specified, a NULL is fine when what we need is version string.
+	return espeakDLL.espeak_Info(None).decode()
 
 def getVariantDict():
 	dir = os.path.join(globalVars.appDir, "synthDrivers", "espeak-ng-data", "voices", "!v")
diff --git a/source/synthDrivers/_sapi4.py b/source/synthDrivers/_sapi4.py
index 0828ce5a634..32d808e8aca 100755
--- a/source/synthDrivers/_sapi4.py
+++ b/source/synthDrivers/_sapi4.py
@@ -5,9 +5,24 @@
 #This file is covered by the GNU General Public License.
 #See the file COPYING for more details.
 
-from ctypes import *
-from ctypes.wintypes import *
-from comtypes import *
+from ctypes import (
+	cast,
+	c_int,
+	c_uint,
+	c_ulong,
+	c_ulonglong,
+	c_wchar,
+	c_wchar_p,
+	c_void_p,
+	HRESULT,
+	POINTER,
+	sizeof,
+	Structure
+)
+from ctypes.wintypes import BYTE, DWORD, LPCWSTR, WORD
+from comtypes import GUID, IUnknown, STDMETHOD
+
+import winKernel
 
 S_OK=0
 
@@ -37,8 +52,6 @@ class VOICECHARSET(c_int):
     CHARSET_IPAPHONETIC = 1
     CHARSET_ENGINEPHONETIC = 2
 
-class FILETIME(Structure):
-    _fields_ = [("dwLowDateTime", DWORD), ("dwHighDateTime", DWORD)]
 
 class LANGUAGEW(Structure):
     _fields_ = [("LanguageID", LANGID),
@@ -111,7 +124,7 @@ class ITTSCentralW(IUnknown):
     STDMETHOD(HRESULT, "Phoneme", [VOICECHARSET, DWORD, SDATA, POINTER(SDATA)]),
     STDMETHOD(HRESULT, "PosnGet", [POINTER(QWORD)]),
     STDMETHOD(HRESULT, "TextData", [VOICECHARSET, DWORD, SDATA, c_void_p, GUID]),
-    STDMETHOD(HRESULT, "ToFileTime", [POINTER(QWORD), POINTER(FILETIME)]),
+    STDMETHOD(HRESULT, "ToFileTime", [POINTER(QWORD), POINTER(winKernel.FILETIME)]),
     STDMETHOD(HRESULT, "AudioPause"),
     STDMETHOD(HRESULT, "AudioResume"),
     STDMETHOD(HRESULT, "AudioReset"),
diff --git a/source/synthDrivers/espeak.py b/source/synthDrivers/espeak.py
index 2f2ad27b54c..8a1b71fade7 100644
--- a/source/synthDrivers/espeak.py
+++ b/source/synthDrivers/espeak.py
@@ -13,7 +13,17 @@
 from synthDriverHandler import SynthDriver, VoiceInfo, synthIndexReached, synthDoneSpeaking
 import speech
 from logHandler import log
-from driverHandler import BooleanDriverSetting
+
+from speech.commands import (
+	IndexCommand,
+	CharacterModeCommand,
+	LangChangeCommand,
+	BreakCommand,
+	PitchCommand,
+	RateCommand,
+	VolumeCommand,
+	PhonemeCommand,
+)
 
 class SynthDriver(SynthDriver):
 	name = "espeak"
@@ -29,14 +39,14 @@ class SynthDriver(SynthDriver):
 		SynthDriver.VolumeSetting(),
 	)
 	supportedCommands = {
-		speech.IndexCommand,
-		speech.CharacterModeCommand,
-		speech.LangChangeCommand,
-		speech.BreakCommand,
-		speech.PitchCommand,
-		speech.RateCommand,
-		speech.VolumeCommand,
-		speech.PhonemeCommand,
+		IndexCommand,
+		CharacterModeCommand,
+		LangChangeCommand,
+		BreakCommand,
+		PitchCommand,
+		RateCommand,
+		VolumeCommand,
+		PhonemeCommand,
 	}
 	supportedNotifications = {synthIndexReached, synthDoneSpeaking}
 
@@ -60,9 +70,9 @@ def _get_language(self):
 		return self._language
 
 	PROSODY_ATTRS = {
-		speech.PitchCommand: "pitch",
-		speech.VolumeCommand: "volume",
-		speech.RateCommand: "rate",
+		PitchCommand: "pitch",
+		VolumeCommand: "volume",
+		RateCommand: "rate",
 	}
 
 	IPA_TO_ESPEAK = {
@@ -91,16 +101,16 @@ def speak(self,speechSequence):
 		for item in speechSequence:
 			if isinstance(item,str):
 				textList.append(self._processText(item))
-			elif isinstance(item,speech.IndexCommand):
+			elif isinstance(item, IndexCommand):
 				textList.append(""%item.index)
-			elif isinstance(item,speech.CharacterModeCommand):
+			elif isinstance(item, CharacterModeCommand):
 				textList.append("" if item.state else "")
-			elif isinstance(item,speech.LangChangeCommand):
+			elif isinstance(item, LangChangeCommand):
 				if langChanged:
 					textList.append("")
 				textList.append(""%(item.lang if item.lang else defaultLanguage).replace('_','-'))
 				langChanged=True
-			elif isinstance(item,speech.BreakCommand):
+			elif isinstance(item, BreakCommand):
 				textList.append('' % item.time)
 			elif type(item) in self.PROSODY_ATTRS:
 				if prosody:
@@ -121,7 +131,7 @@ def speak(self,speechSequence):
 				for attr,val in prosody.items():
 					textList.append(' %s="%d%%"'%(attr,val))
 				textList.append(">")
-			elif isinstance(item,speech.PhonemeCommand):
+			elif isinstance(item, PhonemeCommand):
 				# We can't use str.translate because we want to reject unknown characters.
 				try:
 					phonemes="".join([self.IPA_TO_ESPEAK[char] for char in item.ipa])
diff --git a/source/synthDrivers/oneCore.py b/source/synthDrivers/oneCore.py
index 7163122bdc5..077123029c1 100644
--- a/source/synthDrivers/oneCore.py
+++ b/source/synthDrivers/oneCore.py
@@ -25,6 +25,17 @@
 import winVersion
 import NVDAHelper
 
+from speech.commands import (
+	IndexCommand,
+	CharacterModeCommand,
+	LangChangeCommand,
+	BreakCommand,
+	PitchCommand,
+	RateCommand,
+	VolumeCommand,
+	PhonemeCommand,
+)
+
 #: The number of 100-nanosecond units in 1 second.
 HUNDRED_NS_PER_SEC = 10000000 # 1000000000 ns per sec / 100 ns
 ocSpeech_Callback = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_int, ctypes.c_wchar_p)
@@ -78,9 +89,9 @@ def generateBalancerCommands(self, speechSequence):
 		yield next(commands)
 		# OneCore didn't provide a way to set base prosody values before API version 5.
 		# Therefore, the base values need to be set using SSML.
-		yield self.convertRateCommand(speech.RateCommand(multiplier=1))
-		yield self.convertVolumeCommand(speech.VolumeCommand(multiplier=1))
-		yield self.convertPitchCommand(speech.PitchCommand(multiplier=1))
+		yield self.convertRateCommand(RateCommand(multiplier=1))
+		yield self.convertVolumeCommand(VolumeCommand(multiplier=1))
+		yield self.convertPitchCommand(PitchCommand(multiplier=1))
 		for command in commands:
 			yield command
 
@@ -105,21 +116,21 @@ class SynthDriver(SynthDriver):
 	# Translators: Description for a speech synthesizer.
 	description = _("Windows OneCore voices")
 	supportedCommands = {
-		speech.IndexCommand,
-		speech.CharacterModeCommand,
-		speech.LangChangeCommand,
-		speech.BreakCommand,
-		speech.PitchCommand,
-		speech.RateCommand,
-		speech.VolumeCommand,
-		speech.PhonemeCommand,
+		IndexCommand,
+		CharacterModeCommand,
+		LangChangeCommand,
+		BreakCommand,
+		PitchCommand,
+		RateCommand,
+		VolumeCommand,
+		PhonemeCommand,
 	}
 	supportedNotifications = {synthIndexReached, synthDoneSpeaking}
 
 	@classmethod
 	def check(cls):
 		# Only present this as an available synth if this is Windows 10.
-		return winVersion.isWin10()
+		return winVersion.getWinVer() >= winVersion.WIN10
 
 	def _get_supportsProsodyOptions(self):
 		self.supportsProsodyOptions = self._dll.ocSpeech_supportsProsodyOptions()
diff --git a/source/synthDrivers/sapi4.py b/source/synthDrivers/sapi4.py
index f130ca8ad09..e48a5bb11d4 100755
--- a/source/synthDrivers/sapi4.py
+++ b/source/synthDrivers/sapi4.py
@@ -7,16 +7,39 @@
 import locale
 from collections import OrderedDict
 import winreg
-from comtypes import COMObject, COMError
-from ctypes import *
+from comtypes import CoCreateInstance, COMObject, COMError, GUID
+from ctypes import byref, c_ulong, POINTER
+from ctypes.wintypes import DWORD, WORD
 from synthDriverHandler import SynthDriver,VoiceInfo, synthIndexReached, synthDoneSpeaking
 from logHandler import log
-import speech
-from ._sapi4 import *
+from ._sapi4 import (
+	CLSID_MMAudioDest,
+	CLSID_TTSEnumerator,
+	IAudioMultiMediaDevice,
+	ITTSAttributes,
+	ITTSBufNotifySink,
+	ITTSCentralW,
+	ITTSEnumW,
+	TextSDATA,
+	TTSATTR_MAXPITCH,
+	TTSATTR_MAXSPEED,
+	TTSATTR_MAXVOLUME,
+	TTSATTR_MINPITCH,
+	TTSATTR_MINSPEED,
+	TTSATTR_MINVOLUME,
+	TTSDATAFLAG_TAGGED,
+	TTSFEATURE_PITCH,
+	TTSFEATURE_SPEED,
+	TTSFEATURE_VOLUME,
+	TTSMODEINFO,
+	VOICECHARSET
+)
 import config
 import nvwave
 import weakref
 
+from speech.commands import PitchCommand
+from speech.commands import IndexCommand, SpeechCommand, CharacterModeCommand
 
 class SynthDriverBufSink(COMObject):
 	_com_interfaces_ = [ITTSBufNotifySink]
@@ -93,19 +116,34 @@ def speak(self,speechSequence):
 		textList=[]
 		charMode=False
 		item=None
+		isPitchCommand = False
+		pitch = WORD()
+		self._ttsAttrs.PitchGet(byref(pitch))
+		oldPitch = pitch.value
+
 		for item in speechSequence:
 			if isinstance(item,str):
 				textList.append(item.replace('\\','\\\\'))
-			elif isinstance(item,speech.IndexCommand):
+			elif isinstance(item, IndexCommand):
 				textList.append("\\mrk=%d\\"%item.index)
-			elif isinstance(item,speech.CharacterModeCommand):
+			elif isinstance(item, CharacterModeCommand):
 				textList.append("\\RmS=1\\" if item.state else "\\RmS=0\\")
 				charMode=item.state
-			elif isinstance(item,speech.SpeechCommand):
+			elif isinstance(item, PitchCommand):
+				offset = int(config.conf["speech"]['sapi4']["capPitchChange"])
+				offset = int((self._maxPitch - self._minPitch) * offset / 100)
+				val = oldPitch + offset
+				if val > self._maxPitch:
+					val = self._maxPitch
+				if val < self._minPitch:
+					val = self._minPitch
+				self._ttsAttrs.PitchSet(val)
+				isPitchCommand = True
+			elif isinstance(item, SpeechCommand):
 				log.debugWarning("Unsupported speech command: %s"%item)
 			else:
 				log.error("Unknown speech: %s"%item)
-		if isinstance(item,speech.IndexCommand):
+		if isinstance(item, IndexCommand):
 			# This is the index denoting the end of the speech sequence.
 			self._finalIndex=item.index
 		if charMode:
@@ -117,7 +155,24 @@ def speak(self,speechSequence):
 		textList.append("\\PAU=1\\")
 		text="".join(textList)
 		flags=TTSDATAFLAG_TAGGED
-		self._ttsCentral.TextData(VOICECHARSET.CHARSET_TEXT, flags,TextSDATA(text),self._bufSinkPtr,ITTSBufNotifySink._iid_)
+		if isPitchCommand:
+			self._ttsCentral.TextData(
+				VOICECHARSET.CHARSET_TEXT,
+				flags,
+				TextSDATA(text),
+				self._bufSinkPtr,
+				ITTSBufNotifySink._iid_
+			)
+			self._ttsAttrs.PitchSet(oldPitch)
+			isPitchCommand = False
+		else:
+			self._ttsCentral.TextData(
+				VOICECHARSET.CHARSET_TEXT,
+				flags,
+				TextSDATA(text),
+				self._bufSinkPtr,
+				ITTSBufNotifySink._iid_
+			)
 
 	def cancel(self):
 		self._ttsCentral.AudioReset()
diff --git a/source/synthDrivers/sapi5.py b/source/synthDrivers/sapi5.py
index 45d2e60948b..386b293c33c 100644
--- a/source/synthDrivers/sapi5.py
+++ b/source/synthDrivers/sapi5.py
@@ -23,6 +23,18 @@
 from logHandler import log
 import weakref
 
+from speech.commands import (
+	IndexCommand,
+	CharacterModeCommand,
+	LangChangeCommand,
+	BreakCommand,
+	PitchCommand,
+	RateCommand,
+	VolumeCommand,
+	PhonemeCommand,
+	SpeechCommand,
+)
+
 # SPAudioState enumeration
 SPAS_CLOSED=0
 SPAS_STOP=1
@@ -124,14 +136,14 @@ def EndStream(self, streamNum, pos):
 class SynthDriver(SynthDriver):
 	supportedSettings=(SynthDriver.VoiceSetting(),SynthDriver.RateSetting(),SynthDriver.PitchSetting(),SynthDriver.VolumeSetting())
 	supportedCommands = {
-		speech.IndexCommand,
-		speech.CharacterModeCommand,
-		speech.LangChangeCommand,
-		speech.BreakCommand,
-		speech.PitchCommand,
-		speech.RateCommand,
-		speech.VolumeCommand,
-		speech.PhonemeCommand,
+		IndexCommand,
+		CharacterModeCommand,
+		LangChangeCommand,
+		BreakCommand,
+		PitchCommand,
+		RateCommand,
+		VolumeCommand,
+		PhonemeCommand,
 	}
 	supportedNotifications = {synthIndexReached, synthDoneSpeaking}
 
@@ -312,9 +324,9 @@ def outputTags():
 			if isinstance(item, str):
 				outputTags()
 				textList.append(item.replace("<", "<"))
-			elif isinstance(item, speech.IndexCommand):
+			elif isinstance(item, IndexCommand):
 				textList.append('' % item.index)
-			elif isinstance(item, speech.CharacterModeCommand):
+			elif isinstance(item, CharacterModeCommand):
 				if item.state:
 					tags["spell"] = {}
 				else:
@@ -323,12 +335,12 @@ def outputTags():
 					except KeyError:
 						pass
 				tagsChanged[0] = True
-			elif isinstance(item, speech.BreakCommand):
+			elif isinstance(item, BreakCommand):
 				textList.append('' % item.time)
-			elif isinstance(item, speech.PitchCommand):
+			elif isinstance(item, PitchCommand):
 				tags["pitch"] = {"absmiddle": self._percentToPitch(int(pitch * item.multiplier))}
 				tagsChanged[0] = True
-			elif isinstance(item, speech.VolumeCommand):
+			elif isinstance(item, VolumeCommand):
 				if item.multiplier == 1:
 					try:
 						del tags["volume"]
@@ -337,7 +349,7 @@ def outputTags():
 				else:
 					tags["volume"] = {"level": int(volume * item.multiplier)}
 				tagsChanged[0] = True
-			elif isinstance(item, speech.RateCommand):
+			elif isinstance(item, RateCommand):
 				if item.multiplier == 1:
 					try:
 						del tags["rate"]
@@ -346,7 +358,7 @@ def outputTags():
 				else:
 					tags["rate"] = {"absspeed": self._percentToRate(int(rate * item.multiplier))}
 				tagsChanged[0] = True
-			elif isinstance(item, speech.PhonemeCommand):
+			elif isinstance(item, PhonemeCommand):
 				try:
 					textList.append(u'%s'
 						% (self._convertPhoneme(item.ipa), item.text or u""))
@@ -354,7 +366,7 @@ def outputTags():
 					log.debugWarning("Couldn't convert character in IPA string: %s" % item.ipa)
 					if item.text:
 						textList.append(item.text)
-			elif isinstance(item, speech.SpeechCommand):
+			elif isinstance(item, SpeechCommand):
 				log.debugWarning("Unsupported speech command: %s" % item)
 			else:
 				log.error("Unknown speech: %s" % item)
diff --git a/source/synthDrivers/silence.py b/source/synthDrivers/silence.py
index 3f64adc29c2..1d37d69601d 100644
--- a/source/synthDrivers/silence.py
+++ b/source/synthDrivers/silence.py
@@ -5,7 +5,7 @@
 #See the file COPYING for more details.
 
 import synthDriverHandler
-import speech
+from speech.commands import IndexCommand
 
 class SynthDriver(synthDriverHandler.SynthDriver):
 	"""A dummy synth driver used to disable speech in NVDA.
@@ -23,7 +23,7 @@ def check(cls):
 	def speak(self, speechSequence):
 		self.lastIndex = None
 		for item in speechSequence:
-			if isinstance(item, speech.IndexCommand):
+			if isinstance(item, IndexCommand):
 				self.lastIndex = item.index
 
 	def cancel(self):
diff --git a/source/synthSettingsRing.py b/source/synthSettingsRing.py
index 215dc1eeca6..39e06fe6f84 100644
--- a/source/synthSettingsRing.py
+++ b/source/synthSettingsRing.py
@@ -2,16 +2,17 @@
 import config
 import synthDriverHandler
 import queueHandler
-import driverHandler
+from autoSettingsUtils.driverSetting import BooleanDriverSetting, NumericDriverSetting
+
 
 class SynthSetting(baseObject.AutoPropertyObject):
 	"""a numeric synth setting. Has functions to set, get, increase and decrease its value """
 	def __init__(self,synth,setting,min=0,max=100):
 		self.synth = synth
 		self.setting = setting
-		self.min = setting.minVal  if isinstance(setting,driverHandler.NumericDriverSetting) else min
-		self.max = setting.maxVal  if isinstance(setting,driverHandler.NumericDriverSetting) else max
-		self.step = setting.normalStep if isinstance(setting,driverHandler.NumericDriverSetting) else 1
+		self.min = setting.minVal if isinstance(setting, NumericDriverSetting) else min
+		self.max = setting.maxVal if isinstance(setting, NumericDriverSetting) else max
+		self.step = setting.normalStep if isinstance(setting, NumericDriverSetting) else 1
 
 	def increase(self):
 		val = min(self.max,self.value+self.step)
@@ -139,9 +140,9 @@ def updateSupportedSettings(self,synth):
 			if not s.availableInSettingsRing: continue
 			if prevID == s.id: #restore the last setting
 				self._current=len(list)
-			if isinstance(s,driverHandler.NumericDriverSetting):
+			if isinstance(s, NumericDriverSetting):
 				cls=SynthSetting
-			elif isinstance(s,driverHandler.BooleanDriverSetting):
+			elif isinstance(s, BooleanDriverSetting):
 				cls=BooleanSynthSetting
 			else:
 				cls=StringSynthSetting
diff --git a/source/textInfos/__init__.py b/source/textInfos/__init__.py
index 7fc875cd6ba..6e6efdb88d1 100755
--- a/source/textInfos/__init__.py
+++ b/source/textInfos/__init__.py
@@ -12,13 +12,21 @@
 from abc import abstractmethod
 import weakref
 import re
-from typing import Any, Union, List, Optional, Dict
+from typing import (
+	Any,
+	Union,
+	List,
+	Optional,
+	Dict,
+	Tuple,
+)
 
 import baseObject
 import config
 import controlTypes
 from controlTypes import OutputReason
 import locationHelper
+from logHandler import log
 
 
 SpeechSequence = List[Union[Any, str]]
@@ -51,7 +59,7 @@ def getPresentationCategory(
 			self,
 			ancestors,
 			formatConfig,
-			reason=controlTypes.REASON_CARET,
+			reason=OutputReason.CARET,
 			extraDetail=False
 	):
 		role = self.get("role", controlTypes.ROLE_UNKNOWN)
@@ -77,7 +85,7 @@ def getPresentationCategory(
 
 		name = self.get("name")
 		landmark = self.get("landmark")
-		if reason in (controlTypes.REASON_CARET, controlTypes.REASON_SAYALL, controlTypes.REASON_FOCUS) and (
+		if reason in (OutputReason.CARET, OutputReason.SAYALL, OutputReason.FOCUS) and (
 			(role == controlTypes.ROLE_LINK and not formatConfig["reportLinks"])
 			or (role == controlTypes.ROLE_GRAPHIC and not formatConfig["reportGraphics"])
 			or (role == controlTypes.ROLE_HEADING and not formatConfig["reportHeadings"])
@@ -307,6 +315,24 @@ def __init__(self,obj,position):
 		#: The position with which this instance was constructed.
 		self.basePosition=position
 
+	#: Typing information for auto-property: start
+	start: "TextInfoEndpoint"
+
+	def _get_start(self) -> "TextInfoEndpoint":
+		return TextInfoEndpoint(self, True)
+
+	def _set_start(self, otherEndpoint: "TextInfoEndpoint"):
+		self.start.moveTo(otherEndpoint)
+
+	#: Typing information for auto-property: end
+	end: "TextInfoEndpoint"
+
+	def _get_end(self) -> "TextInfoEndpoint":
+		return TextInfoEndpoint(self, False)
+
+	def _set_end(self, otherEndpoint: "TextInfoEndpoint"):
+		self.end.moveTo(otherEndpoint)
+
 	def _get_obj(self):
 		"""The object containing the range of text being represented."""
 		return self._obj()
@@ -521,15 +547,18 @@ def getTextInChunks(self, unit):
 		"""
 		unitInfo=self.copy()
 		unitInfo.collapse()
-		while unitInfo.compareEndPoints(self,"startToEnd")<0:
+		while unitInfo.start < self.end:
 			unitInfo.expand(unit)
 			chunkInfo=unitInfo.copy()
-			if chunkInfo.compareEndPoints(self,"startToStart")<0:
-				chunkInfo.setEndPoint(self,"startToStart")
-			if chunkInfo.compareEndPoints(self,"endToEnd")>0:
-				chunkInfo.setEndPoint(self,"endToEnd")
+			if chunkInfo.start < self.start:
+				chunkInfo.start = self.start
+			if chunkInfo.end > self.end:
+				chunkInfo.end = self.end
 			yield chunkInfo.text
 			unitInfo.collapse(end=True)
+			if unitInfo.start < chunkInfo.end:
+				log.debugWarning("Could not move TextInfo completely to end, breaking")
+				break
 
 	def getControlFieldSpeech(
 			self,
@@ -624,3 +653,76 @@ def turnPage(self, previous=False):
 		@raise RuntimeError: If there are no further pages.
 		"""
 		raise NotImplementedError
+
+
+class TextInfoEndpoint:
+	"""
+	Represents one end of a TextInfo instance.
+	This object can be compared with another end from the same or a different TextInfo instance,
+	Using the standard math comparison operators:
+	< <= == != >= >
+	"""
+
+	_whichMap: Dict[Tuple[bool, bool], str] = {
+		(True, True): "startToStart",
+		(True, False): "startToEnd",
+		(False, True): "endToStart",
+		(False, False): "endToEnd",
+	}
+
+	def _cmp(self, other: "TextInfoEndpoint") -> int:
+		"""
+		A standard cmp function returning:
+		-1 for less than, 0 for equal and 1 for greater than.
+		"""
+		if (
+			not isinstance(other, TextInfoEndpoint)
+			or not isinstance(other.textInfo, type(self.textInfo))
+		):
+			raise ValueError(f"Cannot compare endpoint with different type: {other}")
+		return self.textInfo.compareEndPoints(other.textInfo, self._whichMap[self.isStart, other.isStart])
+
+	def __init__(
+			self,
+			textInfo: TextInfo,
+			isStart: bool
+	):
+		"""
+		@param textInfo: the TextInfo instance you wish to represent an endpoint of.
+		@param isStart: true to represent the start, false for the end.
+		"""
+		self.textInfo = textInfo
+		self.isStart = isStart
+
+	def __lt__(self, other) -> bool:
+		return self._cmp(other) < 0
+
+	def __le__(self, other) -> bool:
+		return self._cmp(other) <= 0
+
+	def __eq__(self, other) -> bool:
+		return self._cmp(other) == 0
+
+	def __ne__(self, other) -> bool:
+		return self._cmp(other) != 0
+
+	def __ge__(self, other) -> bool:
+		return self._cmp(other) >= 0
+
+	def __gt__(self, other) -> bool:
+		return self._cmp(other) > 0
+
+	def moveTo(self, other: "TextInfoEndpoint") -> None:
+		"""
+		Moves the end of the TextInfo this endpoint represents to the position of the given endpoint.
+		"""
+		if (
+			not isinstance(other, TextInfoEndpoint)
+			or not isinstance(other.textInfo, type(self.textInfo))
+		):
+			raise ValueError(f"Cannot move endpoint to different type: {other}")
+		self.textInfo.setEndPoint(other.textInfo, self._whichMap[(self.isStart, other.isStart)])
+
+	def __repr__(self):
+		endpointLabel = "start" if self.isStart else "end"
+		return f"{endpointLabel} endpoint of {self.textInfo}"
diff --git a/source/touchHandler.py b/source/touchHandler.py
index f3bb6eeb5f3..74cf897a760 100644
--- a/source/touchHandler.py
+++ b/source/touchHandler.py
@@ -302,7 +302,7 @@ def touchSupported(debugLog: bool = False):
 		if debugLog:
 			log.debugWarning("Touch only supported on installed copies")
 		return False
-	if winVersion.winVersion.platform_version < (6, 2, 9200):
+	if winVersion.getWinVer() < winVersion.WIN8:
 		if debugLog:
 			log.debugWarning("Touch only supported on Windows 8 and higher")
 		return False
diff --git a/source/ui.py b/source/ui.py
index 74720c9bbeb..1f7e6fd6558 100644
--- a/source/ui.py
+++ b/source/ui.py
@@ -15,6 +15,7 @@
 from ctypes import windll, byref, POINTER, addressof
 from comtypes import IUnknown
 from comtypes import automation 
+from html import escape
 from logHandler import log
 import gui
 import speech
@@ -55,8 +56,9 @@ def browseableMessage(message,title=None,isHtml=False):
 	if not title:
 		# Translators: The title for the dialog used to present general NVDA messages in browse mode.
 		title = _("NVDA Message")
-	isHtmlArgument = "true" if isHtml else "false"
-	dialogString = u"{isHtml};{title};{message}".format( isHtml = isHtmlArgument , title=title , message=message ) 
+	if not isHtml:
+		message = f"
{escape(message)}
" + dialogString = f"{title};{message}" dialogArguements = automation.VARIANT( dialogString ) gui.mainFrame.prePopup() windll.mshtml.ShowHTMLDialogEx( diff --git a/source/updateCheck.py b/source/updateCheck.py index 0fd12aea8bf..65cefb5904e 100644 --- a/source/updateCheck.py +++ b/source/updateCheck.py @@ -22,7 +22,7 @@ # Avoid a E402 'module level import not at top of file' warning, because several checks are performed above. import gui.contextHelp # noqa: E402 from gui.dpiScalingHelper import DpiScalingHelperMixin, DpiScalingHelperMixinWithoutInit # noqa: E402 -import winVersion +import sys # noqa: E402 import os import inspect import threading @@ -105,12 +105,21 @@ def checkForUpdate(auto=False): @raise RuntimeError: If there is an error checking for an update. """ allowUsageStats=config.conf["update"]['allowUsageStats'] + # #11837: build version string, service pack, and product type manually + # because winVersion.getWinVer adds Windows release name. + winVersion = sys.getwindowsversion() + winVersionText = "{v.major}.{v.minor}.{v.build}".format(v=winVersion) + if winVersion.service_pack_major != 0: + winVersionText += " service pack %d" % winVersion.service_pack_major + if winVersion.service_pack_minor != 0: + winVersionText += ".%d" % winVersion.service_pack_minor + winVersionText += " %s" % ("workstation", "domain controller", "server")[winVersion.product_type - 1] params = { "autoCheck": auto, "allowUsageStats":allowUsageStats, "version": versionInfo.version, "versionType": versionInfo.updateVersionType, - "osVersion": winVersion.winVersionText, + "osVersion": winVersionText, "x64": os.environ.get("PROCESSOR_ARCHITEW6432") == "AMD64", } if auto and allowUsageStats: @@ -601,6 +610,7 @@ def start(self): # and waits for the user to press the Close button. style=wx.PD_CAN_ABORT | wx.PD_ELAPSED_TIME | wx.PD_REMAINING_TIME | wx.PD_AUTO_HIDE, parent=gui.mainFrame) + self._progressDialog.CentreOnScreen() self._progressDialog.Raise() t = threading.Thread( name=f"{self.__class__.__module__}.{self.start.__qualname__}", diff --git a/source/versionInfo.py b/source/versionInfo.py index 67a4612b383..95ab654ce84 100644 --- a/source/versionInfo.py +++ b/source/versionInfo.py @@ -16,7 +16,7 @@ longName = _("NonVisual Desktop Access") description = _("A free and open source screen reader for Microsoft Windows") url = "https://www.nvaccess.org/" -copyrightYears = "2006-2020" +copyrightYears = "2006-2021" copyright = _("Copyright (C) {years} NVDA Contributors").format( years=copyrightYears) aboutMessage = _( diff --git a/source/virtualBuffers/MSHTML.py b/source/virtualBuffers/MSHTML.py index 204a92f9705..74367175bf7 100644 --- a/source/virtualBuffers/MSHTML.py +++ b/source/virtualBuffers/MSHTML.py @@ -46,11 +46,27 @@ def _normalizeFormatField(self, attrs): attrs['language']=languageHandler.normalizeLanguage(language) return attrs - def _normalizeControlField(self,attrs): - level=None - ariaCurrent = attrs.get('HTMLAttrib::aria-current', None) - if ariaCurrent not in (None, "false"): - attrs['current']=ariaCurrent + def _getIsCurrentAttribute(self, attrs: dict) -> controlTypes.IsCurrent: + defaultAriaCurrentStringVal = "false" + ariaCurrentValue = attrs.get('HTMLAttrib::aria-current', defaultAriaCurrentStringVal) + # key 'HTMLAttrib::aria-current' may be in attrs with a value of None + ariaCurrentValue = defaultAriaCurrentStringVal if ariaCurrentValue is None else ariaCurrentValue + try: + ariaCurrent = controlTypes.IsCurrent(ariaCurrentValue) + except ValueError: + log.debugWarning(f"Unknown aria-current value: {ariaCurrentValue}") + ariaCurrent = controlTypes.IsCurrent.NO + return ariaCurrent + + # C901 'MSHTMLTextInfo._normalizeControlField' is too complex (42) + # Look for opportunities to simplify this function. + def _normalizeControlField(self, attrs: dict): # noqa: C901 + level = None + + isCurrent = self._getIsCurrentAttribute(attrs) + if isCurrent != controlTypes.IsCurrent.NO: + attrs['current'] = isCurrent + placeholder = self._getPlaceholderAttribute(attrs, 'HTMLAttrib::aria-placeholder') if placeholder: attrs['placeholder']=placeholder diff --git a/source/virtualBuffers/__init__.py b/source/virtualBuffers/__init__.py index f718f6c4067..39330350ad9 100644 --- a/source/virtualBuffers/__init__.py +++ b/source/virtualBuffers/__init__.py @@ -16,7 +16,7 @@ import NVDAHelper import XMLFormatting import scriptHandler -from scriptHandler import isScriptWaiting, willSayAllResume +from scriptHandler import script import speech import NVDAObjects import api @@ -33,7 +33,6 @@ from logHandler import log import ui import aria -import nvwave import treeInterceptorHandler import watchdog from abc import abstractmethod @@ -539,6 +538,13 @@ def script_refreshBuffer(self,gesture): # Translators: the description for the refreshBuffer script on virtualBuffers. script_refreshBuffer.__doc__ = _("Refreshes the document content") + @script( + description=_( + # Translators: the description for the toggleScreenLayout script on virtualBuffers. + "Toggles on and off if the screen layout is preserved while rendering the document content" + ), + gesture="kb:NVDA+v", + ) def script_toggleScreenLayout(self,gesture): config.conf["virtualBuffers"]["useScreenLayout"]=not config.conf["virtualBuffers"]["useScreenLayout"] if config.conf["virtualBuffers"]["useScreenLayout"]: @@ -547,8 +553,6 @@ def script_toggleScreenLayout(self,gesture): else: # Translators: Presented when use screen layout option is toggled. ui.message(_("Use screen layout off")) - # Translators: the description for the toggleScreenLayout script on virtualBuffers. - script_toggleScreenLayout.__doc__ = _("Toggles on and off if the screen layout is preserved while rendering the document content") def _searchableAttributesForNodeType(self,nodeType): pass @@ -724,5 +728,4 @@ def _isNVDAObjectInApplication_noWalk(self, obj): __gestures = { "kb:NVDA+f5": "refreshBuffer", - "kb:NVDA+v": "toggleScreenLayout", } diff --git a/source/virtualBuffers/adobeFlash.py b/source/virtualBuffers/adobeFlash.py deleted file mode 100644 index 91ec5b68a48..00000000000 --- a/source/virtualBuffers/adobeFlash.py +++ /dev/null @@ -1,132 +0,0 @@ -#virtualBuffers/adobeFlash.py -#A part of NonVisual Desktop Access (NVDA) -#This file is covered by the GNU General Public License. -#See the file COPYING for more details. -#Copyright (C) 2010-2013 NV Access Limited - -from comtypes import COMError -from . import VirtualBuffer, VirtualBufferTextInfo -import controlTypes -import NVDAObjects.IAccessible -import winUser -import mouseHandler -import IAccessibleHandler -import oleacc -from logHandler import log -import textInfos - -class AdobeFlash_TextInfo(VirtualBufferTextInfo): - - def _normalizeControlField(self,attrs): - accRole=attrs['IAccessible::role'] - if accRole.isdigit(): - accRole=int(accRole) - else: - accRole = accRole.lower() - role=IAccessibleHandler.IAccessibleRolesToNVDARoles.get(accRole,controlTypes.ROLE_UNKNOWN) - - states=set(IAccessibleHandler.IAccessibleStatesToNVDAStates[x] for x in [1< 0 - - def __contains__(self,obj): - if self.isWindowless: - if not isinstance(obj, NVDAObjects.IAccessible.IAccessible): - return False - if obj.windowHandle != self.rootDocHandle: - return False - info = obj.IAccessibleIdentity - if not info: - return False - ID=info['objectID'] - try: - self.rootNVDAObject.IAccessibleObject.accChild(ID) - return True - except COMError: - return False - return winUser.isDescendantWindow(self.rootDocHandle, obj.windowHandle) - - def _get_isAlive(self): - if self.isLoading: - return True - root=self.rootNVDAObject - if not root: - return False - if not winUser.isWindow(root.windowHandle) or root.role == controlTypes.ROLE_UNKNOWN: - return False - return True - - def getNVDAObjectFromIdentifier(self, docHandle, ID): - if self.isWindowless: - objId = ID - childId = 0 - else: - objId = winUser.OBJID_CLIENT - childId = ID - return NVDAObjects.IAccessible.getNVDAObjectFromEvent(docHandle, objId, childId) - - def getIdentifierFromNVDAObject(self,obj): - info = obj.IAccessibleIdentity - if info: - # Trust IAccIdentity over the event parameters. - accId = info["objectID"] - else: - accId = obj.event_objectID - if accId is None: - # We don't have event parameters, so we can't get an ID. - raise LookupError - if accId <= 0: - accId = obj.event_childID - return obj.windowHandle, accId - - def _searchableAttribsForNodeType(self,nodeType): - if nodeType=="formField": - attrs={"IAccessible::role":[oleacc.ROLE_SYSTEM_PUSHBUTTON,oleacc.ROLE_SYSTEM_RADIOBUTTON,oleacc.ROLE_SYSTEM_CHECKBUTTON,oleacc.ROLE_SYSTEM_COMBOBOX,oleacc.ROLE_SYSTEM_LIST,oleacc.ROLE_SYSTEM_TEXT]} - elif nodeType=="list": - attrs={"IAccessible::role":[oleacc.ROLE_SYSTEM_LIST]} - elif nodeType=="listItem": - attrs={"IAccessible::role":[oleacc.ROLE_SYSTEM_LISTITEM]} - elif nodeType=="button": - attrs={"IAccessible::role":[oleacc.ROLE_SYSTEM_PUSHBUTTON]} - elif nodeType=="edit": - attrs={"IAccessible::role":[oleacc.ROLE_SYSTEM_TEXT]} - elif nodeType=="radioButton": - attrs={"IAccessible::role":[oleacc.ROLE_SYSTEM_RADIOBUTTON]} - elif nodeType=="comboBox": - attrs={"IAccessible::role":[oleacc.ROLE_SYSTEM_COMBOBOX]} - elif nodeType=="checkBox": - attrs={"IAccessible::role":[oleacc.ROLE_SYSTEM_CHECKBUTTON]} - elif nodeType=="graphic": - attrs={"IAccessible::role":[oleacc.ROLE_SYSTEM_GRAPHIC]} - elif nodeType=="focusable": - attrs={"IAccessible::state_%s"%oleacc.STATE_SYSTEM_FOCUSABLE:[1]} - else: - return None - return attrs - - def _activateNVDAObject(self, obj): - try: - obj.doAction() - return - except: - pass - - log.debugWarning("could not programmatically activate field, trying mouse") - l=obj.location - if not l: - log.debugWarning("no location for field") - return - oldX,oldY=winUser.getCursorPos() - winUser.setCursorPos(*l.center) - mouseHandler.executeMouseEvent(winUser.MOUSEEVENTF_LEFTDOWN,0,0) - mouseHandler.executeMouseEvent(winUser.MOUSEEVENTF_LEFTUP,0,0) - winUser.setCursorPos(oldX,oldY) diff --git a/source/virtualBuffers/gecko_ia2.py b/source/virtualBuffers/gecko_ia2.py index c2ac7be94c3..da9a2b01dbe 100755 --- a/source/virtualBuffers/gecko_ia2.py +++ b/source/virtualBuffers/gecko_ia2.py @@ -17,6 +17,7 @@ from logHandler import log import textInfos from comtypes.gen.IAccessible2Lib import IAccessible2 +from comInterfaces import IAccessible2Lib as IA2 from comtypes import COMError import aria import config @@ -52,9 +53,14 @@ def _normalizeControlField(self,attrs): if attrVal is not None: attrs[attr]=int(attrVal) - current = attrs.get("IAccessible2::attribute_current") - if current not in (None, 'false'): - attrs['current']= current + valForCurrent = attrs.get("IAccessible2::attribute_current", "false") + try: + isCurrent = controlTypes.IsCurrent(valForCurrent) + except ValueError: + log.debugWarning(f"Unknown isCurrent value: {valForCurrent}") + isCurrent = controlTypes.IsCurrent.NO + if isCurrent != controlTypes.IsCurrent.NO: + attrs['current'] = isCurrent placeholder = self._getPlaceholderAttribute(attrs, "IAccessible2::attribute_placeholder") if placeholder is not None: attrs['placeholder']= placeholder @@ -148,19 +154,19 @@ def _getEmbedderFrame(acc): """ try: # 1. Get the containing document. - if not isinstance(acc, IAccessibleHandler.IAccessible2_2): + if not isinstance(acc, IA2.IAccessible2_2): # IAccessible NVDAObjects currently fetch IA2, but we need IA2_2 for relationTargetsOfType. # (Out-of-process, for a single relation, this is cheaper than IA2::relations.) - acc = acc.QueryInterface(IAccessibleHandler.IAccessible2_2) + acc = acc.QueryInterface(IA2.IAccessible2_2) targets, count = acc.relationTargetsOfType(IA2_RELATION_CONTAINING_DOCUMENT, 1) if count == 0: return None - doc = targets[0].QueryInterface(IAccessibleHandler.IAccessible2_2) + doc = targets[0].QueryInterface(IA2.IAccessible2_2) # 2. Get its parent (the embedder); e.g. iframe. embedder = doc.accParent if not embedder: return None - embedder = embedder.QueryInterface(IAccessibleHandler.IAccessible2_2) + embedder = embedder.QueryInterface(IA2.IAccessible2_2) # 3. Make sure this is an iframe/frame. attribs = embedder.attributes if "tag:browser;" in attribs: @@ -208,7 +214,14 @@ def _iterIdsToTryWithAccChild(cls, obj): yield accId def __contains__(self,obj): - if not (isinstance(obj,NVDAObjects.IAccessible.IAccessible) and isinstance(obj.IAccessibleObject,IAccessibleHandler.IAccessible2)) or not obj.windowClassName.startswith('Mozilla') or not winUser.isDescendantWindow(self.rootNVDAObject.windowHandle,obj.windowHandle): + if ( + not ( + isinstance(obj, NVDAObjects.IAccessible.IAccessible) + and isinstance(obj.IAccessibleObject, IA2.IAccessible2) + ) + or not obj.windowClassName.startswith('Mozilla') + or not winUser.isDescendantWindow(self.rootNVDAObject.windowHandle, obj.windowHandle) + ): return False for accId in self._iterIdsToTryWithAccChild(obj): if accId == self.rootID: @@ -241,7 +254,7 @@ def _get_isAlive(self): # stops responding; e.g. it froze, crashed or is being debugged. return True try: - isDefunct=bool(root.IAccessibleObject.states&IAccessibleHandler.IA2_STATE_DEFUNCT) + isDefunct = bool(root.IAccessibleObject.states & IA2.IA2_STATE_DEFUNCT) except COMError: # If IAccessible2 states can not be fetched at all, defunct should be assumed as the object has clearly been disconnected or is dead isDefunct=True @@ -307,11 +320,13 @@ def _searchableTagValues(self, values): def _searchableAttribsForNodeType(self,nodeType): if nodeType.startswith('heading') and nodeType[7:].isdigit(): - attrs={"IAccessible::role":[IAccessibleHandler.IA2_ROLE_HEADING],"IAccessible2::attribute_level":[nodeType[7:]]} + attrs = {"IAccessible::role": [IA2.IA2_ROLE_HEADING], "IAccessible2::attribute_level": [nodeType[7:]]} elif nodeType == "annotation": - attrs={"IAccessible::role":[IAccessibleHandler.IA2_ROLE_CONTENT_DELETION,IAccessibleHandler.IA2_ROLE_CONTENT_INSERTION]} + attrs = { + "IAccessible::role": [IA2.IA2_ROLE_CONTENT_DELETION, IA2.IA2_ROLE_CONTENT_INSERTION] + } elif nodeType=="heading": - attrs={"IAccessible::role":[IAccessibleHandler.IA2_ROLE_HEADING]} + attrs = {"IAccessible::role": [IA2.IA2_ROLE_HEADING]} elif nodeType=="table": attrs={"IAccessible::role":[oleacc.ROLE_SYSTEM_TABLE]} if not config.conf["documentFormatting"]["includeLayoutTables"]: @@ -334,7 +349,7 @@ def _searchableAttribsForNodeType(self,nodeType): oleacc.ROLE_SYSTEM_PUSHBUTTON, oleacc.ROLE_SYSTEM_RADIOBUTTON, oleacc.ROLE_SYSTEM_PAGETAB, - IAccessibleHandler.IA2_ROLE_TOGGLE_BUTTON, + IA2.IA2_ROLE_TOGGLE_BUTTON, ], f"IAccessible::state_{oleacc.STATE_SYSTEM_READONLY}": [None], }, @@ -343,11 +358,11 @@ def _searchableAttribsForNodeType(self,nodeType): oleacc.ROLE_SYSTEM_COMBOBOX, oleacc.ROLE_SYSTEM_TEXT ], - f"IAccessible2::state_{IAccessibleHandler.IA2_STATE_EDITABLE}": [1], + f"IAccessible2::state_{IA2.IA2_STATE_EDITABLE}": [1], }, { - f"IAccessible2::state_{IAccessibleHandler.IA2_STATE_EDITABLE}": [1], - f"parent::IAccessible2::state_{IAccessibleHandler.IA2_STATE_EDITABLE}": [None], + f"IAccessible2::state_{IA2.IA2_STATE_EDITABLE}": [1], + f"parent::IAccessible2::state_{IA2.IA2_STATE_EDITABLE}": [None], }, ] elif nodeType=="list": @@ -355,14 +370,26 @@ def _searchableAttribsForNodeType(self,nodeType): elif nodeType=="listItem": attrs={"IAccessible::role":[oleacc.ROLE_SYSTEM_LISTITEM]} elif nodeType=="button": - attrs={"IAccessible::role":[oleacc.ROLE_SYSTEM_PUSHBUTTON,oleacc.ROLE_SYSTEM_BUTTONMENU,IAccessibleHandler.IA2_ROLE_TOGGLE_BUTTON]} + attrs = { + "IAccessible::role": [ + oleacc.ROLE_SYSTEM_PUSHBUTTON, + oleacc.ROLE_SYSTEM_BUTTONMENU, + IA2.IA2_ROLE_TOGGLE_BUTTON + ] + } elif nodeType=="edit": attrs=[ - {"IAccessible::role":[oleacc.ROLE_SYSTEM_TEXT],"IAccessible2::state_%s"%IAccessibleHandler.IA2_STATE_EDITABLE:[1]}, - {"IAccessible2::state_%s"%IAccessibleHandler.IA2_STATE_EDITABLE:[1],"parent::IAccessible2::state_%s"%IAccessibleHandler.IA2_STATE_EDITABLE:[None]}, + { + "IAccessible::role": [oleacc.ROLE_SYSTEM_TEXT], + f"IAccessible2::state_{IA2.IA2_STATE_EDITABLE}":[1] + }, + { + f"IAccessible2::state_{IA2.IA2_STATE_EDITABLE}": [1], + f"parent::IAccessible2::state_{IA2.IA2_STATE_EDITABLE}":[None] + }, ] elif nodeType=="frame": - attrs={"IAccessible::role":[IAccessibleHandler.IA2_ROLE_INTERNAL_FRAME]} + attrs = {"IAccessible::role": [IA2.IA2_ROLE_INTERNAL_FRAME]} elif nodeType=="separator": attrs={"IAccessible::role":[oleacc.ROLE_SYSTEM_SEPARATOR]} elif nodeType=="radioButton": @@ -381,13 +408,13 @@ def _searchableAttribsForNodeType(self,nodeType): # Search for a tag of blockquote for older implementations before the blockquote IAccessible2 role existed. {"IAccessible2::attribute_tag":self._searchableTagValues(["blockquote"])}, # Also support the new blockquote IAccessible2 role - {"IAccessible::role":[IAccessibleHandler.IA2_ROLE_BLOCK_QUOTE]}, + {"IAccessible::role": [IA2.IA2_ROLE_BLOCK_QUOTE]}, ] elif nodeType=="focusable": attrs={"IAccessible::state_%s"%oleacc.STATE_SYSTEM_FOCUSABLE:[1]} elif nodeType=="landmark": attrs = [ - {"IAccessible::role": [IAccessibleHandler.IA2_ROLE_LANDMARK]}, + {"IAccessible::role": [IA2.IA2_ROLE_LANDMARK]}, {"IAccessible2::attribute_xml-roles": [VBufStorage_findMatch_word(lr) for lr in aria.landmarkRoles]}, {"IAccessible2::attribute_xml-roles": [VBufStorage_findMatch_word("region")], "name": [VBufStorage_findMatch_notEmpty]} diff --git a/source/visionEnhancementProviders/NVDAHighlighter.py b/source/visionEnhancementProviders/NVDAHighlighter.py index 6e20d72d977..728778b82a7 100644 --- a/source/visionEnhancementProviders/NVDAHighlighter.py +++ b/source/visionEnhancementProviders/NVDAHighlighter.py @@ -1,4 +1,3 @@ -# visionEnhancementProviders/NVDAHighlighter.py # A part of NonVisual Desktop Access (NVDA) # This file is covered by the GNU General Public License. # See the file COPYING for more details. @@ -8,6 +7,7 @@ from typing import Optional, Tuple from autoSettingsUtils.autoSettings import SupportedSettingType +from autoSettingsUtils.driverSetting import BooleanDriverSetting import vision from vision.constants import Context from vision.util import getContextRect @@ -29,7 +29,6 @@ import weakref from colors import RGB import core -import driverHandler class HighlightStyle( @@ -229,7 +228,7 @@ def getDisplayName(cls) -> str: def _get_supportedSettings(self) -> SupportedSettingType: return [ - driverHandler.BooleanDriverSetting( + BooleanDriverSetting( 'highlight%s' % (context[0].upper() + context[1:]), _contextOptionLabelsWithAccelerators[context], defaultVal=True diff --git a/source/visionEnhancementProviders/_exampleProvider_autoGui.py b/source/visionEnhancementProviders/_exampleProvider_autoGui.py index ccce002b353..446bcdaa066 100644 --- a/source/visionEnhancementProviders/_exampleProvider_autoGui.py +++ b/source/visionEnhancementProviders/_exampleProvider_autoGui.py @@ -4,7 +4,7 @@ # Copyright (C) 2019 NV Access Limited from vision import providerBase -import driverHandler +from autoSettingsUtils.driverSetting import BooleanDriverSetting, DriverSetting, NumericDriverSetting import gui from autoSettingsUtils.utils import StringParameterInfo from autoSettingsUtils.autoSettings import SupportedSettingType @@ -58,22 +58,22 @@ def getPreInitSettings(cls) -> SupportedSettingType: This is a class method because it does not rely on any instance state in this class. """ return [ - driverHandler.BooleanDriverSetting( + BooleanDriverSetting( "shouldDoX", # value stored in matching property name on class "Should Do X", defaultVal=True ), - driverHandler.BooleanDriverSetting( + BooleanDriverSetting( "shouldDoY", # value stored in matching property name on class "Should Do Y", defaultVal=False ), - driverHandler.NumericDriverSetting( + NumericDriverSetting( "amountOfZ", # value stored in matching property name on class "Amount of Z", defaultVal=11 ), - driverHandler.DriverSetting( + DriverSetting( # options for this come from a property with name generated by # f"available{settingID.capitalize()}s" # Note: @@ -100,7 +100,7 @@ def _getAvailableRuntimeSettings(self) -> SupportedSettingType: settings = [] if self._hasFeature("runtimeOnlySetting_externalValueLoad"): settings.extend([ - driverHandler.NumericDriverSetting( + NumericDriverSetting( "runtimeOnlySetting_externalValueLoad", # value stored in matching property name on class "Runtime Only amount, external value load", # no GUI default @@ -108,7 +108,7 @@ def _getAvailableRuntimeSettings(self) -> SupportedSettingType: ]) if self._hasFeature("runtimeOnlySetting_localDefault"): settings.extend([ - driverHandler.NumericDriverSetting( + NumericDriverSetting( "runtimeOnlySetting_localDefault", # value stored in matching property name on class "Runtime Only amount, local default", defaultVal=50, diff --git a/source/winKernel.py b/source/winKernel.py index 8645d295103..4d616e04b04 100644 --- a/source/winKernel.py +++ b/source/winKernel.py @@ -1,17 +1,17 @@ -#winKernel.py -#A part of NonVisual Desktop Access (NVDA) -#Copyright (C) 2006-2019 NV Access Limited, Rui Batista, Aleksey Sadovoy, Peter Vagner, Mozilla Corporation, Babbage B.V., Joseph Lee -#This file is covered by the GNU General Public License. -#See the file COPYING for more details. +# A part of NonVisual Desktop Access (NVDA) +# Copyright (C) 2006-2021 NV Access Limited, Rui Batista, Aleksey Sadovoy, Peter Vagner, +# Mozilla Corporation, Babbage B.V., Joseph Lee, Łukasz Golonka +# This file is covered by the GNU General Public License. +# See the file COPYING for more details. """Functions that wrap Windows API functions from kernel32.dll and advapi32.dll""" +from typing import Union import contextlib import ctypes import ctypes.wintypes -from ctypes import WinError -from ctypes import * -from ctypes.wintypes import * +from ctypes import byref, c_byte, POINTER, sizeof, Structure, windll, WinError +from ctypes.wintypes import BOOL, DWORD, HANDLE, LARGE_INTEGER, LPWSTR, LPVOID, WORD kernel32=ctypes.windll.kernel32 advapi32 = windll.advapi32 @@ -160,17 +160,62 @@ class SYSTEMTIME(ctypes.Structure): ("wMilliseconds", WORD) ) -def GetDateFormat(Locale,dwFlags,date,lpFormat): - """@Deprecated: use GetDateFormatEx instead.""" - if date is not None: - date=SYSTEMTIME(date.year,date.month,0,date.day,date.hour,date.minute,date.second,0) - lpDate=byref(date) - else: - lpDate=None - bufferLength=kernel32.GetDateFormatW(Locale, dwFlags, lpDate, lpFormat, None, 0) - buf=ctypes.create_unicode_buffer("", bufferLength) - kernel32.GetDateFormatW(Locale, dwFlags, lpDate, lpFormat, buf, bufferLength) - return buf.value + +class FILETIME(Structure): + _fields_ = ( + ("dwLowDateTime", DWORD), + ("dwHighDateTime", DWORD) + ) + + +class TIME_ZONE_INFORMATION(Structure): + _fields_ = ( + ("Bias", ctypes.wintypes.LONG), + ("StandardName", ctypes.wintypes.WCHAR * 32), + ("StandardDate", SYSTEMTIME), + ("StandardBias", ctypes.wintypes.LONG), + ("DaylightName", ctypes.wintypes.WCHAR * 32), + ("DaylightDate", SYSTEMTIME), + ("DaylightBias", ctypes.wintypes.LONG) + ) + + +def time_tToFileTime(time_tToConvert: float) -> FILETIME: + """Converts time_t as returned from `time.time` to a FILETIME structure. + Based on a code snipped from: + https://docs.microsoft.com/en-us/windows/win32/sysinfo/converting-a-time-t-value-to-a-file-time + """ + timeAsFileTime = FILETIME() + res = (int(time_tToConvert) * 10000000) + 116444736000000000 + timeAsFileTime.dwLowDateTime = res + timeAsFileTime.dwHighDateTime = res >> 32 + return timeAsFileTime + + +def FileTimeToSystemTime(lpFileTime: FILETIME, lpSystemTime: SYSTEMTIME) -> None: + if kernel32.FileTimeToSystemTime(byref(lpFileTime), byref(lpSystemTime)) == 0: + raise WinError() + + +def SystemTimeToTzSpecificLocalTime( + lpTimeZoneInformation: Union[TIME_ZONE_INFORMATION, None], + lpUniversalTime: SYSTEMTIME, + lpLocalTime: SYSTEMTIME +) -> None: + """Wrapper for `SystemTimeToTzSpecificLocalTime` from kernel32. + :param lpTimeZoneInformation: Either TIME_ZONE_INFORMATION containing info about the desired time zone + or `None` when the current time zone as configured in Windows settings should be used. + :param lpUniversalTime: SYSTEMTIME structure containing time in UTC wwhich you wish to convert. + : param lpLocalTime: A SYSTEMTIME structure in which time converted to the desired time zone would be placed. + :raises WinError + """ + if lpTimeZoneInformation is not None: + lpTimeZoneInformation = byref(lpTimeZoneInformation) + if kernel32.SystemTimeToTzSpecificLocalTime( + lpTimeZoneInformation, byref(lpUniversalTime), byref(lpLocalTime) + ) == 0: + raise WinError() + def GetDateFormatEx(Locale,dwFlags,date,lpFormat): if date is not None: @@ -183,18 +228,6 @@ def GetDateFormatEx(Locale,dwFlags,date,lpFormat): kernel32.GetDateFormatEx(Locale, dwFlags, lpDate, lpFormat, buf, bufferLength, None) return buf.value -def GetTimeFormat(Locale,dwFlags,date,lpFormat): - """@Deprecated: use GetTimeFormatEx instead.""" - if date is not None: - date=SYSTEMTIME(date.year,date.month,0,date.day,date.hour,date.minute,date.second,0) - lpTime=byref(date) - else: - lpTime=None - bufferLength=kernel32.GetTimeFormatW(Locale,dwFlags,lpTime,lpFormat, None, 0) - buf=ctypes.create_unicode_buffer("", bufferLength) - kernel32.GetTimeFormatW(Locale,dwFlags,lpTime,lpFormat, buf, bufferLength) - return buf.value - def GetTimeFormatEx(Locale,dwFlags,date,lpFormat): if date is not None: date=SYSTEMTIME(date.year,date.month,0,date.day,date.hour,date.minute,date.second,0) @@ -206,8 +239,6 @@ def GetTimeFormatEx(Locale,dwFlags,date,lpFormat): kernel32.GetTimeFormatEx(Locale,dwFlags,lpTime,lpFormat, buf, bufferLength) return buf.value -def openProcess(*args): - return kernel32.OpenProcess(*args) def virtualAllocEx(*args): res = kernel32.VirtualAllocEx(*args) diff --git a/source/winUser.py b/source/winUser.py index e007c443a81..d93543f3c38 100644 --- a/source/winUser.py +++ b/source/winUser.py @@ -13,6 +13,7 @@ from ctypes.wintypes import HWND, RECT, DWORD import winKernel from textUtils import WCHAR_ENCODING +import enum #dll handles user32=windll.user32 @@ -114,12 +115,6 @@ class GUITHREADINFO(Structure): CBS_OWNERDRAWFIXED=0x0010 CBS_OWNERDRAWVARIABLE=0x0020 CBS_HASSTRINGS=0x00200 -WM_NULL=0 -WM_QUIT=18 -WM_COPYDATA=74 -WM_NOTIFY=78 -WM_DEVICECHANGE=537 -WM_USER=1024 #PeekMessage PM_REMOVE=1 PM_NOYIELD=2 @@ -146,6 +141,7 @@ class GUITHREADINFO(Structure): WM_NOTIFY = 78 WM_USER = 1024 WM_QUIT = 18 +WM_DEVICECHANGE = 537 WM_DISPLAYCHANGE = 0x7e WM_GETTEXT=13 WM_GETTEXTLENGTH=14 @@ -377,6 +373,19 @@ class GUITHREADINFO(Structure): # The height of the virtual screen, in pixels. SM_CYVIRTUALSCREEN = 79 + +class MSGFLT(enum.IntEnum): + # Actions associated with ChangeWindowMessageFilterEx + # https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-changewindowmessagefilterex + # Adds the message to the filter. This has the effect of allowing the message to be received. + ALLOW = 1 + # Removes the message from the filter. This has the effect of blocking the message. + DISALLOW = 2 + # Resets the window message filter to the default. + # Any message allowed globally or process-wide will get through. + RESET = 0 + + def setSystemScreenReaderFlag(val): user32.SystemParametersInfoW(SPI_SETSCREENREADER,val,0,SPIF_UPDATEINIFILE|SPIF_SENDCHANGE) diff --git a/source/winVersion.py b/source/winVersion.py index 6a37aa9ecef..f3696cc295a 100644 --- a/source/winVersion.py +++ b/source/winVersion.py @@ -1,65 +1,144 @@ # A part of NonVisual Desktop Access (NVDA) -# Copyright (C) 2006-2020 NV Access Limited +# Copyright (C) 2006-2021 NV Access Limited, Bill Dengler, Joseph Lee # This file is covered by the GNU General Public License. # See the file COPYING for more details. +"""A module used to record Windows versions. +It is also used to define feature checks such as +making sure NVDA can run on a minimum supported version of Windows. +""" + import sys import os -import winUser +import functools + + +@functools.total_ordering +class WinVersion(object): + """ + Represents a Windows release. + Includes version major, minor, build, service pack information, + as well as tools such as checking for specific Windows 10 releases. + """ + + def __init__( + self, + major: int = 0, + minor: int = 0, + build: int = 0, + servicePack: str = "", + productType: str = "" + ): + self.major = major + self.minor = minor + self.build = build + self.servicePack = servicePack + self.productType = productType + + def _windowsVersionToReleaseName(self): + """Returns release names for a given Windows version. + For example, 6.1 will return 'Windows 7'. + For Windows 10, feature update release name will be included. + On server systems, client release names will be returned. + For example, 'Windows 10 1809' will be returned on Server 2019 systems. + """ + if (self.major, self.minor) == (6, 1): + return "Windows 7" + elif (self.major, self.minor) == (6, 2): + return "Windows 8" + elif (self.major, self.minor) == (6, 3): + return "Windows 8.1" + elif self.major == 10: + buildsToReleases = {build: release for release, build in WIN10_RELEASE_NAME_TO_BUILDS.items()} + if self.build in buildsToReleases: + return f"Windows 10 {buildsToReleases[self.build]}" + else: + # Windows Insider build. + return "Windows 10 prerelease" + else: + raise RuntimeError("Unknown Windows release") + + def __repr__(self): + winVersionText = [self._windowsVersionToReleaseName()] + winVersionText.append(f"({self.major}.{self.minor}.{self.build})") + if self.servicePack != "": + winVersionText.append(f"service pack {self.servicePack}") + if self.productType != "": + winVersionText.append(self.productType) + return " ".join(winVersionText) + + def __eq__(self, other): + return ( + (self.major, self.minor, self.build) + == (other.major, other.minor, other.build) + ) + + def __ge__(self, other): + return ( + (self.major, self.minor, self.build) + >= (other.major, other.minor, other.build) + ) + + +# Windows releases to WinVersion instances for easing comparisons. +WIN7 = WinVersion(major=6, minor=1, build=7600) +WIN7_SP1 = WinVersion(major=6, minor=1, build=7601, servicePack="1") +WIN8 = WinVersion(major=6, minor=2, build=9200) +WIN81 = WinVersion(major=6, minor=3, build=9600) +WIN10 = WIN10_1507 = WinVersion(major=10, minor=0, build=10240) +WIN10_1511 = WinVersion(major=10, minor=0, build=10586) +WIN10_1607 = WinVersion(major=10, minor=0, build=14393) +WIN10_1703 = WinVersion(major=10, minor=0, build=15063) +WIN10_1709 = WinVersion(major=10, minor=0, build=16299) +WIN10_1803 = WinVersion(major=10, minor=0, build=17134) +WIN10_1809 = WinVersion(major=10, minor=0, build=17763) +WIN10_1903 = WinVersion(major=10, minor=0, build=18362) +WIN10_1909 = WinVersion(major=10, minor=0, build=18363) +WIN10_2004 = WinVersion(major=10, minor=0, build=19041) +WIN10_20H2 = WinVersion(major=10, minor=0, build=19042) +WIN10_21H1 = WinVersion(major=10, minor=0, build=19043) + + +def getWinVer(): + """Returns a record of current Windows version NVDA is running on. + """ + winVer = sys.getwindowsversion() + return WinVersion( + major=winVer.major, + minor=winVer.minor, + build=winVer.build, + servicePack=winVer.service_pack, + productType=("workstation", "domain controller", "server")[winVer.product_type - 1] + ) -winVersion=sys.getwindowsversion() -winVersionText="{v.major}.{v.minor}.{v.build}".format(v=winVersion) -if winVersion.service_pack_major!=0: - winVersionText+=" service pack %d"%winVersion.service_pack_major - if winVersion.service_pack_minor!=0: - winVersionText+=".%d"%winVersion.service_pack_minor -winVersionText+=" %s" % ("workstation","domain controller","server")[winVersion.product_type-1] def isSupportedOS(): # NVDA can only run on Windows 7 Service pack 1 and above - return (winVersion.major,winVersion.minor,winVersion.service_pack_major) >= (6,1,1) + return getWinVer() >= WIN7_SP1 -def canRunVc2010Builds(): - return isSupportedOS() UWP_OCR_DATA_PATH = os.path.expandvars(r"$windir\OCR") + + def isUwpOcrAvailable(): return os.path.isdir(UWP_OCR_DATA_PATH) -WIN10_VERSIONS_TO_BUILDS = { - 1507: 10240, - 1511: 10586, - 1607: 14393, - 1703: 15063, - 1709: 16299, - 1803: 17134, - 1809: 17763, - 1903: 18362, - 1909: 18363, - 2004: 19041, - 2009: 19042, +WIN10_RELEASE_NAME_TO_BUILDS = { + "1507": 10240, + "1511": 10586, + "1607": 14393, + "1703": 15063, + "1709": 16299, + "1803": 17134, + "1809": 17763, + "1903": 18362, + "1909": 18363, + "2004": 19041, + "20H2": 19042, + "21H1": 19043, } -def isWin10(version: int = 1507, atLeast: bool = True): - """ - Returns True if NVDA is running on the supplied release version of Windows 10. If no argument is supplied, returns True for all public Windows 10 releases. - @param version: a release version of Windows 10 (such as 1903). - @param atLeast: return True if NVDA is running on at least this Windows 10 build (i.e. this version or higher). - """ - if winVersion.major != 10: - return False - try: - if atLeast: - return winVersion.build >= WIN10_VERSIONS_TO_BUILDS[version] - else: - return winVersion.build == WIN10_VERSIONS_TO_BUILDS[version] - except KeyError: - from logHandler import log - log.error("Unknown Windows 10 version {}".format(version)) - return False - - def isFullScreenMagnificationAvailable(): - return (winVersion.major, winVersion.minor) >= (6, 2) + return getWinVer() >= WIN8 diff --git a/tests/lint/.gitignore b/tests/lint/.gitignore index 74720b11b0e..e7b65a16059 100644 --- a/tests/lint/.gitignore +++ b/tests/lint/.gitignore @@ -1,2 +1,3 @@ current.diff current.lint +_lint.diff diff --git a/tests/lint/lintInstall/.gitignore b/tests/lint/lintInstall/.gitignore deleted file mode 100644 index 9ae18088231..00000000000 --- a/tests/lint/lintInstall/.gitignore +++ /dev/null @@ -1 +0,0 @@ -_executed_requirements.txt diff --git a/tests/lint/lintInstall/requirements.txt b/tests/lint/lintInstall/requirements.txt deleted file mode 100644 index 738eb918933..00000000000 --- a/tests/lint/lintInstall/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -####### requirements.txt ####### -# -###### Requirements for automated lint ###### -flake8 ~= 3.7.7 -flake8-tabs == 2.1.0 diff --git a/tests/lint/lintInstall/sconscript b/tests/lint/lintInstall/sconscript deleted file mode 100644 index b3b27f7b514..00000000000 --- a/tests/lint/lintInstall/sconscript +++ /dev/null @@ -1,35 +0,0 @@ -### -# This file is a part of the NVDA project. -# URL: http://www.nvaccess.org/ -# Copyright 2019 NV Access Limited. -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2.0, as published by -# the Free Software Foundation. -# 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. -# This license can be found at: -# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html -### - -import sys - -Import("env") - -doInstall = env.Command( - "_executed_requirements.txt", # $TARGET - "requirements.txt", # $SOURCE - [ - # Install deps from requirements file. - [ - sys.executable, "-m", "pip", - "install", "-r", "$SOURCE", - ], - # Copy the requirements file, this is used to stop scons from - # triggering pip from attempting re-installing when nothing has - # changed. Pip takes a long time to determine that deps are met. - Copy('$TARGET', '$SOURCE') - ] -) - -Return('doInstall') diff --git a/tests/lint/sconscript b/tests/lint/sconscript deleted file mode 100644 index ccd52a1b381..00000000000 --- a/tests/lint/sconscript +++ /dev/null @@ -1,62 +0,0 @@ -### -# This file is a part of the NVDA project. -# URL: http://www.nvaccess.org/ -# Copyright 2019 NV Access Limited. -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2.0, as published by -# the Free Software Foundation. -# 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. -# This license can be found at: -# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html -### - -import os -import sys - -import SCons - -Import("env") - -# Add system path to get access to git. -externalEnv = Environment( - ENV = {'PATH' : os.environ['PATH']} -) - -doLint = env.get("doLint") -baseBranch = env.get("base") -if doLint and not baseBranch: - errorMessage = ( - "Lint can not complete without base branch. " - "Try: 'scons lint base=origin/master' " - "See also /tests/lint/readme.md" - ) - raise SCons.SConf.SConfError(errorMessage) - -# Create a unified diff. Written to file for ease of manual inspection -diffTarget = externalEnv.Command( - "current.diff", - None, - [[sys.executable, "tests/lint/genDiff.py", baseBranch, '$TARGET']] -) - -# Pipe diff to flake8 for linting. -lintTarget = externalEnv.Command( - "current.lint", - "current.diff", - [[ - 'type', '$SOURCE', '|', # provide diff to stdin - sys.executable, - "-Xutf8", # UTF-8 mode (PEP 540, Python >= 3.7) - "-m", "flake8", - '--diff', # accept a unified diff from stdin - '--output-file=$TARGET', # output to a file to allow easier inspection - '--tee', # also output to stdout, so results appear in scons output - '--config="tests/lint/flake8.ini"', # use config file of complex options - '--exit-zero', # even if there are errors, don't stop the build. - ]] -) - -env.Depends(lintTarget, diffTarget) -Return('diffTarget', 'lintTarget') diff --git a/tests/sconscript b/tests/sconscript index 2f110d3cebe..187f1cade0a 100644 --- a/tests/sconscript +++ b/tests/sconscript @@ -16,43 +16,9 @@ import checkPot Import("env", "sourceDir", "pot") -unitTests = env.SConscript("unit/sconscript", exports=["env"]) -env.Depends(unitTests, sourceDir) -env.AlwaysBuild(unitTests) - -systemTests = env.SConscript("system/sconscript", exports=["env"]) -env.Depends(systemTests, sourceDir) -env.AlwaysBuild(systemTests) -env.Alias("systemTests", systemTests) - - -lintInstall = env.SConscript("lint/lintInstall/sconscript", exports=["env"]) -env.Depends(lintInstall, env.Dir("tests/lint/lintInstall/requirements.txt")) -env.Alias("lintInstall", lintInstall) - -lint = env.SConscript("lint/sconscript", exports=["env"]) -env.Depends(lint, lintInstall) -env.AlwaysBuild(lint) -env.Alias("lint", lint) - def checkPotAction(target, source, env): return checkPot.checkPot(source[0].abspath) checkPotTarget = env.Command("checkPot", pot, checkPotAction) env.Depends(checkPotTarget, pot) env.AlwaysBuild(checkPotTarget) env.Alias("checkPot", checkPotTarget) - -# Determine the targets for scons tests. -# If specific tests are explicitly specified, only run those. -explicitUnitTests = env.get("unitTests") -explicitCheckPot = "checkPot" in COMMAND_LINE_TARGETS -explicitSystemTests = "systemTests" in COMMAND_LINE_TARGETS -explicit = explicitUnitTests or explicitCheckPot or explicitSystemTests -tests = [] -if not explicit or explicitUnitTests: - tests.append(unitTests) -if not explicit or explicitCheckPot: - tests.append(checkPotTarget) -if explicit and explicitSystemTests: # only run system tests explicitly - tests.append(systemTests) -env.Alias("tests", tests) diff --git a/tests/system/guiDiff.robot b/tests/system/guiDiff.robot new file mode 100644 index 00000000000..3c6e84d247b --- /dev/null +++ b/tests/system/guiDiff.robot @@ -0,0 +1,8 @@ +--loglevel DEBUG +--outputdir .\testOutput\system +--xunit systemTests.xml +--pythonpath .\tests\system\libraries +--suite NVDASettings +--variable whichNVDA:source +--variable currentVersion:source +--variable cacheFolder:.\tests\system\settingsCache diff --git a/tests/system/libraries/ChromeLib.py b/tests/system/libraries/ChromeLib.py index 99079f96255..74ed48ecdb7 100644 --- a/tests/system/libraries/ChromeLib.py +++ b/tests/system/libraries/ChromeLib.py @@ -1,5 +1,5 @@ # A part of NonVisual Desktop Access (NVDA) -# Copyright (C) 2020 NV Access Limited +# Copyright (C) 2020-2021 NV Access Limited # This file may be used under the terms of the GNU General Public License, version 2 or later. # For more details see: https://www.gnu.org/licenses/gpl-2.0.html @@ -12,8 +12,15 @@ import tempfile as _tempfile from typing import Optional as _Optional from SystemTestSpy import ( + _blockUntilConditionMet, _getLib, ) +from SystemTestSpy.windows import ( + GetForegroundWindowTitle, + GetVisibleWindowTitles, + SetForegroundWindow, +) +import re from robot.libraries.BuiltIn import BuiltIn # Imported for type information @@ -51,6 +58,7 @@ def start_chrome(self, filePath): " --force-renderer-accessibility" " --suppress-message-center-popups" " --disable-notifications" + " -kiosk" f' "{filePath}"', shell=True, alias='chromeAlias', @@ -61,6 +69,15 @@ def start_chrome(self, filePath): _testCaseTitle = "NVDA Browser Test Case" _beforeMarker = "Before Test Case Marker" _afterMarker = "After Test Case Marker" + _loadCompleteString = "Test page load complete" + + @staticmethod + def getUniqueTestCaseTitle(testCase: str) -> str: + return f"{ChromeLib._testCaseTitle} ({abs(hash(testCase))})" + + @staticmethod + def getUniqueTestCaseTitleRegex(testCase: str) -> re.Pattern: + return re.compile(f"^{ChromeLib._testCaseTitle} \\({abs(hash(testCase))}\\)") @staticmethod def _writeTestFile(testCase) -> str: @@ -73,10 +90,11 @@ def _writeTestFile(testCase) -> str: filePath = ChromeLib._getTestCasePath("test.html") fileContents = (f""" - {ChromeLib._testCaseTitle} + {ChromeLib.getUniqueTestCaseTitle(testCase)} - +

{ChromeLib._beforeMarker}

+

Loading...

{testCase}

{ChromeLib._afterMarker}

@@ -92,26 +110,54 @@ def _wasStartMarkerSpoken(self, speech: str): marker = ChromeLib._beforeMarker return marker in speech and documentIndex < speech.index(marker) - def _moveToStartMarker(self, spy): - """ Press F6 until the start marker is spoken + def _waitForStartMarker(self, spy, lastSpeechIndex): + """ Wait until the page loads and NVDA reads the start marker. @param spy: @type spy: SystemTestSpy.speechSpyGlobalPlugin.NVDASpyLib @return: None """ for i in range(10): # set a limit on the number of tries. - # Small changes in Chrome mean the number of tab presses to get into the document can vary. builtIn.sleep("0.5 seconds") # ensure application has time to receive input - actualSpeech = self.getSpeechAfterKey('f6') + spy.wait_for_speech_to_finish() + actualSpeech = spy.get_speech_at_index_until_now(lastSpeechIndex) if self._wasStartMarkerSpoken(actualSpeech): break + lastSpeechIndex = spy.get_last_speech_index() else: # Exceeded the number of tries spy.dump_speech_to_log() builtIn.fail( - "Unable to tab to 'before sample' marker." + "Unable to locate 'before sample' marker." f" Too many attempts looking for '{ChromeLib._beforeMarker}'" " See NVDA log for full speech." ) + def _focusChrome(self, startsWithTestCaseTitle: re.Pattern): + """ Ensure chrome started and is focused. + Different versions of chrome have variations in how the title is presented. + This may mean that there is a separator between document name and application name. + E.G. "htmlTest Google Chrome", "html – Google Chrome" or perhaps no application name at all. + Rather than try to get this right, just use the doc title. + If this continues to be unreliable we could use Selenium or similar to start chrome and inform us + when it is ready. + """ + success, _success = _blockUntilConditionMet( + getValue=lambda: SetForegroundWindow(startsWithTestCaseTitle), + giveUpAfterSeconds=3, + intervalBetweenSeconds=0.5 + ) + if success: + return + windowInformation = "" + try: + windowInformation = f"Foreground Window: {GetForegroundWindowTitle()}.\n" + windowInformation += f"Open Windows: {GetVisibleWindowTitles()}" + except OSError as e: + builtIn.log(f"Couldn't retrieve active window information.\nException: {e}") + raise AssertionError( + "Unable to focus Chrome.\n" + f"{windowInformation}" + ) + def prepareChrome(self, testCase: str) -> None: """ Starts Chrome opening a file containing the HTML sample @@ -123,22 +169,23 @@ def prepareChrome(self, testCase: str) -> None: spy.wait_for_speech_to_finish() lastSpeechIndex = spy.get_last_speech_index() self.start_chrome(path) - # Ensure chrome started - # Different versions of chrome have variations in how the title is presented - # This may mean that there is a separator between document name and - # application name. E.G. "htmlTest Google Chrome", "html – Google Chrome" or perhaps no applcation - # name at all. - # Rather than try to get this right, just wait for the doc title. - # If this continues to be unreliable we could use solenium or similar to start chrome and inform us when - # it is ready. - applicationTitle = f"{self._testCaseTitle}" + self._focusChrome(ChromeLib.getUniqueTestCaseTitleRegex(testCase)) + applicationTitle = ChromeLib.getUniqueTestCaseTitle(testCase) appTitleIndex = spy.wait_for_specific_speech(applicationTitle, afterIndex=lastSpeechIndex) - spy.wait_for_speech_to_finish() - - afterTitleSpeech = spy.get_speech_at_index_until_now(appTitleIndex) - if not self._wasStartMarkerSpoken(afterTitleSpeech): - self._moveToStartMarker(spy) - + self._waitForStartMarker(spy, appTitleIndex) + # Move to the loading status line, and wait fore it to become complete + # the page has fully loaded. + spy.emulateKeyPress('downArrow') + for x in range(10): + builtIn.sleep("0.1 seconds") + actualSpeech = ChromeLib.getSpeechAfterKey('NVDA+UpArrow') + if actualSpeech == self._loadCompleteString: + break + else: # Exceeded the number of tries + spy.dump_speech_to_log() + builtIn.fail( + "Failed to wait for Test page load complete." + ) @staticmethod def getSpeechAfterKey(key) -> str: diff --git a/tests/system/libraries/NvdaLib.py b/tests/system/libraries/NvdaLib.py index 74db03359b1..4155454b3eb 100644 --- a/tests/system/libraries/NvdaLib.py +++ b/tests/system/libraries/NvdaLib.py @@ -49,14 +49,18 @@ def __init__(self): opSys.directory_should_exist(self.stagingDir) self.whichNVDA = builtIn.get_variable_value("${whichNVDA}", "source") + self._installFilePath = builtIn.get_variable_value("${installDir}", None) + self.NVDAInstallerCommandline = None if self.whichNVDA == "source": - self._runNVDAFilePath = _pJoin(self.repoRoot, "source/nvda.pyw") - self.baseNVDACommandline = f"pyw -3.7-32 {self._runNVDAFilePath}" + self._runNVDAFilePath = _pJoin(self.repoRoot, "runnvda.bat") + self.baseNVDACommandline = self._runNVDAFilePath elif self.whichNVDA == "installed": - self._runNVDAFilePath = _pJoin(_expandvars('%PROGRAMFILES%'), 'nvda', 'nvda.exe') + self._runNVDAFilePath = self.findInstalledNVDAPath() self.baseNVDACommandline = f'"{str(self._runNVDAFilePath)}"' + if self._installFilePath is not None: + self.NVDAInstallerCommandline = f'"{str(self._installFilePath)}"' else: - raise AssertionError("RobotFramework should be run with argument: '-v whichNVDA [source|installed]'") + raise AssertionError("RobotFramework should be run with argument: '-v whichNVDA:[source|installed]'") self.profileDir = _pJoin(self.stagingDir, "nvdaProfile") self.logPath = _pJoin(self.profileDir, 'nvda.log') @@ -65,8 +69,27 @@ def __init__(self): "nvdaTestRunLogs" ) + def findInstalledNVDAPath(self) -> Optional[str]: + NVDAFilePath = _pJoin(_expandvars('%PROGRAMFILES%'), 'nvda', 'nvda.exe') + legacyNVDAFilePath = _pJoin(_expandvars('%PROGRAMFILES%'), 'NVDA', 'nvda.exe') + exeErrorMsg = f"Unable to find installed NVDA exe. Paths tried: {NVDAFilePath}, {legacyNVDAFilePath}" + try: + opSys.file_should_exist(NVDAFilePath) + return NVDAFilePath + except AssertionError: + # Older versions of NVDA (<=2020.4) install the exe in NVDA\nvda.exe + opSys.file_should_exist(legacyNVDAFilePath, exeErrorMsg) + return legacyNVDAFilePath + + def ensureInstallerPathsExist(self): + fileWarnMsg = f"Unable to run NVDA installer unless path exists. Path given: {self._installFilePath}" + opSys.file_should_exist(self._installFilePath, fileWarnMsg) + opSys.create_directory(self.profileDir) + opSys.create_directory(self.preservedLogsDir) + def ensurePathsExist(self): - opSys.file_should_exist(self._runNVDAFilePath, "Unable to start NVDA unless path exists.") + fileWarnMsg = f"Unable to run NVDA installer unless path exists. Path given: {self._runNVDAFilePath}" + opSys.file_should_exist(self._runNVDAFilePath, fileWarnMsg) opSys.create_directory(self.profileDir) opSys.create_directory(self.preservedLogsDir) @@ -130,7 +153,28 @@ def _startNVDAProcess(self): ) return handle - def _connectToRemoteServer(self): + def _startNVDAInstallerProcess(self): + """Start NVDA Installer. + Use debug logging, replacing any current instance, using the system test profile directory + """ + _locations.ensureInstallerPathsExist() + command = ( + f"{_locations.NVDAInstallerCommandline}" + f" --debug-logging" + f" -r" + f" -c \"{_locations.profileDir}\"" + f" --log-file \"{_locations.logPath}\"" + ) + self.nvdaHandle = handle = process.start_process( + command, + shell=True, + alias=self.nvdaProcessAlias, + stdout=_pJoin(_locations.preservedLogsDir, self._createTestIdFileName("stdout.txt")), + stderr=_pJoin(_locations.preservedLogsDir, self._createTestIdFileName("stderr.txt")), + ) + return handle + + def _connectToRemoteServer(self, connectionTimeoutSecs=10): """Connects to the nvdaSpyServer Because we do not know how far through the startup NVDA is, we have to poll to check that the server is available. Importing the library immediately seems @@ -146,7 +190,7 @@ def _connectToRemoteServer(self): # therefore we use '_testRemoteServer' to ensure that we can in fact connect before proceeding. _blockUntilConditionMet( getValue=lambda: _testRemoteServer(self._spyServerURI, log=False), - giveUpAfterSeconds=10, + giveUpAfterSeconds=connectionTimeoutSecs, errorMessage=f"Unable to connect to {self._spyAlias}", ) builtIn.log(f"Connecting to {self._spyAlias}", level='DEBUG') @@ -193,6 +237,16 @@ def runKeyword(*args, **kwargs): ) return remoteLib + def start_NVDAInstaller(self, settingsFileName): + builtIn.log(f"Starting NVDA with config: {settingsFileName}") + self.setup_nvda_profile(settingsFileName) + nvdaProcessHandle = self._startNVDAInstallerProcess() + process.process_should_be_running(nvdaProcessHandle) + # Timeout is increased due to the installer load time and start up splash sound + self._connectToRemoteServer(connectionTimeoutSecs=30) + self.nvdaSpy.wait_for_NVDA_startup_to_complete() + return nvdaProcessHandle + def start_NVDA(self, settingsFileName): builtIn.log(f"Starting NVDA with config: {settingsFileName}") self.setup_nvda_profile(settingsFileName) @@ -234,6 +288,22 @@ def quit_NVDA(self): # remove the spy so that if nvda is run manually against this config it does not interfere. self.teardown_nvda_profile() + def quit_NVDAInstaller(self): + builtIn.log("Stopping nvdaSpy server: {}".format(self._spyServerURI)) + self.nvdaSpy.emulateKeyPress("insert+q") + self.nvdaSpy.wait_for_specific_speech("Exit NVDA") + self.nvdaSpy.emulateKeyPress("enter", blockUntilProcessed=False) + builtIn.sleep(1) + try: + _stopRemoteServer(self._spyServerURI, log=False) + except Exception: + raise + finally: + self.save_NVDA_log() + # remove the spy so that if nvda is run manually against this config it does not interfere. + self.teardown_nvda_profile() + + def getSpyLib(): """ Gets the spy library instance. This has been augmented with methods for all supported keywords. diff --git a/tests/system/libraries/SystemTestSpy/configManager.py b/tests/system/libraries/SystemTestSpy/configManager.py index 1db293237a7..cc9f23f91ce 100644 --- a/tests/system/libraries/SystemTestSpy/configManager.py +++ b/tests/system/libraries/SystemTestSpy/configManager.py @@ -48,11 +48,21 @@ def _installSystemTestSpyToScratchPad(repoRoot: str, scratchPadDir: str): _copyPythonLibs( pythonImports=[ # relative to the python path r"robotremoteserver", - r"xmlrpc", ], libsDest=spyPackageLibsDir ) + try: + opSys.directory_should_exist(_pJoin(spyPackageLibsDir, "xmlrpc")) + except AssertionError: + # installed copies of NVDA <= 2020.4 don't copy this over + _copyPythonLibs( + pythonImports=[ # relative to the python path + "xmlrpc", + ], + libsDest=spyPackageLibsDir + ) + # install the global plugin # Despite duplication, specify full paths for clarity. opSys.copy_file( diff --git a/tests/system/libraries/SystemTestSpy/speechSpyGlobalPlugin.py b/tests/system/libraries/SystemTestSpy/speechSpyGlobalPlugin.py index 365daee455c..fe5868cd888 100644 --- a/tests/system/libraries/SystemTestSpy/speechSpyGlobalPlugin.py +++ b/tests/system/libraries/SystemTestSpy/speechSpyGlobalPlugin.py @@ -159,8 +159,7 @@ def wait_for_NVDA_startup_to_complete(self): giveUpAfterSeconds=self._minTimeout(10), errorMessage="Unable to connect to nvdaSpy", ) - if self._isNvdaStartupComplete: - self.reset_all_speech_index() + self.reset_all_speech_index() def get_last_speech(self) -> str: return self._getSpeechAtIndex(-1) diff --git a/tests/system/libraries/SystemTestSpy/speechSpySynthDriver.py b/tests/system/libraries/SystemTestSpy/speechSpySynthDriver.py index 6f31942bc97..ee7d93c843c 100644 --- a/tests/system/libraries/SystemTestSpy/speechSpySynthDriver.py +++ b/tests/system/libraries/SystemTestSpy/speechSpySynthDriver.py @@ -12,7 +12,7 @@ import synthDriverHandler import extensionPoints -import speech +from speech.commands import IndexCommand # inform those who want to know that there is new speech post_speech = extensionPoints.Action() @@ -36,7 +36,7 @@ def check(cls): def speak(self, speechSequence): for item in speechSequence: - if isinstance(item, speech.IndexCommand): + if isinstance(item, IndexCommand): synthDriverHandler.synthIndexReached.notify(synth=self, index=item.index) synthDriverHandler.synthDoneSpeaking.notify(synth=self) post_speech.notify(speechSequence=speechSequence) diff --git a/tests/system/libraries/SystemTestSpy/windows.py b/tests/system/libraries/SystemTestSpy/windows.py new file mode 100644 index 00000000000..6742bad58eb --- /dev/null +++ b/tests/system/libraries/SystemTestSpy/windows.py @@ -0,0 +1,71 @@ +# A part of NonVisual Desktop Access (NVDA) +# Copyright (C) 2021 NV Access Limited +# This file may be used under the terms of the GNU General Public License, version 2 or later. +# For more details see: https://www.gnu.org/licenses/gpl-2.0.html + +"""This module provides functions to interact with the Windows system. +""" + +from ctypes.wintypes import HWND, LPARAM +from ctypes import c_bool, c_int, create_unicode_buffer, POINTER, WINFUNCTYPE, windll, WinError +import re +from typing import Callable, List, NamedTuple + + +class Window(NamedTuple): + hwnd: HWND + title: str + + +def _GetWindowTitle(hwnd: HWND) -> str: + length = windll.user32.GetWindowTextLengthW(hwnd) + if not length: + return '' + buff = create_unicode_buffer(length + 1) + if not windll.user32.GetWindowTextW(hwnd, buff, length + 1): + raise WinError() + return str(buff.value) + + +def _GetWindows( + filterUsingWindow: Callable[[Window], bool] = lambda _: True, +) -> List[Window]: + windows: List[Window] = [] + EnumWindowsProc = WINFUNCTYPE(c_bool, POINTER(c_int), POINTER(c_int)) + + def _append_title(hwnd: HWND, _lParam: LPARAM) -> bool: + window = Window(hwnd, _GetWindowTitle(hwnd)) + if filterUsingWindow(window): + windows.append(window) + return True + + if not windll.user32.EnumWindows(EnumWindowsProc(_append_title), 0): + raise WinError() + return windows + + +def _GetVisibleWindows() -> List[Window]: + return _GetWindows( + filterUsingWindow=lambda window: windll.user32.IsWindowVisible(window.hwnd) and bool(window.title) + ) + + +def SetForegroundWindow(targetTitle: re.Pattern) -> bool: + if re.match(targetTitle, GetForegroundWindowTitle()): + return True + windows = _GetWindows( + filterUsingWindow=lambda window: re.match(targetTitle, window.title) + ) + for window in windows: + return windll.user32.SetForegroundWindow(window.hwnd) + return False + + +def GetVisibleWindowTitles() -> List[str]: + windows = _GetVisibleWindows() + return [w.title for w in windows] + + +def GetForegroundWindowTitle() -> str: + hwnd = windll.user32.GetForegroundWindow() + return _GetWindowTitle(hwnd) diff --git a/tests/system/readme.md b/tests/system/readme.md index e5af3622138..920c9251fd1 100644 --- a/tests/system/readme.md +++ b/tests/system/readme.md @@ -2,50 +2,28 @@ ### Dependencies -To install all required packages move to the root directory of this repo and execute: - -`python -m pip install -r tests/system/requirements.txt` - +This build system uses the Robot test framework to execute the system tests. +Dependencies such as Robot are automatically installed for you when NVDA's build system Python virtual environment is set up, when running any of the high-level commands such as runsystemtests.bat, thus a developer should usually not have to worry about dependencies. + ### Running the tests -You can run the tests with `scons` or manually - -#### Scons (easier) -`scons systemTests` - -To run only specific system tests, - specify them using the `filter` variable on the command line. -This filter accepts wildcard characters. - -``` -scons systemTests filter="Read welcome dialog" -``` - -#### Manually (faster) - -SCons takes quite a long time to initialize and actually start running the tests, -if you are running the tests repeatedly consider running them manually. -These tests should be run from the windows command prompt (cmd.exe) from the root directory - of your NVDA repository. - -``` -python -m robot --argumentfile ./tests/system/robotArgs.robot ./tests/system/robot -``` -Note that the path to the tests directory is required and must be the final argument. +You can run the tests with `runsystemtests.bat`. +Running this script with no arguments will run all system tests found in tests\system\robot, against the current source copy of NVDA. +Any extra arguments provided to this script are forwarded on to Robot. To run a single test, add the `--test` argument (wildcards accepted). ``` -python -m robot --test "starts" ... +runsystemtests --test "starts" ... ``` To run all tests with a particular tag use `-i`: ``` -python -m robot -i "chrome" ... +runsystemtests -i "chrome" ... ``` Other options exit for specifying tests to run (e.g. by suite, tag, etc). -Consult `python -m robot --help` +Consult `runsystemtests --help` ### Getting the results @@ -65,11 +43,11 @@ checkbox labelled by inner element checkbox_labelled_by_inner_element ``` -When the tests are run, the option `--exclude excluded_from_build` is given to Robot. +When the tests are run, the option `--exclude excluded_from_build` is given to Robot internally. See [description of test args](#test-args) ### Test args -Common arguments (for both `scons` and AppVeyor) are kept in the `tests\system\robotArgs.robot` file. +Common arguments are kept in the `tests\system\robotArgs.robot` file. The `whichNVDA` argument allows the tests to be run against an installed copy of NVDA (first ensure it is compatible with the tests). Note valid values are: @@ -77,6 +55,9 @@ of NVDA (first ensure it is compatible with the tests). Note valid values are: the tests are run from an administrator command prompt. * "source" +The `installDir` argument performs a smoke test on the installation process given a path to the installer exe. For example `--variable installDir:".\path\to\nvda_installer.exe"`. +This should be used with `--variable whichNVDA:installed --include installer`. + ### Overview Robot Framework loads and parses the test files and their libraries. @@ -122,3 +103,29 @@ NVDA is started with the `-c` option to specify this profile directory to be use Both Robot Framework and NVDA logs are captured in the `testOutput` directory in the repo root. NVDA logs (NVDA log, stdOut, and stdErr for each test) are under the `nvdaTestRunLogs` directory. The log files are named by suite and test name. + +### Comparing changes to NVDA Settings +`.\runsettingsdiff.bat` is a tool used to compare the settings dialog by reading text and generating screenshots for comparison. The default behaviour is to run using the source code and output to `.\tests\system\settingsCache\source`. + + +#### Usage +To check for unreleased changes to the settings dialogs, one can use this tool to compare against two copies of NVDA. + +The following arguments should be used with the script. + +Default arguments used are stored in `.\tests\system\guiDiff.robot` + +- `--variable whichNVDA:[installed|source]` to decide where to run NVDA from +- `--variable cacheFolder:[filePath]` screenshots and text files of each settings panel are generated in `$cacheFolder\$currentVersion` +- `--variable currentVersion:[nvdaVersion]` where `[nvdaVersion]` is used to name the generated screenshot and cache folder +- `--variable compareVersion:[nvdaVersion]` using a `$nvdaVersion` that this script has already been run against, run the system tests and fail if there are differences between the read text. This generates a multiline diff. + +#### Example usage to compare settings between NVDA 2020.4 and the current source + +1. Install NVDA 2020.4 +1. Run `.\runsettingsdiff.bat -v whichNVDA:installed -v currentVersion:2020.4` +1. Run `.\runsettingsdiff.bat -v whichNVDA:source -v currentVersion:source -v compareVersion:2020.4` + - The test will fail and display a diff of any read changes +1. Use a diff tool to compare folders: + - `diff ./tests/system/settingsCache/2020.4 ./tests/system/settingsCache/source` + - [ImageMagick Compare](https://imagemagick.org/script/compare.php) can be used to compare images diff --git a/tests/system/requirements.txt b/tests/system/requirements.txt deleted file mode 100644 index dd3c3abbea0..00000000000 --- a/tests/system/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -nose -robotframework -robotremoteserver -robotframework-screencaplibrary diff --git a/tests/system/robot/NVDAInstaller.py b/tests/system/robot/NVDAInstaller.py new file mode 100644 index 00000000000..4ed1b0d860e --- /dev/null +++ b/tests/system/robot/NVDAInstaller.py @@ -0,0 +1,70 @@ +# A part of NonVisual Desktop Access (NVDA) +# Copyright (C) 2020-2021 NV Access Limited +# This file may be used under the terms of the GNU General Public License, version 2 or later. +# For more details see: https://www.gnu.org/licenses/gpl-2.0.html + +"""Logic for NVDA installation process tests. +""" + +from robot.libraries.BuiltIn import BuiltIn +# relative import not used for 'systemTestUtils' because the folder is added to the path for 'libraries' +# imported methods start with underscore (_) so they don't get imported into robot files as keywords +from SystemTestSpy import ( + _getLib, +) + +# Imported for type information +from robot.libraries.Process import Process as _ProcessLib + +from AssertsLib import AssertsLib as _AssertsLib + +import NvdaLib as _nvdaLib +from NvdaLib import NvdaLib as _nvdaRobotLib +_nvdaProcessAlias = _nvdaRobotLib.nvdaProcessAlias + +_builtIn: BuiltIn = BuiltIn() +_process: _ProcessLib = _getLib("Process") +_asserts: _AssertsLib = _getLib("AssertsLib") + + +def read_install_dialog(): + "Smoke test the launcher dialogs used to install NVDA" + + spy = _nvdaLib.getSpyLib() + launchDialog = spy.wait_for_specific_speech("NVDA Launcher") # ensure the dialog is present. + spy.wait_for_speech_to_finish() + spy.get_speech_at_index_until_now(launchDialog) + + _builtIn.sleep(1) # the dialog is not always receiving keypresses, wait a little longer for it + # agree to the License Agreement + spy.emulateKeyPress("alt+a") + + # start install + spy.emulateKeyPress("alt+i") + + spy.wait_for_specific_speech("To install NVDA to your hard drive, please press the Continue button.") + + # exit NVDA Installer + spy.emulateKeyPress("escape") + + +def read_portable_copy_dialog(): + "Smoke test the launcher dialogs used to create a portable copy of NVDA" + + spy = _nvdaLib.getSpyLib() + launchDialog = spy.wait_for_specific_speech("NVDA Launcher") # ensure the dialog is present. + spy.wait_for_speech_to_finish() + spy.get_speech_at_index_until_now(launchDialog) + + _builtIn.sleep(1) # the dialog is not always receiving keypresses, wait a little longer for it + # agree to the License Agreement + spy.emulateKeyPress("alt+a") + + # start portable copy + spy.emulateKeyPress("alt+p") + + spy.wait_for_specific_speech( + "To create a portable copy of NVDA, please select the path and other options and then press Continue") + + # exit NVDA Installer + spy.emulateKeyPress("escape") diff --git a/tests/system/robot/NVDAInstaller.robot b/tests/system/robot/NVDAInstaller.robot new file mode 100644 index 00000000000..1c4d20f18ff --- /dev/null +++ b/tests/system/robot/NVDAInstaller.robot @@ -0,0 +1,38 @@ +# A part of NonVisual Desktop Access (NVDA) +# Copyright (C) 2021 NV Access Limited +# This file may be used under the terms of the GNU General Public License, version 2 or later. +# For more details see: https://www.gnu.org/licenses/gpl-2.0.html +*** Settings *** +Documentation Smoke test the installation process +Force Tags smoke test installer +Suite Setup Run Keyword If not r'${installDir}' Fail "--installDir not supplied" + +# for start & quit in Test Setup and Test Teardown +Library NvdaLib.py +Library NVDAInstaller.py +Library ScreenCapLibrary + +Test Setup default startup +Test Teardown default teardown + +*** Keywords *** + +default teardown + ${screenshotName}= create_preserved_test_output_filename failedTest.png + Run Keyword If Test Failed Take Screenshot ${screenShotName} + quit NVDAInstaller + +default startup + start NVDAInstaller standard-dontShowWelcomeDialog.ini + +default pass execution + +*** Test Cases *** + +Read install dialog + [Documentation] Ensure that the install dialog can be read in full + read_install_dialog # run test + +Read install dialog portable copy + [Documentation] Ensure that the portable copy install dialog can be read in full + read_portable_copy_dialog # run test diff --git a/tests/system/robot/NVDASettings.py b/tests/system/robot/NVDASettings.py new file mode 100644 index 00000000000..db318a6f6d8 --- /dev/null +++ b/tests/system/robot/NVDASettings.py @@ -0,0 +1,72 @@ +# A part of NonVisual Desktop Access (NVDA) +# Copyright (C) 2021 NV Access Limited +# This file may be used under the terms of the GNU General Public License, version 2 or later. +# For more details see: https://www.gnu.org/licenses/gpl-2.0.html + +"""Logic for smoketesting the settings. +""" + +from robot.libraries.BuiltIn import BuiltIn +# relative import not used for 'systemTestUtils' because the folder is added to the path for 'libraries' +# imported methods start with underscore (_) so they don't get imported into robot files as keywords +from SystemTestSpy import ( + _getLib, +) + +# Imported for type information +from robot.libraries.Process import Process as _ProcessLib + +from AssertsLib import AssertsLib as _AssertsLib + +import os +from typing import Optional +import NvdaLib as _nvdaLib +from NvdaLib import NvdaLib as _nvdaRobotLib +_nvdaProcessAlias = _nvdaRobotLib.nvdaProcessAlias + +_builtIn: BuiltIn = BuiltIn() +_process: _ProcessLib = _getLib("Process") +_asserts: _AssertsLib = _getLib("AssertsLib") + + +def navigate_to_settings(settingsName): + spy = _nvdaLib.getSpyLib() + # open settings menu + spy.emulateKeyPress("NVDA+n") + spy.emulateKeyPress("p") + spy.emulateKeyPress("s") + spy.emulateKeyPress("leftWindows+upArrow") # maximise + spy.wait_for_speech_to_finish() + + # naviagte to setting + for letter in settingsName.lower(): + spy.emulateKeyPress(letter) + + spy.wait_for_speech_to_finish() + spy.reset_all_speech_index() + + +def read_settings(settingsName, cacheFolder, currentVersion, compareVersion: Optional[str] = None): + spy = _nvdaLib.getSpyLib() + start_speech_index = spy.get_next_speech_index() + advancedWarning = "I understand that changing these settings may cause NVDA to function incorrectly." + + # read new setting + lastSpeech = "" + while "OK" not in lastSpeech: + spy.emulateKeyPress("tab") + spy.wait_for_speech_to_finish() + lastSpeech = spy.get_last_speech() + if lastSpeech.split(" ")[0] == advancedWarning: + spy.emulateKeyPress("space") + + actualSpeech = spy.get_speech_at_index_until_now(start_speech_index) + os.makedirs(f"{cacheFolder}/{currentVersion}", exist_ok=True) + with open(f"{cacheFolder}/{currentVersion}/{settingsName}.txt", "w") as f: + f.write(actualSpeech) + + if compareVersion: + with open(f"{cacheFolder}/{compareVersion}/{settingsName}.txt", "r") as f: + compareText = f.read() + + _asserts.strings_match(compareText, actualSpeech) diff --git a/tests/system/robot/NVDASettings.robot b/tests/system/robot/NVDASettings.robot new file mode 100644 index 00000000000..354485956ea --- /dev/null +++ b/tests/system/robot/NVDASettings.robot @@ -0,0 +1,65 @@ +# A part of NonVisual Desktop Access (NVDA) +# Copyright (C) 2021 NV Access Limited +# This file may be used under the terms of the GNU General Public License, version 2 or later. +# For more details see: https://www.gnu.org/licenses/gpl-2.0.html +*** Settings *** +Documentation Smoke test the settings panel, use for checking diffs +Force Tags NVDA smoke test excluded_from_build + +# for start & quit in Test Setup and Test Test Teardown +Library NvdaLib.py +Library NVDASettings.py +Library ScreenCapLibrary + +Test Setup start NVDA standard-dontShowWelcomeDialog.ini +Test Teardown default teardown + +*** Keywords *** +default run read test + [Arguments] ${settingsName} + navigate to settings ${settingsName} + Create Directory ${cacheFolder}/${currentVersion} + Set Screenshot Directory ${cacheFolder}/${currentVersion} + Take Screenshot ${settingsName}.png + read settings ${settingsName} ${cacheFolder} ${currentVersion} ${compareVersion} + Take Screenshot ${settingsName}-end.png + +default teardown + quit NVDA + +*** Test Cases *** +Read General + default run read test General + +Read Speech + default run read test Speech + +Read Braille + default run read test Braille + +Read Vision + default run read test Vision + +Read Keyboard + default run read test Keyboard + +Read Mouse + default run read test Mouse + +Read Review Cursor + default run read test Review Cursor + +Read Input Composition + default run read test Input Composition + +Read Object Presentation + default run read test Object Presentation + +Read Browse Mode + default run read test Browse Mode + +Read Document Formatting + default run read test Document Formatting + +Read Advanced + default run read test Advanced diff --git a/tests/system/robot/chromeTests.py b/tests/system/robot/chromeTests.py index d9cd0aa6b74..f5baab177c9 100644 --- a/tests/system/robot/chromeTests.py +++ b/tests/system/robot/chromeTests.py @@ -345,3 +345,37 @@ def test_ariaCheckbox_browseMode(): actualSpeech, "Sandwich Condiments grouping list with 4 items Lettuce check box not checked" ) + + +def test_i12147(): + """ + New focus target should be announced if the triggering element is removed when activated. + """ + _chrome.prepareChrome( + f""" +
+ +

target 0

+
+ + """ + ) + # Jump to the first button (the trigger) + actualSpeech = _chrome.getSpeechAfterKey("tab") + _asserts.strings_match( + actualSpeech, + "trigger 0 button" + ) + # Activate the button, we should hear the new focus target. + actualSpeech = _chrome.getSpeechAfterKey("enter") + _asserts.strings_match( + actualSpeech, + "target 0 heading level 4" + ) diff --git a/tests/system/robot/chromeTests.robot b/tests/system/robot/chromeTests.robot index 8063420da2a..b84747cdba6 100644 --- a/tests/system/robot/chromeTests.robot +++ b/tests/system/robot/chromeTests.robot @@ -12,7 +12,7 @@ Library NvdaLib.py Library chromeTests.py Library ScreenCapLibrary -Test Setup start NVDA standard-dontShowWelcomeDialog.ini +Test Setup default setup Test Teardown default teardown *** Keywords *** @@ -22,6 +22,9 @@ default teardown exit chrome quit NVDA +default setup + start NVDA standard-dontShowWelcomeDialog.ini + *** Test Cases *** checkbox labelled by inner element @@ -47,3 +50,6 @@ ARIA checkbox [Documentation] Navigate to an unchecked checkbox in reading mode. [Tags] aria-at test_ariaCheckbox_browseMode +i12147 + [Documentation] New focus target should be announced if the triggering element is removed when activated + test_i12147 diff --git a/tests/system/robotArgs.robot b/tests/system/robotArgs.robot index 009131d5c21..d2eb08a522f 100644 --- a/tests/system/robotArgs.robot +++ b/tests/system/robotArgs.robot @@ -2,5 +2,7 @@ --outputdir testOutput\system --xunit systemTests.xml --pythonpath .\tests\system\libraries +--include NVDA --exclude excluded_from_build --variable whichNVDA:source +--variable installDir: diff --git a/tests/system/sconscript b/tests/system/sconscript deleted file mode 100644 index 2c716fa8280..00000000000 --- a/tests/system/sconscript +++ /dev/null @@ -1,43 +0,0 @@ -### -#This file is a part of the NVDA project. -#URL: http://www.nvaccess.org/ -#Copyright 2017 NV Access Limited. -#This program is free software: you can redistribute it and/or modify -#it under the terms of the GNU General Public License version 2.0, as published by -#the Free Software Foundation. -#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. -#This license can be found at: -#http://www.gnu.org/licenses/old-licenses/gpl-2.0.html -### - -import sys -Import("env") - -import os - -tests = env.get("filter") -# This should probably be limited just to the environment vars needed for NVDA to run. -# EG: testRunnerEnv = Environment( -# ENV={ -# 'PATH': os.environ['PATH'], -# 'TEMP': os.environ['TEMP'], -# 'TMP': os.environ['TMP'], -# }) -# On CI, tests are run directly (using CI env), not via SCons. See appveyor.yml. -# Running tests via SCons is included for developer convenience. Since it is likely that the -# developers environment is appropriate for running NVDA, we will just inherit it. -testRunnerEnv = Environment(ENV=os.environ) # noqa: F821 Flake8 does not recognise scons patterns. - -cmd = [ - sys.executable, "-m", "robot", - "--argumentfile", "./tests/system/robotArgs.robot", -] -if tests: - # run specific tests - cmd += ['--test="{}"'.format(tests)] - -cmd.append("./tests/system/robot") # must be the final argument -target = testRunnerEnv.Command(".", None, [cmd]) -Return('target') \ No newline at end of file diff --git a/tests/system/settingsCache/2020.4/Advanced.txt b/tests/system/settingsCache/2020.4/Advanced.txt new file mode 100644 index 00000000000..f2a772b3af8 --- /dev/null +++ b/tests/system/settingsCache/2020.4/Advanced.txt @@ -0,0 +1,23 @@ +Advanced property page Warning! The following settings are for advanced users. Changing them may cause NVDA to function incorrectly. Please only change these if you know what you are doing or have been specifically instructed by NVDA developers. +I understand that changing these settings may cause NVDA to function incorrectly. check box not checked +space +checked +Restore defaults button +NVDA Development grouping +Enable loading custom code from Developer Scratchpad directory check box checked +Open developer scratchpad directory button +Microsoft UI Automation grouping +Enable selective registration for UI Automation events and property changes check box not checked Alt plus s +Use UI Automation to access Microsoft Word document controls when available check box not checked Alt plus w +Use UI Automation to access the Windows Console when available check box not checked Alt plus o +Speak passwords in UIA consoles (may improve performance) check box not checked Alt plus p +Terminal programs grouping +Use the new typed character support in Windows Console when available check box checked Alt plus y +Speech grouping +Attempt to cancel speech for expired focus events: combo box Default (No) collapsed +Editable Text grouping +Caret movement timeout (in ms) edit selected 100 +Debug logging grouping +Enabled logging categories list +hw Io check box not checked +OK button \ No newline at end of file diff --git a/tests/system/settingsCache/2020.4/Braille.txt b/tests/system/settingsCache/2020.4/Braille.txt new file mode 100644 index 00000000000..5992820b34d --- /dev/null +++ b/tests/system/settingsCache/2020.4/Braille.txt @@ -0,0 +1,19 @@ +Braille property page +Braille display grouping +Braille display edit read only multi line Alt plus d Automatic +Change... button Alt plus h +Output table: combo box Unified English Braille Code grade 1 collapsed Alt plus o +Input table: combo box Unified English Braille Code grade 1 collapsed Alt plus i +Expand to computer braille for the word at the cursor check box checked Alt plus x +Show cursor check box checked Alt plus s +Blink cursor check box checked +Cursor blink rate (ms) edit selected 500 +Cursor shape for focus: combo box Dots 7 and 8 collapsed Alt plus f +Cursor shape for review: combo box Dot 8 collapsed Alt plus r +Show messages combo box Use timeout collapsed +Message timeout (sec) edit Alt plus t selected 4 +Tether Braille: combo box automatically collapsed Alt plus r +Read by paragraph check box not checked Alt plus p +Avoid splitting words when possible check box checked Alt plus w +Focus context presentation: combo box Fill display for context changes collapsed +OK button \ No newline at end of file diff --git a/tests/system/settingsCache/2020.4/Browse Mode.txt b/tests/system/settingsCache/2020.4/Browse Mode.txt new file mode 100644 index 00000000000..464264b901f --- /dev/null +++ b/tests/system/settingsCache/2020.4/Browse Mode.txt @@ -0,0 +1,13 @@ +Browse Mode property page +Maximum number of characters on one line edit Alt plus m selected 100 +Number of lines per page edit Alt plus n selected 25 +Use screen layout (when supported) check box checked Alt plus s +Enable browse mode on page load check box checked Alt plus e +Automatic Say All on page load check box not checked Alt plus s +Include layout tables check box not checked Alt plus a +Automatic focus mode for focus changes check box checked +Automatic focus mode for caret movement check box not checked +Audio indication of focus and browse modes check box not checked +Trap all non command gestures from reaching the document check box checked Alt plus t +Automatically set system focus to focusable elements check box not checked Alt plus f +OK button \ No newline at end of file diff --git a/tests/system/settingsCache/2020.4/Document Formatting.txt b/tests/system/settingsCache/2020.4/Document Formatting.txt new file mode 100644 index 00000000000..f471a0b14e6 --- /dev/null +++ b/tests/system/settingsCache/2020.4/Document Formatting.txt @@ -0,0 +1,39 @@ +Document Formatting property page The following options control the types of document formatting reported by NVDA. +Font grouping +Font name check box not checked Alt plus f +Font size check box not checked Alt plus s +Font attributes check box not checked Alt plus u +Superscripts and subscripts check box not checked Alt plus p +Emphasis check box not checked Alt plus m +Marked (highlighted text) check box checked Alt plus k +Style check box not checked Alt plus y +Colors check box not checked Alt plus c +Document information grouping +Notes and comments check box checked Alt plus t +Editor revisions check box checked Alt plus e +Spelling errors check box checked Alt plus r +Pages and spacing grouping +Pages check box checked Alt plus p +Line numbers check box not checked Alt plus n +Line indentation reporting: combo box Off collapsed Alt plus i +Paragraph indentation check box not checked Alt plus p +Line spacing check box not checked Alt plus l +Alignment check box not checked Alt plus a +Table information grouping +Tables check box checked Alt plus t +Row slash column headers check box checked Alt plus e +Cell coordinates check box checked Alt plus o +Cell borders: combo box Off collapsed Alt plus b +Elements grouping +Headings check box checked Alt plus h +Links check box checked Alt plus k +Graphics check box checked Alt plus g +Lists check box checked Alt plus l +Block quotes check box checked Alt plus q +Groupings check box checked Alt plus g +Landmarks and regions check box checked Alt plus d +Articles check box not checked Alt plus c +Frames check box checked Alt plus m +Clickable check box checked Alt plus c +Report formatting changes after the cursor (can cause a lag) check box not checked Alt plus g +OK button \ No newline at end of file diff --git a/tests/system/settingsCache/2020.4/General.txt b/tests/system/settingsCache/2020.4/General.txt new file mode 100644 index 00000000000..cedb018f4f1 --- /dev/null +++ b/tests/system/settingsCache/2020.4/General.txt @@ -0,0 +1,12 @@ +General property page +NVDA Language (requires restart): combo box User default collapsed Alt plus l +Save configuration when exiting NVDA check box checked Alt plus s +Show exit options when exiting NVDA check box checked Alt plus w +Play sounds when starting or exiting NVDA check box checked Alt plus p +Start NVDA after I sign in check box not checked Alt plus a +Use NVDA during sign in (requires administrator privileges) check box not checked +Use currently saved settings during sign in and on secure screens (requires administrator privileges) button +Automatically check for updates to NVDA check box not checked Alt plus u +Notify for pending update on startup check box not checked Alt plus p +Allow the NVDA project to gather NVDA usage statistics check box not checked +OK button \ No newline at end of file diff --git a/tests/system/settingsCache/2020.4/Input Composition.txt b/tests/system/settingsCache/2020.4/Input Composition.txt new file mode 100644 index 00000000000..b41c7345af6 --- /dev/null +++ b/tests/system/settingsCache/2020.4/Input Composition.txt @@ -0,0 +1,7 @@ +Input Composition property page +Automatically report all available candidates check box checked Alt plus c +Announce selected candidate check box checked Alt plus s +Always include short character description when announcing candidates check box checked Alt plus d +Report changes to the reading string check box checked Alt plus r +Report changes to the composition string check box checked Alt plus c +OK button \ No newline at end of file diff --git a/tests/system/settingsCache/2020.4/Keyboard.txt b/tests/system/settingsCache/2020.4/Keyboard.txt new file mode 100644 index 00000000000..15c15719f6c --- /dev/null +++ b/tests/system/settingsCache/2020.4/Keyboard.txt @@ -0,0 +1,14 @@ +Keyboard property page +Keyboard layout: combo box desktop collapsed Alt plus k +Select NVDA Modifier Keys list +caps lock check box not checked +Speak typed characters check box checked Alt plus c +Speak typed words check box not checked Alt plus w +Speech interrupt for typed characters check box checked Alt plus i +Speech interrupt for Enter key check box checked Alt plus n +Allow skim reading in Say All check box not checked Alt plus r +Beep if typing lowercase letters when caps lock is on check box checked Alt plus b +Speak command keys check box not checked Alt plus o +Play sound for spelling errors while typing check box checked Alt plus s +Handle keys from other applications check box checked Alt plus a +OK button \ No newline at end of file diff --git a/tests/system/settingsCache/2020.4/Mouse.txt b/tests/system/settingsCache/2020.4/Mouse.txt new file mode 100644 index 00000000000..d63a58016cd --- /dev/null +++ b/tests/system/settingsCache/2020.4/Mouse.txt @@ -0,0 +1,9 @@ +Mouse property page +Report mouse shape changes check box not checked Alt plus s +Enable mouse tracking check box checked Alt plus t +Text unit resolution: combo box paragraph collapsed Alt plus u +Report role when mouse enters object check box not checked Alt plus r +Play audio coordinates when mouse moves check box not checked Alt plus p +Brightness controls audio coordinates volume check box not checked Alt plus b +Ignore mouse input from other applications check box not checked Alt plus a +OK button \ No newline at end of file diff --git a/tests/system/settingsCache/2020.4/Object Presentation.txt b/tests/system/settingsCache/2020.4/Object Presentation.txt new file mode 100644 index 00000000000..1bc63892cef --- /dev/null +++ b/tests/system/settingsCache/2020.4/Object Presentation.txt @@ -0,0 +1,12 @@ +Object Presentation property page +Report tooltips check box not checked Alt plus t +Report notifications check box checked Alt plus n +Report object shortcut keys check box checked Alt plus k +Report object position information check box checked Alt plus p +Guess object position information when unavailable check box not checked Alt plus g +Report object descriptions check box checked Alt plus d +Progress bar output: combo box Beep collapsed Alt plus b +Report background progress bars check box not checked Alt plus r +Report dynamic content changes check box checked Alt plus c +Play a sound when auto suggestions appear check box checked Alt plus a +OK button \ No newline at end of file diff --git a/tests/system/settingsCache/2020.4/Review Cursor.txt b/tests/system/settingsCache/2020.4/Review Cursor.txt new file mode 100644 index 00000000000..20adaabba38 --- /dev/null +++ b/tests/system/settingsCache/2020.4/Review Cursor.txt @@ -0,0 +1,6 @@ +Review Cursor property page +Follow system focus check box checked Alt plus f +Follow System Caret check box checked Alt plus c +Follow mouse cursor check box not checked Alt plus m +Simple review mode check box checked Alt plus s +OK button \ No newline at end of file diff --git a/tests/system/settingsCache/2020.4/Speech.txt b/tests/system/settingsCache/2020.4/Speech.txt new file mode 100644 index 00000000000..c5f14f02ab0 --- /dev/null +++ b/tests/system/settingsCache/2020.4/Speech.txt @@ -0,0 +1,14 @@ +Speech property page +Synthesizer grouping +Synthesizer edit read only multi line Alt plus s System test speech spy +Change... button Alt plus h +Automatic language switching (when supported) check box checked +Automatic dialect switching (when supported) check box not checked +Punctuation slash symbol level: combo box some collapsed Alt plus l +Trust voice's language when processing characters and symbols check box checked +Include Unicode Consortium data (including emoji) when processing characters and symbols check box checked +Capital pitch change percentage edit selected 30 +Say cap before capitals check box not checked Alt plus c +Beep for capitals check box not checked Alt plus b +Use spelling functionality if supported check box checked Alt plus s +OK button \ No newline at end of file diff --git a/tests/system/settingsCache/2020.4/Vision.txt b/tests/system/settingsCache/2020.4/Vision.txt new file mode 100644 index 00000000000..ed6e7acd75e --- /dev/null +++ b/tests/system/settingsCache/2020.4/Vision.txt @@ -0,0 +1,11 @@ +Vision property page Configure visual aids. +Visual Highlight grouping +Enable Highlighting check box not checked Alt plus e +Highlight system focus check box not checked Alt plus c +Highlight navigator object check box not checked Alt plus o +Highlight browse mode cursor check box not checked Alt plus m +Screen Curtain grouping +Make screen black (immediate effect) check box not checked +Always show a warning when loading Screen Curtain check box checked Alt plus s +Play sound when toggling Screen Curtain check box checked Alt plus p +OK button \ No newline at end of file diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py index 4e4d26c05b4..a5c7efdafc1 100644 --- a/tests/unit/__init__.py +++ b/tests/unit/__init__.py @@ -21,7 +21,8 @@ import gettext #Localization settings locale.setlocale(locale.LC_ALL,'') -gettext.install('nvda') +translations = gettext.NullTranslations() +translations.install() # The path to the unit tests. UNIT_DIR = os.path.dirname(os.path.abspath(__file__)) diff --git a/tests/unit/sconscript b/tests/unit/sconscript deleted file mode 100644 index 0ac8d561dc5..00000000000 --- a/tests/unit/sconscript +++ /dev/null @@ -1,29 +0,0 @@ -### -#This file is a part of the NVDA project. -#URL: http://www.nvaccess.org/ -#Copyright 2017 NV Access Limited. -#This program is free software: you can redistribute it and/or modify -#it under the terms of the GNU General Public License version 2.0, as published by -#the Free Software Foundation. -#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. -#This license can be found at: -#http://www.gnu.org/licenses/old-licenses/gpl-2.0.html -### - -import sys - -Import("env") - -cmd = [sys.executable, "-m", "unittest"] -tests = env.get("unitTests") -if tests: - # Run specific tests. - tests = tests.split(",") - cmd += ["tests.unit." + test for test in tests] -else: - # Run all tests. - cmd.extend(("discover", "tests.unit", "-t", ".")) -target = env.Command(".", None, [cmd]) -Return('target') diff --git a/tests/unit/test_SpeechWithoutPauses.py b/tests/unit/test_SpeechWithoutPauses.py index a2559c6dc91..720501495c9 100644 --- a/tests/unit/test_SpeechWithoutPauses.py +++ b/tests/unit/test_SpeechWithoutPauses.py @@ -10,7 +10,7 @@ from speech.types import SpeechSequence from speech.commands import EndUtteranceCommand, LangChangeCommand, CallbackCommand -from speech import re_last_pause, SpeechWithoutPauses +from speech import SpeechWithoutPauses from logHandler import log @@ -89,7 +89,7 @@ def old_speakWithoutPauses( # noqa: C901 for index in range(len(speechSequence) - 1, -1, -1): item = speechSequence[index] if isinstance(item, str): - m = re_last_pause.match(item) + m = SpeechWithoutPauses.re_last_pause.match(item) if m: before, after = m.groups() if after: diff --git a/tests/unit/test_characterProcessing.py b/tests/unit/test_characterProcessing.py index d3d77f49caf..ae780627c21 100644 --- a/tests/unit/test_characterProcessing.py +++ b/tests/unit/test_characterProcessing.py @@ -9,6 +9,8 @@ import unittest import re from characterProcessing import SpeechSymbolProcessor +from characterProcessing import SYMLVL_ALL +from characterProcessing import processSpeechSymbols as process class TestComplex(unittest.TestCase): @@ -118,3 +120,11 @@ def test_multiple_group_replacement(self): name="foo" ) self.assertEqual(replaced, "BAT>bar") + + def test_engine(self): + """Test inclusion of group replacement in engine + """ + replaced = process("fr_FR", "Le 03.04.05.", SYMLVL_ALL) + self.assertEqual(replaced, "Le 03 point 04 point 05 point.") + replaced = process("fr_FR", "Le 03/04/05.", SYMLVL_ALL) + self.assertEqual(replaced, "Le 03 barre oblique 04 barre oblique 05 point.") diff --git a/tests/unit/test_controlTypes.py b/tests/unit/test_controlTypes.py index d8b478767a3..a1abd0f8583 100644 --- a/tests/unit/test_controlTypes.py +++ b/tests/unit/test_controlTypes.py @@ -42,7 +42,7 @@ def test_positiveStates(self): controlTypes.processPositiveStates( self.obj.role, self.obj.states, - controlTypes.REASON_FOCUS, + controlTypes.OutputReason.FOCUS, self.obj.states ), {controlTypes.STATE_INVALID_ENTRY, controlTypes.STATE_REQUIRED} @@ -53,7 +53,7 @@ def test_negativeStates(self): controlTypes.processNegativeStates( self.obj.role, self.obj.states, - controlTypes.REASON_FOCUS, + controlTypes.OutputReason.FOCUS, None ), {controlTypes.STATE_CHECKED} @@ -75,7 +75,7 @@ def test_positiveMergedStatesOutput(self): controlTypes.processAndLabelStates( obj.role, obj.states, - controlTypes.REASON_FOCUS, + controlTypes.OutputReason.FOCUS, obj.states, None ), @@ -95,7 +95,7 @@ def test_negativeMergedStatesOutput(self): controlTypes.processAndLabelStates( obj.role, obj.states, - controlTypes.REASON_FOCUS, + controlTypes.OutputReason.FOCUS, obj.states, None ), diff --git a/tests/unit/test_languageHandler.py b/tests/unit/test_languageHandler.py index b142768e554..149f7e4b9ac 100644 --- a/tests/unit/test_languageHandler.py +++ b/tests/unit/test_languageHandler.py @@ -1,20 +1,32 @@ -#tests/unit/test_languageHandler.py -#A part of NonVisual Desktop Access (NVDA) -#This file is covered by the GNU General Public License. -#See the file COPYING for more details. -#Copyright (C) 2017 NV Access Limited +# A part of NonVisual Desktop Access (NVDA) +# This file is covered by the GNU General Public License. +# See the file COPYING for more details. +# Copyright (C) 2017-2021 NV Access Limited """Unit tests for the languageHandler module. """ import unittest import languageHandler -from languageHandler import LCID_NONE +from languageHandler import LCID_NONE, windowsPrimaryLCIDsToLocaleNames +import locale +import ctypes LCID_ENGLISH_US = 0x0409 +UNSUPPORTED_PYTHON_LOCALES = { + "an", + "ckb", + "kmr", + "mn", + "my", + "ne", + "so", +} +TRANSLATABLE_LANGS = set(l[0] for l in languageHandler.getAvailableLanguages()) - {"Windows"} +WINDOWS_LANGS = set(locale.windows_locale.values()).union(windowsPrimaryLCIDsToLocaleNames.values()) -class TestLocaleNameToWindowsLCID(unittest.TestCase): +class TestLocaleNameToWindowsLCID(unittest.TestCase): def test_knownLocale(self): lcid = languageHandler.localeNameToWindowsLCID("en") self.assertEqual(lcid, LCID_ENGLISH_US) @@ -31,3 +43,156 @@ def test_nonStandardLocale(self): def test_invalidLocale(self): lcid = languageHandler.localeNameToWindowsLCID("zzzz") self.assertEqual(lcid, LCID_NONE) + + +class Test_languageHandler_setLocale(unittest.TestCase): + """Tests for the function languageHandler.setLocale""" + + SUPPORTED_LOCALES = [("en", "en_US"), ("fa-IR", "fa_IR"), ("an-ES", "an_ES")] + + def setUp(self): + """ + `setLocale` doesn't change `languageHandler.curLang`, so reset the locale using `setLanguage` to + the current language for each test. + """ + languageHandler.setLanguage(languageHandler.curLang) + + @classmethod + def tearDownClass(cls): + """ + `setLocale` doesn't change `languageHandler.curLang`, so reset the locale using `setLanguage` to + the current language so the tests can continue normally. + """ + languageHandler.setLanguage(languageHandler.curLang) + + def test_SupportedLocale_LocaleIsSet(self): + """ + Tests several locale formats that should result in an expected python locale being set. + """ + for localeName in self.SUPPORTED_LOCALES: + with self.subTest(localeName=localeName): + languageHandler.setLocale(localeName[0]) + self.assertEqual(locale.getlocale()[0], localeName[1]) + + def test_PythonUnsupportedLocale_LocaleUnchanged(self): + """ + Tests several locale formats that python doesn't support which will result in a return to the + current locale + """ + original_locale = locale.getlocale() + for localeName in UNSUPPORTED_PYTHON_LOCALES: + with self.subTest(localeName=localeName): + languageHandler.setLocale(localeName) + self.assertEqual(locale.getlocale(), original_locale) + + def test_NVDASupportedAndPythonSupportedLocale_LanguageCodeMatches(self): + """ + Tests all the translatable languages that NVDA shows in the user preferences + excludes the locales that python doesn't support, as the expected behaviour is different. + """ + for localeName in TRANSLATABLE_LANGS - UNSUPPORTED_PYTHON_LOCALES: + with self.subTest(localeName=localeName): + languageHandler.setLocale(localeName) + current_locale = locale.getlocale() + + if localeName == "uk": + self.assertEqual(current_locale[0], "English_United Kingdom") + else: + pythonLang = current_locale[0].split("_")[0] + langOnly = localeName.split("_")[0] + self.assertEqual( + langOnly, + pythonLang, + f"full values: {localeName} {current_locale[0]}", + ) + + def test_WindowsLang_LocaleCanBeRetrieved(self): + """ + We don't know whether python supports a specific windows locale so just ensure locale isn't + broken after testing these values. + """ + for localeName in WINDOWS_LANGS: + with self.subTest(localeName=localeName): + languageHandler.setLocale(localeName) + locale.getlocale() + + +class Test_LanguageHandler_SetLanguage(unittest.TestCase): + """Tests for the function languageHandler.setLanguage""" + + UNSUPPORTED_WIN_LANGUAGES = ["an", "kmr"] + + def tearDown(self): + """ + Resets the language to whatever it was before the testing suite begun. + """ + languageHandler.setLanguage(self._prevLang) + + def __init__(self, *args, **kwargs): + self._prevLang = languageHandler.getLanguage() + + ctypes.windll.kernel32.SetThreadLocale(0) + defaultThreadLocale = ctypes.windll.kernel32.GetThreadLocale() + self._defaultThreadLocaleName = languageHandler.windowsLCIDToLocaleName( + defaultThreadLocale + ) + + locale.setlocale(locale.LC_ALL, "") + self._defaultPythonLocale = locale.getlocale() + + languageHandler.setLanguage(self._prevLang) + super().__init__(*args, **kwargs) + + def test_NVDASupportedLanguages_LanguageIsSetCorrectly(self): + """ + Tests languageHandler.setLanguage, using all NVDA supported languages, which should do the following: + - set the translation service and languageHandler.curLang + - set the windows locale for the thread (fallback to system default) + - set the python locale for the thread (match the translation service, fallback to system default) + """ + for localeName in TRANSLATABLE_LANGS: + with self.subTest(localeName=localeName): + langOnly = localeName.split("_")[0] + languageHandler.setLanguage(localeName) + # check curLang/translation service is set + self.assertEqual(languageHandler.curLang, localeName) + + # check Windows thread is set + threadLocale = ctypes.windll.kernel32.GetThreadLocale() + threadLocaleName = languageHandler.windowsLCIDToLocaleName(threadLocale) + threadLocaleLang = threadLocaleName.split("_")[0] + if localeName in self.UNSUPPORTED_WIN_LANGUAGES: + # our translatable locale isn't supported by windows + # check that the system locale is unchanged + self.assertEqual(self._defaultThreadLocaleName, threadLocaleName) + else: + # check that the language codes are correctly set for the thread + self.assertEqual( + langOnly, + threadLocaleLang, + f"full values: {localeName} {threadLocaleName}", + ) + + # check that the python locale is set + python_locale = locale.getlocale() + if localeName in UNSUPPORTED_PYTHON_LOCALES: + # our translatable locale isn't supported by python + # check that the system locale is unchanged + self.assertEqual(self._defaultPythonLocale, python_locale) + elif localeName == "uk": + self.assertEqual(python_locale[0], "English_United Kingdom") + else: + # check that the language codes are correctly set for python + pythonLang = python_locale[0].split("_")[0] + self.assertEqual( + langOnly, pythonLang, f"full values: {localeName} {python_locale}" + ) + + def test_WindowsLanguages_NoErrorsThrown(self): + """ + We don't know whether python or our translator system supports a specific windows locale + so just ensure the setLanguage process doesn't fail. + """ + for localeName in WINDOWS_LANGS: + with self.subTest(localeName=localeName): + languageHandler.setLanguage(localeName) diff --git a/tests/unit/test_locationHelper.py b/tests/unit/test_locationHelper.py index c14c8e11fd2..96c17fc4fb2 100644 --- a/tests/unit/test_locationHelper.py +++ b/tests/unit/test_locationHelper.py @@ -1,14 +1,13 @@ -#tests/unit/test_locationHelper.py -#A part of NonVisual Desktop Access (NVDA) -#This file is covered by the GNU General Public License. -#See the file COPYING for more details. -#Copyright (C) 2017 NV Access Limited, Babbage B.V. +# A part of NonVisual Desktop Access (NVDA) +# This file is covered by the GNU General Public License. +# See the file COPYING for more details. +# Copyright (C) 2017-2021 NV Access Limited, Babbage B.V., Łukasz Golonka """Unit tests for the locationHelper module. """ import unittest -from locationHelper import * +from locationHelper import Point, RectLTRB, RectLTWH from ctypes.wintypes import RECT, POINT class TestRectOperators(unittest.TestCase): @@ -192,4 +191,26 @@ def test_ctypesPOINT(self): self.assertFalse(Point(x=3, y=4).yWiseLessOrEq(POINT(x=4, y=3))) # Equality self.assertEqual(POINT(x=4, y=3), Point(x=4, y=3)) - self.assertNotEqual(POINT(x=3, y=4), Point(x=4, y=3)) \ No newline at end of file + self.assertNotEqual(POINT(x=3, y=4), Point(x=4, y=3)) + + +class TestFailuresFromUnexpectedTypes(unittest.TestCase): + + def test_PointFailures(self): + self.assertRaises(TypeError, Point.fromFloatCollection, 22.22, 22, 33) + self.assertRaises(TypeError, Point.fromCompatibleType, 22.22) + self.assertRaises(TypeError, Point.fromDWORD, 22.22) + + def test_RectLTRBFailures(self): + self.assertRaises(TypeError, RectLTRB.fromFloatCollection, 22, 33.33, 44.44) + self.assertRaises(TypeError, RectLTRB.fromCompatibleType, 33.33) + self.assertRaises(TypeError, RectLTRB.fromPoint, 33.33) + self.assertRaises(TypeError, RectLTRB.fromCollection) + self.assertRaises(ValueError, RectLTRB.fromCollection, 22.22) + + def test_RectLTWHFailures(self): + self.assertRaises(TypeError, RectLTWH.fromFloatCollection, 22, 33.33, 44.44) + self.assertRaises(TypeError, RectLTWH.fromCompatibleType, 33.33) + self.assertRaises(TypeError, RectLTWH.fromPoint, 33.33) + self.assertRaises(TypeError, RectLTWH.fromCollection) + self.assertRaises(ValueError, RectLTWH.fromCollection, 22.22) diff --git a/tests/unit/test_scriptHandler.py b/tests/unit/test_scriptHandler.py index 35ffa861823..70127da27a0 100644 --- a/tests/unit/test_scriptHandler.py +++ b/tests/unit/test_scriptHandler.py @@ -1,13 +1,12 @@ -#tests/unit/test_scriptHandler.py -#A part of NonVisual Desktop Access (NVDA) -#This file is covered by the GNU General Public License. -#See the file COPYING for more details. -#Copyright (C) 2018-2019 NV Access Limited, Babbage B.V. +# A part of NonVisual Desktop Access (NVDA) +# This file is covered by the GNU General Public License. +# See the file COPYING for more details. +# Copyright (C) 2018-2021 NV Access Limited, Babbage B.V., Łukasz Golonka """Unit tests for the scriptHandler module.""" import unittest -from scriptHandler import * +from scriptHandler import script from inputCore import SCRCAT_MISC from sayAllHandler import CURSOR_CARET @@ -22,6 +21,7 @@ def test_scriptdecoration(self): gestures=["kb:b", "kb:c"], canPropagate=True, bypassInputHelp=True, + allowInSleepMode=True, resumeSayAllMode=CURSOR_CARET ) def script_test(self, gesture): @@ -32,4 +32,5 @@ def script_test(self, gesture): self.assertCountEqual(script_test.gestures, ["kb:a", "kb:b", "kb:c"]) self.assertTrue(script_test.canPropagate) self.assertTrue(script_test.bypassInputHelp) + self.assertTrue(script_test.allowInSleepMode) self.assertEqual(script_test.resumeSayAllMode, CURSOR_CARET) diff --git a/tests/unit/test_speech.py b/tests/unit/test_speech.py new file mode 100644 index 00000000000..d6821b72654 --- /dev/null +++ b/tests/unit/test_speech.py @@ -0,0 +1,346 @@ +# A part of NonVisual Desktop Access (NVDA) +# This file is covered by the GNU General Public License. +# See the file COPYING for more details. +# Copyright (C) 2021 NV Access Limited, Cyrille Bougot + +"""Unit tests for the speech module. +""" +import unittest +import gettext +import typing +import config +from speech import ( + _getSpellingSpeechAddCharMode, + _getSpellingCharAddCapNotification, + _getSpellingSpeechWithoutCharMode, +) +from speech.commands import ( + EndUtteranceCommand, + CharacterModeCommand, + PitchCommand, + BeepCommand, + LangChangeCommand +) + + +class Test_getSpellingSpeechAddCharMode(unittest.TestCase): + def test_symbolNamesAtStartAndEnd(self): + # Spelling ¡hola! + seq = (c for c in [ + 'inverted exclamation point', + EndUtteranceCommand(), + 'h', + EndUtteranceCommand(), + 'o', + EndUtteranceCommand(), + 'l', + EndUtteranceCommand(), + 'a', + EndUtteranceCommand(), + 'bang', + EndUtteranceCommand() + ]) + expected = repr([ + 'inverted exclamation point', + EndUtteranceCommand(), + CharacterModeCommand(True), + 'h', + EndUtteranceCommand(), + 'o', + EndUtteranceCommand(), + 'l', + EndUtteranceCommand(), + 'a', + EndUtteranceCommand(), + CharacterModeCommand(False), + 'bang', + EndUtteranceCommand() + ]) + output = _getSpellingSpeechAddCharMode(seq) + self.assertEqual(repr(list(output)), expected) + + def test_manySymbolNamesInARow(self): + # Spelling a...b + seq = (c for c in [ + 'a', + EndUtteranceCommand(), + 'dot', + EndUtteranceCommand(), + 'dot', + EndUtteranceCommand(), + 'dot', + EndUtteranceCommand(), + 'b', + EndUtteranceCommand() + ]) + expected = repr([ + CharacterModeCommand(True), + 'a', + EndUtteranceCommand(), + CharacterModeCommand(False), + 'dot', + EndUtteranceCommand(), + 'dot', + EndUtteranceCommand(), + 'dot', + EndUtteranceCommand(), + CharacterModeCommand(True), + 'b', + EndUtteranceCommand() + ]) + output = _getSpellingSpeechAddCharMode(seq) + self.assertEqual(repr(list(output)), expected) + + +class Translation_Fake(gettext.NullTranslations): + originalTranslationFunction: gettext.NullTranslations + translationResults: typing.Dict[str, str] + + def __init__(self, originalTranslationFunction: gettext.NullTranslations): + self.originalTranslationFunction = originalTranslationFunction + self.translationResults = {} + super().__init__() + + def gettext(self, msg: str) -> str: + if msg in self.translationResults: + return self.translationResults[msg] + return self.originalTranslationFunction.gettext(msg) + + +class Test_getSpellingCharAddCapNotification(unittest.TestCase): + translationsFake: Translation_Fake + + @classmethod + def setUpClass(cls): + from . import translations as originalTranslationClass + cls.translationsFake = Translation_Fake(originalTranslationClass) + cls.translationsFake.install() + + @classmethod + def tearDownClass(cls): + cls.translationsFake.originalTranslationFunction.install() + + def tearDown(self) -> None: + self.translationsFake.translationResults.clear() + + def test_noNotifications(self): + expected = repr([ + 'A', + ]) + output = _getSpellingCharAddCapNotification( + speakCharAs='A', + sayCapForCapitals=False, + capPitchChange=0, + beepForCapitals=False, + ) + self.assertEqual(repr(list(output)), expected) + + def test_pitchNotifications(self): + expected = repr([ + PitchCommand(offset=30), + 'A', + PitchCommand() + ]) + output = _getSpellingCharAddCapNotification( + speakCharAs='A', + sayCapForCapitals=False, + capPitchChange=30, + beepForCapitals=False, + ) + self.assertEqual(repr(list(output)), expected) + + def test_beepNotifications(self): + expected = repr([ + BeepCommand(2000, 50, left=50, right=50), + 'A', + ]) + output = _getSpellingCharAddCapNotification( + speakCharAs='A', + sayCapForCapitals=False, + capPitchChange=0, + beepForCapitals=True, + ) + self.assertEqual(repr(list(output)), expected) + + def test_capNotifications(self): + expected = repr([ + 'cap ', + 'A', + ]) + output = _getSpellingCharAddCapNotification( + speakCharAs='A', + sayCapForCapitals=True, + capPitchChange=0, + beepForCapitals=False, + ) + self.assertEqual(repr(list(output)), expected) + + def test_capNotificationsWithPlaceHolderBefore(self): + self.translationsFake.translationResults["cap %s"] = "%s cap" + expected = repr(['A', ' cap', ]) # for English this would be "cap A" + output = _getSpellingCharAddCapNotification( + speakCharAs='A', + sayCapForCapitals=True, + capPitchChange=0, + beepForCapitals=False, + ) + self.assertEqual(repr(list(output)), expected) + + def test_allNotifications(self): + expected = repr([ + PitchCommand(offset=30), + BeepCommand(2000, 50, left=50, right=50), + 'cap ', + 'A', + PitchCommand() + ]) + output = _getSpellingCharAddCapNotification( + speakCharAs='A', + sayCapForCapitals=True, + capPitchChange=30, + beepForCapitals=True, + ) + self.assertEqual(repr(list(output)), expected) + + +class Test_getSpellingSpeechWithoutCharMode(unittest.TestCase): + + def setUp(self): + config.conf['speech']['autoLanguageSwitching'] = False + + def tearDown(self): + # Restore default value + config.conf['speech']['autoLanguageSwitching'] = config.conf.getConfigValidation( + ['speech', 'autoLanguageSwitching'] + ).default + + def test_simpleSpelling(self): + expected = repr([ + 'a', + EndUtteranceCommand(), + 'b', + EndUtteranceCommand(), + 'c', + EndUtteranceCommand(), + ]) + output = _getSpellingSpeechWithoutCharMode( + text='abc', + locale=None, + useCharacterDescriptions=False, + sayCapForCapitals=False, + capPitchChange=0, + beepForCapitals=False, + ) + self.assertEqual(repr(list(output)), expected) + + def test_cap(self): + expected = repr([ + PitchCommand(offset=30), + BeepCommand(2000, 50, left=50, right=50), + 'cap ', + 'A', + PitchCommand(), + EndUtteranceCommand(), + ]) + output = _getSpellingSpeechWithoutCharMode( + text='A', + locale=None, + useCharacterDescriptions=False, + sayCapForCapitals=True, + capPitchChange=30, + beepForCapitals=True, + ) + self.assertEqual(repr(list(output)), expected) + + def test_characterMode(self): + expected = repr([ + 'Alfa', + EndUtteranceCommand(), + ]) + output = _getSpellingSpeechWithoutCharMode( + text='a', + locale='en', + useCharacterDescriptions=True, + sayCapForCapitals=False, + capPitchChange=0, + beepForCapitals=False, + ) + self.assertEqual(repr(list(output)), expected) + + def test_blank(self): + expected = repr([ + 'blank', + ]) + output = _getSpellingSpeechWithoutCharMode( + text='', + locale=None, + useCharacterDescriptions=False, + sayCapForCapitals=False, + capPitchChange=0, + beepForCapitals=False, + ) + self.assertEqual(repr(list(output)), expected) + + def test_onlySpaces(self): + expected = repr([ + 'space', + EndUtteranceCommand(), + 'tab', + EndUtteranceCommand(), + ]) + output = _getSpellingSpeechWithoutCharMode( + text=' \t', + locale=None, + useCharacterDescriptions=False, + sayCapForCapitals=False, + capPitchChange=0, + beepForCapitals=False, + ) + self.assertEqual(repr(list(output)), expected) + + def test_trimRightSpace(self): + expected = repr([ + 'a', + EndUtteranceCommand(), + ]) + output = _getSpellingSpeechWithoutCharMode( + text='a ', + locale=None, + useCharacterDescriptions=False, + sayCapForCapitals=False, + capPitchChange=0, + beepForCapitals=False, + ) + self.assertEqual(repr(list(output)), expected) + + def test_symbol(self): + expected = repr([ + 'bang', + EndUtteranceCommand(), + ]) + output = _getSpellingSpeechWithoutCharMode( + text='!', + locale=None, + useCharacterDescriptions=False, + sayCapForCapitals=False, + capPitchChange=0, + beepForCapitals=False, + ) + self.assertEqual(repr(list(output)), expected) + + def test_languageDetection(self): + config.conf['speech']['autoLanguageSwitching'] = True + expected = repr([ + LangChangeCommand('fr_FR'), + 'a', + EndUtteranceCommand(), + ]) + output = _getSpellingSpeechWithoutCharMode( + text='a', + locale='fr_FR', + useCharacterDescriptions=False, + sayCapForCapitals=False, + capPitchChange=0, + beepForCapitals=False, + ) + self.assertEqual(repr(list(output)), expected) diff --git a/tests/unit/test_speechManager/__init__.py b/tests/unit/test_speechManager/__init__.py index 21c3b968d75..499bc108aec 100644 --- a/tests/unit/test_speechManager/__init__.py +++ b/tests/unit/test_speechManager/__init__.py @@ -16,6 +16,8 @@ ) import speech.manager from speech.commands import ( + BeepCommand, + WaveFileCommand, PitchCommand, ConfigProfileTriggerCommand, _CancellableSpeechCommand, @@ -144,13 +146,13 @@ def testAllValuesHaveSymmetry(self): class TestExpectedClasses(unittest.TestCase): def test_expectedProsodyEqualsMatching(self): - p = speech.PitchCommand(offset=2) - e = ExpectedProsody(speech.PitchCommand(offset=2)) + p = PitchCommand(offset=2) + e = ExpectedProsody(PitchCommand(offset=2)) self.assertEqual(p, e) def test_expectedProsodyNotMatching(self): - p = speech.PitchCommand(offset=2) - e = ExpectedProsody(speech.PitchCommand(offset=5)) + p = PitchCommand(offset=2) + e = ExpectedProsody(PitchCommand(offset=5)) self.assertNotEqual(p, e) @@ -491,17 +493,17 @@ def setUp(self): speechDictHandler.initialize() # setting the synth depends on dictionary["voice"] config.conf['featureFlag']['cancelExpiredFocusSpeech'] = 2 # no - @patch.object(speech.commands.WaveFileCommand, 'run') - @patch.object(speech.commands.BeepCommand, 'run') + @patch.object(WaveFileCommand, 'run') + @patch.object(BeepCommand, 'run') def test_1(self, mock_BeepCommand_run, mock_WaveFileCommand_run): r"""Text, beep, beep, sound, text. Manual Test (in NVDA python console): wx.CallLater(500, speech.speak, [ - u"This is some speech and then comes a", speech.BeepCommand(440, 10), - u"beep. If you liked that, let's ", speech.BeepCommand(880, 10), - u"beep again. I'll speak the rest of this in a ", speech.PitchCommand(offset=50), + u"This is some speech and then comes a", BeepCommand(440, 10), + u"beep. If you liked that, let's ", BeepCommand(880, 10), + u"beep again. I'll speak the rest of this in a ", PitchCommand(offset=50), u"higher pitch. And for the finale, let's ", - speech.WaveFileCommand(r"waves\browseMode.wav"), u"play a sound." + WaveFileCommand(r"waves\browseMode.wav"), u"play a sound." ]) """ smi = SpeechManagerInteractions(self) @@ -514,7 +516,7 @@ def test_1(self, mock_BeepCommand_run, mock_WaveFileCommand_run): "beep. If you liked that, let's ", _beepCommand(880, 10, expectedToBecomeIndex=2), "beep again. I'll speak the rest of this in a ", - speech.PitchCommand(offset=50), + PitchCommand(offset=50), "higher pitch. And for the finale, let's ", _waveFileCommand(r"waves\browseMode.wav", expectedToBecomeIndex=3), "play a sound.", @@ -537,7 +539,7 @@ def test2(self): """Text, end utterance, text. Manual Test (in NVDA python console): wx.CallLater(500, speech.speak, [ - u"This is the first utterance", speech.EndUtteranceCommand(), u"And this is the second" + u"This is the first utterance", EndUtteranceCommand(), u"And this is the second" ]) """ smi = SpeechManagerInteractions(self) @@ -558,20 +560,20 @@ def test2(self): def test3(self): """Change pitch, text, end utterance, text. wx.CallLater(500, speech.speak, [ - speech.PitchCommand(offset=50), + PitchCommand(offset=50), u"This is the first utterance in a higher pitch", - speech.EndUtteranceCommand(), u"And this is the second" + EndUtteranceCommand(), u"And this is the second" ]) Expected: All should be higher pitch. """ smi = SpeechManagerInteractions(self) sequence = [ - speech.PitchCommand(offset=50), + PitchCommand(offset=50), u"This is the first utterance in a higher pitch", # EndUtterance effectively splits the sequence, two sequence numbers are returned # from smi.speak for ease of adding expectations. smi.create_EndUtteranceCommand(expectedToBecomeIndex=1), - smi.create_ExpectedProsodyCommand(speech.PitchCommand(offset=50)), + smi.create_ExpectedProsodyCommand(PitchCommand(offset=50)), u"And this is the second", smi.create_ExpectedIndex(expectedToBecomeIndex=2), ] @@ -587,7 +589,7 @@ def test_6_SPRI(self): """Two utterances at SPRI_NORMAL in same sequence. Two separate sequences at SPRI_NEXT. Manual Test (in NVDA python console): wx.CallLater(500, speech.speak, [ - u"1 2 3 ", u"4 5", speech.EndUtteranceCommand(), u"16 17 18 19 20" + u"1 2 3 ", u"4 5", EndUtteranceCommand(), u"16 17 18 19 20" ]) wx.CallLater(510, speech.speak, [u"6 7 8 9 10"], priority=speech.SPRI_NEXT) wx.CallLater(520, speech.speak, [u"11 12 13 14 15"], priority=speech.SPRI_NEXT) @@ -634,12 +636,12 @@ def test_6_SPRI(self): smi.pumpAll() smi.expect_synthSpeak(fin) - @patch.object(speech.commands.BeepCommand, 'run') + @patch.object(BeepCommand, 'run') def test_7_SPRI(self, mock_BeepCommand_run): """Utterance at SPRI_NORMAL including a beep. Utterance at SPRI_NOW. Manual Test (in NVDA python console): wx.CallLater(500, speech.speak, [ - u"Text before the beep ", speech.BeepCommand(440, 10), + u"Text before the beep ", BeepCommand(440, 10), u"text after the beep, text, text, text, text" ]) wx.CallLater(1500, speech.speak, [u"This is an interruption"], priority=speech.SPRI_NOW) @@ -731,7 +733,7 @@ def test_9_SPRI(self): smi.pumpAll() smi.expect_synthSpeak(second) - @patch.object(speech.commands.BeepCommand, 'run') + @patch.object(BeepCommand, 'run') def test_13_SPRI_interruptBeforeIndexReached(self, mock_BeepCommand_run): """The same as the other test_13, but the first index is not reached before the interruption. In this cases speech manager is expected to finish the first utterance before interrupting. @@ -767,14 +769,14 @@ def test_13_SPRI_interruptBeforeIndexReached(self, mock_BeepCommand_run): smi.pumpAll() smi.expect_synthSpeak(first) - @patch.object(speech.commands.BeepCommand, 'run') + @patch.object(BeepCommand, 'run') def test_13_SPRI_interruptAfterIndexReached(self, mock_BeepCommand_run): """Utterance at SPRI_NORMAL including a pitch change and beep. Utterance at SPRI_NOW. Manual Test (in NVDA python console): wx.CallLater(500, speech.speak, [ - speech.PitchCommand(offset=100), + PitchCommand(offset=100), u"Text before the beep ", - speech.BeepCommand(440, 10), + BeepCommand(440, 10), u"text after the beep, text, text, text, text" ]) wx.CallLater(1500, speech.speak, [u"This is an interruption"], priority=speech.SPRI_NOW) @@ -836,9 +838,9 @@ def test_4_profiles(self): t1 = sayAllHandler.SayAllProfileTrigger() t2 = appModuleHandler.AppProfileTrigger("notepad") wx.CallLater(500, speech.speak, [ - u"Testing testing ", speech.PitchCommand(offset=100), "1 2 3 4", - speech.ConfigProfileTriggerCommand(t1, True), speech.ConfigProfileTriggerCommand(t2, True), - u"5 6 7 8", speech.ConfigProfileTriggerCommand(t1, False), u"9 10 11 12" + u"Testing testing ", PitchCommand(offset=100), "1 2 3 4", + ConfigProfileTriggerCommand(t1, True), ConfigProfileTriggerCommand(t2, True), + u"5 6 7 8", ConfigProfileTriggerCommand(t1, False), u"9 10 11 12" ]) Expected: All text after 1 2 3 4 should be higher pitch. @@ -851,7 +853,7 @@ def test_4_profiles(self): smi.addMockCallMonitoring([t1.enter, t1.exit, t2.enter, t2.exit]) seq = [ "Testing testing ", - speech.PitchCommand(offset=100), + PitchCommand(offset=100), "1 2 3 4", smi.create_ExpectedIndex(1), # The preceeding index is expected, @@ -913,8 +915,8 @@ def test_5_profiles(self): import sayAllHandler trigger = sayAllHandler.SayAllProfileTrigger() wx.CallLater(500, speech.speak, [ - speech.ConfigProfileTriggerCommand(trigger, True), u"5 6 7 8", - speech.ConfigProfileTriggerCommand(trigger, False), + ConfigProfileTriggerCommand(trigger, True), u"5 6 7 8", + ConfigProfileTriggerCommand(trigger, False), u"9 10 11 12" ]) Expected: 5 6 7 8 in different profile, 9 10 11 12 with base config. @@ -955,7 +957,7 @@ def test_10_SPRI_profiles(self): import sayAllHandler; trigger = sayAllHandler.SayAllProfileTrigger(); wx.CallLater(500, speech.speak, [ - speech.ConfigProfileTriggerCommand(trigger, True), + ConfigProfileTriggerCommand(trigger, True), u"This is a normal utterance with a different profile" ]) wx.CallLater(1000, speech.speak, [u"This is an interruption"], priority=speech.SPRI_NOW) @@ -975,7 +977,7 @@ def test_10_SPRI_profiles(self): # before the first utterance can finish, it is interrupted. with smi.expectation(): interrupt = [ - speech.ConfigProfileTriggerCommand(t1, True), + ConfigProfileTriggerCommand(t1, True), "This is an interruption with a different profile", smi.create_ExpectedIndex(expectedToBecomeIndex=2) ] @@ -1001,7 +1003,7 @@ def test_11_SPRI_Profile(self): import sayAllHandler trigger = sayAllHandler.SayAllProfileTrigger() wx.CallLater(500, speech.speak, [ - speech.ConfigProfileTriggerCommand(trigger, True), + ConfigProfileTriggerCommand(trigger, True), u"This is a normal utterance with a different profile" ]) wx.CallLater(1000, speech.speak, [u"This is an interruption"], priority=speech.SPRI_NOW) @@ -1015,7 +1017,7 @@ def test_11_SPRI_Profile(self): smi.addMockCallMonitoring([t1.enter, t1.exit]) with smi.expectation(): first = [ - speech.ConfigProfileTriggerCommand(t1, True), + ConfigProfileTriggerCommand(t1, True), "This is a normal utterance with a different profile", smi.create_ExpectedIndex(expectedToBecomeIndex=1) ] @@ -1066,11 +1068,11 @@ def test_12_SPRI_profile(self): t1 = sayAllHandler.SayAllProfileTrigger() t2 = appModuleHandler.AppProfileTrigger("notepad") wx.CallLater(500, speech.speak, [ - speech.ConfigProfileTriggerCommand(t1, True), + ConfigProfileTriggerCommand(t1, True), u"This is a normal utterance with profile 1" ]) wx.CallLater(1000, speech.speak, [ - speech.ConfigProfileTriggerCommand(t2, True), + ConfigProfileTriggerCommand(t2, True), u"This is an interruption with profile 2" ], priority=speech.SPRI_NOW) Expected: Normal speaks with profile 1 but gets interrupted, interruption speaks with profile 2, @@ -1082,7 +1084,7 @@ def test_12_SPRI_profile(self): smi.addMockCallMonitoring([t1.enter, t1.exit, t2.enter, t2.exit]) with smi.expectation(): first = [ - speech.ConfigProfileTriggerCommand(t1, True), + ConfigProfileTriggerCommand(t1, True), "This is a normal utterance with profile 1", smi.create_ExpectedIndex(expectedToBecomeIndex=1) ] @@ -1094,7 +1096,7 @@ def test_12_SPRI_profile(self): # Before the first index is reached, there is an interruption with smi.expectation(): interrupt = [ - speech.ConfigProfileTriggerCommand(t2, True), + ConfigProfileTriggerCommand(t2, True), "This is an interruption with profile 2", smi.create_ExpectedIndex(expectedToBecomeIndex=2) ] diff --git a/tests/unit/test_speechManager/speechManagerTestHarness.py b/tests/unit/test_speechManager/speechManagerTestHarness.py index 102d24efbce..f7f5e370064 100644 --- a/tests/unit/test_speechManager/speechManagerTestHarness.py +++ b/tests/unit/test_speechManager/speechManagerTestHarness.py @@ -30,6 +30,10 @@ BeepCommand, WaveFileCommand, EndUtteranceCommand, + BaseProsodyCommand, + RateCommand, + VolumeCommand, + PitchCommand, ConfigProfileTriggerCommand, ) from speech.types import _IndexT @@ -60,15 +64,15 @@ class ExpectedProsody: commands that are sent to the synth. This may be as a result of resuming a previous utterance. """ expectedProsody: Union[ - speech.commands.PitchCommand, - speech.commands.RateCommand, - speech.commands.VolumeCommand + PitchCommand, + RateCommand, + VolumeCommand ] def __eq__(self, other): if type(self.expectedProsody) != type(other): return False - if isinstance(other, speech.commands.BaseProsodyCommand): + if isinstance(other, BaseProsodyCommand): return repr(other) == repr(self.expectedProsody) return False @@ -223,7 +227,7 @@ def _updateKnownSequences(self, seq) -> List[_SentSequenceIndex]: """ startOfUtteranceIndexes = set( i + 1 for i, item in enumerate(seq) - if isinstance(item, speech.commands.EndUtteranceCommand) + if isinstance(item, EndUtteranceCommand) ) startOfUtteranceIndexes.add(len(seq)) # ensure the last index is included start = 0 diff --git a/tests/unit/test_speechXml.py b/tests/unit/test_speechXml.py index 17966d04d7c..89cf45d6f1c 100644 --- a/tests/unit/test_speechXml.py +++ b/tests/unit/test_speechXml.py @@ -12,6 +12,15 @@ from speechXml import REPLACEMENT_CHAR import speech +from speech.commands import ( + PitchCommand, + VolumeCommand, + LangChangeCommand, + CharacterModeCommand, + IndexCommand, + PhonemeCommand, +) + class TestEscapeXml(unittest.TestCase): """Test the _escapeXml function. """ @@ -229,16 +238,16 @@ def test_convertComplex(self): converter = speechXml.SsmlConverter("en_US") xml = converter.convertToXml([ "t1", - speech.PitchCommand(multiplier=2), - speech.VolumeCommand(multiplier=2), + PitchCommand(multiplier=2), + VolumeCommand(multiplier=2), "t2", - speech.PitchCommand(), - speech.LangChangeCommand("de_DE"), - speech.CharacterModeCommand(True), - speech.IndexCommand(1), + PitchCommand(), + LangChangeCommand("de_DE"), + CharacterModeCommand(True), + IndexCommand(1), "c", - speech.CharacterModeCommand(False), - speech.PhonemeCommand("phIpa", text="phText") + CharacterModeCommand(False), + PhonemeCommand("phIpa", text="phText") ]) self.assertEqual(xml, '' diff --git a/tests/unit/test_textInfos.py b/tests/unit/test_textInfos.py index b9615f5f528..15393e2fd96 100644 --- a/tests/unit/test_textInfos.py +++ b/tests/unit/test_textInfos.py @@ -131,3 +131,45 @@ def test_mixedSurrogatePairsNonSurrogatesAndSingleSurrogatesBackward(self): ti.move(textInfos.UNIT_CHARACTER, -1) ti.expand(textInfos.UNIT_CHARACTER) # Range at a self.assertEqual(ti.offsets, (0, 1)) # One offset + + +class TestEndpoints(unittest.TestCase): + + def test_TextInfoEndpoint_largerAndSmaller(self): + obj = BasicTextProvider(text="abcdef") + ti = obj.makeTextInfo(Offsets(0, 2)) + smaller = ti.start + larger = ti.end + self.assertTrue(smaller < larger) + self.assertFalse(larger < smaller) + self.assertTrue(smaller <= larger) + self.assertFalse(larger <= smaller) + self.assertFalse(smaller >= larger) + self.assertTrue(larger >= smaller) + self.assertFalse(smaller > larger) + self.assertTrue(larger > smaller) + self.assertTrue(smaller != larger) + self.assertTrue(larger != smaller) + + def test_TextInfoEndpoint_equal(self): + obj = BasicTextProvider(text="abcdef") + ti = obj.makeTextInfo(Offsets(1, 1)) + self.assertTrue(ti.start == ti.end) + self.assertFalse(ti.start != ti.end) + self.assertFalse(ti.start < ti.end) + self.assertTrue(ti.start <= ti.end) + self.assertTrue(ti.start >= ti.end) + self.assertFalse(ti.start > ti.end) + + def test_setEndpoint(self): + obj = BasicTextProvider(text="abcdef") + ti1 = obj.makeTextInfo(Offsets(0, 2)) + ti2 = obj.makeTextInfo(Offsets(3, 5)) + ti1.end = ti2.end + self.assertEqual((ti1._startOffset, ti1._endOffset), (0, 5)) + ti1.start = ti2.start + self.assertEqual((ti1._startOffset, ti1._endOffset), (3, 5)) + ti1.end = ti2.start + self.assertEqual((ti1._startOffset, ti1._endOffset), (3, 3)) + ti1.start = ti2.end + self.assertEqual((ti1._startOffset, ti1._endOffset), (5, 5)) diff --git a/tests/unit/test_winVersion.py b/tests/unit/test_winVersion.py new file mode 100644 index 00000000000..78bf0e22330 --- /dev/null +++ b/tests/unit/test_winVersion.py @@ -0,0 +1,37 @@ +# A part of NonVisual Desktop Access (NVDA) +# This file is covered by the GNU General Public License. +# See the file COPYING for more details. +# Copyright (C) 2021 NV Access Limited, Joseph Lee + +"""Unit tests for the Windows version module.""" + +import unittest +import sys +import winVersion + + +class TestWinVersion(unittest.TestCase): + + def test_getWinVer(self): + # Test a 3-tuple consisting of version major, minor, build. + # sys.getwindowsversion() internally returns a named tuple, so comparing tuples is possible. + currentWinVer = winVersion.getWinVer() + winVerPython = sys.getwindowsversion() + self.assertTupleEqual( + (currentWinVer.major, currentWinVer.minor, currentWinVer.build), + winVerPython[:3] + ) + + def test_getWinVerFromNonExistentRelease(self): + # Test the fact that there is no Windows 10 2003 (2004 exists, however). + with self.assertRaises(AttributeError): + # Flake8 F841: local variable name is assigned to but never used + may2020Update = winVersion.WIN10_2003 # NOQA: F841 + + def test_moreRecentWinVer(self): + # Specifically to test operators. + minimumWinVer = winVersion.WIN7_SP1 + audioDuckingAvailable = winVersion.WIN8 + self.assertGreaterEqual( + audioDuckingAvailable, minimumWinVer + ) diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index 1a0122718b8..21e82032ed4 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -3,6 +3,128 @@ What's New in NVDA %!includeconf: ../changes.t2tconf += 2021.1 = + +== New Features == +- Early support for UIA with Chromium based browsers (such as Edge). (#12025) +- Optional experimental support for Microsoft Excel via UI Automation. Only recommended for Microsoft Excel build 16.0.13522.10000 or higher. (#12210) +- Easier navigation of output in NVDA Python Console. (#9784) + - alt+up/down jumps to the previous/next output result (add shift for selecting). + - control+l clears the output pane. + + +== Changes == +- In browse mode, controls can now be activated with braille cursor routing on their descriptor (ie. "lnk" for a link). This is especially useful for activating eg. check-boxes with no labels. (#7447) +- NVDA now prevents the user from performing Windows 10 OCR if screen curtain is enabled. (#11911) +- Updated Unicode Common Locale Data Repository (CLDR) to 38.1. (#11943) +- Added more mathematical symbols to the symbols dictionary. (#11467) +- The user guide, changes file, and key commands listing now have a refreshed appearance. (#12027) +- "Unsupported" now reported when attempting to toggle screen layout in applications that do not support it, such as Microsoft Word. (#7297) +- 'Attempt to cancel speech for expired focus events' option in the advanced settings panel now enabled by default. + - This behaviour can be disabled by default with by setting this option to "No". + - Web applications (E.G. Gmail) no longer speak outdated information when moving focus rapidly. (#10885) +- Espeak-ng has been updated to 1.51-dev commit cad1c8e87fcccf677a445202e340f61980450a84. (#12202, #12280) +- Updated liblouis braille translator to [3.17.0 https://github.com/liblouis/liblouis/releases/tag/v3.17.0]. (#12137) + - New braille tables: Belarusian literary braille, Belarusian computer braille, Urdu grade 1, Urdu grade 2. +- Support for Adobe Flash content has been removed from NVDA due to the use of Flash being actively discouraged by Adobe. (#11131) +- NVDA will exit even with windows still open, the exit process now closes all NVDA windows and dialogs (#1740) +- The Speech Viewer can now be closed with `alt+F4` and has a standard close button for easier interaction with users of pointing devices. (#12330) +- The Braille Viewer now has a standard close button for easier interaction with users of pointing devices. (#12328) +- In the Elements List dialog, the accelerator key on the "Activate" button has been removed in some locales to avoid collision with an element type radio button label. When available, the button is still the default of the dialog and as such can still be invoked by simply pressing enter from the elements list itself. (#6167) + + +== Bug Fixes == +- The list of messages in Outlook 2010 is once again readable. (#12241) +- In terminal programs on Windows 10 version 1607 and later, when inserting or deleting characters in the middle of a line, the characters to the right of the caret are no longer read out. (#3200) + - This experimental fix must be manually enabled in NVDA's advanced settings panel by changing the diff algorithm to Diff Match Patch. +- Fixed access to edit fields in MCS Electronics IDE's. (#11966) +- In MS Outlook, inappropriate distance reporting when shift+tabbing from the message body to the subject field should not occur anymore. (#10254) +- In the Python Console, inserting a tab for indentation at the beginning of a non-empty input line and performing tab-completion in the middle of an input line are now supported. (#11532) +- Formatting information and other browseable messages no longer present unexpected blank lines when screen layout is turned off. (#12004) +- It is now possible to read comments in MS Word with UIA enabled. (#9285) +- Performance when interacting with Visual Studio has been improved. (#12171) +- Fix graphical bugs such as missing elements when using NVDA with a right-to-left layout. (#8859) +- Respect the GUI layout direction based on the NVDA language, not the system locale. (#638) + - known issue for right-to-left languages: the right border of groupings clips with labels/controls. (#12181) +- The python locale is set to match the language selected in preferences consistently, and will occur when using the default language. (#12214) +- - TextInfo.getTextInChunks no longer freezes when called on Rich Edit controls such as the NVDA log viewer. (#11613) +- It is once again possible to use NVDA in a languages containing underscores in the locale name such as de_CH on Windows 10 1803 and 1809. (#12250) +- In WordPad, configuration of superscript/subscript reporting works as expected. (#12262) +- NVDA no longer fails to announce the newly focused content on a web page if the old focus disappears and is replaced by the new focus in the same position. (#12147) +- Strikethrough, superscript and subscript formatting for entire Excel cells are now reported if the corresponding option is enabled. (#12264) +- Fixed copying config during installation from a portable copy when default destination config directory is empty. (#12071, #12205) +- Fixed incorrect announcement of some letters with accents or diacritic when 'Say cap before capitals' option is checked. (#11948) +- Fixed the pitch change failure in Sapi4 speech synthesizer. (#12311) +- The NVDA installer now also honors the ``--minimal`` command line parameter and does not play the start-up sound, following the same documented behavior as an installed or portable copy NVDA executable. (#12289) + + +== Changes for Developers == +- Note: this is a Add-on API compatibility breaking release. Add-ons will need to be re-tested and have their manifest updated. +- NVDA's build system now fetches all Python dependencies with pip and stores them in a Python virtual environment. This is all done transparently. + - To build NVDA, SCons should continue to be used in the usual way. E.g. executing scons.bat in the root of the repository. Running ``py -m SCons`` is no longer supported, and ``scons.py`` has also been removed. + - To run NVDA from source, rather than executing ``source/nvda.pyw`` directly, the developer should now use ``runnvda.bat`` in the root of the repository. If you do try to execute ``source/nvda.pyw``, a message box will alert you this is no longer supported. + - To perform unit tests, execute ``rununittests.bat []`` + - To perform system tests: execute ``runsystemtests.bat [] `` + - To perform linting, execute ``runlint.bat `` + - Please refer to readme.md for more details. +- The following Python dependencies have also been upgraded: + - comtypes updated to 1.1.8. + - pySerial updated to 3.5. + - wxPython updated to 4.1.1. + - Py2exe updated to 0.10.1.0. +- `LiveText._getTextLines` has been removed. (#11639) + - Instead, override `_getText` which returns a string of all text in the object. +- `LiveText` objects can now calculate diffs by character. (#11639) + - To alter the diff behaviour for some object, override the `diffAlgo` property (see the docstring for details). +- When defining a script with the script decorator, the 'allowInSleepMode' boolean argument can be specified to control if a script is available in sleep mode or not. (#11979) +- The following functions are removed from the config module: + - canStartOnSecureScreens - use config.isInstalledCopy instead. + - hasUiAccess and execElevated - use them from the systemUtils module. + - getConfigDirs - use globalVars.appArgs.configPath instead +- Module level REASON_* constants are removed from controlTypes - please use controlTypes.OutputReason instead. (#11969) +- REASON_QUICKNAV has been removed from browseMode - use controlTypes.OutputReason.QUICKNAV instead. (#11969) +- 'NVDAObject' (and derivatives) property 'isCurrent' now strictly returns Enum class 'controlTypes.IsCurrent'. (#11782) + - 'isCurrent' is no longer Optional, and thus will not return None. + - When an object is not current 'controlTypes.IsCurrent.NO' is returned. +- The 'controlTypes.isCurrentLabels' mapping has been removed. (#11782) + - Instead use the 'displayString' property on a 'controlTypes.IsCurrent' enum value. EG 'controlTypes.IsCurrent.YES.displayString' +- 'NVDAObject' (and derivatives) property 'isCurrent' now strictly returns 'controlTypes.IsCurrent'. (#11782) + - 'isCurrent' is no longer Optional, and thus will not return None, when an object is not current 'controlTypes.IsCurrent.NO' is returned. +- The 'controlTypes.isCurrentLabels' has been removed, instead use the 'displayString' property on a 'controlTypes.IsCurrent' enum value. (#11782) + - EG 'controlTypes.IsCurrent.YES.displayString' +- `winKernel.GetTimeFormat` has been removed - use `winKernel.GetTimeFormatEx` instead (#12139) +- `winKernel.GetDateFormat` has been removed - use `winKernel.GetDateFormatEx` instead (#12139) +- `gui.DriverSettingsMixin` has been removed - use `gui.AutoSettingsMixin` (#12144) +- `speech.getSpeechForSpelling` has been removed - use `speech.getSpellingSpeech` (#12145) +- Commands cannot be directly imported from speech as `import speech; speech.ExampleCommand()` or `import speech.manager; speech.manager.ExampleCommand()` - use `from speech.commands import ExampleCommand` instead (#12126) +- `speakTextInfo` will no longer send speech through `speakWithoutPauses` if reason is `SAYALL`, as `sayAllhandler` does this manually now. (#12150) +- The `synthDriverHandler` module is no longer star imported into `globalCommands` and `gui.settingsDialogs` - use `from synthDriverHandler import synthFunctionExample` instead. (#12172) +- `ROLE_EQUATION` has been removed from controlTypes - use `ROLE_MATH`` instead. (#12164) +- `autoSettingsUtils.driverSetting` classes are removed from `driverHandler` - please use them from `autoSettingsUtils.driverSetting`. (#12168) +- `autoSettingsUtils.utils` classes are removed from `driverHandler` - please use them from `autoSettingsUtils.utils`. (#12168) +- Support of `TextInfo`s that do not inherit from `contentRecog.BaseContentRecogTextInfo` is removed. (#12157) +- `speech.speakWithoutPauses` has been removed - please use `speech.SpeechWithoutPauses(speakFunc=speech.speak).speakWithoutPauses` instead. (#12195) +- `speech.re_last_pause` has been removed - please use `speech.SpeechWithoutPauses.re_last_pause` instead. (#12195) +- `WelcomeDialog`, `LauncherDialog` and `AskAllowUsageStatsDialog` are moved to the `gui.startupDialogs`. (#12105) +- `getDocFilePath` has been moved from `gui` to the `documentationUtils` module. (#12105) +- The gui.accPropServer module as well as the AccPropertyOverride and ListCtrlAccPropServer classes from the gui.nvdaControls module have been removed in favor of WX' native support for overriding accessibility properties. When enhancing accessibility of WX controls, implement wx.Accessible instead. (#12215) +- Files in `source/comInterfaces/` are now more easily consumable by developer tools such as IDEs. (#12201) +- Convenience methods and types have been added to the winVersion module for getting and comparing Windows versions. (#11909) + - isWin10 function found in winVersion module has been removed. + - class winVersion.WinVersion is a comparable and order-able type encapsulating Windows version information. + - Function winVersion.getWinVer has been added to get a winVersion.WinVersion representing the currently running OS. + - Convenience constants have been added for known Windows releases, see winVersion.WIN* constants. +- IAccessibleHandler no longer star imports everything from IAccessible and IA2 COM interfaces - please use them directly. (#12232) +- TextInfo objects now have start and end properties which can be compared mathematically with operators such as < <= == != >= >. (#11613) + - E.g. ti1.start <= ti2.end + - This usage is now prefered instead of ti1.compareEndPoints(ti2,"startToEnd") <= 0 +- TextInfo start and end properties can also be set to each other. + - E.g. ti1.start = ti2.end + - This usage is prefered instead of ti1.SetEndPoint(ti2,"startToEnd") +- `wx.CENTRE_ON_SCREEN` and `wx.CENTER_ON_SCREEN` are removed, use `self.CentreOnScreen()` instead. (#12309) +- `easeOfAccess.isSupported` has been removed, NVDA only supports versions of Windows where this evaluates to `True`. (#12222) + + = 2020.4 = This release includes new Chinese Input methods, an update to Liblouis and the elements list (NVDA+f7) now works in focus mode. Context sensitive help is now available when pressing F1 in NVDA dialogs. @@ -23,6 +145,7 @@ Plus many other important bug fixes and improvements. - Added the --copy-portable-config command line parameter that allows you to automatically copy the provided configuration to the user account when silently installing NVDA. (#9676) - Braille routing is now supported with the Braille Viewer for mouse users, hover to route to a braille cell. (#11804) - NVDA will now automatically detect the Humanware Brailliant BI 40X and 20X devices via both USB and Bluetooth. (#11819) +- NVDA now reports the categories assigned to an appointment in Microsoft Outlook, if any. (#11598) == Changes == @@ -66,7 +189,7 @@ Plus many other important bug fixes and improvements. - ARIA treegrids are now exposed as normal tables in browse mode in both Firefox and Chrome. (#9715) - A reverse search can now be initiated with 'find previous' via NVDA+shift+F3 (#11770) - An NVDA script is no longer treated as being repeated if an unrelated key press happens in between the two executions of the script. (#11388) -- Strong and emphasis tags in Internet Explorer can again be suppressed from being reported by returning off Report Emphasis in NVDA's Document Formatting settings. (#11808) +- Strong and emphasis tags in Internet Explorer can again be suppressed from being reported by turning off Report Emphasis in NVDA's Document Formatting settings. (#11808) - A freeze of several seconds experienced by a small amount of users when arrowing between cells in Excel should no longer occur. (#11818) - In Microsoft Teams builds with version numbers like 1.3.00.28xxx, NVDA no longer fails reading messages in chats or Teams channels due to an incorrectly focused menu. (#11821) - Text marked both as being a spelling and grammar error at the same time in Google Chrome will be appropriately announced as both a spelling and grammar error by NVDA. (#11787) @@ -3075,4 +3198,3 @@ Major highlights of this release include support for 64 bit editions of Windows; - NVDA now asks if it should save configuration and restart if the user has just changed the language in the User Interface Settings Dialog. NVDA must be restarted for the language change to fully take effect. - If a synthesizer can not be loaded, when choosing it from the synthesizer dialog, a message box alerts the user to the fact. - When loading a synthesizer for the first time, NVDA lets the synthesizer choose the most suitable voice, rate and pitch parameters, rather than forcing it to defaults it thinks are ok. This fixes a problem where Eloquence and Viavoice sapi4 synths start speaking way too fast for the first time. - diff --git a/user_docs/en/userGuide.t2t b/user_docs/en/userGuide.t2t index 996404544f8..c22198ed92d 100644 --- a/user_docs/en/userGuide.t2t +++ b/user_docs/en/userGuide.t2t @@ -498,7 +498,6 @@ This includes documents in the following applications: - Microsoft Edge - Adobe Reader - Foxit Reader -- Adobe Flash - Supported books in Amazon Kindle for PC - @@ -596,7 +595,7 @@ Use the following keys for performing searches: %kc:endInclude ++ Embedded Objects ++[ImbeddedObjects] -Pages can include rich content using technologies such as Adobe Flash, Oracle Java and HTML5, as well as applications and dialogs. +Pages can include rich content using technologies such as Oracle Java and HTML5, as well as applications and dialogs. Where these are encountered in browse mode, NVDA will report "embedded object", "application" or "dialog", respectively. You can quickly move to them using the o and shift+o embedded object single letter navigation keys. To interact with these objects, you can press enter on them. @@ -794,6 +793,8 @@ For this situation, NVDA contains a feature called "screen curtain" which can be You can enable the Screen Curtain in the [vision category #VisionSettings] of the [NVDA Settings #NVDASettings] dialog. +When the screen curtain is active some tasks directly based on what appears on the screen such as performing [OCR #Win10Ocr] or taking a screenshot cannot be achieved. + + Content Recognition +[ContentRecognition] When authors don't provide sufficient information for a screen reader user to determine the content of something, various tools can be used to attempt to recognize the content from an image. NVDA supports the optical character recognition (OCR) functionality built into Windows 10 to recognize text from images. @@ -815,6 +816,8 @@ NVDA can use this to recognize text from images or inaccessible applications. You can set the language to use for text recognition in the [Windows 10 OCR category #Win10OcrSettings] of the [NVDA Settings #NVDASettings] dialog. Additional languages can be installed by opening the Start menu, choosing Settings, selecting Time & Language -> Region & Language and then choosing Add a language. +Windows 10 OCR may be partially or fully incompatible with [NVDA vision enhancements #Vision] or other external visual aids. You will need to disable these aids before proceeding to a recognition. + %kc:beginInclude To recognize the text in the current navigator object using Windows 10 OCR, press NVDA+r. %kc:endInclude @@ -1743,7 +1746,7 @@ You can configure reporting of: - Pages and spacing - Page numbers - Line numbers - - Line indentation reporting [(Off, Speech, Tones, Both Speech and Tones) #lineIndentationOptions] + - Line indentation reporting [(Off, Speech, Tones, Both Speech and Tones) #DocumentFormattingSettingsLineIndentation] - Paragraph indentation (e.g. hanging indent, first line indent) - Line spacing (single, double, etc.) - Alignment @@ -1751,7 +1754,7 @@ You can configure reporting of: - Tables - Row/column headers - Cell coordinates - - Cell borders [(Off, Styles, Both Colours and Styles) + - Cell borders (Off, Styles, Both Colours and Styles) - Elements - Headings - Links @@ -1836,6 +1839,24 @@ When this option is enabled, NVDA will use a new, work in progress version of it ==== Speak passwords in UIA consoles ====[AdvancedSettingsWinConsoleSpeakPasswords] This setting controls whether characters are spoken by [speak typed characters #KeyboardSettingsSpeakTypedCharacters] or [speak typed words #KeyboardSettingsSpeakTypedWords] in situations where the screen does not update (such as password entry) in the Windows Console with UI automation support enabled. For security purposes, this setting should be left disabled. However, you may wish to enable it if you experience performance issues or instability with typed character and/or word reporting while using NVDA's new experimental console support. +==== Use UIA with Microsoft Edge and other Chromium based browsers when available ====[ChromiumUIA] +Allows specifying when UIA will be used when it is available in Chromium based browsers such as Microsoft Edge. +UIA support for Chromium based browsers is early in development and may not provide the same level of access as IA2. +The combo box has the following options: +- Default (Only when necessary): The NVDA default, currently this is "Only when necessary". This default may change in the future as the technology matures. +- Only when necessary: When NVDA is unable to inject into the browser process in order to use IA2 and UIA is available, then NVDA will fall back to using UIA. +- Yes: If the browser makes UIA available, NVDA will use it. +- No: Don't use UIA, even if NVDA is unable to inject in process. This may be useful for developers debugging issues with IA2 and want to ensure that NVDA does not fall back to UIA. +- + +==== Use UI automation to access Microsoft Excel spreadsheet controls when available ====[UseUiaForExcel] +When this option is enabled, NVDA will try to use the Microsoft UI Automation accessibility API in order to fetch information from Microsoft Excel Spreadsheet controls. +This is an experimental feature, and some features of Microsoft Excel may not be available in this mode. +For instance, NVDA's Elements List for listing formulas and comments, and Browse mode quick navigation to jump to form fields on a spreadsheet features are not available. +However, for basic spreadsheet navigating / editing, this option may provide a vast performance improvement. +We still do not recommend that the majority of users turn this on by default, though we do welcome users of Microsoft Excel build 16.0.13522.10000 or higher to test this feature and provide feedback. +Microsoft Excel's UI automation implementation is ever changing, and versions of Microsoft Office older than 16.0.13522.10000 may not expose enough information for this option to be of any use. + ==== Use the new typed character support in Windows Console when available ====[AdvancedSettingsKeyboardSupportInLegacy] This option enables an alternative method for detecting typed characters in Windows command consoles. While it improves performance and prevents some console output from being spelled out, it may be incompatible with some terminal programs. @@ -1843,6 +1864,20 @@ This feature is available and enabled by default on Windows 10 versions 1607 and Warning: with this option enabled, typed characters that do not appear onscreen, such as passwords, will not be suppressed. In untrusted environments, you may temporarily disable [speak typed characters #KeyboardSettingsSpeakTypedCharacters] and [speak typed words #KeyboardSettingsSpeakTypedWords] when entering passwords. +==== Diff algorithm ====[DiffAlgo] +This setting controls how NVDA determines the new text to speak in terminals. +The diff algorithm combo box has three options: +- Automatic: as of NVDA 2021.1, this option is equivalent to Difflib. +In a future release, it may be changed to Diff Match Patch pending positive user testing. +- allow Diff Match Patch: This option causes NVDA to calculate changes to terminal text by character. +It may improve performance when large volumes of text are written to the console and allow more accurate reporting of changes made in the middle of lines. +However, it may be incompatible with some applications. +This feature is supported in Windows Console on Windows 10 versions 1607 and later. +Additionally, it may be available in other terminals on earlier Windows releases. +- force Difflib: this option causes NVDA to calculate changes to terminal text by line. +It is identical to NVDA's behaviour in versions 2020.4 and earlier. +- + ==== Attempt to cancel speech for expired focus events ====[CancelExpiredFocusSpeech] This option enables behaviour which attempts to cancel speech for expired focus events. In particular moving quickly through messages in Gmail with Chrome can cause NVDA to speak outdated information. diff --git a/user_docs/global.t2tconf b/user_docs/global.t2tconf index 64a5dfe9d1b..5757bfe8b8d 100644 --- a/user_docs/global.t2tconf +++ b/user_docs/global.t2tconf @@ -18,5 +18,7 @@ % Therefore, strip the BOM from the title. %!PostProc(html): \<(TITLE|H1)\>\xef\xbb\xbf <\1> +%!Options: --style styles.css + % This provides the macros NVDA_VERSION, NVDA_URL and NVDA_COPYRIGHT_YEARS. %!includeconf: build.t2tconf diff --git a/user_docs/styles.css b/user_docs/styles.css new file mode 100644 index 00000000000..2e0cac7a03b --- /dev/null +++ b/user_docs/styles.css @@ -0,0 +1,57 @@ +body { + margin: 0 auto; + max-width: 50em; + font-family: "Helvetica", "Arial", sans-serif; + line-height: 1.5; + padding: 4em 1em; + color: #333; +} + +h1 { + color:white; + background-color: #472F5F; /* NVDA purple */ +} + +h2 { + margin-top: 1em; + padding-top: 1em; +} + +h2, h3, h4, a { + color: #472F5F; /* NVDA purple */ +} + +strong { + color: #000; +} + +/* tables */ +table, td, th { + border:1px solid black; +} +table{ + border-collapse: collapse; +} +td, th { + padding: 0.25em; +} +th { + color: #472F5F; /* NVDA purple */ +} + +/* code / pre */ +code, +pre { + background: #f5f7f9; + border-bottom: 1px solid #d8dee9; + color: #a7adba; +} + +code { + padding: 2px 4px; + vertical-align: text-bottom; +} +pre { + padding: 1em; + border-left: 2px solid #69c; +} diff --git a/venvUtils/ensureAndActivate.bat b/venvUtils/ensureAndActivate.bat new file mode 100644 index 00000000000..7d6526a8dd5 --- /dev/null +++ b/venvUtils/ensureAndActivate.bat @@ -0,0 +1,28 @@ +@echo off +rem this script ensures the NVDA build system Python virtual environment is created and up to date, +rem and then activates it. +rem This is an internal script and should not be used directly. + +rem Ensure the environment is created and up to date +py -3.7-32 "%~dp0\ensureVenv.py" +if ERRORLEVEL 1 goto :EOF + +rem Set the necessary environment variables to have Python use this virtual environment. +rem This should set all the necessary environment variables that the standard .venv\scripts\activate.bat does +rem Except that we set VIRTUAL_ENV to a path relative to this script, +rem rather than it being hard-coded to where the virtual environment was first created. + +rem unset the PYTHONHOME variable so as to ensure that Python does not use a customized Python standard library. +set PYTHONHOME= +rem set the VIRTUAL_ENV variable instructing Python to use a virtual environment +rem py.exe will honor VIRTUAL_ENV and launch the python.exe that it finds in %VIRTUAL_ENV%\scripts. +rem %VIRTUAL_ENV%\scripts\python.exe will find pyvenv.cfg in its parent directory, +rem which is actually what then causes Python to use the site-packages found in this virtual environment. +set VIRTUAL_ENV=%~dp0..\.venv +rem Add the virtual environment's scripts directory to the path +set PATH=%VIRTUAL_ENV%\scripts;%PATH% +rem Set an NVDA-specific variable to identify this official NVDA virtual environment from other 3rd party ones +set NVDA_VENV=%VIRTUAL_ENV% +rem mention the environment in the prompt to make it obbvious it is active +rem just in case this script is executed outside of a local block and not cleaned up. +set PROMPT=[NVDA Venv] %PROMPT% diff --git a/venvUtils/ensureVenv.py b/venvUtils/ensureVenv.py new file mode 100644 index 00000000000..bf11e404cc4 --- /dev/null +++ b/venvUtils/ensureVenv.py @@ -0,0 +1,148 @@ +# A part of NonVisual Desktop Access (NVDA) +# Copyright (C) 2021 NV Access Limited +# This file may be used under the terms of the GNU General Public License, version 2 or later. +# For more details see: https://www.gnu.org/licenses/gpl-2.0.html + + +import sys +import os +import subprocess +import shutil +from typing import Set + +""" +A script to ensure that the NVDA build system's Python virtual environment is created and up to date. +""" + +top_dir: str = os.path.join(os.path.abspath(os.path.dirname(__file__)), "..") +venv_path: str = os.path.join(top_dir, ".venv") +requirements_path: str = os.path.join(top_dir, "requirements.txt") +venv_orig_requirements_path: str = os.path.join(venv_path, "_requirements.txt") +venv_python_version_path: str = os.path.join(venv_path, "python_version") + + +def askYesNoQuestion(message: str) -> bool: + """ + Displays the given message to the user and accepts y or n as input. + Any other input causes the question to be asked again. + @returns: True for y and n for False. + """ + while True: + answer = input( + message + " [y/n]: " + ) + if answer == 'n': + return False + elif answer == 'y': + return True + else: + continue # ask again + + +def fetchRequirementsSet(path: str) -> Set[str]: + """ + Fetches all the package lines from a pip requirements.txt file + returning them as a set of strings. + The returned set could be compared with a set from another file + which would allow easy identification of which requirements were added or removed. + """ + with open(path, "r") as f: + lines = [x.strip() for x in f.readlines()] + lines = [x for x in lines if x and not x.isspace() and not x.startswith('#')] + return set(lines) + + +def createVenvAndPopulate(): + """ + Creates the NVDA build system's Python virtual environment and installs all required packages. + this function will overwrite any existing virtual environment found at c{venv_path}. + """ + print("Creating virtual environment...", flush=True) + subprocess.run( + [ + sys.executable, + "-m", "venv", + "--clear", + venv_path, + ], + check=True + ) + with open(venv_python_version_path, "w") as f: + f.write(sys.version) + print("Installing packages in virtual environment...", flush=True) + subprocess.run( + [ + # Activate virtual environment + os.path.join(venv_path, "scripts", "activate.bat"), + "&&", + # Ensure we have the latest version of pip + "py", "-m", "pip", + "install", "--upgrade", "pip", + "&&", + # Install required packages with pip + "py", "-m", "pip", + "install", "-r", requirements_path, + ], + check=True, + shell=True, + ) + shutil.copy(requirements_path, venv_orig_requirements_path) + + +def ensureVenvAndRequirements(): + """ + Ensures that the NVDA build system's Python virtual environment is created and up to date. + If a previous virtual environment exists but has a miss-matching Python version + or pip package requirements have changed, + The virtual environment is recreated with the updated version of Python and packages. + If a virtual environment is found but does not seem to be ours, + This function asks the user if it should be overwritten or not. + """ + if not os.path.exists(venv_path): + print("Virtual environment does not exist.") + return createVenvAndPopulate() + if ( + not os.path.exists(venv_python_version_path) + or not os.path.exists(venv_orig_requirements_path) + ): + if askYesNoQuestion( + f"Virtual environment at {venv_path} probably not created by NVDA. " + "This directory must be removed before continuing. Should it be removed?" + ): + return createVenvAndPopulate() + else: + print("Aborting") + sys.exit(1) + venv_python_version = open(venv_python_version_path, "r").read() + if venv_python_version != sys.version: + print(f"Python version changed. Was {venv_python_version}, now is {sys.version}") + return createVenvAndPopulate() + oldRequirements = fetchRequirementsSet(venv_orig_requirements_path) + newRequirements = fetchRequirementsSet(requirements_path) + addedRequirements = newRequirements - oldRequirements + if addedRequirements: + print(f"Added or changed package requirements. {addedRequirements}") + return createVenvAndPopulate() + + +if __name__ == '__main__': + # Ensure we are not inside an already active Python virtual environment. + virtualEnv = os.getenv("VIRTUAL_ENV") + if virtualEnv: + print( + "Error: It looks like another Python virtual environment is already active in this shell.\n" + "Please deactivate the current Python virtual environment and try again." + ) + sys.exit(1) + if ( + sys.version_info.minor == 7 + and sys.version_info.micro == 6 + ): + # #10696: Building with Python 3.7.6 fails. Inform user and exit. + Py376FailMsg = ( + "Error: Building with Python 3.7.6 is not possible.\n" + "Please use a more recent version of Python 3." + ) + print(Py376FailMsg) + sys.exit(1) + ensureVenvAndRequirements() diff --git a/venvUtils/exportPackageList.bat b/venvUtils/exportPackageList.bat new file mode 100644 index 00000000000..b98d9ee14fc --- /dev/null +++ b/venvUtils/exportPackageList.bat @@ -0,0 +1,8 @@ +@echo off +setlocal +if "%VIRTUAL_ENV%" == "" ( + call "%~dp0\ensureAndActivate.bat" + if ERRORLEVEL 1 goto :EOF +) +py -m pip freeze >%1 +endlocal diff --git a/venvUtils/venvCmd.bat b/venvUtils/venvCmd.bat new file mode 100644 index 00000000000..06e4572d5e3 --- /dev/null +++ b/venvUtils/venvCmd.bat @@ -0,0 +1,29 @@ +@echo off +rem this script executes the single given command and arguments inside the NVDA build system's Python virtual environment. +rem It activates the environment, creating / updating it first if necessary, +rem then executes the command, +rem and then finally deactivates the environment. + +rem This script also supports running in an already fully activated NVDA Python environment. +rem If this is detected, the command is executed directly instead. +if "%VIRTUAL_ENV%" NEQ "" ( + if "%NVDA_VENV%" NEQ "%VIRTUAL_ENV%" ( + echo Warning: Detected a custom Python virtual environment. + echo It is recommended to run all NVDA build system commands outside of any existing Python virtual environment, unless you really know what you are doing. + ) + echo directly calling %* + call %* + goto :EOF +) + +rem call setlocal to make sure that any environment variable changes made by activating the virtual environment +rem can be completely undone when endlocal is called or this script exits. +setlocal +echo Ensuring NVDA Python virtual environment +call "%~dp0\ensureAndActivate.bat" +if ERRORLEVEL 1 goto :EOF +echo call %* +call %* +rem the virtual environment will now be deactivated as endlocal will be reached. +echo Deactivating NVDA Python virtual environment +endlocal