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.REASON_FOCUS and obj.role==controlTypes.ROLE_LISTITEM and controlTypes.STATE_SELECTABLE in obj.states:
- return True
- return super(EdgeHTMLTreeInterceptor,self).shouldPassThrough(obj,reason=reason)
-
- def makeTextInfo(self,position):
- try:
- return super(EdgeHTMLTreeInterceptor,self).makeTextInfo(position)
- except RuntimeError as e:
- # sometimes the stored TextRange we have for the caret/selection can die if the page mutates too much.
- # Therefore, if we detect this, just give back the first position in the document, updating our stored version as we go.
- if position in (textInfos.POSITION_SELECTION,textInfos.POSITION_CARET):
- log.debugWarning("%s died. Using first position instead."%position)
- info=self.makeTextInfo(textInfos.POSITION_FIRST)
- self._selection=info
- return info
- raise e
-
-class EdgeHTMLRoot(EdgeNode):
-
- treeInterceptorClass=EdgeHTMLTreeInterceptor
-
- def _get_shouldCreateTreeInterceptor(self):
- return self.role==controlTypes.ROLE_DOCUMENT
-
- def _get_role(self):
- role=super(EdgeHTMLRoot,self).role
- if role==controlTypes.ROLE_PANE:
- role=controlTypes.ROLE_DOCUMENT
- return role
diff --git a/source/NVDAObjects/UIA/excel.py b/source/NVDAObjects/UIA/excel.py
new file mode 100644
index 00000000000..84e6eb49912
--- /dev/null
+++ b/source/NVDAObjects/UIA/excel.py
@@ -0,0 +1,474 @@
+# 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
+
+from typing import Optional, Tuple
+import UIAHandler
+import _UIAHandler
+import _UIAConstants
+from _UIAConstants import (
+ UIAutomationType,
+)
+import colors
+import locationHelper
+import controlTypes
+from _UIACustomProps import (
+ CustomPropertyInfo,
+)
+from comtypes import GUID
+from scriptHandler import script
+import ui
+from logHandler import log
+from . import UIA
+
+
+class ExcelCustomProperties:
+ """ UIA 'custom properties' specific to Excel.
+ Once registered, all subsequent registrations will return the same ID value.
+ This class should be used as a singleton via ExcelCustomProperties.get()
+ to prevent unnecessary work by repeatedly interacting with UIA.
+ """
+ #: Singleton instance
+ _instance: "Optional[ExcelCustomProperties]" = None
+
+ @classmethod
+ def get(cls) -> "ExcelCustomProperties":
+ """Get the singleton instance or initialise it.
+ """
+ if cls._instance is None:
+ cls._instance = cls()
+ return cls._instance
+
+ def __init__(self):
+ self.cellFormula = CustomPropertyInfo(
+ guid=GUID("{E244641A-2785-41E9-A4A7-5BE5FE531507}"),
+ programmaticName="CellFormula",
+ uiaType=UIAutomationType.STRING,
+ )
+
+ self.cellNumberFormat = CustomPropertyInfo(
+ guid=GUID("{626CF4A0-A5AE-448B-A157-5EA4D1D057D7}"),
+ programmaticName="CellNumberFormat",
+ uiaType=UIAutomationType.STRING,
+ )
+
+ self.hasDataValidation = CustomPropertyInfo(
+ guid=GUID("{29F2E049-5DE9-4444-8338-6784C5D18ADF}"),
+ programmaticName="HasDataValidation",
+ uiaType=UIAutomationType.BOOL,
+ )
+
+ self.hasDataValidationDropdown = CustomPropertyInfo(
+ guid=GUID("{1B93A5CD-0956-46ED-9BBF-016C1B9FD75F}"),
+ programmaticName="HasDataValidationDropdown",
+ uiaType=UIAutomationType.BOOL,
+ )
+
+ self.dataValidationPrompt = CustomPropertyInfo(
+ guid=GUID("{7AAEE221-E14D-4DA4-83FE-842AAF06A9B7}"),
+ programmaticName="DataValidationPrompt",
+ uiaType=UIAutomationType.STRING,
+ )
+
+ self.hasConditionalFormatting = CustomPropertyInfo(
+ guid=GUID("{DFEF6BBD-7A50-41BD-971F-B5D741569A2B}"),
+ programmaticName="HasConditionalFormatting",
+ uiaType=UIAutomationType.BOOL,
+ )
+
+ self.commentReplyCount = CustomPropertyInfo(
+ guid=GUID("{312F7536-259A-47C7-B192-AA16352522C4}"),
+ programmaticName="CommentReplyCount",
+ uiaType=UIAutomationType.INT,
+ )
+
+ self.areGridLinesVisible = CustomPropertyInfo(
+ guid=GUID("{4BB56516-F354-44CF-A5AA-96B52E968CFD}"),
+ programmaticName="AreGridlinesVisible",
+ uiaType=UIAutomationType.BOOL,
+ )
+
+
+class ExcelObject(UIA):
+ """Common base class for all Excel UIA objects
+ """
+ _UIAExcelCustomProps = ExcelCustomProperties.get()
+
+
+class ExcelCell(ExcelObject):
+
+ # selecting cells causes duplicate focus events
+ shouldAllowDuplicateUIAFocusEvent = True
+
+ name = ""
+ role = controlTypes.ROLE_TABLECELL
+ rowHeaderText = None
+ columnHeaderText = None
+
+ #: Typing information for auto-property: _get_areGridlinesVisible
+ areGridlinesVisible: bool
+
+ def _get_areGridlinesVisible(self) -> bool:
+ parent = self.parent
+ # There will be at least one grid element between the cell and the sheet.
+ # There could be multiple as there might be a data table defined on the sheet.
+ while parent and parent.role == controlTypes.ROLE_TABLE:
+ parent = parent.parent
+ if parent:
+ return parent._getUIACacheablePropertyValue(self._UIAExcelCustomProps.areGridLinesVisible.id)
+ else:
+ log.debugWarning("Could not locate worksheet element.")
+ return False
+
+ #: Typing information for auto-property: _get_outlineColor
+ outlineColor: Optional[Tuple[colors.RGB]]
+
+ def _get_outlineColor(self) -> Optional[Tuple[colors.RGB]]:
+ val = self._getUIACacheablePropertyValue(_UIAHandler.UIA_OutlineColorPropertyId, True)
+ if isinstance(val, tuple):
+ return tuple(colors.RGB.fromCOLORREF(v) for v in val)
+ return None
+
+ #: Typing information for auto-property: _get_outlineThickness
+ outlineThickness: Optional[Tuple[float]]
+
+ def _get_outlineThickness(self) -> Optional[Tuple[float]]:
+ val = self._getUIACacheablePropertyValue(_UIAHandler.UIA_OutlineThicknessPropertyId, True)
+ if isinstance(val, tuple):
+ return val
+ return None
+
+ #: Typing information for auto-property: _get_fillColor
+ fillColor: Optional[colors.RGB]
+
+ def _get_fillColor(self) -> Optional[colors.RGB]:
+ val = self._getUIACacheablePropertyValue(_UIAHandler.UIA_FillColorPropertyId, True)
+ if isinstance(val, int):
+ return colors.RGB.fromCOLORREF(val)
+ return None
+
+ #: Typing information for auto-property: _get_fillType
+ fillType: Optional[_UIAConstants.FillType]
+
+ def _get_fillType(self) -> Optional[_UIAConstants.FillType]:
+ val = self._getUIACacheablePropertyValue(_UIAHandler.UIA_FillTypePropertyId, True)
+ if isinstance(val, int):
+ try:
+ return _UIAConstants.FillType(val)
+ except ValueError:
+ pass
+ return None
+
+ #: Typing information for auto-property: _get_rotation
+ rotation: Optional[float]
+
+ def _get_rotation(self) -> Optional[float]:
+ val = self._getUIACacheablePropertyValue(_UIAHandler.UIA_RotationPropertyId, True)
+ if isinstance(val, float):
+ return val
+ return None
+
+ #: Typing information for auto-property: _get_cellSize
+ cellSize: locationHelper.Point
+
+ def _get_cellSize(self) -> locationHelper.Point:
+ val = self._getUIACacheablePropertyValue(_UIAHandler.UIA_SizePropertyId, True)
+ x = val[0]
+ y = val[1]
+ return locationHelper.Point(x, y)
+
+ @script(
+ description=pgettext(
+ "excel-UIA",
+ # Translators: the description of a script
+ "Shows a browseable message Listing information about a cell's "
+ "appearance such as outline and fill colors, rotation and size"
+ ),
+ gestures=["kb:NVDA+o"],
+ )
+ def script_showCellAppearanceInfo(self, gesture):
+ infoList = []
+ tmpl = pgettext(
+ "excel-UIA",
+ # Translators: The width of the cell in points
+ "Cell width: {0.x:.1f} pt"
+ )
+ infoList.append(tmpl.format(self.cellSize))
+
+ tmpl = pgettext(
+ "excel-UIA",
+ # Translators: The height of the cell in points
+ "Cell height: {0.y:.1f} pt"
+ )
+ infoList.append(tmpl.format(self.cellSize))
+
+ if self.rotation is not None:
+ tmpl = pgettext(
+ "excel-UIA",
+ # Translators: The rotation in degrees of an Excel cell
+ "Rotation: {0} degrees"
+ )
+ infoList.append(tmpl.format(self.rotation))
+
+ if self.outlineColor is not None:
+ tmpl = pgettext(
+ "excel-UIA",
+ # Translators: The outline (border) colors of an Excel cell.
+ "Outline color: top={0.name}, bottom={1.name}, left={2.name}, right={3.name}"
+ )
+ infoList.append(tmpl.format(*self.outlineColor))
+
+ if self.outlineThickness is not None:
+ tmpl = pgettext(
+ "excel-UIA",
+ # Translators: The outline (border) thickness values of an Excel cell.
+ "Outline thickness: top={0}, bottom={1}, left={2}, right={3}"
+ )
+ infoList.append(tmpl.format(*self.outlineThickness))
+
+ if self.fillColor is not None:
+ tmpl = pgettext(
+ "excel-UIA",
+ # Translators: The fill color of an Excel cell
+ "Fill color: {0.name}"
+ )
+ infoList.append(tmpl.format(self.fillColor))
+
+ if self.fillType is not None:
+ tmpl = pgettext(
+ "excel-UIA",
+ # Translators: The fill type (pattern, gradient etc) of an Excel Cell
+ "Fill type: {0}"
+ )
+ infoList.append(tmpl.format(_UIAConstants.FillTypeLabels[self.fillType]))
+ numberFormat = self._getUIACacheablePropertyValue(
+ self._UIAExcelCustomProps.cellNumberFormat.id
+ )
+ if numberFormat:
+ # Translators: the number format of an Excel cell
+ tmpl = _("Number format: {0}")
+ infoList.append(tmpl.format(numberFormat))
+ hasDataValidation = self._getUIACacheablePropertyValue(
+ self._UIAExcelCustomProps.hasDataValidation.id
+ )
+ if hasDataValidation:
+ # Translators: If an excel cell has data validation set
+ tmpl = _("Has data validation")
+ infoList.append(tmpl)
+ dataValidationPrompt = self._getUIACacheablePropertyValue(
+ self._UIAExcelCustomProps.dataValidationPrompt.id
+ )
+ if dataValidationPrompt:
+ # Translators: the data validation prompt (input message) for an Excel cell
+ tmpl = _("Data validation prompt: {0}")
+ infoList.append(tmpl.format(dataValidationPrompt))
+ hasConditionalFormatting = self._getUIACacheablePropertyValue(
+ self._UIAExcelCustomProps.hasConditionalFormatting.id
+ )
+ if hasConditionalFormatting:
+ # Translators: If an excel cell has conditional formatting
+ tmpl = _("Has conditional formatting")
+ infoList.append(tmpl)
+ if self.areGridlinesVisible:
+ # Translators: If an excel cell has visible gridlines
+ tmpl = _("Gridlines are visible")
+ infoList.append(tmpl)
+ infoString = "\n".join(infoList)
+ ui.browseableMessage(
+ infoString,
+ title=pgettext(
+ "excel-UIA",
+ # Translators: Title for a browsable message that describes the appearance of a cell in Excel
+ "Cell Appearance"
+ )
+ )
+
+ def _hasSelection(self):
+ return (
+ self.selectionContainer
+ and 1 < self.selectionContainer.getSelectedItemsCount()
+ )
+
+ def _get_value(self):
+ if self._hasSelection():
+ return
+ return super().value
+
+ def _get_errorText(self):
+ for typeId, element in self.UIAAnnotationObjects.items():
+ if typeId in {
+ UIAHandler.AnnotationType_DataValidationError,
+ UIAHandler.AnnotationType_FormulaError,
+ UIAHandler.AnnotationType_CircularReferenceError,
+ }:
+ return element.GetCurrentPropertyValue(UIAHandler.UIA_FullDescriptionPropertyId)
+
+ def _get_description(self):
+ """
+ Prepends error messages and collaborator presence to any existing description.
+ """
+ descriptionList = []
+ if self.errorText:
+ # Translators: an error message on a cell in Microsoft Excel
+ descriptionList.append(
+ # Translators: an error message on a cell in Microsoft Excel.
+ _("Error: {errorText}").format(errorText=self.errorText)
+ )
+ presence = self.UIAAnnotationObjects.get(UIAHandler.AnnotationType_Author)
+ if presence:
+ author = presence.GetCurrentPropertyValue(UIAHandler.UIA_AnnotationAuthorPropertyId)
+ descriptionList.append(
+ # Translators: a mesage when another author is editing a cell in a shared Excel spreadsheet.
+ _("{author} is editing").format(
+ author=author
+ )
+ )
+ baseDescription = super().description
+ if baseDescription:
+ descriptionList.append(baseDescription)
+ return ", ".join(descriptionList)
+
+ #: Typing information for auto-property: _get__isContentTooLargeForCell
+ _isContentTooLargeForCell: bool
+
+ def _get__isContentTooLargeForCell(self) -> bool:
+ if not self.UIATextPattern:
+ return False
+ r = self.UIATextPattern.documentRange
+ vr = self.UIATextPattern.getvisibleRanges().getElement(0)
+ return len(vr.getText(-1)) < len(r.getText(-1))
+
+ #: Typing information for auto-property: _get__nextCellHasContent
+ _nextCellHasContent: bool
+
+ def _get__nextCellHasContent(self) -> bool:
+ nextCell = self.next
+ if nextCell and nextCell.UIATextPattern:
+ return bool(nextCell.UIATextPattern.documentRange.getText(-1))
+ return False
+
+ def _get_states(self):
+ states = super().states
+ if self._isContentTooLargeForCell:
+ if not self._nextCellHasContent:
+ states.add(controlTypes.STATE_OVERFLOWING)
+ else:
+ states.add(controlTypes.STATE_CROPPED)
+ if self._getUIACacheablePropertyValue(self._UIAExcelCustomProps.cellFormula.id):
+ states.add(controlTypes.STATE_HASFORMULA)
+ if self._getUIACacheablePropertyValue(self._UIAExcelCustomProps.hasDataValidationDropdown.id):
+ states.add(controlTypes.STATE_HASPOPUP)
+ return states
+
+ def _get_cellCoordsText(self):
+ if self._hasSelection():
+ sc = self._getUIACacheablePropertyValue(
+ UIAHandler.UIA_SelectionItemSelectionContainerPropertyId
+ ).QueryInterface(UIAHandler.IUIAutomationElement)
+
+ firstSelected = sc.GetCurrentPropertyValue(
+ UIAHandler.UIA_Selection2FirstSelectedItemPropertyId
+ ).QueryInterface(UIAHandler.IUIAutomationElement)
+
+ firstAddress = firstSelected.GetCurrentPropertyValue(
+ UIAHandler.UIA_NamePropertyId
+ ).replace('"', '')
+
+ firstValue = firstSelected.GetCurrentPropertyValue(
+ UIAHandler.UIA_ValueValuePropertyId
+ )
+
+ lastSelected = sc.GetCurrentPropertyValue(
+ UIAHandler.UIA_Selection2LastSelectedItemPropertyId
+ ).QueryInterface(UIAHandler.IUIAutomationElement)
+
+ lastAddress = lastSelected.GetCurrentPropertyValue(
+ UIAHandler.UIA_NamePropertyId
+ ).replace('"', '')
+
+ lastValue = lastSelected.GetCurrentPropertyValue(
+ UIAHandler.UIA_ValueValuePropertyId
+ )
+
+ cellCoordsTemplate = pgettext(
+ "excel-UIA",
+ # Translators: Excel, report range of cell coordinates
+ "{firstAddress} {firstValue} through {lastAddress} {lastValue}"
+ )
+ return cellCoordsTemplate.format(
+ firstAddress=firstAddress,
+ firstValue=firstValue,
+ lastAddress=lastAddress,
+ lastValue=lastValue
+ )
+ name = super().name
+ # Later builds of Excel 2016 quote the letter coordinate.
+ # We don't want the quotes.
+ name = name.replace('"', '')
+ return name
+
+ @script(
+ # Translators: the description for a script for Excel
+ description=_("Reports the note or comment thread on the current cell"),
+ gesture="kb:NVDA+alt+c")
+ def script_reportComment(self, gesture):
+ commentsElement = self.UIAAnnotationObjects.get(UIAHandler.AnnotationType_Comment)
+ if commentsElement:
+ comment = commentsElement.GetCurrentPropertyValue(UIAHandler.UIA_FullDescriptionPropertyId)
+ author = commentsElement.GetCurrentPropertyValue(UIAHandler.UIA_AnnotationAuthorPropertyId)
+ numReplies = commentsElement.GetCurrentPropertyValue(self._UIAExcelCustomProps.commentReplyCount.id)
+ if numReplies == 0:
+ # Translators: a comment on a cell in Microsoft excel.
+ text = _("{comment} by {author}").format(
+ comment=comment,
+ author=author
+ )
+ else:
+ # Translators: a comment on a cell in Microsoft excel.
+ text = _("{comment} by {author} with {numReplies} replies").format(
+ comment=comment,
+ author=author,
+ numReplies=numReplies
+ )
+ ui.message(text)
+ else:
+ # Translators: A message in Excel when there is no note
+ ui.message(_("No note or comment thread on this cell"))
+
+
+class ExcelWorksheet(ExcelObject):
+ role = controlTypes.ROLE_TABLE
+
+ # The grid UIAElement dies each time the sheet is scrolled.
+ # Therefore this grid would be announced in the focus ancestory each time which is bad.
+ # Suppress this.
+ isPresentableFocusAncestor = False
+
+ def _get_parent(self):
+ parent = super().parent
+ # We want to present the parent (sheet) in the focus ancestry
+ # As that is what has the sheet name.
+ # Making this change in an overlay class was considered, however
+ # the parent (sheet) has no useful properties (such as className) to be easily identified,
+ # and identifying it by one of its children would be more costly than the current approach.
+ parent.isPresentableFocusAncestor = True
+ # However, the selection state on the sheet is not useful, so remove it.
+ parent.states.discard(controlTypes.STATE_SELECTED)
+ return parent
+
+
+class CellEdit(ExcelObject):
+ name = ""
+
+
+class BadExcelFormulaEdit(ExcelObject):
+ """
+ Suppresses focus events on the old formula bar, which does not have a usable UIA implementation.
+ Excel used to focus the EXCEL6 window with the formula bar when editing a cell.
+ However, now focus is moved to an edit control within the actual cell being edited,
+ in the EXCEL7 window.
+ Sometimes however focus (probably proxied from MSAA) still gets occasionally fired.
+ """
+
+ shouldAllowUIAFocusEvent = False
diff --git a/source/NVDAObjects/UIA/spartanEdge.py b/source/NVDAObjects/UIA/spartanEdge.py
new file mode 100644
index 00000000000..76020b29b7c
--- /dev/null
+++ b/source/NVDAObjects/UIA/spartanEdge.py
@@ -0,0 +1,413 @@
+# 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-2021 NV Access Limited, Babbage B.V.
+
+import winVersion
+from logHandler import log
+import eventHandler
+import controlTypes
+import textInfos
+import UIAHandler
+from UIAUtils import (
+ getChildrenWithCacheFromUIATextRange,
+ getEnclosingElementWithCacheFromUIATextRange,
+)
+from . import UIA, web
+
+"""
+ A module for using the legacy Edge browser (code name spartan) via UIA.
+ Specialisations on the UIA.web module.
+"""
+
+
+class EdgeTextInfo(web.UIAWebTextInfo):
+ ...
+
+
+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 0 != super(EdgeTextInfo, tempInfo).move(textInfos.UNIT_CHARACTER, 1):
+ 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 0 == super(EdgeTextInfo, self).move(textInfos.UNIT_CHARACTER, -1):
+ 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
+
+ # C901 '_getTextWithFieldsForUIARange' is too complex
+ # Note: when working here look for opportunities to simplify
+ # and move logic out into smaller helper functions.
+ def _getTextWithFieldsForUIARange( # noqa: C901
+ 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 repeat!
+ # 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(f"rootElement: {rootElement.currentLocalizedControlType}")
+ log.debug(f"full text: {textRange.getText(-1)}")
+ log.debug(f"includeRoot: {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(f"enclosingElement: {enclosingElement.currentLocalizedControlType}")
+ startRange.MoveEndpointByRange(
+ UIAHandler.TextPatternRangeEndpoint_End,
+ enclosingRange,
+ UIAHandler.TextPatternRangeEndpoint_End
+ )
+ if 0 < startRange.CompareEndpoints(
+ UIAHandler.TextPatternRangeEndpoint_End,
+ textRange,
+ UIAHandler.TextPatternRangeEndpoint_End
+ ):
+ startRange.MoveEndpointByRange(
+ UIAHandler.TextPatternRangeEndpoint_End,
+ textRange,
+ UIAHandler.TextPatternRangeEndpoint_End
+ )
+ # Ensure we don't now have a collapsed range
+ if 0 >= startRange.CompareEndpoints(
+ UIAHandler.TextPatternRangeEndpoint_End,
+ startRange,
+ UIAHandler.TextPatternRangeEndpoint_Start
+ ):
+ 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(f"parentElement: {parentElement.currentLocalizedControlType}")
+ isRoot = UIAHandler.handler.clientObject.compareElements(parentElement, rootElement)
+ log.debug(f"isRoot: {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 = 0 > enclosingRange.CompareEndpoints(
+ UIAHandler.TextPatternRangeEndpoint_Start,
+ startRange,
+ UIAHandler.TextPatternRangeEndpoint_Start
+ )
+ clippedEnd = 0 < enclosingRange.CompareEndpoints(
+ UIAHandler.TextPatternRangeEndpoint_End,
+ startRange,
+ UIAHandler.TextPatternRangeEndpoint_End
+ )
+ 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(f"parentElement: {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 0 < tempRange.CompareEndpoints(
+ UIAHandler.TextPatternRangeEndpoint_End,
+ textRange,
+ UIAHandler.TextPatternRangeEndpoint_End
+ ):
+ tempRange.MoveEndpointByRange(
+ UIAHandler.TextPatternRangeEndpoint_End,
+ textRange,
+ UIAHandler.TextPatternRangeEndpoint_End
+ )
+ clippedEnd = True
+ else:
+ clippedEnd = False
+ if field:
+ clippedStart = 0 > parentRange.CompareEndpoints(
+ UIAHandler.TextPatternRangeEndpoint_Start,
+ textRange,
+ UIAHandler.TextPatternRangeEndpoint_Start
+ )
+ field['_startOfNode'] = not clippedStart
+ field['_endOfNode'] = not clippedEnd
+ if 0 < tempRange.CompareEndpoints(
+ UIAHandler.TextPatternRangeEndpoint_End,
+ tempRange,
+ UIAHandler.TextPatternRangeEndpoint_Start
+ ):
+ 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(web.UIAWeb):
+
+ _edgeIsPreGapRemoval = winVersion.getWinVer().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(
+ obj=self,
+ position=None,
+ _rangeObj=textRange
+ )
+ lastCharInfo._rangeObj = textRange
+ charInfo = lastCharInfo.copy()
+ charInfo.collapse()
+ # charInfo is a EdgeTextInfo_preGapRemoval, so why is
+ # EdgeTextInfo.move used instead of
+ # EdgeTextInfo_preGapRemoval.move?
+ while 0 != super(EdgeTextInfo, charInfo).move(
+ textInfos.UNIT_CHARACTER,
+ 1
+ ):
+ charInfo.setEndPoint(lastCharInfo, "startToStart")
+ if charInfo.text or charInfo._hasEmbedded():
+ break
+ lastCharInfo.setEndPoint(charInfo, "startToEnd")
+ charInfo.collapse(True)
+ return textRange
+
+ 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
+
+
+class EdgeList(web.List):
+ ...
+
+
+class EdgeHTMLRootContainer(EdgeNode):
+
+ def event_gainFocus(self):
+ firstChild = self.firstChild
+ if isinstance(firstChild, UIA):
+ eventHandler.executeEvent("gainFocus", firstChild)
+ return
+ return super().event_gainFocus()
+
+
+class EdgeHTMLTreeInterceptor(web.UIAWebTreeInterceptor):
+
+ def _get_documentConstantIdentifier(self):
+ return self.rootNVDAObject.parent.name
+
+
+class EdgeHTMLRoot(EdgeNode):
+
+ treeInterceptorClass = EdgeHTMLTreeInterceptor
+
+ def _get_shouldCreateTreeInterceptor(self):
+ return self.role == controlTypes.ROLE_DOCUMENT
+
+ def _isIframe(self):
+ """Override, the root node is never an iFrame"""
+ return False
+
+ def _get_role(self):
+ role = super().role
+ if role == controlTypes.ROLE_PANE:
+ role = controlTypes.ROLE_DOCUMENT
+ return role
diff --git a/source/NVDAObjects/UIA/web.py b/source/NVDAObjects/UIA/web.py
new file mode 100644
index 00000000000..70fc013d675
--- /dev/null
+++ b/source/NVDAObjects/UIA/web.py
@@ -0,0 +1,511 @@
+# 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-2020 NV Access Limited, Babbage B.V., Leonard de Ruijter
+
+from comtypes import COMError
+from comtypes.automation import VARIANT
+from ctypes import byref
+
+from . import (
+ UIATextInfo,
+ UIA,
+)
+
+from logHandler import log
+import controlTypes
+import cursorManager
+import re
+import aria
+import textInfos
+from UIABrowseMode import (
+ UIABrowseModeDocument,
+ UIABrowseModeDocumentTextInfo,
+ UIATextRangeQuickNavItem,
+ UIAControlQuicknavIterator,
+)
+from UIAUtils import (
+ createUIAMultiPropertyCondition,
+ getUIATextAttributeValueFromRange,
+)
+import UIAHandler
+
+"""
+UIA.web module provides a common base for behavior and utilities relevant to web browsers.
+There are specialisations for families of browsers and further specialisations for individual browsers where
+necessary. Note that not all browsers support UIA.
+"""
+
+
+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 UIAWebTextInfo(UIATextInfo):
+
+ def _get_UIAElementAtStartWithReplacedContent(self):
+ """Fetches the deepest UIAElement at the start of the text range
+ whose name has been overridden by the author (such as aria-label).
+ """
+ element = self.UIAElementAtStart
+ condition = createUIAMultiPropertyCondition(
+ {
+ UIAHandler.UIA_ControlTypePropertyId: self.UIAControlTypesWhereNameIsContent
+ },
+ {
+ UIAHandler.UIA_ControlTypePropertyId: UIAHandler.UIA_ListControlTypeId,
+ UIAHandler.UIA_IsKeyboardFocusablePropertyId: True,
+ }
+ )
+ # 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_ControlTypePropertyId)
+ cacheRequest.addProperty(UIAHandler.UIA_IsKeyboardFocusablePropertyId)
+ 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):
+ # Interactive lists
+ controlType = element.getCachedPropertyValue(UIAHandler.UIA_ControlTypePropertyId)
+ if controlType == UIAHandler.UIA_ListControlTypeId:
+ isFocusable = element.getCachedPropertyValue(UIAHandler.UIA_IsKeyboardFocusablePropertyId)
+ if isFocusable:
+ return element
+ # Nodes with an aria label or labelledby attribute
+ 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().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().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()._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 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
+ # embedded object characters (which can appear in Edgium)
+ # should also be treated as whitespace
+ # allowing to be replaced by an overridden label
+ text = text.replace('\ufffc', '')
+ 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
+
+ # C901 'getTextWithFields' is too complex
+ # Note: when working on getTextWithFields, look for opportunities to simplify
+ # and move logic out into smaller helper functions.
+ def getTextWithFields(self, formatConfig=None): # noqa: C901
+ # 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().getTextWithFields(formatConfig)
+ # 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 index < len(fields):
+ field = fields[index]
+ if index > 1 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 UIAWeb(UIA):
+
+ _TextInfo = UIAWebTextInfo
+
+ def _isIframe(self):
+ role = super().role
+ return (
+ role == controlTypes.ROLE_PANE
+ and self.UIATextPattern
+ )
+
+ def _get_role(self):
+ if self._isIframe():
+ 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:
+ return newRole
+ return super().role
+
+ def _get_states(self):
+ states = super().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(r"current=(?!false)(\w+);")
+
+ def _get_isCurrent(self) -> controlTypes.IsCurrent:
+ ariaProperties = self._getUIACacheablePropertyValue(UIAHandler.UIA_AriaPropertiesPropertyId)
+ match = self.RE_ARIA_CURRENT_PROP_VALUE.search(ariaProperties)
+ if match:
+ valueOfAriaCurrent = match.group(1)
+ try:
+ return controlTypes.IsCurrent(valueOfAriaCurrent)
+ except ValueError:
+ log.debugWarning(
+ f"Unknown aria-current value: {valueOfAriaCurrent}, ariaProperties: {ariaProperties}"
+ )
+ return controlTypes.IsCurrent.NO
+
+ 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_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 List(UIAWeb):
+
+ # non-focusable lists are readonly lists (ensures correct NVDA presentation category)
+ def _get_states(self):
+ states = super().states
+ if controlTypes.STATE_FOCUSABLE not in states:
+ states.add(controlTypes.STATE_READONLY)
+ return states
+
+
+class HeadingControlQuickNavItem(UIATextRangeQuickNavItem):
+
+ @property
+ def level(self):
+ if not hasattr(self, '_level'):
+ styleVal = getUIATextAttributeValueFromRange(self.textInfo._rangeObj, UIAHandler.UIA_StyleIdAttributeId)
+ if UIAHandler.StyleId_Heading1 <= styleVal <= UIAHandler.StyleId_Heading6:
+ self._level = styleVal - (UIAHandler.StyleId_Heading1 - 1)
+ else:
+ self._level = None
+ return self._level
+
+ def isChild(self, parent):
+ return self.level > parent.level
+
+
+def HeadingControlQuicknavIterator(itemType, document, position, direction="next"):
+ """
+ A helper for L{UIAWebTreeInterceptor._iterNodesByType}
+ that specifically yields L{HeadingControlQuickNavItem} 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.
+ """
+ # Some UI Automation web implementations expose 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:]
+ itemIter = UIAControlQuicknavIterator(
+ itemType, document, position, condition, direction=direction, itemClass=HeadingControlQuickNavItem
+ )
+ for item in itemIter:
+ # Verify this is the correct heading level via text attributes
+ if item.level and (not levelString or levelString == str(item.level)):
+ yield item
+
+
+class UIAWebTreeInterceptor(cursorManager.ReviewCursorManager, UIABrowseModeDocument):
+ TextInfo = UIABrowseModeDocumentTextInfo
+
+ def makeTextInfo(self, position):
+ try:
+ return super().makeTextInfo(position)
+ except RuntimeError as e:
+ # sometimes the stored TextRange we have for the caret/selection can die if the page mutates too much.
+ # Therefore, if we detect this, just give back the first position in the document,
+ # updating our stored version as we go.
+ if position in (textInfos.POSITION_SELECTION, textInfos.POSITION_CARET):
+ log.debugWarning(f"{position} died. Using first position instead")
+ info = self.makeTextInfo(textInfos.POSITION_FIRST)
+ self._selection = info
+ return info
+ raise e
+
+ 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 @@
-