diff --git a/.github/workflows/automaticRelease.yaml b/.github/workflows/automaticRelease.yaml index 64f94a7f..7ab9f930 100644 --- a/.github/workflows/automaticRelease.yaml +++ b/.github/workflows/automaticRelease.yaml @@ -25,14 +25,14 @@ jobs: git commit -m "Update translations" --allow-empty git pull git push - - zip-rules: + + zip-rules: name: zip up the rules directory runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v3 - - name: Build Rust Library + - name: Build Rust Library run: | cargo build --target x86_64-unknown-linux-gnu # doesn't need a release build since all that we want is the Rules dir - name: create rules.zip @@ -42,7 +42,7 @@ jobs: filename: 'Rules.zip' directory: 'addon/globalPlugins/MathCAT' path: 'Rules' - - name: Upload Rules.zip + - name: Upload Rules.zip uses: actions/upload-artifact@v4 with: name: 'Rules.zip' @@ -50,7 +50,7 @@ jobs: compression-level: 0 retention-days: 1 - rust-32: + rust-32: name: Build 32 bit windows pyd file runs-on: windows-latest # needs to run on windows because of bzip2 steps: @@ -61,7 +61,7 @@ jobs: with: python-version: 3.11 architecture: 'x86' - - name: Build Rust Library + - name: Build Rust Library run: | cargo build --target i686-pc-windows-msvc --release - name: Setup Example dir @@ -79,15 +79,15 @@ jobs: filename: '../libmathcat_py-32-3.11-win.zip' directory: 'Example' path: 'libmathcat_py.pyd' - - name: Upload 32 bit pyd file + - name: Upload 32 bit pyd file uses: actions/upload-artifact@v4 with: name: libmathcat_py-32-3.11-win.zip path: libmathcat_py-32-3.11-win.zip compression-level: 0 retention-days: 1 - - rust-64: + + rust-64: name: Build 64 bit windows pyd file runs-on: windows-latest # needs to run on windows because of bzip2 steps: @@ -98,7 +98,7 @@ jobs: with: python-version: 3.11 architecture: 'x64' - - name: Build Rust Library + - name: Build Rust Library run: | cargo build --target x86_64-pc-windows-msvc --release - name: Setup Example dir @@ -116,15 +116,15 @@ jobs: run: | cd Example python test.py - - name: Upload 64 bit pyd file + - name: Upload 64 bit pyd file uses: actions/upload-artifact@v4 with: name: libmathcat_py-64-3.11-win.zip path: libmathcat_py-64-3.11-win.zip compression-level: 0 retention-days: 1 - - linux-64: + + linux-64: name: Build linux pyd file (64-bit intel) runs-on: ubuntu-latest steps: @@ -135,7 +135,7 @@ jobs: with: python-version: 3.11 architecture: 'x64' - - name: Build Rust Library + - name: Build Rust Library run: | cargo build --target x86_64-unknown-linux-gnu --release - name: Setup Example dir @@ -154,14 +154,14 @@ jobs: filename: '../libmathcat_py-64-3.11-linux.zip' directory: 'Example' path: 'libmathcat_py.so' - - name: Upload 64 bit pyd file + - name: Upload 64 bit pyd file uses: actions/upload-artifact@v4 with: name: libmathcat_py-64-3.11-linux.zip path: libmathcat_py-64-3.11-linux.zip compression-level: 0 retention-days: 1 - + build-addon: name: build-addon continue-on-error: false @@ -202,7 +202,7 @@ jobs: - name: Run scons to build .addon file run: | scons - - name: Upload the addon + - name: Upload the addon uses: actions/upload-artifact@v4 with: name: addon @@ -210,7 +210,7 @@ jobs: compression-level: 0 retention-days: 1 - pre-release: + pre-release: name: Pre Release continue-on-error: false needs: [zip-rules, rust-32, rust-64, linux-64, build-addon] diff --git a/.github/workflows/checkTranslatorsComments.yml b/.github/workflows/checkTranslatorsComments.yaml similarity index 97% rename from .github/workflows/checkTranslatorsComments.yml rename to .github/workflows/checkTranslatorsComments.yaml index a8598c5c..8cbe7aac 100644 --- a/.github/workflows/checkTranslatorsComments.yml +++ b/.github/workflows/checkTranslatorsComments.yaml @@ -29,7 +29,7 @@ jobs: pip install scons markdown sudo apt update sudo apt install gettext - + - name: Generate the .pot file run: scons pot @@ -43,7 +43,7 @@ jobs: # checkPot.EXPECTED_MESSAGES_WITHOUT_COMMENTS = set() # res = checkPot.checkPot('$(ls *.pot)') # exit(res) - #shell: python + #shell: python run: | python -c "import checkPot;checkPot.EXPECTED_MESSAGES_WITHOUT_COMMENTS = set();exit(checkPot.checkPot('$(ls *.pot)'))" echo "nb_errors=$?" >> "$GITHUB_OUTPUT" @@ -58,4 +58,3 @@ jobs: echo "Translators comments: FAIL" exit 1; fi - \ No newline at end of file diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml deleted file mode 100644 index db3c66cb..00000000 --- a/.github/workflows/lint.yaml +++ /dev/null @@ -1,36 +0,0 @@ -name: Lint - -on: - push: - branches: - - main - pull_request: - branches: - - main - -jobs: - - lint: - runs-on: windows-latest - - steps: - - name: Checkout code - uses: actions/checkout@v3 - with: - path: code - - name: Set up Python 3.11 - uses: actions/setup-python@v4 - with: - python-version: 3.11 - - name: debugging - run: dir -l code - - name: Install dependencies (flake8) - run: pip install -r code/.github/workflows/requirements.txt - - name: Lint - run: flake8 code/addon --max-line-length=130 --extend-exclude=code/addon/globalPlugins/MathCAT/yaml/* - - name: type check - uses: jakebailey/pyright-action@v1 - with: - working-directory: code/addon/globalPlugins/MathCAT - python-version: 3.11 - pylance-version: latest-release \ No newline at end of file diff --git a/.github/workflows/manualRelease.yaml b/.github/workflows/manualRelease.yaml index 54dcf356..21952ea4 100644 --- a/.github/workflows/manualRelease.yaml +++ b/.github/workflows/manualRelease.yaml @@ -3,7 +3,7 @@ name: Manual release on: workflow_dispatch: inputs: - version: + version: description: 'Add-on version' required: true default: '0.0.0' @@ -20,7 +20,7 @@ jobs: buildAndUpload: continue-on-error: true runs-on: ubuntu-latest - + permissions: contents: write @@ -50,7 +50,7 @@ jobs: f.seek(0) f.write(text) f.truncate() - shell: python + shell: python - name: Build add-on run: scons - name: Push changes @@ -67,7 +67,7 @@ jobs: gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} passphrase: ${{ secrets.PASSPHRASE }} - if: ${{ inputs.signAddOn }} - name: Sign add-on + name: Sign add-on run: gpg --detach-sign *.nvda-addon - name: Calculate sha256 run: sha256sum *.nvda-addon >> sha256.txt @@ -82,4 +82,3 @@ jobs: artifacts: "*.nvda-addon,*.sig,publicKey.asc,sha256.txt" generateReleaseNotes: true prerelease: ${{ inputs.prerelease }} - diff --git a/.github/workflows/requirements.txt b/.github/workflows/requirements.txt deleted file mode 100644 index 79ef110f..00000000 --- a/.github/workflows/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -####### requirements.txt ####### -# -###### Requirements for automated lint ###### -flake8 >= 3.8 -flake8-tabs >= 2.3 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 0be8af1c..51f2b434 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ manifest.ini *.nvda-addon .sconsign.dblite /[0-9]*.[0-9]*.[0-9]*.json +.venv/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..d5c872f7 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,96 @@ + +# https://pre-commit.ci/ +# Configuration for Continuous Integration service +ci: + skip: [pyrightLocal] + autoupdate_schedule: monthly + autoupdate_commit_msg: "Pre-commit auto-update" + autofix_commit_msg: "Pre-commit auto-fix" + submodules: true + +default_language_version: + python: python3.11 + +repos: +- repo: https://github.com/pre-commit-ci/pre-commit-ci-config + rev: v1.6.1 + hooks: + - id: check-pre-commit-ci-config + +- repo: meta + hooks: + # ensures that exclude directives apply to any file in the repository. + - id: check-useless-excludes + # ensures that the configured hooks apply to at least one file in the repository. + - id: check-hooks-apply + +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + # Prevents commits to certain branches + - id: no-commit-to-branch + args: ["--branch", "main"] + # Checks that large files have not been added. Default cut-off for "large" files is 500kb. + - id: check-added-large-files + # Checks python syntax + - id: check-ast + # Checks for filenames that will conflict on case insensitive filesystems (the majority of Windows filesystems, most of the time) + - id: check-case-conflict + # Checks for artifacts from resolving merge conflicts. + - id: check-merge-conflict + # Checks Python files for debug statements, such as python's breakpoint function, or those inserted by some IDEs. + - id: debug-statements + # Removes trailing whitespace. + - id: trailing-whitespace + types_or: [python, batch, markdown, toml, yaml, rust] + # Ensures all files end in 1 (and only 1) newline. + - id: end-of-file-fixer + types_or: [python, batch, markdown, toml, yaml, rust] + # Removes the UTF-8 BOM from files that have it. + # See https://github.com/nvaccess/nvda/blob/master/projectDocs/dev/codingStandards.md#encoding + - id: fix-byte-order-marker + types_or: [python, batch, markdown, toml, yaml, rust] + # Validates TOML files. + - id: check-toml + # Validates YAML files. + - id: check-yaml + # Validates XML files. + # Ensures that links to lines in files under version control point to a particular commit. + - id: check-vcs-permalinks + # Avoids using reserved Windows filenames. + - id: check-illegal-windows-names + +- repo: https://github.com/asottile/add-trailing-comma + rev: v3.1.0 + hooks: + # Ruff preserves indent/new-line formatting of function arguments, list items, and similar iterables, + # if a trailing comma is added. + # This adds a trailing comma to args/iterable items in case it was missed. + - id: add-trailing-comma + +- repo: https://github.com/astral-sh/ruff-pre-commit + # Matches Ruff version in pyproject. + rev: v0.8.1 + hooks: + - id: ruff + name: lint with ruff + args: [ --fix ] + - id: ruff-format + name: format with ruff + +- repo: https://github.com/RobertCraigie/pyright-python + rev: v1.1.394 + hooks: + - id: pyright + alias: pyrightLocal + name: Check types with pyright + +- repo: https://github.com/RobertCraigie/pyright-python + rev: v1.1.396 + hooks: + - id: pyright + alias: pyrightCI + name: Check types with pyright + # use nodejs version of pyright and install pyproject.toml for CI + additional_dependencies: [".", "pyright[nodejs]"] + stages: [manual] # Only run from CI manually diff --git a/Cargo.toml b/Cargo.toml index 2549373d..7b4eeed5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,7 @@ version = "0.22" features = ["extension-module", "abi3"] [build-dependencies] -zip = { version = "2.6", default-features = false, features = ["bzip2"] } +zip = { version = "3.0", default-features = false, features = ["bzip2"] } mathcat = {version = "=0.6.9", features = ["include-zip"]} # for building, we want the zip files so we can include them separately # mathcat = { path = "../MathCAT/", features = ["include-zip"]} # for building, we want the zip files so we can include them separately # for testing MathCAT without having to publish a new version (change two occurrences) diff --git a/Example/test.py b/Example/test.py index 37038ac5..579d0c79 100644 --- a/Example/test.py +++ b/Example/test.py @@ -8,7 +8,7 @@ import os import sys -import libmathcat_py as libmathcat # type: ignore +import libmathcat_py as libmathcat # import shutil # if os.path.exists("libmathcat_py.pyd"): @@ -17,91 +17,91 @@ def setMathCATPreferences(): - try: - libmathcat.SetRulesDir( - # this assumes the Rules dir is in the same dir a the library. Modify as needed - os.path.join(os.path.dirname(os.path.abspath(__file__)), "Rules") - ) - except Exception as e: - sys.exit(f"problem with finding the MathCAT rules: {e}") - - try: - libmathcat.SetPreference("TTS", "none") - libmathcat.SetPreference("Language", "en") # Also "id" and "vi" - libmathcat.SetPreference("SpeechStyle", "SimpleSpeak") # Also "ClearSpeak" - libmathcat.SetPreference("Verbosity", "Verbose") # also terse "Terse"/"Medium" - libmathcat.SetPreference("CapitalLetters_UseWord", "true") # if "true", X => "cap x" - libmathcat.SetPreference("BrailleCode", "Nemeth") - except Exception as e: - sys.exit(f"problem with setting a preference: {e}") + try: + libmathcat.SetRulesDir( + # this assumes the Rules dir is in the same dir a the library. Modify as needed + os.path.join(os.path.dirname(os.path.abspath(__file__)), "Rules"), + ) + except Exception as e: + sys.exit(f"problem with finding the MathCAT rules: {e}") + + try: + libmathcat.SetPreference("TTS", "none") + libmathcat.SetPreference("Language", "en") # Also "id" and "vi" + libmathcat.SetPreference("SpeechStyle", "SimpleSpeak") # Also "ClearSpeak" + libmathcat.SetPreference("Verbosity", "Verbose") # also terse "Terse"/"Medium" + libmathcat.SetPreference("CapitalLetters_UseWord", "true") # if "true", X => "cap x" + libmathcat.SetPreference("BrailleCode", "Nemeth") + except Exception as e: + sys.exit(f"problem with setting a preference: {e}") def setMathMLForMathCAT(mathml: str): - try: - libmathcat.SetMathML(mathml) - except Exception as e: - sys.exit(f"problem with setMathML: {e}") + try: + libmathcat.SetMathML(mathml) + except Exception as e: + sys.exit(f"problem with setMathML: {e}") def getSpeech(): - try: - return libmathcat.GetSpokenText() - except Exception as e: - sys.exit(f"problem with getting speech for MathML: {e}") + try: + return libmathcat.GetSpokenText() + except Exception as e: + sys.exit(f"problem with getting speech for MathML: {e}") def getBraille(): - try: - return libmathcat.GetBraille("") - except Exception as e: - sys.exit(f"problem with getting braille for MathML: {e}") + try: + return libmathcat.GetBraille("") + except Exception as e: + sys.exit(f"problem with getting braille for MathML: {e}") def test(): - setMathCATPreferences() # you only need to this once - print("Using MathCAT version '{}'".format(libmathcat.GetVersion())) - - mathml = " 1 X " - setMathMLForMathCAT(mathml) - speech = getSpeech() - if speech != '1 over cap x': - sys.exit(f"MathML: {mathml}\nSpeech: '{speech}'") - braille = getBraille() - if braille != '⠹⠂⠌⠠⠭⠼': - sys.exit(f"MathML: {mathml}\nBraille: '{braille}'") - - mathml = "xy" - setMathMLForMathCAT(mathml) - speech = getSpeech() - if speech != 'x cross product y': - sys.exit(f"MathML: {mathml}\nSpeech: '{speech}'") - - mathml = " x 3 " - setMathMLForMathCAT(mathml) - speech = getSpeech() - if speech != 'x cubed': - sys.exit(f"MathML: {mathml}\nSpeech: '{speech}'") - - mathml = " x T " - setMathMLForMathCAT(mathml) - speech = getSpeech() - if speech != 'x transpose': - sys.exit(f"MathML: {mathml}\nSpeech: '{speech}'") - - mathml = "!" - setMathMLForMathCAT(mathml) - speech = getSpeech() - if speech != 'x factorial': - sys.exit(f"MathML: {mathml}\nSpeech: '{speech}'") - - mathml = "\ + setMathCATPreferences() # you only need to this once + print("Using MathCAT version '{}'".format(libmathcat.GetVersion())) + + mathml = " 1 X " + setMathMLForMathCAT(mathml) + speech = getSpeech() + if speech != "1 over cap x": + sys.exit(f"MathML: {mathml}\nSpeech: '{speech}'") + braille = getBraille() + if braille != "⠹⠂⠌⠠⠭⠼": + sys.exit(f"MathML: {mathml}\nBraille: '{braille}'") + + mathml = "xy" + setMathMLForMathCAT(mathml) + speech = getSpeech() + if speech != "x cross product y": + sys.exit(f"MathML: {mathml}\nSpeech: '{speech}'") + + mathml = " x 3 " + setMathMLForMathCAT(mathml) + speech = getSpeech() + if speech != "x cubed": + sys.exit(f"MathML: {mathml}\nSpeech: '{speech}'") + + mathml = " x T " + setMathMLForMathCAT(mathml) + speech = getSpeech() + if speech != "x transpose": + sys.exit(f"MathML: {mathml}\nSpeech: '{speech}'") + + mathml = "!" + setMathMLForMathCAT(mathml) + speech = getSpeech() + if speech != "x factorial": + sys.exit(f"MathML: {mathml}\nSpeech: '{speech}'") + + mathml = "\ (73)" - setMathMLForMathCAT(mathml) - speech = getSpeech() - if speech != '7 choose 3': - sys.exit(f"MathML: {mathml}\nSpeech: '{speech}'") + setMathMLForMathCAT(mathml) + speech = getSpeech() + if speech != "7 choose 3": + sys.exit(f"MathML: {mathml}\nSpeech: '{speech}'") - print("Test was successful!") + print("Test was successful!") test() diff --git a/addon/globalPlugins/MathCAT/MathCAT.py b/addon/globalPlugins/MathCAT/MathCAT.py index b378bae0..5dd95536 100644 --- a/addon/globalPlugins/MathCAT/MathCAT.py +++ b/addon/globalPlugins/MathCAT/MathCAT.py @@ -9,7 +9,6 @@ # The plugin also requires the use of a small python dll: python3.dll # python3.dll has "Copyright © 2001-2022 Python Software Foundation; All Rights Reserved" - # Note: this code is a lot of cut/paste from other code and very likely could be substantially improved/cleaned. import braille # we generate braille import mathPres # math plugin stuff @@ -23,7 +22,7 @@ import winKernel import gui -from . import libmathcat_py as libmathcat # type: ignore +from . import libmathcat_py as libmathcat from typing import List, Dict from keyboardHandler import KeyboardInputGesture # navigation key strokes from logHandler import log # logging @@ -37,15 +36,15 @@ # speech/SSML processing borrowed from NVDA's mathPres/mathPlayer.py from speech.commands import ( - BeepCommand, - PitchCommand, - VolumeCommand, - RateCommand, - LangChangeCommand, - BreakCommand, - CharacterModeCommand, - PhonemeCommand, - IndexCommand, + BeepCommand, + PitchCommand, + VolumeCommand, + RateCommand, + LangChangeCommand, + BreakCommand, + CharacterModeCommand, + PhonemeCommand, + IndexCommand, ) from textUtils import WCHAR_ENCODING @@ -58,26 +57,26 @@ addonHandler.initTranslation() RE_MATHML_SPEECH = re.compile( - # Break. - r" ?" - # Pronunciation of characters. - r"|(?P[^<]+) ?" - # Specific pronunciation. - r"|(?P[^ <]+) ?" - # Prosody. - r"| ?" - r"|(?P) ?" - r"| ?" # hack for beeps - # Other tags, which we don't care about. - r"|<[^>]+> ?" - # Actual content. - r"|(?P[^<]+)" + # Break. + r" ?" + # Pronunciation of characters. + r"|(?P[^<]+) ?" + # Specific pronunciation. + r"|(?P[^ <]+) ?" + # Prosody. + r"| ?" + r"|(?P) ?" + r"| ?" # hack for beeps + # Other tags, which we don't care about. + r"|<[^>]+> ?" + # Actual content. + r"|(?P[^<]+)", ) PROSODY_COMMANDS = { - "pitch": PitchCommand, - "volume": VolumeCommand, - "rate": RateCommand, + "pitch": PitchCommand, + "volume": VolumeCommand, + "rate": RateCommand, } RE_MATH_LANG = re.compile(r"""""") @@ -87,559 +86,562 @@ def getLanguageToUse(mathMl: str = "") -> str: - """Get the language specified in a math tag if the language pref is Auto, else the language preference.""" - mathCATLanguageSetting = "Auto" - try: - # ignore regional differences if the MathCAT language setting doesn't have it. - mathCATLanguageSetting = libmathcat.GetPreference("Language") - except Exception as e: - log.exception(e) - - # log.info(f"getLanguageToUse: {mathCATLanguageSetting}") - if mathCATLanguageSetting != "Auto": - return mathCATLanguageSetting - - languageMatch = RE_MATH_LANG.search(mathMl) - language = (languageMatch.group(2) if languageMatch else getCurrentLanguage()) # seems to be current voice's language - language = language.lower().replace("_", "-") - if language == "cmn": - language = "zh-cmn" - elif language == "yue": - language = "zh-yue" - return language + """Get the language specified in a math tag if the language pref is Auto, else the language preference.""" + mathCATLanguageSetting = "Auto" + try: + # ignore regional differences if the MathCAT language setting doesn't have it. + mathCATLanguageSetting = libmathcat.GetPreference("Language") + except Exception as e: + log.exception(e) + + # log.info(f"getLanguageToUse: {mathCATLanguageSetting}") + if mathCATLanguageSetting != "Auto": + return mathCATLanguageSetting + + languageMatch = RE_MATH_LANG.search(mathMl) + language = ( + languageMatch.group(2) if languageMatch else getCurrentLanguage() + ) # seems to be current voice's language + language = language.lower().replace("_", "-") + if language == "cmn": + language = "zh-cmn" + elif language == "yue": + language = "zh-yue" + return language def ConvertSSMLTextForNVDA(text: str) -> list: - """Change the SSML in the text into NVDA's command structure. - The environment is examined to determine whether a language switch is needed""" - # MathCAT's default rate is 180 wpm. - # Assume that 0% is 80 wpm and 100% is 450 wpm and scale accordingly. - # log.info(f"\nSpeech str: '{text}'") - - # find MathCAT's language setting and store it away (could be "Auto") - # if MathCAT's setting doesn't match NVDA's language setting, change the language that is used - mathCATLanguageSetting = "en" # set in case GetPreference fails - try: - mathCATLanguageSetting = libmathcat.GetPreference("Language") - except Exception as e: - log.exception(e) - language = getLanguageToUse() - nvdaLanguage = getCurrentLanguage().replace("_", "-") - # log.info(f"mathCATLanguageSetting={mathCATLanguageSetting}, lang={language}, NVDA={nvdaLanguage}") - - _monkeyPatchESpeak() - - synth = getSynth() - # I tried the engines on a 180 word excerpt. The speeds do not change linearly and differ a it between engines - # At "50" espeak finished in 46 sec, sapi in 75 sec, and one core in 70; at '100' one core was much slower than the others - wpm = 2*getSynth()._get_rate() - breakMulti = 180.0 / wpm - supported_commands = synth.supportedCommands - use_break = BreakCommand in supported_commands - use_pitch = PitchCommand in supported_commands - # use_rate = RateCommand in supported_commands - # use_volume = VolumeCommand in supported_commands - use_phoneme = PhonemeCommand in supported_commands - # as of 7/23, oneCore voices do not implement the CharacterModeCommand despite it being in supported_commands - use_character = (CharacterModeCommand in supported_commands and synth.name != "oneCore") - out = [] - if mathCATLanguageSetting != language: - # log.info(f"Setting language to {language}") - try: - libmathcat.SetPreference("Language", language) - except Exception as e: - log.exception(e) - language = mathCATLanguageSetting # didn't set the language - if language != nvdaLanguage: - out.append(LangChangeCommand(language)) - - resetProsody = [] - # log.info(f"\ntext: {text}") - for m in RE_MATHML_SPEECH.finditer(text): - if m.lastgroup == "break": - if use_break: - out.append(BreakCommand(time=int(int(m.group("break")) * breakMulti))) - elif m.lastgroup == "char": - ch = m.group("char") - if use_character: - out.extend((CharacterModeCommand(True), ch, CharacterModeCommand(False))) - else: - out.extend((" ", "eigh" if ch == "a" and language.startswith("en") else ch, " ")) - elif m.lastgroup == "beep": - out.append(BeepCommand(2000, 50)) - elif m.lastgroup == "pitch": - if use_pitch: - out.append(PitchCommand(multiplier=int(m.group(m.lastgroup)))) - resetProsody.append(PitchCommand) - elif m.lastgroup in PROSODY_COMMANDS: - command = PROSODY_COMMANDS[m.lastgroup] - if command in supported_commands: - out.append(command(multiplier=int(m.group(m.lastgroup)) / 100.0)) - resetProsody.append(command) - elif m.lastgroup == "prosodyReset": - # for command in resetProsody: # only supported commands were added, so no need to check - command = resetProsody.pop() - out.append(command(multiplier=1)) - elif m.lastgroup == "phonemeText": - if use_phoneme: - out.append(PhonemeCommand(m.group("ipa"), text=m.group("phonemeText"))) - else: - out.append(m.group("phonemeText")) - elif m.lastgroup == "content": - # MathCAT puts out spaces between words, the speak command seems to want to glom the strings together at times, - # so we need to add individual " "s to the output - out.extend((" ", m.group(0), " ")) - # there is a bug in MS Word that concats the math and the next character outside of math, so we add a space - out.append(" ") - - if mathCATLanguageSetting != language: - # restore the old value (probably "Auto") - try: - libmathcat.SetPreference("Language", mathCATLanguageSetting) - except Exception as e: - log.exception(e) - if language != nvdaLanguage: - out.append(LangChangeCommand(None)) - # log.info(f"Speech commands: '{out}'") - return out + """Change the SSML in the text into NVDA's command structure. + The environment is examined to determine whether a language switch is needed""" + # MathCAT's default rate is 180 wpm. + # Assume that 0% is 80 wpm and 100% is 450 wpm and scale accordingly. + # log.info(f"\nSpeech str: '{text}'") + + # find MathCAT's language setting and store it away (could be "Auto") + # if MathCAT's setting doesn't match NVDA's language setting, change the language that is used + mathCATLanguageSetting = "en" # set in case GetPreference fails + try: + mathCATLanguageSetting = libmathcat.GetPreference("Language") + except Exception as e: + log.exception(e) + language = getLanguageToUse() + nvdaLanguage = getCurrentLanguage().replace("_", "-") + # log.info(f"mathCATLanguageSetting={mathCATLanguageSetting}, lang={language}, NVDA={nvdaLanguage}") + + _monkeyPatchESpeak() + + synth = getSynth() + # I tried the engines on a 180 word excerpt. The speeds do not change linearly and differ a it between engines + # At "50" espeak finished in 46 sec, sapi in 75 sec, and one core in 70; at '100' one core was much slower than the others + wpm = 2 * getSynth()._get_rate() + breakMulti = 180.0 / wpm + supported_commands = synth.supportedCommands + use_break = BreakCommand in supported_commands + use_pitch = PitchCommand in supported_commands + # use_rate = RateCommand in supported_commands + # use_volume = VolumeCommand in supported_commands + use_phoneme = PhonemeCommand in supported_commands + # as of 7/23, oneCore voices do not implement the CharacterModeCommand despite it being in supported_commands + use_character = CharacterModeCommand in supported_commands and synth.name != "oneCore" + out = [] + if mathCATLanguageSetting != language: + # log.info(f"Setting language to {language}") + try: + libmathcat.SetPreference("Language", language) + except Exception as e: + log.exception(e) + language = mathCATLanguageSetting # didn't set the language + if language != nvdaLanguage: + out.append(LangChangeCommand(language)) + + resetProsody = [] + # log.info(f"\ntext: {text}") + for m in RE_MATHML_SPEECH.finditer(text): + if m.lastgroup == "break": + if use_break: + out.append(BreakCommand(time=int(int(m.group("break")) * breakMulti))) + elif m.lastgroup == "char": + ch = m.group("char") + if use_character: + out.extend((CharacterModeCommand(True), ch, CharacterModeCommand(False))) + else: + out.extend((" ", "eigh" if ch == "a" and language.startswith("en") else ch, " ")) + elif m.lastgroup == "beep": + out.append(BeepCommand(2000, 50)) + elif m.lastgroup == "pitch": + if use_pitch: + out.append(PitchCommand(multiplier=int(m.group(m.lastgroup)))) + resetProsody.append(PitchCommand) + elif m.lastgroup in PROSODY_COMMANDS: + command = PROSODY_COMMANDS[m.lastgroup] + if command in supported_commands: + out.append(command(multiplier=int(m.group(m.lastgroup)) / 100.0)) + resetProsody.append(command) + elif m.lastgroup == "prosodyReset": + # for command in resetProsody: # only supported commands were added, so no need to check + command = resetProsody.pop() + out.append(command(multiplier=1)) + elif m.lastgroup == "phonemeText": + if use_phoneme: + out.append(PhonemeCommand(m.group("ipa"), text=m.group("phonemeText"))) + else: + out.append(m.group("phonemeText")) + elif m.lastgroup == "content": + # MathCAT puts out spaces between words, the speak command seems to want to glom the strings together at times, + # so we need to add individual " "s to the output + out.extend((" ", m.group(0), " ")) + # there is a bug in MS Word that concats the math and the next character outside of math, so we add a space + out.append(" ") + + if mathCATLanguageSetting != language: + # restore the old value (probably "Auto") + try: + libmathcat.SetPreference("Language", mathCATLanguageSetting) + except Exception as e: + log.exception(e) + if language != nvdaLanguage: + out.append(LangChangeCommand(None)) + # log.info(f"Speech commands: '{out}'") + return out class MathCATInteraction(mathPres.MathInteractionNVDAObject): - # Put MathML or other formats on the clipboard. - # MathML is put on the clipboard using the two formats below (defined by MathML spec) - # We use both formats because some apps may only use one or the other - # Note: filed https://github.com/nvaccess/nvda/issues/13240 to make this usable outside of MathCAT - CF_MathML = windll.user32.RegisterClipboardFormatW("MathML") - CF_MathML_Presentation = windll.user32.RegisterClipboardFormatW( - "MathML Presentation" - ) - # log.info("2**** MathCAT registering data formats: - # CF_MathML %x, CF_MathML_Presentation %x" % (CF_MathML, CF_MathML_Presentation)) - - def __init__(self, provider=None, mathMl: Optional[str] = None): - super(MathCATInteraction, self).__init__(provider=provider, mathMl=mathMl) - if mathMl is None: - self.init_mathml = "" - else: - self.init_mathml = mathMl - - def reportFocus(self): - super(MathCATInteraction, self).reportFocus() - # try to get around espeak bug where voice slows down - if _synthesizer_rate and getSynth().name == 'espeak': - getSynth()._set_rate(_synthesizer_rate) - try: - text = libmathcat.DoNavigateCommand("ZoomIn") - speech.speak(ConvertSSMLTextForNVDA(text)) - except Exception as e: - log.exception(e) - # Translators: this message directs users to look in the log file - speech.speakMessage(_("Error in starting navigation of math: see NVDA error log for details")) - finally: - # try to get around espeak bug where voice slows down - if _synthesizer_rate and getSynth().name == 'espeak': - # log.info(f'reportFocus: reset to {_synthesizer_rate}') - getSynth()._set_rate(_synthesizer_rate) - - def getBrailleRegions(self, review: bool = False): - # log.info("***MathCAT start getBrailleRegions") - yield braille.NVDAObjectRegion(self, appendText=" ") - region = braille.Region() - region.focusToHardLeft = True - # libmathcat.SetBrailleWidth(braille.handler.displaySize) - try: - region.rawText = libmathcat.GetBraille("") - except Exception as e: - log.exception(e) - # Translators: this message directs users to look in the log file - speech.speakMessage(_("Error in brailling math: see NVDA error log for details")) - region.rawText = "" - - # log.info("***MathCAT end getBrailleRegions ***") - yield region - - def getScript(self, gesture: KeyboardInputGesture): - # Pass most keys to MathCAT. Pretty ugly. - if ( - isinstance(gesture, KeyboardInputGesture) and - "NVDA" not in gesture.modifierNames and - gesture.mainKeyName in { - "leftArrow", - "rightArrow", - "upArrow", - "downArrow", - "home", - "end", - "space", - "backspace", - "enter", - "0", - "1", - "2", - "3", - "4", - "5", - "6", - "7", - "8", - "9", - } - # or len(gesture.mainKeyName) == 1 - ): - return self.script_navigate - else: - return super().getScript(gesture) - - def script_navigate(self, gesture: KeyboardInputGesture): - try: - # try to get around espeak bug where voice slows down - if _synthesizer_rate and getSynth().name == 'espeak': - getSynth()._set_rate(_synthesizer_rate) - if (gesture is not None): # == None when initial focus -- handled in reportFocus() - modNames = gesture.modifierNames - text = libmathcat.DoNavigateKeyPress( - gesture.vkCode, - "shift" in modNames, - "control" in modNames, - "alt" in modNames, - False, - ) - # log.info(f"Navigate speech for {gesture.vkCode}/(s={'shift' in modNames}, c={'control' in modNames}): '{text}'") - speech.speak(ConvertSSMLTextForNVDA(text)) - except Exception as e: - log.exception(e) - # Translators: this message directs users to look in the log file - speech.speakMessage(_("Error in navigating math: see NVDA error log for details")) - finally: - # try to get around espeak bug where voice slows down - if _synthesizer_rate and getSynth().name == 'espeak': - # log.info(f'script_navigate: reset to {_synthesizer_rate}') - getSynth()._set_rate(_synthesizer_rate) - - if not braille.handler.enabled: - return - - try: - # update the braille to reflect the nav position (might be excess code, but it works) - nav_node = libmathcat.GetNavigationMathMLId() - braille_chars = libmathcat.GetBraille(nav_node[0]) - # log.info(f'braille display = {config.conf["braille"]["display"]}, braille_chars: {braille_chars}') - region = braille.Region() - region.rawText = braille_chars - region.focusToHardLeft = True - region.update() - braille.handler.buffer.regions.append(region) - braille.handler.buffer.focus(region) - braille.handler.buffer.update() - braille.handler.update() - except Exception as e: - log.exception(e) - # Translators: this message directs users to look in the log file - speech.speakMessage(_("Error in brailling math: see NVDA error log for details")) - - _startsWithMath = re.compile("\\s*?\n" + mathml + "" - ) # copy will fix up name spacing - elif self.init_mathml != "": - mathml = self.init_mathml - if copy_as == "speech": - # save the old MathML, set the navigation MathML as MathMl, get the speech, then reset the MathML - saved_mathml: str = self.init_mathml - saved_tts = libmathcat.GetPreference("TTS") - if saved_mathml == '': # shouldn't happen - raise Exception("Internal error -- MathML not set for copy") - libmathcat.SetPreference("TTS", "None") - libmathcat.SetMathML(mathml) - # get the speech text and collapse the whitespace - text_to_copy = ' '.join(libmathcat.GetSpokenText().split()) - libmathcat.SetPreference("TTS", saved_tts) - libmathcat.SetMathML(saved_mathml) - else: - text_to_copy = self._wrapMathMLForClipBoard(mathml) - - self._copyToClipAsMathML(text_to_copy, copy_as == "mathml") - # Translators: copy to clipboard - ui.message(_("copy as ") + copy_as) - except Exception as e: - log.exception(e) - # Translators: this message directs users to look in the log file - speech.speakMessage(_("unable to copy math: see NVDA error log for details")) - - # not a perfect match sequence, but should capture normal MathML - # not a perfect match sequence, but should capture normal MathML - _mathTagHasNameSpace = re.compile("") - _hasAddedId = re.compile(" id='[^'].+' data-id-added='true'") - _hasDataAttr = re.compile(" data-[^=]+='[^']*'") - - def _wrapMathMLForClipBoard(self, text: str) -> str: - # cleanup the MathML a little - text = re.sub(self._hasAddedId, "", text) - mathml_with_ns = re.sub(self._hasDataAttr, "", text) - if not re.match(self._mathTagHasNameSpace, mathml_with_ns): - mathml_with_ns = mathml_with_ns.replace( - "math", "math xmlns='http://www.w3.org/1998/Math/MathML'", 1 - ) - return mathml_with_ns - - def _copyToClipAsMathML(self, text: str, is_mathml: bool, notify: Optional[bool] = False) -> bool: - """Copies the given text to the windows clipboard. - @returns: True if it succeeds, False otherwise. - @param text: the text which will be copied to the clipboard - @param notify: whether to emit a confirmation message - """ - # copied from api.py and modified to use CF_MathML_Presentation - if not isinstance(text, str) or len(text) == 0: - return False - - try: - with winUser.openClipboard(gui.mainFrame.Handle): - winUser.emptyClipboard() - if is_mathml: - self._setClipboardData(self.CF_MathML, '' + text) - self._setClipboardData(self.CF_MathML_Presentation, '' + text) - self._setClipboardData(winUser.CF_UNICODETEXT, text) - got = getClipData() - except OSError: - if notify: - ui.reportTextCopiedToClipboard() # No argument reports a failure. - return False - if got == text: - if notify: - ui.reportTextCopiedToClipboard(text) - return True - if notify: - ui.reportTextCopiedToClipboard() # No argument reports a failure. - return False - - def _setClipboardData(self, format, data: str): - # Need to support MathML Presentation, so this copied from winUser.py and the first two lines are commented out - # For now only unicode is a supported format - # if format!=CF_UNICODETEXT: - # raise ValueError("Unsupported format") - text = data - bufLen = len(text.encode(WCHAR_ENCODING, errors="surrogatepass")) + 2 - # Allocate global memory - h = winKernel.HGLOBAL.alloc(winKernel.GMEM_MOVEABLE, bufLen) - # Acquire a lock to the global memory receiving a local memory address - with h.lock() as addr: - # Write the text into the allocated memory - buf = (c_wchar * bufLen).from_address(addr) - buf.value = text - # Set the clipboard data with the global memory - if not windll.user32.SetClipboardData(format, h): - raise WinError() - # NULL the global memory handle so that it is not freed at the end of scope as the clipboard now has it. - h.forget() + # Put MathML or other formats on the clipboard. + # MathML is put on the clipboard using the two formats below (defined by MathML spec) + # We use both formats because some apps may only use one or the other + # Note: filed https://github.com/nvaccess/nvda/issues/13240 to make this usable outside of MathCAT + CF_MathML = windll.user32.RegisterClipboardFormatW("MathML") + CF_MathML_Presentation = windll.user32.RegisterClipboardFormatW( + "MathML Presentation", + ) + # log.info("2**** MathCAT registering data formats: + # CF_MathML %x, CF_MathML_Presentation %x" % (CF_MathML, CF_MathML_Presentation)) + + def __init__(self, provider=None, mathMl: Optional[str] = None): + super(MathCATInteraction, self).__init__(provider=provider, mathMl=mathMl) + if mathMl is None: + self.init_mathml = "" + else: + self.init_mathml = mathMl + + def reportFocus(self): + super(MathCATInteraction, self).reportFocus() + # try to get around espeak bug where voice slows down + if _synthesizer_rate and getSynth().name == "espeak": + getSynth()._set_rate(_synthesizer_rate) + try: + text = libmathcat.DoNavigateCommand("ZoomIn") + speech.speak(ConvertSSMLTextForNVDA(text)) + except Exception as e: + log.exception(e) + # Translators: this message directs users to look in the log file + speech.speakMessage(_("Error in starting navigation of math: see NVDA error log for details")) + finally: + # try to get around espeak bug where voice slows down + if _synthesizer_rate and getSynth().name == "espeak": + # log.info(f'reportFocus: reset to {_synthesizer_rate}') + getSynth()._set_rate(_synthesizer_rate) + + def getBrailleRegions(self, review: bool = False): + # log.info("***MathCAT start getBrailleRegions") + yield braille.NVDAObjectRegion(self, appendText=" ") + region = braille.Region() + region.focusToHardLeft = True + # libmathcat.SetBrailleWidth(braille.handler.displaySize) + try: + region.rawText = libmathcat.GetBraille("") + except Exception as e: + log.exception(e) + # Translators: this message directs users to look in the log file + speech.speakMessage(_("Error in brailling math: see NVDA error log for details")) + region.rawText = "" + + # log.info("***MathCAT end getBrailleRegions ***") + yield region + + def getScript(self, gesture: KeyboardInputGesture): + # Pass most keys to MathCAT. Pretty ugly. + if ( + isinstance(gesture, KeyboardInputGesture) + and "NVDA" not in gesture.modifierNames + and gesture.mainKeyName + in { + "leftArrow", + "rightArrow", + "upArrow", + "downArrow", + "home", + "end", + "space", + "backspace", + "enter", + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + } + # or len(gesture.mainKeyName) == 1 + ): + return self.script_navigate + else: + return super().getScript(gesture) + + def script_navigate(self, gesture: KeyboardInputGesture): + try: + # try to get around espeak bug where voice slows down + if _synthesizer_rate and getSynth().name == "espeak": + getSynth()._set_rate(_synthesizer_rate) + if gesture is not None: # == None when initial focus -- handled in reportFocus() + modNames = gesture.modifierNames + text = libmathcat.DoNavigateKeyPress( + gesture.vkCode, + "shift" in modNames, + "control" in modNames, + "alt" in modNames, + False, + ) + # log.info(f"Navigate speech for {gesture.vkCode}/(s={'shift' in modNames}, c={'control' in modNames}): '{text}'") + speech.speak(ConvertSSMLTextForNVDA(text)) + except Exception as e: + log.exception(e) + # Translators: this message directs users to look in the log file + speech.speakMessage(_("Error in navigating math: see NVDA error log for details")) + finally: + # try to get around espeak bug where voice slows down + if _synthesizer_rate and getSynth().name == "espeak": + # log.info(f'script_navigate: reset to {_synthesizer_rate}') + getSynth()._set_rate(_synthesizer_rate) + + if not braille.handler.enabled: + return + + try: + # update the braille to reflect the nav position (might be excess code, but it works) + nav_node = libmathcat.GetNavigationMathMLId() + braille_chars = libmathcat.GetBraille(nav_node[0]) + # log.info(f'braille display = {config.conf["braille"]["display"]}, braille_chars: {braille_chars}') + region = braille.Region() + region.rawText = braille_chars + region.focusToHardLeft = True + region.update() + braille.handler.buffer.regions.append(region) + braille.handler.buffer.focus(region) + braille.handler.buffer.update() + braille.handler.update() + except Exception as e: + log.exception(e) + # Translators: this message directs users to look in the log file + speech.speakMessage(_("Error in brailling math: see NVDA error log for details")) + + _startsWithMath = re.compile("\\s*?" # copy will fix up name spacing + elif self.init_mathml != "": + mathml = self.init_mathml + if copy_as == "speech": + # save the old MathML, set the navigation MathML as MathMl, get the speech, then reset the MathML + saved_mathml: str = self.init_mathml + saved_tts = libmathcat.GetPreference("TTS") + if saved_mathml == "": # shouldn't happen + raise Exception("Internal error -- MathML not set for copy") + libmathcat.SetPreference("TTS", "None") + libmathcat.SetMathML(mathml) + # get the speech text and collapse the whitespace + text_to_copy = " ".join(libmathcat.GetSpokenText().split()) + libmathcat.SetPreference("TTS", saved_tts) + libmathcat.SetMathML(saved_mathml) + else: + text_to_copy = self._wrapMathMLForClipBoard(mathml) + + self._copyToClipAsMathML(text_to_copy, copy_as == "mathml") + # Translators: copy to clipboard + ui.message(_("copy as ") + copy_as) + except Exception as e: + log.exception(e) + # Translators: this message directs users to look in the log file + speech.speakMessage(_("unable to copy math: see NVDA error log for details")) + + # not a perfect match sequence, but should capture normal MathML + # not a perfect match sequence, but should capture normal MathML + _mathTagHasNameSpace = re.compile("") + _hasAddedId = re.compile(" id='[^'].+' data-id-added='true'") + _hasDataAttr = re.compile(" data-[^=]+='[^']*'") + + def _wrapMathMLForClipBoard(self, text: str) -> str: + # cleanup the MathML a little + text = re.sub(self._hasAddedId, "", text) + mathml_with_ns = re.sub(self._hasDataAttr, "", text) + if not re.match(self._mathTagHasNameSpace, mathml_with_ns): + mathml_with_ns = mathml_with_ns.replace( + "math", + "math xmlns='http://www.w3.org/1998/Math/MathML'", + 1, + ) + return mathml_with_ns + + def _copyToClipAsMathML(self, text: str, is_mathml: bool, notify: Optional[bool] = False) -> bool: + """Copies the given text to the windows clipboard. + @returns: True if it succeeds, False otherwise. + @param text: the text which will be copied to the clipboard + @param notify: whether to emit a confirmation message + """ + # copied from api.py and modified to use CF_MathML_Presentation + if not isinstance(text, str) or len(text) == 0: + return False + + try: + with winUser.openClipboard(gui.mainFrame.Handle): + winUser.emptyClipboard() + if is_mathml: + self._setClipboardData(self.CF_MathML, '' + text) + self._setClipboardData(self.CF_MathML_Presentation, '' + text) + self._setClipboardData(winUser.CF_UNICODETEXT, text) + got = getClipData() + except OSError: + if notify: + ui.reportTextCopiedToClipboard() # No argument reports a failure. + return False + if got == text: + if notify: + ui.reportTextCopiedToClipboard(text) + return True + if notify: + ui.reportTextCopiedToClipboard() # No argument reports a failure. + return False + + def _setClipboardData(self, format, data: str): + # Need to support MathML Presentation, so this copied from winUser.py and the first two lines are commented out + # For now only unicode is a supported format + # if format!=CF_UNICODETEXT: + # raise ValueError("Unsupported format") + text = data + bufLen = len(text.encode(WCHAR_ENCODING, errors="surrogatepass")) + 2 + # Allocate global memory + h = winKernel.HGLOBAL.alloc(winKernel.GMEM_MOVEABLE, bufLen) + # Acquire a lock to the global memory receiving a local memory address + with h.lock() as addr: + # Write the text into the allocated memory + buf = (c_wchar * bufLen).from_address(addr) + buf.value = text + # Set the clipboard data with the global memory + if not windll.user32.SetClipboardData(format, h): + raise WinError() + # NULL the global memory handle so that it is not freed at the end of scope as the clipboard now has it. + h.forget() class MathCAT(mathPres.MathPresentationProvider): - def __init__(self): - # super(MathCAT, self).__init__(*args, **kwargs) - - try: - # IMPORTANT -- SetRulesDir must be the first call to libmathcat besides GetVersion() - rules_dir = path.join(path.dirname(path.abspath(__file__)), "Rules") - log.info(f"MathCAT {libmathcat.GetVersion()} installed. Using rules dir: {rules_dir}") - libmathcat.SetRulesDir(rules_dir) - libmathcat.SetPreference("TTS", "SSML") - except Exception as e: - log.exception(e) - # Translators: this message directs users to look in the log file - speech.speakMessage(_("MathCAT initialization failed: see NVDA error log for details")) - - def getSpeechForMathMl(self, mathml: str): - global _synthesizer_rate - synth = getSynth() - synthConfig = config.conf["speech"][synth.name] - if synth.name == 'espeak': - _synthesizer_rate = synthConfig['rate'] - # log.info(f'_synthesizer_rate={_synthesizer_rate}, get_rate()={getSynth()._get_rate()}') - getSynth()._set_rate(_synthesizer_rate) - # log.info(f'..............get_rate()={getSynth()._get_rate()}, name={synth.name}') - try: - # need to set Language before the MathML for DecimalSeparator canonicalization - language = getLanguageToUse(mathml) - # MathCAT should probably be extended to accept "extlang" tagging, but it uses lang-region tagging at the moment - libmathcat.SetPreference("Language", language) - libmathcat.SetMathML(mathml) - except Exception as e: - log.exception(e) - log.exception(f"MathML is {mathml}") - # Translators: this message directs users to look in the log file - speech.speakMessage(_("Illegal MathML found: see NVDA error log for details")) - libmathcat.SetMathML("") # set it to something - try: - supported_commands = synth.supportedCommands - # Set preferences for capital letters - libmathcat.SetPreference( - "CapitalLetters_Beep", - "true" if synthConfig["beepForCapitals"] else "false", - ) - libmathcat.SetPreference( - "CapitalLetters_UseWord", - "true" if synthConfig["sayCapForCapitals"] else "false", - ) - # log.info(f"Speech text: {libmathcat.GetSpokenText()}") - if PitchCommand in supported_commands: - libmathcat.SetPreference("CapitalLetters_Pitch", str(synthConfig["capPitchChange"])) - if self._add_sounds(): - return ( - [BeepCommand(800, 25)] - + ConvertSSMLTextForNVDA(libmathcat.GetSpokenText()) - + [BeepCommand(600, 15)] - ) - else: - return ConvertSSMLTextForNVDA(libmathcat.GetSpokenText()) - - except Exception as e: - log.exception(e) - # Translators: this message directs users to look in the log file - speech.speakMessage(_("Error in speaking math: see NVDA error log for details")) - return [""] - finally: - # try to get around espeak bug where voice slows down - if _synthesizer_rate and getSynth().name == 'espeak': - # log.info(f'getSpeechForMathMl: reset to {_synthesizer_rate}') - getSynth()._set_rate(_synthesizer_rate) - - def _add_sounds(self): - try: - return libmathcat.GetPreference("SpeechSound") != "None" - except Exception as e: - log.exception(f"MathCAT: An exception occurred in _add_sounds: {e}") - return False - - def getBrailleForMathMl(self, mathml: str): - # log.info("***MathCAT getBrailleForMathMl") - try: - libmathcat.SetMathML(mathml) - except Exception as e: - log.exception(e) - log.exception(f"MathML is {mathml}") - # Translators: this message directs users to look in the log file - speech.speakMessage(_("Illegal MathML found: see NVDA error log for details")) - libmathcat.SetMathML("") # set it to something - try: - return libmathcat.GetBraille("") - except Exception as e: - log.exception(e) - # Translators: this message directs users to look in the log file - speech.speakMessage(_("Error in brailling math: see NVDA error log for details")) - return "" - - def interactWithMathMl(self, mathml: str): - MathCATInteraction(provider=self, mathMl=mathml).setFocus() - MathCATInteraction(provider=self, mathMl=mathml).script_navigate(None) + def __init__(self): + # super(MathCAT, self).__init__(*args, **kwargs) + + try: + # IMPORTANT -- SetRulesDir must be the first call to libmathcat besides GetVersion() + rules_dir = path.join(path.dirname(path.abspath(__file__)), "Rules") + log.info(f"MathCAT {libmathcat.GetVersion()} installed. Using rules dir: {rules_dir}") + libmathcat.SetRulesDir(rules_dir) + libmathcat.SetPreference("TTS", "SSML") + except Exception as e: + log.exception(e) + # Translators: this message directs users to look in the log file + speech.speakMessage(_("MathCAT initialization failed: see NVDA error log for details")) + + def getSpeechForMathMl(self, mathml: str): + global _synthesizer_rate + synth = getSynth() + synthConfig = config.conf["speech"][synth.name] + if synth.name == "espeak": + _synthesizer_rate = synthConfig["rate"] + # log.info(f'_synthesizer_rate={_synthesizer_rate}, get_rate()={getSynth()._get_rate()}') + getSynth()._set_rate(_synthesizer_rate) + # log.info(f'..............get_rate()={getSynth()._get_rate()}, name={synth.name}') + try: + # need to set Language before the MathML for DecimalSeparator canonicalization + language = getLanguageToUse(mathml) + # MathCAT should probably be extended to accept "extlang" tagging, but it uses lang-region tagging at the moment + libmathcat.SetPreference("Language", language) + libmathcat.SetMathML(mathml) + except Exception as e: + log.exception(e) + log.exception(f"MathML is {mathml}") + # Translators: this message directs users to look in the log file + speech.speakMessage(_("Illegal MathML found: see NVDA error log for details")) + libmathcat.SetMathML("") # set it to something + try: + supported_commands = synth.supportedCommands + # Set preferences for capital letters + libmathcat.SetPreference( + "CapitalLetters_Beep", + "true" if synthConfig["beepForCapitals"] else "false", + ) + libmathcat.SetPreference( + "CapitalLetters_UseWord", + "true" if synthConfig["sayCapForCapitals"] else "false", + ) + # log.info(f"Speech text: {libmathcat.GetSpokenText()}") + if PitchCommand in supported_commands: + libmathcat.SetPreference("CapitalLetters_Pitch", str(synthConfig["capPitchChange"])) + if self._add_sounds(): + return ( + [BeepCommand(800, 25)] + + ConvertSSMLTextForNVDA(libmathcat.GetSpokenText()) + + [BeepCommand(600, 15)] + ) + else: + return ConvertSSMLTextForNVDA(libmathcat.GetSpokenText()) + + except Exception as e: + log.exception(e) + # Translators: this message directs users to look in the log file + speech.speakMessage(_("Error in speaking math: see NVDA error log for details")) + return [""] + finally: + # try to get around espeak bug where voice slows down + if _synthesizer_rate and getSynth().name == "espeak": + # log.info(f'getSpeechForMathMl: reset to {_synthesizer_rate}') + getSynth()._set_rate(_synthesizer_rate) + + def _add_sounds(self): + try: + return libmathcat.GetPreference("SpeechSound") != "None" + except Exception as e: + log.exception(f"MathCAT: An exception occurred in _add_sounds: {e}") + return False + + def getBrailleForMathMl(self, mathml: str): + # log.info("***MathCAT getBrailleForMathMl") + try: + libmathcat.SetMathML(mathml) + except Exception as e: + log.exception(e) + log.exception(f"MathML is {mathml}") + # Translators: this message directs users to look in the log file + speech.speakMessage(_("Illegal MathML found: see NVDA error log for details")) + libmathcat.SetMathML("") # set it to something + try: + return libmathcat.GetBraille("") + except Exception as e: + log.exception(e) + # Translators: this message directs users to look in the log file + speech.speakMessage(_("Error in brailling math: see NVDA error log for details")) + return "" + + def interactWithMathMl(self, mathml: str): + MathCATInteraction(provider=self, mathMl=mathml).setFocus() + MathCATInteraction(provider=self, mathMl=mathml).script_navigate(None) CACHED_SYNTH = None def _monkeyPatchESpeak(): - global CACHED_SYNTH - currentSynth = getSynth() - if currentSynth.name != "espeak" or CACHED_SYNTH == currentSynth: - return # already patched + global CACHED_SYNTH + currentSynth = getSynth() + if currentSynth.name != "espeak" or CACHED_SYNTH == currentSynth: + return # already patched - CACHED_SYNTH = currentSynth - currentSynth.speak = patched_speak.__get__(currentSynth, type(currentSynth)) + CACHED_SYNTH = currentSynth + currentSynth.speak = patched_speak.__get__(currentSynth, type(currentSynth)) def patched_speak(self, speechSequence: SpeechSequence): # noqa: C901 - # log.info(f"\npatched_speak input: {speechSequence}") - textList: List[str] = [] - langChanged = False - prosody: Dict[str, int] = {} - # We output malformed XML, as we might close an outer tag after opening an inner one; e.g. - # . - # However, eSpeak doesn't seem to mind. - for item in speechSequence: - if isinstance(item, str): - textList.append(self._processText(item)) - elif isinstance(item, IndexCommand): - textList.append('' % item.index) - elif isinstance(item, CharacterModeCommand): - textList.append('' if item.state else "") - elif isinstance(item, LangChangeCommand): - langChangeXML = self._handleLangChangeCommand(item, langChanged) - textList.append(langChangeXML) - langChanged = True - elif isinstance(item, BreakCommand): - textList.append(f'') - elif isinstance(item, RateCommand): - if item.multiplier == 1: - textList.append('') - else: - textList.append(f"") - elif type(item) in self.PROSODY_ATTRS: - if prosody: - # Close previous prosody tag. - textList.append('') # hack added for cutoff speech (issue #55) - textList.append("") - attr = self.PROSODY_ATTRS[type(item)] - if item.multiplier == 1: - # Returning to normal. - try: - del prosody[attr] - except KeyError: - pass - else: - prosody[attr] = int(item.multiplier * 100) - if not prosody: - continue - textList.append("") - elif isinstance(item, PhonemeCommand): - # We can't use str.translate because we want to reject unknown characters. - try: - phonemes = "".join([self.IPA_TO_ESPEAK[char] for char in item.ipa]) - # There needs to be a space after the phoneme command. - # Otherwise, eSpeak will announce a subsequent SSML tag instead of processing it. - textList.append("[[%s]] " % phonemes) - except KeyError: - log.debugWarning("Unknown character in IPA string: %s" % item.ipa) - if item.text: - textList.append(self._processText(item.text)) - else: - log.exception("Unknown speech: %s" % item) - # Close any open tags. - if langChanged: - textList.append("") - if prosody: - textList.append("") - text = "".join(textList) - # log.info(f"monkey-patched text={text}") - oldRate: int = getSynth()._get_rate() - _espeak.speak(text) - # try to get around espeak bug where voice slows down - getSynth()._set_rate(oldRate) + # log.info(f"\npatched_speak input: {speechSequence}") + textList: List[str] = [] + langChanged = False + prosody: Dict[str, int] = {} + # We output malformed XML, as we might close an outer tag after opening an inner one; e.g. + # . + # However, eSpeak doesn't seem to mind. + for item in speechSequence: + if isinstance(item, str): + textList.append(self._processText(item)) + elif isinstance(item, IndexCommand): + textList.append('' % item.index) + elif isinstance(item, CharacterModeCommand): + textList.append('' if item.state else "") + elif isinstance(item, LangChangeCommand): + langChangeXML = self._handleLangChangeCommand(item, langChanged) + textList.append(langChangeXML) + langChanged = True + elif isinstance(item, BreakCommand): + textList.append(f'') + elif isinstance(item, RateCommand): + if item.multiplier == 1: + textList.append("") + else: + textList.append(f"") + elif type(item) in self.PROSODY_ATTRS: + if prosody: + # Close previous prosody tag. + textList.append('') # hack added for cutoff speech (issue #55) + textList.append("") + attr = self.PROSODY_ATTRS[type(item)] + if item.multiplier == 1: + # Returning to normal. + try: + del prosody[attr] + except KeyError: + pass + else: + prosody[attr] = int(item.multiplier * 100) + if not prosody: + continue + textList.append("") + elif isinstance(item, PhonemeCommand): + # We can't use str.translate because we want to reject unknown characters. + try: + phonemes = "".join([self.IPA_TO_ESPEAK[char] for char in item.ipa]) + # There needs to be a space after the phoneme command. + # Otherwise, eSpeak will announce a subsequent SSML tag instead of processing it. + textList.append("[[%s]] " % phonemes) + except KeyError: + log.debugWarning("Unknown character in IPA string: %s" % item.ipa) + if item.text: + textList.append(self._processText(item.text)) + else: + log.exception("Unknown speech: %s" % item) + # Close any open tags. + if langChanged: + textList.append("") + if prosody: + textList.append("") + text = "".join(textList) + # log.info(f"monkey-patched text={text}") + oldRate: int = getSynth()._get_rate() + _espeak.speak(text) + # try to get around espeak bug where voice slows down + getSynth()._set_rate(oldRate) diff --git a/addon/globalPlugins/MathCAT/MathCATPreferences.py b/addon/globalPlugins/MathCAT/MathCATPreferences.py index 06e42e51..4820ff88 100644 --- a/addon/globalPlugins/MathCAT/MathCATPreferences.py +++ b/addon/globalPlugins/MathCAT/MathCATPreferences.py @@ -42,644 +42,681 @@ class UserInterface(MathCATgui.MathCATPreferencesDialog): - def __init__(self, parent): - # initialize parent class - MathCATgui.MathCATPreferencesDialog.__init__(self, parent) - - # load the logo into the dialog - full_path_to_logo = ( - os.path.join(os.path.dirname(os.path.abspath(__file__)), "logo.png") - ) - if os.path.exists(full_path_to_logo): - self.m_bitmapLogo.SetBitmap(wx.Bitmap(full_path_to_logo)) - - # load in the system values followed by the user prefs (if any) - UserInterface.load_default_preferences() - UserInterface.load_user_preferences() - - # hack for "CopyAs" because its location in the prefs is not yet fixed - if "CopyAs" not in user_preferences["Navigation"]: - user_preferences["Navigation"]["CopyAs"] = ( - user_preferences["Other"]["CopyAs"] if "CopyAs" in user_preferences["Other"] else "MathML" - ) - UserInterface.validate_user_preferences() - - if "NVDAAddOn" in user_preferences: - # set the categories selection to what we used on last run - self.m_listBoxPreferencesTopic.SetSelection(user_preferences["NVDAAddOn"]["LastCategory"]) - # show the appropriate dialogue page - self.m_simplebookPanelsCategories.SetSelection(self.m_listBoxPreferencesTopic.GetSelection()) - else: - # set the categories selection to the first item - self.m_listBoxPreferencesTopic.SetSelection(0) - user_preferences["NVDAAddOn"] = {"LastCategory": "0"} - # populate the languages and braille codes - UserInterface.GetLanguages(self) - UserInterface.GetBrailleCodes(self) - # set the ui items to match the preferences - UserInterface.set_ui_values(self) - - @staticmethod - def path_to_languages_folder(): - # the user preferences file is stored at: MathCAT\Rules\Languages - return os.path.join(os.path.dirname(os.path.abspath(__file__)), "Rules", "Languages") - - @staticmethod - def pathToBrailleFolder(): - # the user preferences file is stored at: MathCAT\Rules\Languages - return os.path.join(os.path.dirname(os.path.abspath(__file__)), "Rules", "Braille") - - @staticmethod - def LanguagesDict() -> Dict[str, str]: - languages = { - "aa": "Afar", - "ab": "Аҧсуа", - "af": "Afrikaans", - "ak": "Akana", - "an": "Aragonés", - "ar": "العربية", - "as": "অসমীয়া", - "av": "Авар", - "ay": "Aymar", - "az": "Azərbaycanca / آذربايجان", - "ba": "Башҡорт", - "be": "Беларуская", - "bg": "Български", - "bh": "भोजपुरी", - "bi": "Bislama", - "bm": "Bahamanian", - "bn": "বাংলা", - "bo": "བོད་ཡིག / Bod skad", - "bs": "Bosanski", - "ca": "Català", - "ce": "Нохчийн", - "ch": "Chamoru", - "co": "Corsu", - "cr": "Nehiyaw", - "cs": "Česky", - "cu": "словѣньскъ / slověnĭskŭ", - "cv": "Чăваш", - "cy": "Cymraeg", - "da": "Dansk", - "de": "Deutsch", - "dv": "ދިވެހިބަސް", - "dz": "རྫོང་ཁ", - "ee": "Ɛʋɛ", - "el": "Ελληνικά", - "en": "English", - "en-GB": "English, United Kingdom", - "en-US": "English, United States", - "eo": "Esperanto", - "es": "Español", - "fa": "فارسی", - "fi": "Suomi", - "fj": "Na Vosa Vakaviti", - "fo": "Føroyskt", - "fr": "Français", - "fy": "Frysk", - "ga": "Gaeilge", - "gd": "Gàidhlig", - "gl": "Galego", - "gn": "Avañe'ẽ", - "gu": "ગુજરાતી", - "gv": "Gaelg", - "ha": "هَوُسَ", - "he": "עברית", - "hi": "हिन्दी", - "ho": "Hiri Motu", - "hr": "Hrvatski", - "ht": "Krèyol ayisyen", - "hu": "Magyar", - "hy": "Հայերեն", - "hz": "Otsiherero", - "ia": "Interlingua", - "id": "Bahasa Indonesia", - "ig": "Igbo", - "ii": "ꆇꉙ / 四川彝语", - "ik": "Iñupiak", - "io": "Ido", - "is": "Íslenska", - "it": "Italiano", - "iu": "ᐃᓄᒃᑎᑐᑦ", - "ja": "日本語", - "jv": "Basa Jawa", - "ka": "ქართული", - "kg": "KiKongo", - "ki": "Gĩkũyũ", - "kj": "Kuanyama", - "kk": "Қазақша", - "km": "ភាសាខ្មែរ", - "kn": "ಕನ್ನಡ", - "ko": "한국어", - "ks": "कॉशुर / کٲش", - "ku": "Kurdî", - "kv": "Коми", - "kw": "Kernewek", - "ky": "Kırgızca / Кыргызча", - "la": "Latina", - "lb": "Lëtzebuergesch", - "lg": "Luganda", - "li": "Limburgs", - "ln": "Lingála", - "lo": "ລາວ / Pha xa lao", - "lt": "Lietuvių", - "lv": "Latviešu", - "mg": "Malagasy", - "mh": "Kajin Majel / Ebon", - "mk": "Македонски", - "ml": "മലയാളം", - "mn": "Монгол", - "mo": "Moldovenească", - "ms": "Bahasa Melayu", - "mt": "bil-Malti", - "my": "Myanmasa", - "na": "Dorerin Naoero", - "ne": "नेपाली", - "ng": "Oshiwambo", - "nl": "Nederlands", - "nn": "Norsk (nynorsk)", - "nr": "isiNdebele", - "nv": "Diné bizaad", - "ny": "Chi-Chewa", - "oc": "Occitan", - "oj": "ᐊᓂᔑᓈᐯᒧᐎᓐ / Anishinaabemowin", - "om": "Oromoo", - "os": "Иронау", - "pa": "ਪੰਜਾਬੀ / پنجابی", - "pi": "Pāli / पाऴि", - "pl": "Polski", - "ps": "پښتو", - "pt": "Português", - "qu": "Runa Simi", - "rm": "Rumantsch", - "ro": "Română", - "ru": "Русский", - "rw": "Kinyarwandi", - "sa": "संस्कृतम्", - "sc": "Sardu", - "sd": "सिंधी / سنڌي", - "se": "Davvisámegiella", - "sg": "Sängö", - "sh": "Srpskohrvatski / Српскохрватски", - "si": "සිංහල", - "sk": "Slovenčina", - "sl": "Slovenščina", - "sm": "Gagana Samoa", - "sn": "chiShona", - "so": "Soomaaliga", - "sq": "Shqip", - "sr": "Српски", - "ss": "SiSwati", - "st": "Sesotho", - "su": "Basa Sunda", - "sv": "Svenska", - "sw": "Kiswahili", - "ta": "தமிழ்", - "tg": "Тоҷикӣ", - "th": "ไทย / Phasa Thai", - "ti": "ትግርኛ", - "tk": "Туркмен / تركمن", - "tl": "Tagalog", - "to": "Lea Faka-Tonga", - "tr": "Türkçe", - "ts": "Xitsonga", - "tt": "Tatarça", - "tw": "Twi", - "ty": "Reo Mā`ohi", - "ug": "Uyƣurqə / ئۇيغۇرچە", - "uk": "Українська", - "ur": "اردو", - "uz": "Ўзбек", - "ve": "Tshivenḓa", - "vi": "Tiếng Việt", - "vo": "Volapük", - "wa": "Walon", - "wo": "Wollof", - "xh": "isiXhosa", - "yi": "ייִדיש", - "yo": "Yorùbá", - "za": "Cuengh / Tôô / 壮语", - "zh": "中文", - "zh-HANS": "Chinese, Simplified", - "zh-HANT": "Chinese, Traditional", - "zh-TW": "Chinese, Traditional, Taiwan", - "zu": "isiZulu", - } - return languages - - def get_rules_files(self, pathToDir: str, processSubDirs: Callable[[str, str], List[str]] | None) -> List[str]: - language = os.path.basename(pathToDir) - ruleFiles = [os.path.basename(file) for file in glob.glob(os.path.join(pathToDir, "*_Rules.yaml"))] - for dir in os.listdir(pathToDir): - if os.path.isdir(os.path.join(pathToDir, dir)): - if processSubDirs: - ruleFiles.extend(processSubDirs(dir, language)) - - if len(ruleFiles) == 0: - # look in the .zip file for the style files, including regional subdirs -- it might not have been unzipped - try: - zip_file = ZipFile(f"{pathToDir}\\{language}.zip", "r") - for file in zip_file.namelist(): - if file.endswith('_Rules.yaml'): - ruleFiles.append(file) - elif zip_file.getinfo(file).is_dir() and processSubDirs: - ruleFiles.extend(processSubDirs(dir, language)) - except Exception as e: - log.debugWarning(f"MathCAT Dialog: didn't find zip file {zip_file}. Error: {e}") - - return ruleFiles - - def GetLanguages(self): - - def addRegionalLanguages(subDir: str, language: str) -> List[str]: - # the language variants are in folders named using ISO 3166-1 alpha-2 - # codes https://en.wikipedia.org/wiki/ISO_3166-2 - # check if there are language variants in the language folder - if subDir != "SharedRules": - languagesDict = UserInterface.LanguagesDict() - # add to the listbox the text for this language variant together with the code - regionalCode = language + "-" + subDir.upper() - if languagesDict.get(regionalCode, "missing") != "missing": - self.m_choiceLanguage.Append(f"{languagesDict[regionalCode]} ({language}-{subDir})") - elif languagesDict.get(language, "missing") != "missing": - self.m_choiceLanguage.Append(f"{languagesDict[language]} ({regionalCode})") - else: - self.m_choiceLanguage.Append(f"{language} ({regionalCode})") - return [os.path.basename(file) for file in glob.glob(os.path.join(subDir, "*_Rules.yaml"))] - return [] - - # initialise the language list - languagesDict = UserInterface.LanguagesDict() - # clear the language names in the dialog - self.m_choiceLanguage.Clear() - # Translators: menu item -- use the language of the voice chosen in the NVDA speech settings dialog - # "Auto" == "Automatic" -- other items in menu are "English (en)", etc., so this matches that style - self.m_choiceLanguage.Append(_("Use Voice's Language (Auto)")) - # populate the available language names in the dialog - # the implemented languages are in folders named using the relevant ISO 639-1 - # code https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes - languageDir = UserInterface.path_to_languages_folder() - for language in os.listdir(languageDir): - pathToLanguageDir = os.path.join(UserInterface.path_to_languages_folder(), language) - if os.path.isdir(pathToLanguageDir): - # only add this language if there is a xxx_Rules.yaml file - if len(self.get_rules_files(pathToLanguageDir, addRegionalLanguages)) > 0: - # add to the listbox the text for this language together with the code - if languagesDict.get(language, "missing") != "missing": - self.m_choiceLanguage.Append(languagesDict[language] + " (" + language + ")") - else: - self.m_choiceLanguage.Append(language + " (" + language + ")") - - def GetLanguageCode(self): - lang_selection = self.m_choiceLanguage.GetStringSelection() - lang_code = lang_selection[ - lang_selection.find("(") + 1: lang_selection.find(")") - ] - return lang_code - - def GetSpeechStyles(self, this_SpeechStyle: str): - """Get all the speech styles for the current language. - This sets the SpeechStyles dialog entry""" - from speech import getCurrentLanguage - - def getSpeechStyleFromDirectory(dir: str, lang: str) -> list[str]: - r"""Get the speech styles from any regional dialog, from the main language, dir and if there isn't from the zip file. - The 'lang', if it has a region dialect, is of the form 'en\uk' - The returned list is sorted alphabetically""" - # start with the regional dialect, then add on any (unique) styles in the main dir - main_lang = lang.split("\\")[0] # does the right thing even if there is no regional directory - all_style_files = [] - if lang.find("\\") >= 0: - all_style_files = [os.path.basename(name) for name in glob.glob(dir + lang + "\\*_Rules.yaml")] - all_style_files.extend( - [os.path.basename(name) for name in glob.glob(dir + main_lang + "\\*_Rules.yaml")] - ) - all_style_files = list(set(all_style_files)) # make them unique - if len(all_style_files) == 0: - # look in the .zip file for the style files -- this will have regional variants, but also have that dir - try: - zip_file = dir + main_lang + "\\" + main_lang + ".zip" - zip_file = ZipFile(zip_file, "r") # file might not exist - all_style_files = [name.split("/")[-1] for name in zip_file.namelist() if name.endswith('_Rules.yaml')] - except Exception as e: - log.debugWarning(f"MathCAT Dialog: didn't find zip file {zip_file}. Error: {e}") - all_style_files.sort() - return all_style_files - - # clear the SpeechStyle choices - self.m_choiceSpeechStyle.Clear() - # get the currently selected language code - languageCode = UserInterface.GetLanguageCode(self) - - if languageCode == "Auto": - # list the speech styles for the current voice rather than have none listed - languageCode = getCurrentLanguage().lower().replace("_", "-") - languageCode = languageCode.replace("-", "\\") - - languagePath = UserInterface.path_to_languages_folder() + "\\" - # log.info(f"languagePath={languagePath}") - # populate the m_choiceSpeechStyle choices - all_style_files = [ - # remove "_Rules.yaml" from the list - name[: name.find("_Rules.yaml")] for name in getSpeechStyleFromDirectory(languagePath, languageCode) - ] - # There isn't a LiteralSpeak rules file since it has no language-specific rules. We add it at the end. - # Translators: at the moment, do NOT translate this string as some code specifically looks for this name. - all_style_files.append("LiteralSpeak") - for name in all_style_files: - self.m_choiceSpeechStyle.Append((name)) - try: - # set the SpeechStyle to the same as previous - self.m_choiceSpeechStyle.SetStringSelection( - this_SpeechStyle if this_SpeechStyle in all_style_files else all_style_files[0] - ) - except Exception as e: - log.exception(f"MathCAT: An exception occurred in GetSpeechStyles evaluating set SetStringSelection: {e}") - # that didn't work, choose the first in the list - self.m_choiceSpeechStyle.SetSelection(0) - - def GetBrailleCodes(self): - # initialise the braille code list - self.m_choiceBrailleMathCode.Clear() - # populate the available braille codes in the dialog - # the dir names are used, not the rule file names because the dir names have to be unique - pathToBrailleFolder = UserInterface.pathToBrailleFolder() - for braille_code in os.listdir(pathToBrailleFolder): - pathToBrailleCode = os.path.join(pathToBrailleFolder, braille_code) - if os.path.isdir(pathToBrailleCode): - if len(self.get_rules_files(pathToBrailleCode, None)) > 0: - self.m_choiceBrailleMathCode.Append(braille_code) - - def set_ui_values(self): - # set the UI elements to the ones read from the preference file(s) - try: - self.m_choiceImpairment.SetSelection( - Speech_Impairment.index(user_preferences["Speech"]["Impairment"]) - ) - try: - lang_pref = user_preferences["Speech"]["Language"] - self.m_choiceLanguage.SetSelection(0) - i = 1 # no need to test i == 0 - while i < self.m_choiceLanguage.GetCount(): - if f"({lang_pref})" in self.m_choiceLanguage.GetString(i): - self.m_choiceLanguage.SetSelection(i) - break - i += 1 - except Exception as e: - log.exception( - f"MathCAT: An exception occurred in set_ui_values ('{user_preferences['Speech']['Language']}'): {e}" - ) - # the language in the settings file is not in the folder structure, something went wrong, - # set to the first in the list - self.m_choiceLanguage.SetSelection(0) - try: - # now get the available SpeechStyles from the folder structure and set to the preference setting is possible - self.GetSpeechStyles(str(user_preferences["Speech"]["SpeechStyle"])) - except Exception as e: - log.exception(f"MathCAT: An exception occurred in set_ui_values (getting SpeechStyle): {e}") - self.m_choiceSpeechStyle.Append( - "Error when setting SpeechStyle for " + self.m_choiceLanguage.GetStringSelection() - ) - # set the rest of the UI elements - self.m_choiceDecimalSeparator.SetSelection( - Speech_DecimalSeparator.index(user_preferences["Other"]["DecimalSeparator"]) - ) - self.m_choiceSpeechAmount.SetSelection(Speech_Verbosity.index(user_preferences["Speech"]["Verbosity"])) - self.m_sliderRelativeSpeed.SetValue(user_preferences["Speech"]["MathRate"]) - pause_factor = ( - 0 - if int(user_preferences["Speech"]["PauseFactor"]) <= 1 - else round( - math.log( - int(user_preferences["Speech"]["PauseFactor"]) / PAUSE_FACTOR_SCALE, - PAUSE_FACTOR_LOG_BASE, - ) - ) - ) - self.m_sliderPauseFactor.SetValue(pause_factor) - self.m_checkBoxSpeechSound.SetValue(user_preferences["Speech"]["SpeechSound"] == "Beep") - self.m_choiceSpeechForChemical.SetSelection(Speech_Chemistry.index(user_preferences["Speech"]["Chemistry"])) - - self.m_choiceNavigationMode.SetSelection(Navigation_NavMode.index(user_preferences["Navigation"]["NavMode"])) - self.m_checkBoxResetNavigationMode.SetValue(user_preferences["Navigation"]["ResetNavMode"]) - self.m_choiceSpeechAmountNavigation.SetSelection( - Navigation_NavVerbosity.index(user_preferences["Navigation"]["NavVerbosity"]) - ) - if user_preferences["Navigation"]["Overview"]: - self.m_choiceNavigationSpeech.SetSelection(1) - else: - self.m_choiceNavigationSpeech.SetSelection(0) - self.m_checkBoxResetNavigationSpeech.SetValue(user_preferences["Navigation"]["ResetOverview"]) - self.m_checkBoxAutomaticZoom.SetValue(user_preferences["Navigation"]["AutoZoomOut"]) - self.m_choiceCopyAs.SetSelection(Navigation_CopyAs.index(user_preferences["Navigation"]["CopyAs"])) - - self.m_choiceBrailleHighlights.SetSelection( - Braille_BrailleNavHighlight.index(user_preferences["Braille"]["BrailleNavHighlight"]) - ) - try: - braille_pref = user_preferences["Braille"]["BrailleCode"] - i = 0 - while braille_pref != self.m_choiceBrailleMathCode.GetString(i): - i = i + 1 - if i == self.m_choiceBrailleMathCode.GetCount(): - break - if braille_pref == self.m_choiceBrailleMathCode.GetString(i): - self.m_choiceBrailleMathCode.SetSelection(i) - else: - self.m_choiceBrailleMathCode.SetSelection(0) - except Exception as e: - log.exception(f"MathCAT: An exception occurred while trying to set the Braille code: {e}") - # the braille code in the settings file is not in the folder structure, something went wrong, - # set to the first in the list - self.m_choiceBrailleMathCode.SetSelection(0) - except KeyError as err: - print("Key not found", err) - - def get_ui_values(self): - global user_preferences - # read the values from the UI and update the user preferences dictionary - user_preferences["Speech"]["Impairment"] = Speech_Impairment[self.m_choiceImpairment.GetSelection()] - user_preferences["Speech"]["Language"] = self.GetLanguageCode() - user_preferences["Other"]["DecimalSeparator"] = Speech_DecimalSeparator[self.m_choiceDecimalSeparator.GetSelection()] - user_preferences["Speech"]["SpeechStyle"] = self.m_choiceSpeechStyle.GetStringSelection() - user_preferences["Speech"]["Verbosity"] = Speech_Verbosity[self.m_choiceSpeechAmount.GetSelection()] - user_preferences["Speech"]["MathRate"] = self.m_sliderRelativeSpeed.GetValue() - pf_slider = self.m_sliderPauseFactor.GetValue() - pause_factor = ( - 0 - if pf_slider == 0 - else round(PAUSE_FACTOR_SCALE * math.pow(PAUSE_FACTOR_LOG_BASE, pf_slider)) - ) # avoid log(0) - user_preferences["Speech"]["PauseFactor"] = pause_factor - if self.m_checkBoxSpeechSound.GetValue(): - user_preferences["Speech"]["SpeechSound"] = "Beep" - else: - user_preferences["Speech"]["SpeechSound"] = "None" - user_preferences["Speech"]["Chemistry"] = Speech_Chemistry[self.m_choiceSpeechForChemical.GetSelection()] - user_preferences["Navigation"]["NavMode"] = Navigation_NavMode[self.m_choiceNavigationMode.GetSelection()] - user_preferences["Navigation"]["ResetNavMode"] = self.m_checkBoxResetNavigationMode.GetValue() - user_preferences["Navigation"]["NavVerbosity"] = Navigation_NavVerbosity[ - self.m_choiceSpeechAmountNavigation.GetSelection() - ] - user_preferences["Navigation"]["Overview"] = self.m_choiceNavigationSpeech.GetSelection() != 0 - user_preferences["Navigation"]["ResetOverview"] = self.m_checkBoxResetNavigationSpeech.GetValue() - user_preferences["Navigation"]["AutoZoomOut"] = self.m_checkBoxAutomaticZoom.GetValue() - user_preferences["Navigation"]["CopyAs"] = Navigation_CopyAs[self.m_choiceCopyAs.GetSelection()] - - user_preferences["Braille"]["BrailleNavHighlight"] = ( - Braille_BrailleNavHighlight[self.m_choiceBrailleHighlights.GetSelection()] - ) - user_preferences["Braille"]["BrailleCode"] = self.m_choiceBrailleMathCode.GetStringSelection() - if "NVDAAddOn" not in user_preferences: - user_preferences["NVDAAddOn"] = {"LastCategory": "0"} - user_preferences["NVDAAddOn"]["LastCategory"] = self.m_listBoxPreferencesTopic.GetSelection() - - @staticmethod - def path_to_default_preferences(): - return ( - os.path.join(os.path.dirname(os.path.abspath(__file__)), "Rules", "prefs.yaml") - ) - - @staticmethod - def path_to_user_preferences_folder(): - # the user preferences file is stored at: C:\Users\AppData\Roaming\MathCAT\prefs.yaml - return os.path.join(os.path.expandvars('%APPDATA%'), "MathCAT") - - @staticmethod - def path_to_user_preferences(): - # the user preferences file is stored at: C:\Users\AppData\Roaming\MathCAT\prefs.yaml - return os.path.join(UserInterface.path_to_user_preferences_folder(), "prefs.yaml") - - @staticmethod - def load_default_preferences(): - global user_preferences - # load default preferences into the user preferences data structure (overwrites existing) - if os.path.exists(UserInterface.path_to_default_preferences()): - with open( - UserInterface.path_to_default_preferences(), encoding="utf-8" - ) as f: - user_preferences = yaml.load(f, Loader=yaml.FullLoader) # type: ignore - - @staticmethod - def load_user_preferences(): - global user_preferences - # merge user file values into the user preferences data structure - if os.path.exists(UserInterface.path_to_user_preferences()): - with open(UserInterface.path_to_user_preferences(), encoding="utf-8") as f: - # merge with the default preferences, overwriting with the user's values - user_preferences.update(yaml.load(f, Loader=yaml.FullLoader)) # type: ignore - - @staticmethod - def validate(key1: str, key2: str, valid_values: List[Union[str, bool]], default_value: Union[str, bool]): - global user_preferences - try: - if valid_values == []: - # any value is valid - if user_preferences[key1][key2] != "": - return - - else: - # any value in the list is valid - if user_preferences[key1][key2] in valid_values: - return - except Exception as e: - log.exception(f"MathCAT: An exception occurred in validate: {e}") - # the preferences entry does not exist - if key1 not in user_preferences: - user_preferences[key1] = {key2: default_value} - else: - user_preferences[key1][key2] = default_value - - @staticmethod - def validate_int(key1: str, key2: str, valid_values: List[int], default_value: int): - global user_preferences - try: - # any value between lower and upper bounds is valid - if int(user_preferences[key1][key2]) >= valid_values[0] and int(user_preferences[key1][key2]) <= valid_values[1]: - return - except Exception as e: - log.exception(f"MathCAT: An exception occurred in validate_int: {e}") - # the preferences entry does not exist - if key1 not in user_preferences: - user_preferences[key1] = {key2: default_value} - else: - user_preferences[key1][key2] = default_value - - @staticmethod - def validate_user_preferences(): - # check each user preference value to ensure it is present and valid, set default value if not - # Speech: - # Impairment: Blindness # LearningDisability, LowVision, Blindness - UserInterface.validate("Speech", "Impairment", ["LearningDisability", "LowVision", "Blindness"], "Blindness") - # Language: en # any known language code and sub-code -- could be en-uk, etc - UserInterface.validate("Speech", "Language", [], "en") - # Verbosity: Medium # Terse, Medium, Verbose - UserInterface.validate("Speech", "Verbosity", ["Terse", "Medium", "Verbose"], "Medium") - # MathRate: 100 # Change from text speech rate (%) - UserInterface.validate_int("Speech", "MathRate", [0, 200], 100) - # PauseFactor: 100 # TBC - UserInterface.validate_int("Speech", "PauseFactor", [0, 1000], 100) - # SpeechSound: None # make a sound when starting/ending math speech -- None, Beep - UserInterface.validate("Speech", "SpeechSound", ["None", "Beep"], "None") - # SpeechStyle: ClearSpeak # Any known speech style (falls back to ClearSpeak) - UserInterface.validate("Speech", "SpeechStyle", [], "ClearSpeak") - # SubjectArea: General # FIX: still working on this - UserInterface.validate("Speech", "SubjectArea", [], "General") - # Chemistry: SpellOut # SpellOut (H 2 0), AsCompound (Water), Off (H sub 2 O) - UserInterface.validate("Speech", "Chemistry", ["SpellOut", "Off"], "SpellOut") - # Navigation: - # NavMode: Enhanced # Enhanced, Simple, Character - UserInterface.validate("Navigation", "NavMode", ["Enhanced", "Simple", "Character"], "Enhanced") - # ResetNavMode: false # remember previous value and use it - UserInterface.validate("Navigation", "ResetNavMode", [False, True], False) - # Overview: false # speak the expression or give a description/overview - UserInterface.validate("Navigation", "Overview", [False, True], False) - # ResetOverview: true # remember previous value and use it - UserInterface.validate("Navigation", "ResetOverview", [False, True], True) - # NavVerbosity: Medium # Terse, Medium, Full (words to say for nav command) - UserInterface.validate("Navigation", "NavVerbosity", ["Terse", "Medium", "Full"], "Medium") - # AutoZoomOut: true # Auto zoom out of 2D exprs (use shift-arrow to force zoom out if unchecked) - UserInterface.validate("Navigation", "AutoZoomOut", [False, True], True) - # CopyAs: MathML # MathML, LaTeX, ASCIIMath, Speech - UserInterface.validate("Navigation", "CopyAs", ["MathML", "LaTeX", "ASCIIMath", "Speech"], "MathML") - # Braille: - # BrailleNavHighlight: EndPoints - # Highlight with dots 7 & 8 the current nav node -- values are Off, FirstChar, EndPoints, All - UserInterface.validate("Braille", "BrailleNavHighlight", ["Off", "FirstChar", "EndPoints", "All"], "EndPoints") - # BrailleCode: "Nemeth" # Any supported braille code (currently Nemeth, UEB, CMU, Vietnam) - UserInterface.validate("Braille", "BrailleCode", [], "Nemeth") - - @staticmethod - def write_user_preferences(): - # Language is special because it is set elsewhere by SetPreference which overrides the user_prefs -- so set it here - from . import libmathcat_py as libmathcat # type: ignore - try: - libmathcat.SetPreference("Language", user_preferences["Speech"]["Language"]) - except Exception as e: - log.exception( - f'Error in trying to set MathCAT "Language" preference to "{user_preferences["Speech"]["Language"]}": {e}' - ) - if not os.path.exists(UserInterface.path_to_user_preferences_folder()): - # create a folder for the user preferences - os.mkdir(UserInterface.path_to_user_preferences_folder()) - with open(UserInterface.path_to_user_preferences(), "w", encoding="utf-8") as f: - # write values to the user preferences file, NOT the default - yaml.dump(user_preferences, stream=f, allow_unicode=True) - - def OnRelativeSpeedChanged(self, event): - rate = self.m_sliderRelativeSpeed.GetValue() - # Translators: this is a test string that is spoken. Only translate "the square root of x squared plus y squared" - text = _("the square root of x squared plus y squared").replace("XXX", str(rate), 1) - speak(ConvertSSMLTextForNVDA(text)) - - def OnPauseFactorChanged(self, event): - rate = self.m_sliderRelativeSpeed.GetValue() - pf_slider = self.m_sliderPauseFactor.GetValue() - pause_factor = 0 if pf_slider == 0 else round(PAUSE_FACTOR_SCALE * math.pow(PAUSE_FACTOR_LOG_BASE, pf_slider)) - text = _( - # Translators: this is a test string that is spoken. Only translate "the fraction with numerator" - # and other parts NOT inside '<.../>', - "the fraction with numerator \ + def __init__(self, parent): + # initialize parent class + MathCATgui.MathCATPreferencesDialog.__init__(self, parent) + + # load the logo into the dialog + full_path_to_logo = os.path.join(os.path.dirname(os.path.abspath(__file__)), "logo.png") + if os.path.exists(full_path_to_logo): + self.m_bitmapLogo.SetBitmap(wx.Bitmap(full_path_to_logo)) + + # load in the system values followed by the user prefs (if any) + UserInterface.load_default_preferences() + UserInterface.load_user_preferences() + + # hack for "CopyAs" because its location in the prefs is not yet fixed + if "CopyAs" not in user_preferences["Navigation"]: + user_preferences["Navigation"]["CopyAs"] = ( + user_preferences["Other"]["CopyAs"] if "CopyAs" in user_preferences["Other"] else "MathML" + ) + UserInterface.validate_user_preferences() + + if "NVDAAddOn" in user_preferences: + # set the categories selection to what we used on last run + self.m_listBoxPreferencesTopic.SetSelection(user_preferences["NVDAAddOn"]["LastCategory"]) + # show the appropriate dialogue page + self.m_simplebookPanelsCategories.SetSelection(self.m_listBoxPreferencesTopic.GetSelection()) + else: + # set the categories selection to the first item + self.m_listBoxPreferencesTopic.SetSelection(0) + user_preferences["NVDAAddOn"] = {"LastCategory": "0"} + # populate the languages and braille codes + UserInterface.GetLanguages(self) + UserInterface.GetBrailleCodes(self) + # set the ui items to match the preferences + UserInterface.set_ui_values(self) + + @staticmethod + def path_to_languages_folder(): + # the user preferences file is stored at: MathCAT\Rules\Languages + return os.path.join(os.path.dirname(os.path.abspath(__file__)), "Rules", "Languages") + + @staticmethod + def pathToBrailleFolder(): + # the user preferences file is stored at: MathCAT\Rules\Languages + return os.path.join(os.path.dirname(os.path.abspath(__file__)), "Rules", "Braille") + + @staticmethod + def LanguagesDict() -> Dict[str, str]: + languages = { + "aa": "Afar", + "ab": "Аҧсуа", + "af": "Afrikaans", + "ak": "Akana", + "an": "Aragonés", + "ar": "العربية", + "as": "অসমীয়া", + "av": "Авар", + "ay": "Aymar", + "az": "Azərbaycanca / آذربايجان", + "ba": "Башҡорт", + "be": "Беларуская", + "bg": "Български", + "bh": "भोजपुरी", + "bi": "Bislama", + "bm": "Bahamanian", + "bn": "বাংলা", + "bo": "བོད་ཡིག / Bod skad", + "bs": "Bosanski", + "ca": "Català", + "ce": "Нохчийн", + "ch": "Chamoru", + "co": "Corsu", + "cr": "Nehiyaw", + "cs": "Česky", + "cu": "словѣньскъ / slověnĭskŭ", + "cv": "Чăваш", + "cy": "Cymraeg", + "da": "Dansk", + "de": "Deutsch", + "dv": "ދިވެހިބަސް", + "dz": "རྫོང་ཁ", + "ee": "Ɛʋɛ", + "el": "Ελληνικά", + "en": "English", + "en-GB": "English, United Kingdom", + "en-US": "English, United States", + "eo": "Esperanto", + "es": "Español", + "fa": "فارسی", + "fi": "Suomi", + "fj": "Na Vosa Vakaviti", + "fo": "Føroyskt", + "fr": "Français", + "fy": "Frysk", + "ga": "Gaeilge", + "gd": "Gàidhlig", + "gl": "Galego", + "gn": "Avañe'ẽ", + "gu": "ગુજરાતી", + "gv": "Gaelg", + "ha": "هَوُسَ", + "he": "עברית", + "hi": "हिन्दी", + "ho": "Hiri Motu", + "hr": "Hrvatski", + "ht": "Krèyol ayisyen", + "hu": "Magyar", + "hy": "Հայերեն", + "hz": "Otsiherero", + "ia": "Interlingua", + "id": "Bahasa Indonesia", + "ig": "Igbo", + "ii": "ꆇꉙ / 四川彝语", + "ik": "Iñupiak", + "io": "Ido", + "is": "Íslenska", + "it": "Italiano", + "iu": "ᐃᓄᒃᑎᑐᑦ", + "ja": "日本語", + "jv": "Basa Jawa", + "ka": "ქართული", + "kg": "KiKongo", + "ki": "Gĩkũyũ", + "kj": "Kuanyama", + "kk": "Қазақша", + "km": "ភាសាខ្មែរ", + "kn": "ಕನ್ನಡ", + "ko": "한국어", + "ks": "कॉशुर / کٲش", + "ku": "Kurdî", + "kv": "Коми", + "kw": "Kernewek", + "ky": "Kırgızca / Кыргызча", + "la": "Latina", + "lb": "Lëtzebuergesch", + "lg": "Luganda", + "li": "Limburgs", + "ln": "Lingála", + "lo": "ລາວ / Pha xa lao", + "lt": "Lietuvių", + "lv": "Latviešu", + "mg": "Malagasy", + "mh": "Kajin Majel / Ebon", + "mk": "Македонски", + "ml": "മലയാളം", + "mn": "Монгол", + "mo": "Moldovenească", + "ms": "Bahasa Melayu", + "mt": "bil-Malti", + "my": "Myanmasa", + "na": "Dorerin Naoero", + "ne": "नेपाली", + "ng": "Oshiwambo", + "nl": "Nederlands", + "nn": "Norsk (nynorsk)", + "nr": "isiNdebele", + "nv": "Diné bizaad", + "ny": "Chi-Chewa", + "oc": "Occitan", + "oj": "ᐊᓂᔑᓈᐯᒧᐎᓐ / Anishinaabemowin", + "om": "Oromoo", + "os": "Иронау", + "pa": "ਪੰਜਾਬੀ / پنجابی", + "pi": "Pāli / पाऴि", + "pl": "Polski", + "ps": "پښتو", + "pt": "Português", + "qu": "Runa Simi", + "rm": "Rumantsch", + "ro": "Română", + "ru": "Русский", + "rw": "Kinyarwandi", + "sa": "संस्कृतम्", + "sc": "Sardu", + "sd": "सिंधी / سنڌي", + "se": "Davvisámegiella", + "sg": "Sängö", + "sh": "Srpskohrvatski / Српскохрватски", + "si": "සිංහල", + "sk": "Slovenčina", + "sl": "Slovenščina", + "sm": "Gagana Samoa", + "sn": "chiShona", + "so": "Soomaaliga", + "sq": "Shqip", + "sr": "Српски", + "ss": "SiSwati", + "st": "Sesotho", + "su": "Basa Sunda", + "sv": "Svenska", + "sw": "Kiswahili", + "ta": "தமிழ்", + "tg": "Тоҷикӣ", + "th": "ไทย / Phasa Thai", + "ti": "ትግርኛ", + "tk": "Туркмен / تركمن", + "tl": "Tagalog", + "to": "Lea Faka-Tonga", + "tr": "Türkçe", + "ts": "Xitsonga", + "tt": "Tatarça", + "tw": "Twi", + "ty": "Reo Mā`ohi", + "ug": "Uyƣurqə / ئۇيغۇرچە", + "uk": "Українська", + "ur": "اردو", + "uz": "Ўзбек", + "ve": "Tshivenḓa", + "vi": "Tiếng Việt", + "vo": "Volapük", + "wa": "Walon", + "wo": "Wollof", + "xh": "isiXhosa", + "yi": "ייִדיש", + "yo": "Yorùbá", + "za": "Cuengh / Tôô / 壮语", + "zh": "中文", + "zh-HANS": "Chinese, Simplified", + "zh-HANT": "Chinese, Traditional", + "zh-TW": "Chinese, Traditional, Taiwan", + "zu": "isiZulu", + } + return languages + + def get_rules_files( + self, + pathToDir: str, + processSubDirs: Callable[[str, str], List[str]] | None, + ) -> List[str]: + language = os.path.basename(pathToDir) + ruleFiles = [os.path.basename(file) for file in glob.glob(os.path.join(pathToDir, "*_Rules.yaml"))] + for dir in os.listdir(pathToDir): + if os.path.isdir(os.path.join(pathToDir, dir)): + if processSubDirs: + ruleFiles.extend(processSubDirs(dir, language)) + + if len(ruleFiles) == 0: + # look in the .zip file for the style files, including regional subdirs -- it might not have been unzipped + try: + zip_file = ZipFile(f"{pathToDir}\\{language}.zip", "r") + for file in zip_file.namelist(): + if file.endswith("_Rules.yaml"): + ruleFiles.append(file) + elif zip_file.getinfo(file).is_dir() and processSubDirs: + ruleFiles.extend(processSubDirs(dir, language)) + except Exception as e: + log.debugWarning(f"MathCAT Dialog: didn't find zip file {zip_file}. Error: {e}") + + return ruleFiles + + def GetLanguages(self): + def addRegionalLanguages(subDir: str, language: str) -> List[str]: + # the language variants are in folders named using ISO 3166-1 alpha-2 + # codes https://en.wikipedia.org/wiki/ISO_3166-2 + # check if there are language variants in the language folder + if subDir != "SharedRules": + languagesDict = UserInterface.LanguagesDict() + # add to the listbox the text for this language variant together with the code + regionalCode = language + "-" + subDir.upper() + if languagesDict.get(regionalCode, "missing") != "missing": + self.m_choiceLanguage.Append(f"{languagesDict[regionalCode]} ({language}-{subDir})") + elif languagesDict.get(language, "missing") != "missing": + self.m_choiceLanguage.Append(f"{languagesDict[language]} ({regionalCode})") + else: + self.m_choiceLanguage.Append(f"{language} ({regionalCode})") + return [os.path.basename(file) for file in glob.glob(os.path.join(subDir, "*_Rules.yaml"))] + return [] + + # initialise the language list + languagesDict = UserInterface.LanguagesDict() + # clear the language names in the dialog + self.m_choiceLanguage.Clear() + # Translators: menu item -- use the language of the voice chosen in the NVDA speech settings dialog + # "Auto" == "Automatic" -- other items in menu are "English (en)", etc., so this matches that style + self.m_choiceLanguage.Append(_("Use Voice's Language (Auto)")) + # populate the available language names in the dialog + # the implemented languages are in folders named using the relevant ISO 639-1 + # code https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes + languageDir = UserInterface.path_to_languages_folder() + for language in os.listdir(languageDir): + pathToLanguageDir = os.path.join(UserInterface.path_to_languages_folder(), language) + if os.path.isdir(pathToLanguageDir): + # only add this language if there is a xxx_Rules.yaml file + if len(self.get_rules_files(pathToLanguageDir, addRegionalLanguages)) > 0: + # add to the listbox the text for this language together with the code + if languagesDict.get(language, "missing") != "missing": + self.m_choiceLanguage.Append(languagesDict[language] + " (" + language + ")") + else: + self.m_choiceLanguage.Append(language + " (" + language + ")") + + def GetLanguageCode(self): + lang_selection = self.m_choiceLanguage.GetStringSelection() + lang_code = lang_selection[lang_selection.find("(") + 1 : lang_selection.find(")")] + return lang_code + + def GetSpeechStyles(self, this_SpeechStyle: str): + """Get all the speech styles for the current language. + This sets the SpeechStyles dialog entry""" + from speech import getCurrentLanguage + + def getSpeechStyleFromDirectory(dir: str, lang: str) -> list[str]: + r"""Get the speech styles from any regional dialog, from the main language, dir and if there isn't from the zip file. + The 'lang', if it has a region dialect, is of the form 'en\uk' + The returned list is sorted alphabetically""" + # start with the regional dialect, then add on any (unique) styles in the main dir + main_lang = lang.split("\\")[0] # does the right thing even if there is no regional directory + all_style_files = [] + if lang.find("\\") >= 0: + all_style_files = [ + os.path.basename(name) for name in glob.glob(dir + lang + "\\*_Rules.yaml") + ] + all_style_files.extend( + [os.path.basename(name) for name in glob.glob(dir + main_lang + "\\*_Rules.yaml")], + ) + all_style_files = list(set(all_style_files)) # make them unique + if len(all_style_files) == 0: + # look in the .zip file for the style files -- this will have regional variants, but also have that dir + try: + zip_file = dir + main_lang + "\\" + main_lang + ".zip" + zip_file = ZipFile(zip_file, "r") # file might not exist + all_style_files = [ + name.split("/")[-1] for name in zip_file.namelist() if name.endswith("_Rules.yaml") + ] + except Exception as e: + log.debugWarning(f"MathCAT Dialog: didn't find zip file {zip_file}. Error: {e}") + all_style_files.sort() + return all_style_files + + # clear the SpeechStyle choices + self.m_choiceSpeechStyle.Clear() + # get the currently selected language code + languageCode = UserInterface.GetLanguageCode(self) + + if languageCode == "Auto": + # list the speech styles for the current voice rather than have none listed + languageCode = getCurrentLanguage().lower().replace("_", "-") + languageCode = languageCode.replace("-", "\\") + + languagePath = UserInterface.path_to_languages_folder() + "\\" + # log.info(f"languagePath={languagePath}") + # populate the m_choiceSpeechStyle choices + all_style_files = [ + # remove "_Rules.yaml" from the list + name[: name.find("_Rules.yaml")] + for name in getSpeechStyleFromDirectory(languagePath, languageCode) + ] + # There isn't a LiteralSpeak rules file since it has no language-specific rules. We add it at the end. + # Translators: at the moment, do NOT translate this string as some code specifically looks for this name. + all_style_files.append("LiteralSpeak") + for name in all_style_files: + self.m_choiceSpeechStyle.Append((name)) + try: + # set the SpeechStyle to the same as previous + self.m_choiceSpeechStyle.SetStringSelection( + this_SpeechStyle if this_SpeechStyle in all_style_files else all_style_files[0], + ) + except Exception as e: + log.exception( + f"MathCAT: An exception occurred in GetSpeechStyles evaluating set SetStringSelection: {e}", + ) + # that didn't work, choose the first in the list + self.m_choiceSpeechStyle.SetSelection(0) + + def GetBrailleCodes(self): + # initialise the braille code list + self.m_choiceBrailleMathCode.Clear() + # populate the available braille codes in the dialog + # the dir names are used, not the rule file names because the dir names have to be unique + pathToBrailleFolder = UserInterface.pathToBrailleFolder() + for braille_code in os.listdir(pathToBrailleFolder): + pathToBrailleCode = os.path.join(pathToBrailleFolder, braille_code) + if os.path.isdir(pathToBrailleCode): + if len(self.get_rules_files(pathToBrailleCode, None)) > 0: + self.m_choiceBrailleMathCode.Append(braille_code) + + def set_ui_values(self): + # set the UI elements to the ones read from the preference file(s) + try: + self.m_choiceImpairment.SetSelection( + Speech_Impairment.index(user_preferences["Speech"]["Impairment"]), + ) + try: + lang_pref = user_preferences["Speech"]["Language"] + self.m_choiceLanguage.SetSelection(0) + i = 1 # no need to test i == 0 + while i < self.m_choiceLanguage.GetCount(): + if f"({lang_pref})" in self.m_choiceLanguage.GetString(i): + self.m_choiceLanguage.SetSelection(i) + break + i += 1 + except Exception as e: + log.exception( + f"MathCAT: An exception occurred in set_ui_values ('{user_preferences['Speech']['Language']}'): {e}", + ) + # the language in the settings file is not in the folder structure, something went wrong, + # set to the first in the list + self.m_choiceLanguage.SetSelection(0) + try: + # now get the available SpeechStyles from the folder structure and set to the preference setting is possible + self.GetSpeechStyles(str(user_preferences["Speech"]["SpeechStyle"])) + except Exception as e: + log.exception(f"MathCAT: An exception occurred in set_ui_values (getting SpeechStyle): {e}") + self.m_choiceSpeechStyle.Append( + "Error when setting SpeechStyle for " + self.m_choiceLanguage.GetStringSelection(), + ) + # set the rest of the UI elements + self.m_choiceDecimalSeparator.SetSelection( + Speech_DecimalSeparator.index(user_preferences["Other"]["DecimalSeparator"]), + ) + self.m_choiceSpeechAmount.SetSelection( + Speech_Verbosity.index(user_preferences["Speech"]["Verbosity"]), + ) + self.m_sliderRelativeSpeed.SetValue(user_preferences["Speech"]["MathRate"]) + pause_factor = ( + 0 + if int(user_preferences["Speech"]["PauseFactor"]) <= 1 + else round( + math.log( + int(user_preferences["Speech"]["PauseFactor"]) / PAUSE_FACTOR_SCALE, + PAUSE_FACTOR_LOG_BASE, + ), + ) + ) + self.m_sliderPauseFactor.SetValue(pause_factor) + self.m_checkBoxSpeechSound.SetValue(user_preferences["Speech"]["SpeechSound"] == "Beep") + self.m_choiceSpeechForChemical.SetSelection( + Speech_Chemistry.index(user_preferences["Speech"]["Chemistry"]), + ) + + self.m_choiceNavigationMode.SetSelection( + Navigation_NavMode.index(user_preferences["Navigation"]["NavMode"]), + ) + self.m_checkBoxResetNavigationMode.SetValue(user_preferences["Navigation"]["ResetNavMode"]) + self.m_choiceSpeechAmountNavigation.SetSelection( + Navigation_NavVerbosity.index(user_preferences["Navigation"]["NavVerbosity"]), + ) + if user_preferences["Navigation"]["Overview"]: + self.m_choiceNavigationSpeech.SetSelection(1) + else: + self.m_choiceNavigationSpeech.SetSelection(0) + self.m_checkBoxResetNavigationSpeech.SetValue(user_preferences["Navigation"]["ResetOverview"]) + self.m_checkBoxAutomaticZoom.SetValue(user_preferences["Navigation"]["AutoZoomOut"]) + self.m_choiceCopyAs.SetSelection( + Navigation_CopyAs.index(user_preferences["Navigation"]["CopyAs"]), + ) + + self.m_choiceBrailleHighlights.SetSelection( + Braille_BrailleNavHighlight.index(user_preferences["Braille"]["BrailleNavHighlight"]), + ) + try: + braille_pref = user_preferences["Braille"]["BrailleCode"] + i = 0 + while braille_pref != self.m_choiceBrailleMathCode.GetString(i): + i = i + 1 + if i == self.m_choiceBrailleMathCode.GetCount(): + break + if braille_pref == self.m_choiceBrailleMathCode.GetString(i): + self.m_choiceBrailleMathCode.SetSelection(i) + else: + self.m_choiceBrailleMathCode.SetSelection(0) + except Exception as e: + log.exception(f"MathCAT: An exception occurred while trying to set the Braille code: {e}") + # the braille code in the settings file is not in the folder structure, something went wrong, + # set to the first in the list + self.m_choiceBrailleMathCode.SetSelection(0) + except KeyError as err: + print("Key not found", err) + + def get_ui_values(self): + global user_preferences + # read the values from the UI and update the user preferences dictionary + user_preferences["Speech"]["Impairment"] = Speech_Impairment[self.m_choiceImpairment.GetSelection()] + user_preferences["Speech"]["Language"] = self.GetLanguageCode() + user_preferences["Other"]["DecimalSeparator"] = Speech_DecimalSeparator[ + self.m_choiceDecimalSeparator.GetSelection() + ] + user_preferences["Speech"]["SpeechStyle"] = self.m_choiceSpeechStyle.GetStringSelection() + user_preferences["Speech"]["Verbosity"] = Speech_Verbosity[self.m_choiceSpeechAmount.GetSelection()] + user_preferences["Speech"]["MathRate"] = self.m_sliderRelativeSpeed.GetValue() + pf_slider = self.m_sliderPauseFactor.GetValue() + pause_factor = ( + 0 if pf_slider == 0 else round(PAUSE_FACTOR_SCALE * math.pow(PAUSE_FACTOR_LOG_BASE, pf_slider)) + ) # avoid log(0) + user_preferences["Speech"]["PauseFactor"] = pause_factor + if self.m_checkBoxSpeechSound.GetValue(): + user_preferences["Speech"]["SpeechSound"] = "Beep" + else: + user_preferences["Speech"]["SpeechSound"] = "None" + user_preferences["Speech"]["Chemistry"] = Speech_Chemistry[ + self.m_choiceSpeechForChemical.GetSelection() + ] + user_preferences["Navigation"]["NavMode"] = Navigation_NavMode[ + self.m_choiceNavigationMode.GetSelection() + ] + user_preferences["Navigation"]["ResetNavMode"] = self.m_checkBoxResetNavigationMode.GetValue() + user_preferences["Navigation"]["NavVerbosity"] = Navigation_NavVerbosity[ + self.m_choiceSpeechAmountNavigation.GetSelection() + ] + user_preferences["Navigation"]["Overview"] = self.m_choiceNavigationSpeech.GetSelection() != 0 + user_preferences["Navigation"]["ResetOverview"] = self.m_checkBoxResetNavigationSpeech.GetValue() + user_preferences["Navigation"]["AutoZoomOut"] = self.m_checkBoxAutomaticZoom.GetValue() + user_preferences["Navigation"]["CopyAs"] = Navigation_CopyAs[self.m_choiceCopyAs.GetSelection()] + + user_preferences["Braille"]["BrailleNavHighlight"] = Braille_BrailleNavHighlight[ + self.m_choiceBrailleHighlights.GetSelection() + ] + user_preferences["Braille"]["BrailleCode"] = self.m_choiceBrailleMathCode.GetStringSelection() + if "NVDAAddOn" not in user_preferences: + user_preferences["NVDAAddOn"] = {"LastCategory": "0"} + user_preferences["NVDAAddOn"]["LastCategory"] = self.m_listBoxPreferencesTopic.GetSelection() + + @staticmethod + def path_to_default_preferences(): + return os.path.join(os.path.dirname(os.path.abspath(__file__)), "Rules", "prefs.yaml") + + @staticmethod + def path_to_user_preferences_folder(): + # the user preferences file is stored at: C:\Users\AppData\Roaming\MathCAT\prefs.yaml + return os.path.join(os.path.expandvars("%APPDATA%"), "MathCAT") + + @staticmethod + def path_to_user_preferences(): + # the user preferences file is stored at: C:\Users\AppData\Roaming\MathCAT\prefs.yaml + return os.path.join(UserInterface.path_to_user_preferences_folder(), "prefs.yaml") + + @staticmethod + def load_default_preferences(): + global user_preferences + # load default preferences into the user preferences data structure (overwrites existing) + if os.path.exists(UserInterface.path_to_default_preferences()): + with open( + UserInterface.path_to_default_preferences(), + encoding="utf-8", + ) as f: + user_preferences = yaml.load(f, Loader=yaml.FullLoader) + + @staticmethod + def load_user_preferences(): + global user_preferences + # merge user file values into the user preferences data structure + if os.path.exists(UserInterface.path_to_user_preferences()): + with open(UserInterface.path_to_user_preferences(), encoding="utf-8") as f: + # merge with the default preferences, overwriting with the user's values + user_preferences.update(yaml.load(f, Loader=yaml.FullLoader)) + + @staticmethod + def validate(key1: str, key2: str, valid_values: List[Union[str, bool]], default_value: Union[str, bool]): + global user_preferences + try: + if valid_values == []: + # any value is valid + if user_preferences[key1][key2] != "": + return + + else: + # any value in the list is valid + if user_preferences[key1][key2] in valid_values: + return + except Exception as e: + log.exception(f"MathCAT: An exception occurred in validate: {e}") + # the preferences entry does not exist + if key1 not in user_preferences: + user_preferences[key1] = {key2: default_value} + else: + user_preferences[key1][key2] = default_value + + @staticmethod + def validate_int(key1: str, key2: str, valid_values: List[int], default_value: int): + global user_preferences + try: + # any value between lower and upper bounds is valid + if ( + int(user_preferences[key1][key2]) >= valid_values[0] + and int(user_preferences[key1][key2]) <= valid_values[1] + ): + return + except Exception as e: + log.exception(f"MathCAT: An exception occurred in validate_int: {e}") + # the preferences entry does not exist + if key1 not in user_preferences: + user_preferences[key1] = {key2: default_value} + else: + user_preferences[key1][key2] = default_value + + @staticmethod + def validate_user_preferences(): + # check each user preference value to ensure it is present and valid, set default value if not + # Speech: + # Impairment: Blindness # LearningDisability, LowVision, Blindness + UserInterface.validate( + "Speech", + "Impairment", + ["LearningDisability", "LowVision", "Blindness"], + "Blindness", + ) + # Language: en # any known language code and sub-code -- could be en-uk, etc + UserInterface.validate("Speech", "Language", [], "en") + # Verbosity: Medium # Terse, Medium, Verbose + UserInterface.validate("Speech", "Verbosity", ["Terse", "Medium", "Verbose"], "Medium") + # MathRate: 100 # Change from text speech rate (%) + UserInterface.validate_int("Speech", "MathRate", [0, 200], 100) + # PauseFactor: 100 # TBC + UserInterface.validate_int("Speech", "PauseFactor", [0, 1000], 100) + # SpeechSound: None # make a sound when starting/ending math speech -- None, Beep + UserInterface.validate("Speech", "SpeechSound", ["None", "Beep"], "None") + # SpeechStyle: ClearSpeak # Any known speech style (falls back to ClearSpeak) + UserInterface.validate("Speech", "SpeechStyle", [], "ClearSpeak") + # SubjectArea: General # FIX: still working on this + UserInterface.validate("Speech", "SubjectArea", [], "General") + # Chemistry: SpellOut # SpellOut (H 2 0), AsCompound (Water), Off (H sub 2 O) + UserInterface.validate("Speech", "Chemistry", ["SpellOut", "Off"], "SpellOut") + # Navigation: + # NavMode: Enhanced # Enhanced, Simple, Character + UserInterface.validate("Navigation", "NavMode", ["Enhanced", "Simple", "Character"], "Enhanced") + # ResetNavMode: false # remember previous value and use it + UserInterface.validate("Navigation", "ResetNavMode", [False, True], False) + # Overview: false # speak the expression or give a description/overview + UserInterface.validate("Navigation", "Overview", [False, True], False) + # ResetOverview: true # remember previous value and use it + UserInterface.validate("Navigation", "ResetOverview", [False, True], True) + # NavVerbosity: Medium # Terse, Medium, Full (words to say for nav command) + UserInterface.validate("Navigation", "NavVerbosity", ["Terse", "Medium", "Full"], "Medium") + # AutoZoomOut: true # Auto zoom out of 2D exprs (use shift-arrow to force zoom out if unchecked) + UserInterface.validate("Navigation", "AutoZoomOut", [False, True], True) + # CopyAs: MathML # MathML, LaTeX, ASCIIMath, Speech + UserInterface.validate("Navigation", "CopyAs", ["MathML", "LaTeX", "ASCIIMath", "Speech"], "MathML") + # Braille: + # BrailleNavHighlight: EndPoints + # Highlight with dots 7 & 8 the current nav node -- values are Off, FirstChar, EndPoints, All + UserInterface.validate( + "Braille", + "BrailleNavHighlight", + ["Off", "FirstChar", "EndPoints", "All"], + "EndPoints", + ) + # BrailleCode: "Nemeth" # Any supported braille code (currently Nemeth, UEB, CMU, Vietnam) + UserInterface.validate("Braille", "BrailleCode", [], "Nemeth") + + @staticmethod + def write_user_preferences(): + # Language is special because it is set elsewhere by SetPreference which overrides the user_prefs -- so set it here + from . import libmathcat_py as libmathcat + + try: + libmathcat.SetPreference("Language", user_preferences["Speech"]["Language"]) + except Exception as e: + log.exception( + f'Error in trying to set MathCAT "Language" preference to "{user_preferences["Speech"]["Language"]}": {e}', + ) + if not os.path.exists(UserInterface.path_to_user_preferences_folder()): + # create a folder for the user preferences + os.mkdir(UserInterface.path_to_user_preferences_folder()) + with open(UserInterface.path_to_user_preferences(), "w", encoding="utf-8") as f: + # write values to the user preferences file, NOT the default + yaml.dump(user_preferences, stream=f, allow_unicode=True) + + def OnRelativeSpeedChanged(self, event): + rate = self.m_sliderRelativeSpeed.GetValue() + # Translators: this is a test string that is spoken. Only translate "the square root of x squared plus y squared" + text = _("the square root of x squared plus y squared").replace( + "XXX", + str(rate), + 1, + ) + speak(ConvertSSMLTextForNVDA(text)) + + def OnPauseFactorChanged(self, event): + rate = self.m_sliderRelativeSpeed.GetValue() + pf_slider = self.m_sliderPauseFactor.GetValue() + pause_factor = ( + 0 if pf_slider == 0 else round(PAUSE_FACTOR_SCALE * math.pow(PAUSE_FACTOR_LOG_BASE, pf_slider)) + ) + text = _( + # Translators: this is a test string that is spoken. Only translate "the fraction with numerator" + # and other parts NOT inside '<.../>', + "the fraction with numerator \ x to the \ n -th\ power plus 1\ @@ -687,96 +724,96 @@ def OnPauseFactorChanged(self, event): x to the \ n -thpower\ minus 1\ - end fraction " - ).format( - rate=rate, - pause_factor_128=128 * pause_factor // 100, - pause_factor_150=150 * pause_factor // 100, - pause_factor_300=300 * pause_factor // 100, - pause_factor_600=600 * pause_factor // 100, - ) - speak(ConvertSSMLTextForNVDA(text)) - - def OnClickOK(self, event): - UserInterface.get_ui_values(self) - UserInterface.write_user_preferences() - self.Destroy() - - def OnClickCancel(self, event): - self.Destroy() - - def OnClickApply(self, event): - UserInterface.get_ui_values(self) - UserInterface.write_user_preferences() - - def OnClickReset(self, event): - UserInterface.load_default_preferences() - UserInterface.validate_user_preferences() - UserInterface.set_ui_values(self) - - def OnClickHelp(self, event): - webbrowser.open("https://nsoiffer.github.io/MathCAT/users.html") - - def OnListBoxCategories(self, event): - # the category changed, now show the appropriate dialogue page - self.m_simplebookPanelsCategories.SetSelection(self.m_listBoxPreferencesTopic.GetSelection()) - - def OnLanguage(self, event): - # the language changed, get the SpeechStyles for the new language - UserInterface.GetSpeechStyles(self, self.m_choiceSpeechStyle.GetStringSelection()) - - def MathCATPreferencesDialogOnCharHook(self, event: wx.KeyEvent): - # designed choice is that Enter is the same as clicking OK, and Escape is the same as clicking Cancel - keyCode = event.GetKeyCode() - if keyCode == wx.WXK_ESCAPE: - UserInterface.OnClickCancel(self, event) - return - if keyCode == wx.WXK_RETURN: - UserInterface.OnClickOK(self, event) - if keyCode == wx.WXK_TAB: - if event.GetModifiers() == wx.MOD_CONTROL: - # cycle the category forward - new_category = self.m_listBoxPreferencesTopic.GetSelection() + 1 - if new_category == 3: - new_category = 0 - self.m_listBoxPreferencesTopic.SetSelection(new_category) - # update the ui to show the new page - UserInterface.OnListBoxCategories(self, event) - # set the focus into the category list box - self.m_listBoxPreferencesTopic.SetFocus() - # jump out so the tab key is not processed - return - if event.GetModifiers() == wx.MOD_CONTROL | wx.MOD_SHIFT: - # cycle the category back - new_category = self.m_listBoxPreferencesTopic.GetSelection() - 1 - if new_category == -1: - new_category = 2 - self.m_listBoxPreferencesTopic.SetSelection(new_category) - # update the ui to show the new page - UserInterface.OnListBoxCategories(self, event) - # update the ui to show the new page - self.m_listBoxPreferencesTopic.SetFocus() - # jump out so the tab key is not processed - return - if event.GetModifiers() == wx.MOD_NONE and ( - MathCATgui.MathCATPreferencesDialog.FindFocus() == self.m_listBoxPreferencesTopic - ): - if self.m_listBoxPreferencesTopic.GetSelection() == 0: - self.m_choiceImpairment.SetFocus() - elif self.m_listBoxPreferencesTopic.GetSelection() == 1: - self.m_choiceNavigationMode.SetFocus() - elif self.m_listBoxPreferencesTopic.GetSelection() == 2: - self.m_choiceBrailleMathCode.SetFocus() - return - if (event.GetModifiers() == wx.MOD_SHIFT) and ( - MathCATgui.MathCATPreferencesDialog.FindFocus() == self.m_buttonOK - ): - if self.m_listBoxPreferencesTopic.GetSelection() == 0: - self.m_choiceSpeechForChemical.SetFocus() - elif self.m_listBoxPreferencesTopic.GetSelection() == 1: - self.m_choiceSpeechAmountNavigation.SetFocus() - elif self.m_listBoxPreferencesTopic.GetSelection() == 2: - self.m_choiceBrailleHighlights.SetFocus() - return - # continue handling keyboard event - event.Skip() + end fraction ", + ).format( + rate=rate, + pause_factor_128=128 * pause_factor // 100, + pause_factor_150=150 * pause_factor // 100, + pause_factor_300=300 * pause_factor // 100, + pause_factor_600=600 * pause_factor // 100, + ) + speak(ConvertSSMLTextForNVDA(text)) + + def OnClickOK(self, event): + UserInterface.get_ui_values(self) + UserInterface.write_user_preferences() + self.Destroy() + + def OnClickCancel(self, event): + self.Destroy() + + def OnClickApply(self, event): + UserInterface.get_ui_values(self) + UserInterface.write_user_preferences() + + def OnClickReset(self, event): + UserInterface.load_default_preferences() + UserInterface.validate_user_preferences() + UserInterface.set_ui_values(self) + + def OnClickHelp(self, event): + webbrowser.open("https://nsoiffer.github.io/MathCAT/users.html") + + def OnListBoxCategories(self, event): + # the category changed, now show the appropriate dialogue page + self.m_simplebookPanelsCategories.SetSelection(self.m_listBoxPreferencesTopic.GetSelection()) + + def OnLanguage(self, event): + # the language changed, get the SpeechStyles for the new language + UserInterface.GetSpeechStyles(self, self.m_choiceSpeechStyle.GetStringSelection()) + + def MathCATPreferencesDialogOnCharHook(self, event: wx.KeyEvent): + # designed choice is that Enter is the same as clicking OK, and Escape is the same as clicking Cancel + keyCode = event.GetKeyCode() + if keyCode == wx.WXK_ESCAPE: + UserInterface.OnClickCancel(self, event) + return + if keyCode == wx.WXK_RETURN: + UserInterface.OnClickOK(self, event) + if keyCode == wx.WXK_TAB: + if event.GetModifiers() == wx.MOD_CONTROL: + # cycle the category forward + new_category = self.m_listBoxPreferencesTopic.GetSelection() + 1 + if new_category == 3: + new_category = 0 + self.m_listBoxPreferencesTopic.SetSelection(new_category) + # update the ui to show the new page + UserInterface.OnListBoxCategories(self, event) + # set the focus into the category list box + self.m_listBoxPreferencesTopic.SetFocus() + # jump out so the tab key is not processed + return + if event.GetModifiers() == wx.MOD_CONTROL | wx.MOD_SHIFT: + # cycle the category back + new_category = self.m_listBoxPreferencesTopic.GetSelection() - 1 + if new_category == -1: + new_category = 2 + self.m_listBoxPreferencesTopic.SetSelection(new_category) + # update the ui to show the new page + UserInterface.OnListBoxCategories(self, event) + # update the ui to show the new page + self.m_listBoxPreferencesTopic.SetFocus() + # jump out so the tab key is not processed + return + if event.GetModifiers() == wx.MOD_NONE and ( + MathCATgui.MathCATPreferencesDialog.FindFocus() == self.m_listBoxPreferencesTopic + ): + if self.m_listBoxPreferencesTopic.GetSelection() == 0: + self.m_choiceImpairment.SetFocus() + elif self.m_listBoxPreferencesTopic.GetSelection() == 1: + self.m_choiceNavigationMode.SetFocus() + elif self.m_listBoxPreferencesTopic.GetSelection() == 2: + self.m_choiceBrailleMathCode.SetFocus() + return + if (event.GetModifiers() == wx.MOD_SHIFT) and ( + MathCATgui.MathCATPreferencesDialog.FindFocus() == self.m_buttonOK + ): + if self.m_listBoxPreferencesTopic.GetSelection() == 0: + self.m_choiceSpeechForChemical.SetFocus() + elif self.m_listBoxPreferencesTopic.GetSelection() == 1: + self.m_choiceSpeechAmountNavigation.SetFocus() + elif self.m_listBoxPreferencesTopic.GetSelection() == 2: + self.m_choiceBrailleHighlights.SetFocus() + return + # continue handling keyboard event + event.Skip() diff --git a/addon/globalPlugins/MathCAT/MathCATgui.py b/addon/globalPlugins/MathCAT/MathCATgui.py index d46f56d7..0a3b6328 100644 --- a/addon/globalPlugins/MathCAT/MathCATgui.py +++ b/addon/globalPlugins/MathCAT/MathCATgui.py @@ -1,4 +1,5 @@ import wx + # import wx.xrc import gettext import addonHandler @@ -12,833 +13,850 @@ class MathCATPreferencesDialog(wx.Dialog): - def __init__(self, parent): - wx.Dialog.__init__( - self, - parent, - id=wx.ID_ANY, - # Translators: title for MathCAT preferences dialog - title=_("MathCAT Preferences"), - pos=wx.DefaultPosition, - size=wx.Size(-1, -1), - style=wx.DEFAULT_DIALOG_STYLE, - ) - - self.SetSizeHints(wx.DefaultSize, wx.DefaultSize) - - gbSizerMathCATPreferences = wx.GridBagSizer(0, 0) - gbSizerMathCATPreferences.SetFlexibleDirection(wx.BOTH) - gbSizerMathCATPreferences.SetNonFlexibleGrowMode(wx.FLEX_GROWMODE_SPECIFIED) - - self.m_panelCategories = wx.Panel( - self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL - ) - bSizerCategories = wx.BoxSizer(wx.VERTICAL) - - self.m_staticTextCategories = wx.StaticText( - self.m_panelCategories, - wx.ID_ANY, - # Translators: A heading that labels three navigation pane tab names in the MathCAT dialog - _("Categories:"), - wx.DefaultPosition, - wx.DefaultSize, - 0, - ) - self.m_staticTextCategories.Wrap(-1) - - bSizerCategories.Add(self.m_staticTextCategories, 0, wx.ALL, 5) - - m_listBoxPreferencesTopicChoices = [ - # Translators: these are navigation pane headings for the MathCAT preferences dialog under the title "Categories" - _("Speech"), - # Translators: these are navigation pane headings for the MathCAT preferences dialog under the title "Categories" - _("Navigation"), - # Translators: these are navigation pane headings for the MathCAT preferences dialog under the title "Categories" - _("Braille"), - ] - self.m_listBoxPreferencesTopic = wx.ListBox( - self.m_panelCategories, - wx.ID_ANY, - wx.Point(-1, -1), - wx.Size(-1, -1), - m_listBoxPreferencesTopicChoices, - wx.LB_NO_SB | wx.LB_SINGLE, - ) - bSizerCategories.Add(self.m_listBoxPreferencesTopic, 0, wx.ALL, 5) - - bSizerCategories.Add((0, 0), 1, wx.EXPAND, 5) - - self.m_bitmapLogo = wx.StaticBitmap( - self.m_panelCategories, - wx.ID_ANY, - wx.NullBitmap, - wx.DefaultPosition, - wx.Size(126, 85), - 0, - ) - bSizerCategories.Add(self.m_bitmapLogo, 0, wx.ALL, 5) - - self.m_panelCategories.SetSizer(bSizerCategories) - self.m_panelCategories.Layout() - bSizerCategories.Fit(self.m_panelCategories) - gbSizerMathCATPreferences.Add( - self.m_panelCategories, - wx.GBPosition(0, 0), - wx.GBSpan(1, 1), - wx.EXPAND | wx.ALL, - 5, - ) - - self.m_simplebookPanelsCategories = wx.Simplebook( - self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 0 - ) - self.m_panelSpeech = wx.Panel( - self.m_simplebookPanelsCategories, - wx.ID_ANY, - wx.DefaultPosition, - wx.DefaultSize, - wx.BORDER_SIMPLE | wx.TAB_TRAVERSAL, - ) - bSizerSpeech = wx.BoxSizer(wx.VERTICAL) - - bSizerImpairment = wx.BoxSizer(wx.HORIZONTAL) - - self.m_staticTextImpairment = wx.StaticText( - self.m_panelSpeech, - wx.ID_ANY, - # Translators: this is the text label for whom to target the speech for (options are below) - _("Generate speech for:"), - wx.DefaultPosition, - wx.DefaultSize, - 0, - ) - self.m_staticTextImpairment.Wrap(-1) - - bSizerImpairment.Add(self.m_staticTextImpairment, 0, wx.ALL, 5) - - m_choiceImpairmentChoices = [ - # Translators: these are the categories of impairments that MathCAT supports - # Translators: Learning disabilities includes dyslexia and ADHD - _("Learning disabilities"), - # Translators: target people who are blind - _("Blindness"), - # Translators: target people who have low vision - _("Low vision"), - ] - self.m_choiceImpairment = wx.Choice( - self.m_panelSpeech, - wx.ID_ANY, - wx.DefaultPosition, - wx.DefaultSize, - m_choiceImpairmentChoices, - 0, - ) - self.m_choiceImpairment.SetSelection(1) - bSizerImpairment.Add(self.m_choiceImpairment, 0, wx.ALL, 5) - - bSizerSpeech.Add(bSizerImpairment, 1, wx.EXPAND, 5) - - bSizerLanguage = wx.BoxSizer(wx.HORIZONTAL) - - self.m_staticTextLanguage = wx.StaticText( - self.m_panelSpeech, - wx.ID_ANY, - # Translators: label for pull down allowing users to choose the speech language for math - _("Language:"), - wx.DefaultPosition, - wx.DefaultSize, - 0, - ) - self.m_staticTextLanguage.Wrap(-1) - - bSizerLanguage.Add(self.m_staticTextLanguage, 0, wx.ALL, 5) - - m_choiceLanguageChoices = ["xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"] - self.m_choiceLanguage = wx.Choice( - self.m_panelSpeech, - wx.ID_ANY, - wx.DefaultPosition, - wx.DefaultSize, - m_choiceLanguageChoices, - 0, - ) - self.m_choiceLanguage.SetSelection(0) - bSizerLanguage.Add(self.m_choiceLanguage, 0, wx.ALL, 5) - - bSizerSpeech.Add(bSizerLanguage, 1, wx.EXPAND, 5) - - bSizerDecimalSeparator = wx.BoxSizer(wx.HORIZONTAL) - - self.m_staticTextDecimalSeparator = wx.StaticText( - self.m_panelSpeech, - wx.ID_ANY, - # Translators: label for pull down to specify what character to use in numbers as the decimal separator - _("Decimal separator for numbers:"), - wx.DefaultPosition, - wx.DefaultSize, - 0, - ) - self.m_staticTextDecimalSeparator.Wrap(-1) - - bSizerDecimalSeparator.Add(self.m_staticTextDecimalSeparator, 0, wx.ALL, 5) - - # Translators: options for decimal separator. - m_choiceDecimalSeparatorChoices = [ - # Translators: options for decimal separator -- "Auto" = automatically pick the choice based on the language - _("Auto"), - # options for decimal separator -- use "." (and use ", " for block separators) - ("."), - # options for decimal separator -- use "," (and use ". " for block separators) - (","), - # Translators: options for decimal separator -- "Custom" = user sets it - # Currently there is no UI for how it is done yet, but eventually there will be a dialog that pops up to set it - _("Custom"), - ] - self.m_choiceDecimalSeparator = wx.Choice( - self.m_panelSpeech, - wx.ID_ANY, - wx.DefaultPosition, - wx.DefaultSize, - m_choiceDecimalSeparatorChoices, - 0, - ) - self.m_choiceDecimalSeparator.SetSelection(0) - bSizerDecimalSeparator.Add(self.m_choiceDecimalSeparator, 0, wx.ALL, 5) - - bSizerSpeech.Add(bSizerDecimalSeparator, 1, wx.EXPAND, 5) - - bSizerSpeechStyle = wx.BoxSizer(wx.HORIZONTAL) - - self.m_staticTextSpeechStyle = wx.StaticText( - self.m_panelSpeech, - wx.ID_ANY, - # Translators: label for pull down allowing users to choose the "style" (version, rules) of speech for math - _("Speech style:"), - wx.DefaultPosition, - wx.DefaultSize, - 0, - ) - self.m_staticTextSpeechStyle.Wrap(-1) - - bSizerSpeechStyle.Add(self.m_staticTextSpeechStyle, 0, wx.ALL, 5) - - m_choiceSpeechStyleChoices = ["xxxxxxxxxxxxxxxx"] - self.m_choiceSpeechStyle = wx.Choice( - self.m_panelSpeech, - wx.ID_ANY, - wx.DefaultPosition, - wx.DefaultSize, - m_choiceSpeechStyleChoices, - 0, - ) - self.m_choiceSpeechStyle.SetSelection(0) - bSizerSpeechStyle.Add(self.m_choiceSpeechStyle, 0, wx.ALL, 5) - - bSizerSpeech.Add(bSizerSpeechStyle, 1, wx.EXPAND, 5) - - bSizerSpeechAmount = wx.BoxSizer(wx.HORIZONTAL) - - self.m_staticTextSpeechAmount = wx.StaticText( - self.m_panelSpeech, - wx.ID_ANY, - # Translators: label for pull down to specify how verbose/terse the speech should be - _("Speech verbosity:"), - wx.DefaultPosition, - wx.DefaultSize, - 0, - ) - self.m_staticTextSpeechAmount.Wrap(-1) - - bSizerSpeechAmount.Add(self.m_staticTextSpeechAmount, 0, wx.ALL, 5) - - # Translators: options for speech verbosity. - m_choiceSpeechAmountChoices = [ - # Translators: options for speech verbosity -- "terse" = use less words - _("Terse"), - # Translators: options for speech verbosity -- "medium" = try to be nether too terse nor too verbose words - _("Medium"), - # Translators: options for speech verbosity -- "verbose" = use more words - _("Verbose"), - ] - self.m_choiceSpeechAmount = wx.Choice( - self.m_panelSpeech, - wx.ID_ANY, - wx.DefaultPosition, - wx.DefaultSize, - m_choiceSpeechAmountChoices, - 0, - ) - self.m_choiceSpeechAmount.SetSelection(0) - bSizerSpeechAmount.Add(self.m_choiceSpeechAmount, 0, wx.ALL, 5) - - bSizerSpeech.Add(bSizerSpeechAmount, 1, wx.EXPAND, 5) - - bSizerRelativeSpeed = wx.BoxSizer(wx.HORIZONTAL) - - self.m_staticTextRelativeSpeed = wx.StaticText( - self.m_panelSpeech, - wx.ID_ANY, - # Translators: label for slider that specifies a percentage of the normal speech rate that should be used for math - _("Relative speech rate:"), - wx.DefaultPosition, - wx.DefaultSize, - 0, - ) - self.m_staticTextRelativeSpeed.Wrap(-1) - - bSizerRelativeSpeed.Add(self.m_staticTextRelativeSpeed, 0, wx.ALL, 5) - - self.m_sliderRelativeSpeed = wx.Slider( - self.m_panelSpeech, - wx.ID_ANY, - 100, - 10, - 100, - wx.DefaultPosition, - wx.DefaultSize, - wx.SL_HORIZONTAL, - ) - self.m_sliderRelativeSpeed.SetLineSize(9) - bSizerRelativeSpeed.Add(self.m_sliderRelativeSpeed, 0, wx.ALL, 5) - - bSizerSpeech.Add(bSizerRelativeSpeed, 1, wx.EXPAND, 5) - - bSizerPauseFactor = wx.BoxSizer(wx.HORIZONTAL) - - self.m_staticPauseFactor = wx.StaticText( - self.m_panelSpeech, - wx.ID_ANY, - # Translators: label for slider that specifies relative factor to increase or decrease pauses in the math speech - _("Pause factor:"), - wx.DefaultPosition, - wx.DefaultSize, - 0, - ) - self.m_staticPauseFactor.Wrap(-1) - - bSizerPauseFactor.Add(self.m_staticPauseFactor, 0, wx.ALL, 5) - - self.m_sliderPauseFactor = wx.Slider( - self.m_panelSpeech, - wx.ID_ANY, - 7, - 0, - 14, - wx.DefaultPosition, - wx.DefaultSize, - wx.SL_HORIZONTAL, - ) - bSizerPauseFactor.Add(self.m_sliderPauseFactor, 0, wx.ALL, 5) - - bSizerSpeech.Add(bSizerPauseFactor, 1, wx.EXPAND, 5) - - bSizerSpeechSound = wx.BoxSizer(wx.HORIZONTAL) - - self.m_checkBoxSpeechSound = wx.CheckBox( - self.m_panelSpeech, - wx.ID_ANY, - # Translators: label for check box controling a beep sound - _("Make a sound when starting/ending math speech"), - wx.DefaultPosition, - wx.DefaultSize, - 0, - ) - bSizerSpeechSound.Add(self.m_checkBoxSpeechSound, 0, wx.ALL, 5) - - bSizerSpeech.Add(bSizerSpeechSound, 1, wx.EXPAND, 5) - - bSizerSubjectArea = wx.BoxSizer(wx.HORIZONTAL) - - self.m_staticTextSubjectArea = wx.StaticText( - self.m_panelSpeech, - wx.ID_ANY, - # Translators: label for pull down to specify a subject area (Geometry, Calculus, ...) - _("Subject area to be used when it cannot be determined automatically:"), - wx.DefaultPosition, - wx.DefaultSize, - 0, - ) - self.m_staticTextSubjectArea.Wrap(-1) - - bSizerSubjectArea.Add(self.m_staticTextSubjectArea, 0, wx.ALL, 5) - - # Translators: a generic (non-specific) math subject area - m_choiceSubjectAreaChoices = [_("General")] - self.m_choiceSubjectArea = wx.Choice( - self.m_panelSpeech, - wx.ID_ANY, - wx.DefaultPosition, - wx.DefaultSize, - m_choiceSubjectAreaChoices, - 0, - ) - self.m_choiceSubjectArea.SetSelection(0) - bSizerSubjectArea.Add(self.m_choiceSubjectArea, 0, wx.ALL, 5) - - bSizerSpeech.Add(bSizerSubjectArea, 1, wx.EXPAND, 5) - - bSizerSpeechForChemical = wx.BoxSizer(wx.HORIZONTAL) - - self.m_staticTextSpeechForChemical = wx.StaticText( - self.m_panelSpeech, - wx.ID_ANY, - # Translators: label for pull down to specify how verbose/terse the speech should be - _("Speech for chemical formulas:"), - wx.DefaultPosition, - wx.DefaultSize, - 0, - ) - self.m_staticTextSpeechForChemical.Wrap(-1) - - bSizerSpeechForChemical.Add(self.m_staticTextSpeechForChemical, 0, wx.ALL, 5) - - m_choiceSpeechForChemicalChoices = [ - # Translators: values for chemistry options with example speech in parenthesis - _("Spell it out (H 2 O)"), - # Translators: values for chemistry options with example speech in parenthesis (never interpret as chemistry) - _("Off (H sub 2 O)"), - ] - self.m_choiceSpeechForChemical = wx.Choice( - self.m_panelSpeech, - wx.ID_ANY, - wx.DefaultPosition, - wx.DefaultSize, - m_choiceSpeechForChemicalChoices, - 0, - ) - self.m_choiceSpeechForChemical.SetSelection(0) - bSizerSpeechForChemical.Add(self.m_choiceSpeechForChemical, 0, wx.ALL, 5) - - bSizerSpeech.Add(bSizerSpeechForChemical, 1, wx.EXPAND, 5) - - self.m_panelSpeech.SetSizer(bSizerSpeech) - self.m_panelSpeech.Layout() - bSizerSpeech.Fit(self.m_panelSpeech) - self.m_simplebookPanelsCategories.AddPage(self.m_panelSpeech, "a page", False) - self.m_panelNavigation = wx.Panel( - self.m_simplebookPanelsCategories, - wx.ID_ANY, - wx.DefaultPosition, - wx.DefaultSize, - wx.BORDER_SIMPLE | wx.TAB_TRAVERSAL, - ) - bSizerNavigation = wx.BoxSizer(wx.VERTICAL) - - sbSizerNavigationMode = wx.StaticBoxSizer( - wx.StaticBox( - self.m_panelNavigation, - wx.ID_ANY, - # Translators: label for pull down to specify one of three modes use to navigate math expressions - _("Navigation mode to use when beginning to navigate an equation:"), - ), - wx.VERTICAL, - ) - - m_choiceNavigationModeChoices = [ - # Translators: names of different modes of navigation. "Enhanced" mode understands math structure - _("Enhanced"), - # Translators: "Simple" walks by character expect for things like fractions, roots, and scripts - _("Simple"), - # Translators: "Character" moves around by character, automatically moving into fractions, etc - _("Character"), - ] - self.m_choiceNavigationMode = wx.Choice( - sbSizerNavigationMode.GetStaticBox(), - wx.ID_ANY, - wx.DefaultPosition, - wx.DefaultSize, - m_choiceNavigationModeChoices, - 0, - ) - self.m_choiceNavigationMode.SetSelection(1) - sbSizerNavigationMode.Add(self.m_choiceNavigationMode, 0, wx.ALL, 5) - - self.m_checkBoxResetNavigationMode = wx.CheckBox( - sbSizerNavigationMode.GetStaticBox(), - wx.ID_ANY, - # Translators: label for checkbox that controls whether any changes to the navigation mode should be preserved - _("Reset navigation mode on entry to an expression"), - wx.DefaultPosition, - wx.DefaultSize, - 0, - ) - sbSizerNavigationMode.Add(self.m_checkBoxResetNavigationMode, 0, wx.ALL, 5) - - bSizerNavigation.Add(sbSizerNavigationMode, 1, wx.EXPAND, 5) - - sbSizerNavigationSpeech = wx.StaticBoxSizer( - wx.StaticBox( - self.m_panelNavigation, - wx.ID_ANY, - # Translators: label for pull down to specify whether the expression is spoken or described (an overview) - _("Navigation speech to use when beginning to navigate an equation:"), - ), - wx.VERTICAL, - ) - - # Translators: either "Speak" the expression or give a description (overview) of the expression - m_choiceNavigationSpeechChoices = [ - # Translators: "Speak" the expression after moving to it - _("Speak"), - # Translators: "Describe" the expression after moving to it ("overview is a synonym") - _("Describe/overview"), - ] - self.m_choiceNavigationSpeech = wx.Choice( - sbSizerNavigationSpeech.GetStaticBox(), - wx.ID_ANY, - wx.DefaultPosition, - wx.DefaultSize, - m_choiceNavigationSpeechChoices, - 0, - ) - self.m_choiceNavigationSpeech.SetSelection(1) - sbSizerNavigationSpeech.Add(self.m_choiceNavigationSpeech, 0, wx.ALL, 5) - - self.m_checkBoxResetNavigationSpeech = wx.CheckBox( - sbSizerNavigationSpeech.GetStaticBox(), - wx.ID_ANY, - # Translators: label for checkbox that controls whether any changes to the speak vs overview reading should be ignored - _("Reset navigation speech on entry to an expression"), - wx.DefaultPosition, - wx.DefaultSize, - 0, - ) - self.m_checkBoxResetNavigationSpeech.SetValue(True) - sbSizerNavigationSpeech.Add(self.m_checkBoxResetNavigationSpeech, 0, wx.ALL, 5) - - bSizerNavigation.Add(sbSizerNavigationSpeech, 1, wx.EXPAND, 5) - - bSizerNavigationZoom = wx.BoxSizer(wx.VERTICAL) - - self.m_checkBoxAutomaticZoom = wx.CheckBox( - self.m_panelNavigation, - wx.ID_ANY, - # Translators: label for checkbox that controls whether arrow keys move out of fractions, etc., - # or whether you have to manually back out of the fraction, etc. - _("Automatic zoom out of 2D notations"), - wx.DefaultPosition, - wx.DefaultSize, - 0, - ) - bSizerNavigationZoom.Add(self.m_checkBoxAutomaticZoom, 0, wx.ALL, 5) - - bSizerSpeechAmountNavigation = wx.BoxSizer(wx.HORIZONTAL) - - self.m_staticTextSpeechAmountNavigation = wx.StaticText( - self.m_panelNavigation, - wx.ID_ANY, - # Translators: label for pull down to specify whether you want a terse or verbose reading of navigation commands - _("Speech amount for navigation:"), - wx.DefaultPosition, - wx.DefaultSize, - 0, - ) - self.m_staticTextSpeechAmountNavigation.Wrap(-1) - - bSizerSpeechAmountNavigation.Add(self.m_staticTextSpeechAmountNavigation, 0, wx.ALL, 5) - - # Translators: options for navigation verbosity. - m_choiceSpeechAmountNavigationChoices = [ - # Translators: options for navigation verbosity -- "terse" = use less words - _("Terse"), - # Translators: options for navigation verbosity -- "medium" = try to be nether too terse nor too verbose words - _("Medium"), - # Translators: options for navigation verbosity -- "verbose" = use more words - _("Verbose"), - ] - self.m_choiceSpeechAmountNavigation = wx.Choice( - self.m_panelNavigation, - wx.ID_ANY, - wx.DefaultPosition, - wx.DefaultSize, - m_choiceSpeechAmountNavigationChoices, - 0, - ) - self.m_choiceSpeechAmountNavigation.SetSelection(0) - bSizerSpeechAmountNavigation.Add(self.m_choiceSpeechAmountNavigation, 0, wx.ALL, 5) - - bSizerNavigationZoom.Add(bSizerSpeechAmountNavigation, 1, wx.EXPAND, 5) - - bSizerNavigation.Add(bSizerNavigationZoom, 1, wx.EXPAND, 5) - - bSizerCopyAs = wx.BoxSizer(wx.HORIZONTAL) - - self.m_staticTextCopyMathAs = wx.StaticText( - self.m_panelNavigation, - wx.ID_ANY, - # Translators: label for pull down to specify how math will be copied to the clipboard - _("Copy math as:"), - wx.DefaultPosition, - wx.DefaultSize, - 0, - ) - self.m_staticTextCopyMathAs.Wrap(-1) - - bSizerCopyAs.Add(self.m_staticTextCopyMathAs, 0, wx.ALL, 5) - - # Translators: options for copy math as. - m_choiceCopyAsChoices = [ - # Translators: options for Copy expression to clipboard as -- "MathML" - _("MathML"), - # Translators: options for Copy expression to clipboard as -- "LaTeX" - _("LaTeX"), - # Translators: options for Copy expression to clipboard as -- "ASCIIMath" - _("ASCIIMath"), - # Translators: options for Copy expression to clipboard as -- speech text - _("Speech"), - ] - self.m_choiceCopyAs = wx.Choice( - self.m_panelNavigation, - wx.ID_ANY, - wx.DefaultPosition, - wx.DefaultSize, - m_choiceCopyAsChoices, - 0, - ) - self.m_choiceCopyAs.SetSelection(0) - bSizerCopyAs.Add(self.m_choiceCopyAs, 0, wx.ALL, 5) - - bSizerNavigation.Add(bSizerCopyAs, 1, wx.EXPAND, 5) - - self.m_panelNavigation.SetSizer(bSizerNavigation) - self.m_panelNavigation.Layout() - bSizerNavigation.Fit(self.m_panelNavigation) - self.m_simplebookPanelsCategories.AddPage( - self.m_panelNavigation, "a page", False - ) - self.m_panelBraille = wx.Panel( - self.m_simplebookPanelsCategories, - wx.ID_ANY, - wx.DefaultPosition, - wx.DefaultSize, - wx.BORDER_SIMPLE | wx.TAB_TRAVERSAL, - ) - bSizerBraille = wx.BoxSizer(wx.VERTICAL) - - bSizerBrailleMathCode = wx.BoxSizer(wx.HORIZONTAL) - - self.m_staticTextBrailleMathCode = wx.StaticText( - self.m_panelBraille, - wx.ID_ANY, - # Translators: label for pull down to specify which braille code to use - _("Braille math code for refreshable displays:"), - wx.DefaultPosition, - wx.DefaultSize, - 0, - ) - self.m_staticTextBrailleMathCode.Wrap(-1) - - bSizerBrailleMathCode.Add(self.m_staticTextBrailleMathCode, 0, wx.ALL, 5) - m_choiceBrailleMathCodeChoices = ["xxxxxxxxxxx"] - self.m_choiceBrailleMathCode = wx.Choice( - self.m_panelBraille, - wx.ID_ANY, - wx.DefaultPosition, - wx.DefaultSize, - m_choiceBrailleMathCodeChoices, - 0, - ) - self.m_choiceBrailleMathCode.SetSelection(1) - bSizerBrailleMathCode.Add(self.m_choiceBrailleMathCode, 0, wx.ALL, 5) - - bSizerBraille.Add(bSizerBrailleMathCode, 1, wx.EXPAND, 5) - - bSizerBrailleHighlights = wx.BoxSizer(wx.HORIZONTAL) - - self.m_staticTextBrailleHighlights = wx.StaticText( - self.m_panelBraille, - wx.ID_ANY, - # Translators: label for pull down to specify how braille dots should be modified when navigating/selecting subexprs - _("Highlight with dots 7 && 8 the current nav node:"), - wx.DefaultPosition, - wx.DefaultSize, - 0, - ) - self.m_staticTextBrailleHighlights.Wrap(-1) - - bSizerBrailleHighlights.Add(self.m_staticTextBrailleHighlights, 0, wx.ALL, 5) - - m_choiceBrailleHighlightsChoices = [ - # Translators: options for using dots 7 and 8: - # Translators: "off" -- don't highlight - _("Off"), - # Translators: "First character" -- only the first character of the current navigation node uses dots 7 & 8 - _("First character"), - # Translators: "Endpoints" -- only the first and last character of the current navigation node uses dots 7 & 8 - _("Endpoints"), - # Translators: "All" -- all the characters for the current navigation node use dots 7 & 8 - _("All"), - ] - self.m_choiceBrailleHighlights = wx.Choice( - self.m_panelBraille, - wx.ID_ANY, - wx.DefaultPosition, - wx.DefaultSize, - m_choiceBrailleHighlightsChoices, - 0, - ) - self.m_choiceBrailleHighlights.SetSelection(1) - bSizerBrailleHighlights.Add(self.m_choiceBrailleHighlights, 0, wx.ALL, 5) - - bSizerBraille.Add(bSizerBrailleHighlights, 1, wx.EXPAND, 5) - - bSizerBraille.Add((0, 0), 1, wx.EXPAND, 5) - - bSizerBraille.Add((0, 0), 1, wx.EXPAND, 5) - - bSizerBraille.Add((0, 0), 1, wx.EXPAND, 5) - - bSizerBraille.Add((0, 0), 1, wx.EXPAND, 5) - - bSizerBraille.Add((0, 0), 1, wx.EXPAND, 5) - - self.m_panelBraille.SetSizer(bSizerBraille) - self.m_panelBraille.Layout() - bSizerBraille.Fit(self.m_panelBraille) - self.m_simplebookPanelsCategories.AddPage(self.m_panelBraille, "a page", False) - - gbSizerMathCATPreferences.Add( - self.m_simplebookPanelsCategories, - wx.GBPosition(0, 1), - wx.GBSpan(1, 1), - wx.EXPAND | wx.ALL, - 10, - ) - - self.m_staticlineAboveButtons = wx.StaticLine(self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL) - gbSizerMathCATPreferences.Add( - self.m_staticlineAboveButtons, - wx.GBPosition(1, 0), - wx.GBSpan(1, 2), - wx.EXPAND | wx.ALL, - 5, - ) - - self.m_panelButtons = wx.Panel(self, wx.ID_ANY, wx.Point(-1, -1), wx.DefaultSize, 0) - bSizerButtons = wx.BoxSizer(wx.HORIZONTAL) - - bSizerButtons.Add((0, 0), 1, wx.EXPAND, 5) - - self.m_buttonOK = wx.Button( - self.m_panelButtons, - wx.ID_ANY, - # Translators: dialog "ok" button - _("OK"), - wx.DefaultPosition, - wx.DefaultSize, - 0, - ) - bSizerButtons.Add(self.m_buttonOK, 0, wx.ALL, 5) - - self.m_buttonCancel = wx.Button( - self.m_panelButtons, - wx.ID_ANY, - # Translators: dialog "cancel" button - _("Cancel"), - wx.DefaultPosition, - wx.DefaultSize, - 0, - ) - bSizerButtons.Add(self.m_buttonCancel, 0, wx.ALL, 5) - - self.m_buttonApply = wx.Button( - self.m_panelButtons, - wx.ID_ANY, - # Translators: dialog "apply" button - _("Apply"), - wx.DefaultPosition, - wx.DefaultSize, - 0, - ) - bSizerButtons.Add(self.m_buttonApply, 0, wx.ALL, 5) - - self.m_buttonReset = wx.Button( - self.m_panelButtons, - wx.ID_ANY, - # Translators: button to reset all the preferences to their default values - _("Reset to defaults"), - wx.DefaultPosition, - wx.DefaultSize, - 0, - ) - bSizerButtons.Add(self.m_buttonReset, 0, wx.ALL, 5) - - self.m_buttonHelp = wx.Button( - self.m_panelButtons, - wx.ID_ANY, - # Translators: button to bring up a help page - _("Help"), - wx.DefaultPosition, - wx.DefaultSize, - 0, - ) - bSizerButtons.Add(self.m_buttonHelp, 0, wx.ALL, 5) - - self.m_panelButtons.SetSizer(bSizerButtons) - self.m_panelButtons.Layout() - bSizerButtons.Fit(self.m_panelButtons) - gbSizerMathCATPreferences.Add( - self.m_panelButtons, - wx.GBPosition(2, 1), - wx.GBSpan(1, 2), - wx.EXPAND | wx.ALL, - 5, - ) - - self.SetSizer(gbSizerMathCATPreferences) - self.Layout() - gbSizerMathCATPreferences.Fit(self) - - self.Centre(wx.BOTH) - - # Connect Events - self.Bind(wx.EVT_CHAR_HOOK, self.MathCATPreferencesDialogOnCharHook) - self.Bind(wx.EVT_KEY_UP, self.MathCATPreferencesDialogOnKeyUp) - self.m_listBoxPreferencesTopic.Bind(wx.EVT_LISTBOX, self.OnListBoxCategories) - self.m_choiceLanguage.Bind(wx.EVT_CHOICE, self.OnLanguage) - self.m_sliderRelativeSpeed.Bind( - wx.EVT_SCROLL_CHANGED, self.OnRelativeSpeedChanged - ) - self.m_sliderPauseFactor.Bind(wx.EVT_SCROLL_CHANGED, self.OnPauseFactorChanged) - self.m_buttonOK.Bind(wx.EVT_BUTTON, self.OnClickOK) - self.m_buttonCancel.Bind(wx.EVT_BUTTON, self.OnClickCancel) - self.m_buttonApply.Bind(wx.EVT_BUTTON, self.OnClickApply) - self.m_buttonReset.Bind(wx.EVT_BUTTON, self.OnClickReset) - self.m_buttonHelp.Bind(wx.EVT_BUTTON, self.OnClickHelp) - - def __del__(self): - pass - - # Virtual event handlers, override them in your derived class - def MathCATPreferencesDialogOnCharHook(self, event): - event.Skip() - - def MathCATPreferencesDialogOnKeyUp(self, event): - event.Skip() - - def OnListBoxCategories(self, event): - event.Skip() - - def OnLanguage(self, event): - event.Skip() - - def OnRelativeSpeedChanged(self, event): - event.Skip() - - def OnPauseFactorChanged(self, event): - event.Skip() - - def OnClickOK(self, event): - event.Skip() - - def OnClickCancel(self, event): - event.Skip() - - def OnClickApply(self, event): - event.Skip() - - def OnClickReset(self, event): - event.Skip() - - def OnClickHelp(self, event): - event.Skip() + def __init__(self, parent): + wx.Dialog.__init__( + self, + parent, + id=wx.ID_ANY, + # Translators: title for MathCAT preferences dialog + title=_("MathCAT Preferences"), + pos=wx.DefaultPosition, + size=wx.Size(-1, -1), + style=wx.DEFAULT_DIALOG_STYLE, + ) + + self.SetSizeHints(wx.DefaultSize, wx.DefaultSize) + + gbSizerMathCATPreferences = wx.GridBagSizer(0, 0) + gbSizerMathCATPreferences.SetFlexibleDirection(wx.BOTH) + gbSizerMathCATPreferences.SetNonFlexibleGrowMode(wx.FLEX_GROWMODE_SPECIFIED) + + self.m_panelCategories = wx.Panel( + self, + wx.ID_ANY, + wx.DefaultPosition, + wx.DefaultSize, + wx.TAB_TRAVERSAL, + ) + bSizerCategories = wx.BoxSizer(wx.VERTICAL) + + self.m_staticTextCategories = wx.StaticText( + self.m_panelCategories, + wx.ID_ANY, + # Translators: A heading that labels three navigation pane tab names in the MathCAT dialog + _("Categories:"), + wx.DefaultPosition, + wx.DefaultSize, + 0, + ) + self.m_staticTextCategories.Wrap(-1) + + bSizerCategories.Add(self.m_staticTextCategories, 0, wx.ALL, 5) + + m_listBoxPreferencesTopicChoices = [ + # Translators: these are navigation pane headings for the MathCAT preferences dialog under the title "Categories" + _("Speech"), + # Translators: these are navigation pane headings for the MathCAT preferences dialog under the title "Categories" + _("Navigation"), + # Translators: these are navigation pane headings for the MathCAT preferences dialog under the title "Categories" + _("Braille"), + ] + self.m_listBoxPreferencesTopic = wx.ListBox( + self.m_panelCategories, + wx.ID_ANY, + wx.Point(-1, -1), + wx.Size(-1, -1), + m_listBoxPreferencesTopicChoices, + wx.LB_NO_SB | wx.LB_SINGLE, + ) + bSizerCategories.Add(self.m_listBoxPreferencesTopic, 0, wx.ALL, 5) + + bSizerCategories.Add((0, 0), 1, wx.EXPAND, 5) + + self.m_bitmapLogo = wx.StaticBitmap( + self.m_panelCategories, + wx.ID_ANY, + wx.NullBitmap, + wx.DefaultPosition, + wx.Size(126, 85), + 0, + ) + bSizerCategories.Add(self.m_bitmapLogo, 0, wx.ALL, 5) + + self.m_panelCategories.SetSizer(bSizerCategories) + self.m_panelCategories.Layout() + bSizerCategories.Fit(self.m_panelCategories) + gbSizerMathCATPreferences.Add( + self.m_panelCategories, + wx.GBPosition(0, 0), + wx.GBSpan(1, 1), + wx.EXPAND | wx.ALL, + 5, + ) + + self.m_simplebookPanelsCategories = wx.Simplebook( + self, + wx.ID_ANY, + wx.DefaultPosition, + wx.DefaultSize, + 0, + ) + self.m_panelSpeech = wx.Panel( + self.m_simplebookPanelsCategories, + wx.ID_ANY, + wx.DefaultPosition, + wx.DefaultSize, + wx.BORDER_SIMPLE | wx.TAB_TRAVERSAL, + ) + bSizerSpeech = wx.BoxSizer(wx.VERTICAL) + + bSizerImpairment = wx.BoxSizer(wx.HORIZONTAL) + + self.m_staticTextImpairment = wx.StaticText( + self.m_panelSpeech, + wx.ID_ANY, + # Translators: this is the text label for whom to target the speech for (options are below) + _("Generate speech for:"), + wx.DefaultPosition, + wx.DefaultSize, + 0, + ) + self.m_staticTextImpairment.Wrap(-1) + + bSizerImpairment.Add(self.m_staticTextImpairment, 0, wx.ALL, 5) + + m_choiceImpairmentChoices = [ + # Translators: these are the categories of impairments that MathCAT supports + # Translators: Learning disabilities includes dyslexia and ADHD + _("Learning disabilities"), + # Translators: target people who are blind + _("Blindness"), + # Translators: target people who have low vision + _("Low vision"), + ] + self.m_choiceImpairment = wx.Choice( + self.m_panelSpeech, + wx.ID_ANY, + wx.DefaultPosition, + wx.DefaultSize, + m_choiceImpairmentChoices, + 0, + ) + self.m_choiceImpairment.SetSelection(1) + bSizerImpairment.Add(self.m_choiceImpairment, 0, wx.ALL, 5) + + bSizerSpeech.Add(bSizerImpairment, 1, wx.EXPAND, 5) + + bSizerLanguage = wx.BoxSizer(wx.HORIZONTAL) + + self.m_staticTextLanguage = wx.StaticText( + self.m_panelSpeech, + wx.ID_ANY, + # Translators: label for pull down allowing users to choose the speech language for math + _("Language:"), + wx.DefaultPosition, + wx.DefaultSize, + 0, + ) + self.m_staticTextLanguage.Wrap(-1) + + bSizerLanguage.Add(self.m_staticTextLanguage, 0, wx.ALL, 5) + + m_choiceLanguageChoices = ["xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"] + self.m_choiceLanguage = wx.Choice( + self.m_panelSpeech, + wx.ID_ANY, + wx.DefaultPosition, + wx.DefaultSize, + m_choiceLanguageChoices, + 0, + ) + self.m_choiceLanguage.SetSelection(0) + bSizerLanguage.Add(self.m_choiceLanguage, 0, wx.ALL, 5) + + bSizerSpeech.Add(bSizerLanguage, 1, wx.EXPAND, 5) + + bSizerDecimalSeparator = wx.BoxSizer(wx.HORIZONTAL) + + self.m_staticTextDecimalSeparator = wx.StaticText( + self.m_panelSpeech, + wx.ID_ANY, + # Translators: label for pull down to specify what character to use in numbers as the decimal separator + _("Decimal separator for numbers:"), + wx.DefaultPosition, + wx.DefaultSize, + 0, + ) + self.m_staticTextDecimalSeparator.Wrap(-1) + + bSizerDecimalSeparator.Add(self.m_staticTextDecimalSeparator, 0, wx.ALL, 5) + + # Translators: options for decimal separator. + m_choiceDecimalSeparatorChoices = [ + # Translators: options for decimal separator -- "Auto" = automatically pick the choice based on the language + _("Auto"), + # options for decimal separator -- use "." (and use ", " for block separators) + ("."), + # options for decimal separator -- use "," (and use ". " for block separators) + (","), + # Translators: options for decimal separator -- "Custom" = user sets it + # Currently there is no UI for how it is done yet, but eventually there will be a dialog that pops up to set it + _("Custom"), + ] + self.m_choiceDecimalSeparator = wx.Choice( + self.m_panelSpeech, + wx.ID_ANY, + wx.DefaultPosition, + wx.DefaultSize, + m_choiceDecimalSeparatorChoices, + 0, + ) + self.m_choiceDecimalSeparator.SetSelection(0) + bSizerDecimalSeparator.Add(self.m_choiceDecimalSeparator, 0, wx.ALL, 5) + + bSizerSpeech.Add(bSizerDecimalSeparator, 1, wx.EXPAND, 5) + + bSizerSpeechStyle = wx.BoxSizer(wx.HORIZONTAL) + + self.m_staticTextSpeechStyle = wx.StaticText( + self.m_panelSpeech, + wx.ID_ANY, + # Translators: label for pull down allowing users to choose the "style" (version, rules) of speech for math + _("Speech style:"), + wx.DefaultPosition, + wx.DefaultSize, + 0, + ) + self.m_staticTextSpeechStyle.Wrap(-1) + + bSizerSpeechStyle.Add(self.m_staticTextSpeechStyle, 0, wx.ALL, 5) + + m_choiceSpeechStyleChoices = ["xxxxxxxxxxxxxxxx"] + self.m_choiceSpeechStyle = wx.Choice( + self.m_panelSpeech, + wx.ID_ANY, + wx.DefaultPosition, + wx.DefaultSize, + m_choiceSpeechStyleChoices, + 0, + ) + self.m_choiceSpeechStyle.SetSelection(0) + bSizerSpeechStyle.Add(self.m_choiceSpeechStyle, 0, wx.ALL, 5) + + bSizerSpeech.Add(bSizerSpeechStyle, 1, wx.EXPAND, 5) + + bSizerSpeechAmount = wx.BoxSizer(wx.HORIZONTAL) + + self.m_staticTextSpeechAmount = wx.StaticText( + self.m_panelSpeech, + wx.ID_ANY, + # Translators: label for pull down to specify how verbose/terse the speech should be + _("Speech verbosity:"), + wx.DefaultPosition, + wx.DefaultSize, + 0, + ) + self.m_staticTextSpeechAmount.Wrap(-1) + + bSizerSpeechAmount.Add(self.m_staticTextSpeechAmount, 0, wx.ALL, 5) + + # Translators: options for speech verbosity. + m_choiceSpeechAmountChoices = [ + # Translators: options for speech verbosity -- "terse" = use less words + _("Terse"), + # Translators: options for speech verbosity -- "medium" = try to be nether too terse nor too verbose words + _("Medium"), + # Translators: options for speech verbosity -- "verbose" = use more words + _("Verbose"), + ] + self.m_choiceSpeechAmount = wx.Choice( + self.m_panelSpeech, + wx.ID_ANY, + wx.DefaultPosition, + wx.DefaultSize, + m_choiceSpeechAmountChoices, + 0, + ) + self.m_choiceSpeechAmount.SetSelection(0) + bSizerSpeechAmount.Add(self.m_choiceSpeechAmount, 0, wx.ALL, 5) + + bSizerSpeech.Add(bSizerSpeechAmount, 1, wx.EXPAND, 5) + + bSizerRelativeSpeed = wx.BoxSizer(wx.HORIZONTAL) + + self.m_staticTextRelativeSpeed = wx.StaticText( + self.m_panelSpeech, + wx.ID_ANY, + # Translators: label for slider that specifies a percentage of the normal speech rate that should be used for math + _("Relative speech rate:"), + wx.DefaultPosition, + wx.DefaultSize, + 0, + ) + self.m_staticTextRelativeSpeed.Wrap(-1) + + bSizerRelativeSpeed.Add(self.m_staticTextRelativeSpeed, 0, wx.ALL, 5) + + self.m_sliderRelativeSpeed = wx.Slider( + self.m_panelSpeech, + wx.ID_ANY, + 100, + 10, + 100, + wx.DefaultPosition, + wx.DefaultSize, + wx.SL_HORIZONTAL, + ) + self.m_sliderRelativeSpeed.SetLineSize(9) + bSizerRelativeSpeed.Add(self.m_sliderRelativeSpeed, 0, wx.ALL, 5) + + bSizerSpeech.Add(bSizerRelativeSpeed, 1, wx.EXPAND, 5) + + bSizerPauseFactor = wx.BoxSizer(wx.HORIZONTAL) + + self.m_staticPauseFactor = wx.StaticText( + self.m_panelSpeech, + wx.ID_ANY, + # Translators: label for slider that specifies relative factor to increase or decrease pauses in the math speech + _("Pause factor:"), + wx.DefaultPosition, + wx.DefaultSize, + 0, + ) + self.m_staticPauseFactor.Wrap(-1) + + bSizerPauseFactor.Add(self.m_staticPauseFactor, 0, wx.ALL, 5) + + self.m_sliderPauseFactor = wx.Slider( + self.m_panelSpeech, + wx.ID_ANY, + 7, + 0, + 14, + wx.DefaultPosition, + wx.DefaultSize, + wx.SL_HORIZONTAL, + ) + bSizerPauseFactor.Add(self.m_sliderPauseFactor, 0, wx.ALL, 5) + + bSizerSpeech.Add(bSizerPauseFactor, 1, wx.EXPAND, 5) + + bSizerSpeechSound = wx.BoxSizer(wx.HORIZONTAL) + + self.m_checkBoxSpeechSound = wx.CheckBox( + self.m_panelSpeech, + wx.ID_ANY, + # Translators: label for check box controling a beep sound + _("Make a sound when starting/ending math speech"), + wx.DefaultPosition, + wx.DefaultSize, + 0, + ) + bSizerSpeechSound.Add(self.m_checkBoxSpeechSound, 0, wx.ALL, 5) + + bSizerSpeech.Add(bSizerSpeechSound, 1, wx.EXPAND, 5) + + bSizerSubjectArea = wx.BoxSizer(wx.HORIZONTAL) + + self.m_staticTextSubjectArea = wx.StaticText( + self.m_panelSpeech, + wx.ID_ANY, + # Translators: label for pull down to specify a subject area (Geometry, Calculus, ...) + _("Subject area to be used when it cannot be determined automatically:"), + wx.DefaultPosition, + wx.DefaultSize, + 0, + ) + self.m_staticTextSubjectArea.Wrap(-1) + + bSizerSubjectArea.Add(self.m_staticTextSubjectArea, 0, wx.ALL, 5) + + # Translators: a generic (non-specific) math subject area + m_choiceSubjectAreaChoices = [_("General")] + self.m_choiceSubjectArea = wx.Choice( + self.m_panelSpeech, + wx.ID_ANY, + wx.DefaultPosition, + wx.DefaultSize, + m_choiceSubjectAreaChoices, + 0, + ) + self.m_choiceSubjectArea.SetSelection(0) + bSizerSubjectArea.Add(self.m_choiceSubjectArea, 0, wx.ALL, 5) + + bSizerSpeech.Add(bSizerSubjectArea, 1, wx.EXPAND, 5) + + bSizerSpeechForChemical = wx.BoxSizer(wx.HORIZONTAL) + + self.m_staticTextSpeechForChemical = wx.StaticText( + self.m_panelSpeech, + wx.ID_ANY, + # Translators: label for pull down to specify how verbose/terse the speech should be + _("Speech for chemical formulas:"), + wx.DefaultPosition, + wx.DefaultSize, + 0, + ) + self.m_staticTextSpeechForChemical.Wrap(-1) + + bSizerSpeechForChemical.Add(self.m_staticTextSpeechForChemical, 0, wx.ALL, 5) + + m_choiceSpeechForChemicalChoices = [ + # Translators: values for chemistry options with example speech in parenthesis + _("Spell it out (H 2 O)"), + # Translators: values for chemistry options with example speech in parenthesis (never interpret as chemistry) + _("Off (H sub 2 O)"), + ] + self.m_choiceSpeechForChemical = wx.Choice( + self.m_panelSpeech, + wx.ID_ANY, + wx.DefaultPosition, + wx.DefaultSize, + m_choiceSpeechForChemicalChoices, + 0, + ) + self.m_choiceSpeechForChemical.SetSelection(0) + bSizerSpeechForChemical.Add(self.m_choiceSpeechForChemical, 0, wx.ALL, 5) + + bSizerSpeech.Add(bSizerSpeechForChemical, 1, wx.EXPAND, 5) + + self.m_panelSpeech.SetSizer(bSizerSpeech) + self.m_panelSpeech.Layout() + bSizerSpeech.Fit(self.m_panelSpeech) + self.m_simplebookPanelsCategories.AddPage(self.m_panelSpeech, "a page", False) + self.m_panelNavigation = wx.Panel( + self.m_simplebookPanelsCategories, + wx.ID_ANY, + wx.DefaultPosition, + wx.DefaultSize, + wx.BORDER_SIMPLE | wx.TAB_TRAVERSAL, + ) + bSizerNavigation = wx.BoxSizer(wx.VERTICAL) + + sbSizerNavigationMode = wx.StaticBoxSizer( + wx.StaticBox( + self.m_panelNavigation, + wx.ID_ANY, + # Translators: label for pull down to specify one of three modes use to navigate math expressions + _("Navigation mode to use when beginning to navigate an equation:"), + ), + wx.VERTICAL, + ) + + m_choiceNavigationModeChoices = [ + # Translators: names of different modes of navigation. "Enhanced" mode understands math structure + _("Enhanced"), + # Translators: "Simple" walks by character expect for things like fractions, roots, and scripts + _("Simple"), + # Translators: "Character" moves around by character, automatically moving into fractions, etc + _("Character"), + ] + self.m_choiceNavigationMode = wx.Choice( + sbSizerNavigationMode.GetStaticBox(), + wx.ID_ANY, + wx.DefaultPosition, + wx.DefaultSize, + m_choiceNavigationModeChoices, + 0, + ) + self.m_choiceNavigationMode.SetSelection(1) + sbSizerNavigationMode.Add(self.m_choiceNavigationMode, 0, wx.ALL, 5) + + self.m_checkBoxResetNavigationMode = wx.CheckBox( + sbSizerNavigationMode.GetStaticBox(), + wx.ID_ANY, + # Translators: label for checkbox that controls whether any changes to the navigation mode should be preserved + _("Reset navigation mode on entry to an expression"), + wx.DefaultPosition, + wx.DefaultSize, + 0, + ) + sbSizerNavigationMode.Add(self.m_checkBoxResetNavigationMode, 0, wx.ALL, 5) + + bSizerNavigation.Add(sbSizerNavigationMode, 1, wx.EXPAND, 5) + + sbSizerNavigationSpeech = wx.StaticBoxSizer( + wx.StaticBox( + self.m_panelNavigation, + wx.ID_ANY, + # Translators: label for pull down to specify whether the expression is spoken or described (an overview) + _("Navigation speech to use when beginning to navigate an equation:"), + ), + wx.VERTICAL, + ) + + # Translators: either "Speak" the expression or give a description (overview) of the expression + m_choiceNavigationSpeechChoices = [ + # Translators: "Speak" the expression after moving to it + _("Speak"), + # Translators: "Describe" the expression after moving to it ("overview is a synonym") + _("Describe/overview"), + ] + self.m_choiceNavigationSpeech = wx.Choice( + sbSizerNavigationSpeech.GetStaticBox(), + wx.ID_ANY, + wx.DefaultPosition, + wx.DefaultSize, + m_choiceNavigationSpeechChoices, + 0, + ) + self.m_choiceNavigationSpeech.SetSelection(1) + sbSizerNavigationSpeech.Add(self.m_choiceNavigationSpeech, 0, wx.ALL, 5) + + self.m_checkBoxResetNavigationSpeech = wx.CheckBox( + sbSizerNavigationSpeech.GetStaticBox(), + wx.ID_ANY, + # Translators: label for checkbox that controls whether any changes to the speak vs overview reading should be ignored + _("Reset navigation speech on entry to an expression"), + wx.DefaultPosition, + wx.DefaultSize, + 0, + ) + self.m_checkBoxResetNavigationSpeech.SetValue(True) + sbSizerNavigationSpeech.Add(self.m_checkBoxResetNavigationSpeech, 0, wx.ALL, 5) + + bSizerNavigation.Add(sbSizerNavigationSpeech, 1, wx.EXPAND, 5) + + bSizerNavigationZoom = wx.BoxSizer(wx.VERTICAL) + + self.m_checkBoxAutomaticZoom = wx.CheckBox( + self.m_panelNavigation, + wx.ID_ANY, + # Translators: label for checkbox that controls whether arrow keys move out of fractions, etc., + # or whether you have to manually back out of the fraction, etc. + _("Automatic zoom out of 2D notations"), + wx.DefaultPosition, + wx.DefaultSize, + 0, + ) + bSizerNavigationZoom.Add(self.m_checkBoxAutomaticZoom, 0, wx.ALL, 5) + + bSizerSpeechAmountNavigation = wx.BoxSizer(wx.HORIZONTAL) + + self.m_staticTextSpeechAmountNavigation = wx.StaticText( + self.m_panelNavigation, + wx.ID_ANY, + # Translators: label for pull down to specify whether you want a terse or verbose reading of navigation commands + _("Speech amount for navigation:"), + wx.DefaultPosition, + wx.DefaultSize, + 0, + ) + self.m_staticTextSpeechAmountNavigation.Wrap(-1) + + bSizerSpeechAmountNavigation.Add(self.m_staticTextSpeechAmountNavigation, 0, wx.ALL, 5) + + # Translators: options for navigation verbosity. + m_choiceSpeechAmountNavigationChoices = [ + # Translators: options for navigation verbosity -- "terse" = use less words + _("Terse"), + # Translators: options for navigation verbosity -- "medium" = try to be nether too terse nor too verbose words + _("Medium"), + # Translators: options for navigation verbosity -- "verbose" = use more words + _("Verbose"), + ] + self.m_choiceSpeechAmountNavigation = wx.Choice( + self.m_panelNavigation, + wx.ID_ANY, + wx.DefaultPosition, + wx.DefaultSize, + m_choiceSpeechAmountNavigationChoices, + 0, + ) + self.m_choiceSpeechAmountNavigation.SetSelection(0) + bSizerSpeechAmountNavigation.Add(self.m_choiceSpeechAmountNavigation, 0, wx.ALL, 5) + + bSizerNavigationZoom.Add(bSizerSpeechAmountNavigation, 1, wx.EXPAND, 5) + + bSizerNavigation.Add(bSizerNavigationZoom, 1, wx.EXPAND, 5) + + bSizerCopyAs = wx.BoxSizer(wx.HORIZONTAL) + + self.m_staticTextCopyMathAs = wx.StaticText( + self.m_panelNavigation, + wx.ID_ANY, + # Translators: label for pull down to specify how math will be copied to the clipboard + _("Copy math as:"), + wx.DefaultPosition, + wx.DefaultSize, + 0, + ) + self.m_staticTextCopyMathAs.Wrap(-1) + + bSizerCopyAs.Add(self.m_staticTextCopyMathAs, 0, wx.ALL, 5) + + # Translators: options for copy math as. + m_choiceCopyAsChoices = [ + # Translators: options for Copy expression to clipboard as -- "MathML" + _("MathML"), + # Translators: options for Copy expression to clipboard as -- "LaTeX" + _("LaTeX"), + # Translators: options for Copy expression to clipboard as -- "ASCIIMath" + _("ASCIIMath"), + # Translators: options for Copy expression to clipboard as -- speech text + _("Speech"), + ] + self.m_choiceCopyAs = wx.Choice( + self.m_panelNavigation, + wx.ID_ANY, + wx.DefaultPosition, + wx.DefaultSize, + m_choiceCopyAsChoices, + 0, + ) + self.m_choiceCopyAs.SetSelection(0) + bSizerCopyAs.Add(self.m_choiceCopyAs, 0, wx.ALL, 5) + + bSizerNavigation.Add(bSizerCopyAs, 1, wx.EXPAND, 5) + + self.m_panelNavigation.SetSizer(bSizerNavigation) + self.m_panelNavigation.Layout() + bSizerNavigation.Fit(self.m_panelNavigation) + self.m_simplebookPanelsCategories.AddPage( + self.m_panelNavigation, + "a page", + False, + ) + self.m_panelBraille = wx.Panel( + self.m_simplebookPanelsCategories, + wx.ID_ANY, + wx.DefaultPosition, + wx.DefaultSize, + wx.BORDER_SIMPLE | wx.TAB_TRAVERSAL, + ) + bSizerBraille = wx.BoxSizer(wx.VERTICAL) + + bSizerBrailleMathCode = wx.BoxSizer(wx.HORIZONTAL) + + self.m_staticTextBrailleMathCode = wx.StaticText( + self.m_panelBraille, + wx.ID_ANY, + # Translators: label for pull down to specify which braille code to use + _("Braille math code for refreshable displays:"), + wx.DefaultPosition, + wx.DefaultSize, + 0, + ) + self.m_staticTextBrailleMathCode.Wrap(-1) + + bSizerBrailleMathCode.Add(self.m_staticTextBrailleMathCode, 0, wx.ALL, 5) + m_choiceBrailleMathCodeChoices = ["xxxxxxxxxxx"] + self.m_choiceBrailleMathCode = wx.Choice( + self.m_panelBraille, + wx.ID_ANY, + wx.DefaultPosition, + wx.DefaultSize, + m_choiceBrailleMathCodeChoices, + 0, + ) + self.m_choiceBrailleMathCode.SetSelection(1) + bSizerBrailleMathCode.Add(self.m_choiceBrailleMathCode, 0, wx.ALL, 5) + + bSizerBraille.Add(bSizerBrailleMathCode, 1, wx.EXPAND, 5) + + bSizerBrailleHighlights = wx.BoxSizer(wx.HORIZONTAL) + + self.m_staticTextBrailleHighlights = wx.StaticText( + self.m_panelBraille, + wx.ID_ANY, + # Translators: label for pull down to specify how braille dots should be modified when navigating/selecting subexprs + _("Highlight with dots 7 && 8 the current nav node:"), + wx.DefaultPosition, + wx.DefaultSize, + 0, + ) + self.m_staticTextBrailleHighlights.Wrap(-1) + + bSizerBrailleHighlights.Add(self.m_staticTextBrailleHighlights, 0, wx.ALL, 5) + + m_choiceBrailleHighlightsChoices = [ + # Translators: options for using dots 7 and 8: + # Translators: "off" -- don't highlight + _("Off"), + # Translators: "First character" -- only the first character of the current navigation node uses dots 7 & 8 + _("First character"), + # Translators: "Endpoints" -- only the first and last character of the current navigation node uses dots 7 & 8 + _("Endpoints"), + # Translators: "All" -- all the characters for the current navigation node use dots 7 & 8 + _("All"), + ] + self.m_choiceBrailleHighlights = wx.Choice( + self.m_panelBraille, + wx.ID_ANY, + wx.DefaultPosition, + wx.DefaultSize, + m_choiceBrailleHighlightsChoices, + 0, + ) + self.m_choiceBrailleHighlights.SetSelection(1) + bSizerBrailleHighlights.Add(self.m_choiceBrailleHighlights, 0, wx.ALL, 5) + + bSizerBraille.Add(bSizerBrailleHighlights, 1, wx.EXPAND, 5) + + bSizerBraille.Add((0, 0), 1, wx.EXPAND, 5) + + bSizerBraille.Add((0, 0), 1, wx.EXPAND, 5) + + bSizerBraille.Add((0, 0), 1, wx.EXPAND, 5) + + bSizerBraille.Add((0, 0), 1, wx.EXPAND, 5) + + bSizerBraille.Add((0, 0), 1, wx.EXPAND, 5) + + self.m_panelBraille.SetSizer(bSizerBraille) + self.m_panelBraille.Layout() + bSizerBraille.Fit(self.m_panelBraille) + self.m_simplebookPanelsCategories.AddPage(self.m_panelBraille, "a page", False) + + gbSizerMathCATPreferences.Add( + self.m_simplebookPanelsCategories, + wx.GBPosition(0, 1), + wx.GBSpan(1, 1), + wx.EXPAND | wx.ALL, + 10, + ) + + self.m_staticlineAboveButtons = wx.StaticLine( + self, + wx.ID_ANY, + wx.DefaultPosition, + wx.DefaultSize, + wx.LI_HORIZONTAL, + ) + gbSizerMathCATPreferences.Add( + self.m_staticlineAboveButtons, + wx.GBPosition(1, 0), + wx.GBSpan(1, 2), + wx.EXPAND | wx.ALL, + 5, + ) + + self.m_panelButtons = wx.Panel(self, wx.ID_ANY, wx.Point(-1, -1), wx.DefaultSize, 0) + bSizerButtons = wx.BoxSizer(wx.HORIZONTAL) + + bSizerButtons.Add((0, 0), 1, wx.EXPAND, 5) + + self.m_buttonOK = wx.Button( + self.m_panelButtons, + wx.ID_ANY, + # Translators: dialog "ok" button + _("OK"), + wx.DefaultPosition, + wx.DefaultSize, + 0, + ) + bSizerButtons.Add(self.m_buttonOK, 0, wx.ALL, 5) + + self.m_buttonCancel = wx.Button( + self.m_panelButtons, + wx.ID_ANY, + # Translators: dialog "cancel" button + _("Cancel"), + wx.DefaultPosition, + wx.DefaultSize, + 0, + ) + bSizerButtons.Add(self.m_buttonCancel, 0, wx.ALL, 5) + + self.m_buttonApply = wx.Button( + self.m_panelButtons, + wx.ID_ANY, + # Translators: dialog "apply" button + _("Apply"), + wx.DefaultPosition, + wx.DefaultSize, + 0, + ) + bSizerButtons.Add(self.m_buttonApply, 0, wx.ALL, 5) + + self.m_buttonReset = wx.Button( + self.m_panelButtons, + wx.ID_ANY, + # Translators: button to reset all the preferences to their default values + _("Reset to defaults"), + wx.DefaultPosition, + wx.DefaultSize, + 0, + ) + bSizerButtons.Add(self.m_buttonReset, 0, wx.ALL, 5) + + self.m_buttonHelp = wx.Button( + self.m_panelButtons, + wx.ID_ANY, + # Translators: button to bring up a help page + _("Help"), + wx.DefaultPosition, + wx.DefaultSize, + 0, + ) + bSizerButtons.Add(self.m_buttonHelp, 0, wx.ALL, 5) + + self.m_panelButtons.SetSizer(bSizerButtons) + self.m_panelButtons.Layout() + bSizerButtons.Fit(self.m_panelButtons) + gbSizerMathCATPreferences.Add( + self.m_panelButtons, + wx.GBPosition(2, 1), + wx.GBSpan(1, 2), + wx.EXPAND | wx.ALL, + 5, + ) + + self.SetSizer(gbSizerMathCATPreferences) + self.Layout() + gbSizerMathCATPreferences.Fit(self) + + self.Centre(wx.BOTH) + + # Connect Events + self.Bind(wx.EVT_CHAR_HOOK, self.MathCATPreferencesDialogOnCharHook) + self.Bind(wx.EVT_KEY_UP, self.MathCATPreferencesDialogOnKeyUp) + self.m_listBoxPreferencesTopic.Bind(wx.EVT_LISTBOX, self.OnListBoxCategories) + self.m_choiceLanguage.Bind(wx.EVT_CHOICE, self.OnLanguage) + self.m_sliderRelativeSpeed.Bind( + wx.EVT_SCROLL_CHANGED, + self.OnRelativeSpeedChanged, + ) + self.m_sliderPauseFactor.Bind(wx.EVT_SCROLL_CHANGED, self.OnPauseFactorChanged) + self.m_buttonOK.Bind(wx.EVT_BUTTON, self.OnClickOK) + self.m_buttonCancel.Bind(wx.EVT_BUTTON, self.OnClickCancel) + self.m_buttonApply.Bind(wx.EVT_BUTTON, self.OnClickApply) + self.m_buttonReset.Bind(wx.EVT_BUTTON, self.OnClickReset) + self.m_buttonHelp.Bind(wx.EVT_BUTTON, self.OnClickHelp) + + def __del__(self): + pass + + # Virtual event handlers, override them in your derived class + def MathCATPreferencesDialogOnCharHook(self, event): + event.Skip() + + def MathCATPreferencesDialogOnKeyUp(self, event): + event.Skip() + + def OnListBoxCategories(self, event): + event.Skip() + + def OnLanguage(self, event): + event.Skip() + + def OnRelativeSpeedChanged(self, event): + event.Skip() + + def OnPauseFactorChanged(self, event): + event.Skip() + + def OnClickOK(self, event): + event.Skip() + + def OnClickCancel(self, event): + event.Skip() + + def OnClickApply(self, event): + event.Skip() + + def OnClickReset(self, event): + event.Skip() + + def OnClickHelp(self, event): + event.Skip() diff --git a/addon/globalPlugins/MathCAT/__init__.py b/addon/globalPlugins/MathCAT/__init__.py index 6d1975eb..0c65cd50 100644 --- a/addon/globalPlugins/MathCAT/__init__.py +++ b/addon/globalPlugins/MathCAT/__init__.py @@ -10,14 +10,15 @@ # python3.dll has "Copyright © 2001-2022 Python Software Foundation; All Rights Reserved" -import globalPluginHandler # we are a global plugin +import globalPluginHandler # we are a global plugin import globalVars -import mathPres # math plugin stuff +import mathPres # math plugin stuff import wx import addonHandler from gui import mainFrame from .MathCAT import MathCAT from .MathCATPreferences import UserInterface + # Import the _ function for translation _ = wx.GetTranslation addonHandler.initTranslation() @@ -25,24 +26,24 @@ class GlobalPlugin(globalPluginHandler.GlobalPlugin): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - # MathCAT.__init__(self) - self.add_MathCAT_menu() - - def add_MathCAT_menu(self): - if not globalVars.appArgs.secure: - self.preferencesMenu = mainFrame.sysTrayIcon.preferencesMenu - # Translators: this show up in the NVDA preferences dialog. It opens the MathCAT preferences dialog - self.settings = self.preferencesMenu.Append(wx.ID_ANY, _("&MathCAT Settings...")) - mainFrame.sysTrayIcon.Bind(wx.EVT_MENU, self.on_settings, self.settings) - - def on_settings(self, evt): - mainFrame.popupSettingsDialog(UserInterface) - - def terminate(self): - try: - if not globalVars.appArgs.secure: - self.preferencesMenu.Remove(self.settings) - except (AttributeError, RuntimeError): - pass + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # MathCAT.__init__(self) + self.add_MathCAT_menu() + + def add_MathCAT_menu(self): + if not globalVars.appArgs.secure: + self.preferencesMenu = mainFrame.sysTrayIcon.preferencesMenu + # Translators: this show up in the NVDA preferences dialog. It opens the MathCAT preferences dialog + self.settings = self.preferencesMenu.Append(wx.ID_ANY, _("&MathCAT Settings...")) + mainFrame.sysTrayIcon.Bind(wx.EVT_MENU, self.on_settings, self.settings) + + def on_settings(self, evt): + mainFrame.popupSettingsDialog(UserInterface) + + def terminate(self): + try: + if not globalVars.appArgs.secure: + self.preferencesMenu.Remove(self.settings) + except (AttributeError, RuntimeError): + pass diff --git a/addon/globalPlugins/MathCAT/yaml/__init__.py b/addon/globalPlugins/MathCAT/yaml/__init__.py index 2ec4f203..87271d0a 100644 --- a/addon/globalPlugins/MathCAT/yaml/__init__.py +++ b/addon/globalPlugins/MathCAT/yaml/__init__.py @@ -154,9 +154,11 @@ def unsafe_load_all(stream): """ return load_all(stream, UnsafeLoader) -def emit(events, stream=None, Dumper=Dumper, - canonical=None, indent=None, width=None, - allow_unicode=None, line_break=None): +def emit( + events, stream=None, Dumper=Dumper, + canonical=None, indent=None, width=None, + allow_unicode=None, line_break=None, +): """ Emit YAML parsing events into a stream. If stream is None, return the produced string instead. @@ -165,8 +167,10 @@ def emit(events, stream=None, Dumper=Dumper, if stream is None: stream = io.StringIO() getvalue = stream.getvalue - dumper = Dumper(stream, canonical=canonical, indent=indent, width=width, - allow_unicode=allow_unicode, line_break=line_break) + dumper = Dumper( + stream, canonical=canonical, indent=indent, width=width, + allow_unicode=allow_unicode, line_break=line_break, + ) try: for event in events: dumper.emit(event) @@ -175,11 +179,13 @@ def emit(events, stream=None, Dumper=Dumper, if getvalue: return getvalue() -def serialize_all(nodes, stream=None, Dumper=Dumper, - canonical=None, indent=None, width=None, - allow_unicode=None, line_break=None, - encoding=None, explicit_start=None, explicit_end=None, - version=None, tags=None): +def serialize_all( + nodes, stream=None, Dumper=Dumper, + canonical=None, indent=None, width=None, + allow_unicode=None, line_break=None, + encoding=None, explicit_start=None, explicit_end=None, + version=None, tags=None, +): """ Serialize a sequence of representation trees into a YAML stream. If stream is None, return the produced string instead. @@ -191,10 +197,12 @@ def serialize_all(nodes, stream=None, Dumper=Dumper, else: stream = io.BytesIO() getvalue = stream.getvalue - dumper = Dumper(stream, canonical=canonical, indent=indent, width=width, - allow_unicode=allow_unicode, line_break=line_break, - encoding=encoding, version=version, tags=tags, - explicit_start=explicit_start, explicit_end=explicit_end) + dumper = Dumper( + stream, canonical=canonical, indent=indent, width=width, + allow_unicode=allow_unicode, line_break=line_break, + encoding=encoding, version=version, tags=tags, + explicit_start=explicit_start, explicit_end=explicit_end, + ) try: dumper.open() for node in nodes: @@ -212,12 +220,14 @@ def serialize(node, stream=None, Dumper=Dumper, **kwds): """ return serialize_all([node], stream, Dumper=Dumper, **kwds) -def dump_all(documents, stream=None, Dumper=Dumper, - default_style=None, default_flow_style=False, - canonical=None, indent=None, width=None, - allow_unicode=None, line_break=None, - encoding=None, explicit_start=None, explicit_end=None, - version=None, tags=None, sort_keys=True): +def dump_all( + documents, stream=None, Dumper=Dumper, + default_style=None, default_flow_style=False, + canonical=None, indent=None, width=None, + allow_unicode=None, line_break=None, + encoding=None, explicit_start=None, explicit_end=None, + version=None, tags=None, sort_keys=True, +): """ Serialize a sequence of Python objects into a YAML stream. If stream is None, return the produced string instead. @@ -229,12 +239,14 @@ def dump_all(documents, stream=None, Dumper=Dumper, else: stream = io.BytesIO() getvalue = stream.getvalue - dumper = Dumper(stream, default_style=default_style, - default_flow_style=default_flow_style, - canonical=canonical, indent=indent, width=width, - allow_unicode=allow_unicode, line_break=line_break, - encoding=encoding, version=version, tags=tags, - explicit_start=explicit_start, explicit_end=explicit_end, sort_keys=sort_keys) + dumper = Dumper( + stream, default_style=default_style, + default_flow_style=default_flow_style, + canonical=canonical, indent=indent, width=width, + allow_unicode=allow_unicode, line_break=line_break, + encoding=encoding, version=version, tags=tags, + explicit_start=explicit_start, explicit_end=explicit_end, sort_keys=sort_keys, + ) try: dumper.open() for data in documents: @@ -268,8 +280,10 @@ def safe_dump(data, stream=None, **kwds): """ return dump_all([data], stream, Dumper=SafeDumper, **kwds) -def add_implicit_resolver(tag, regexp, first=None, - Loader=None, Dumper=Dumper): +def add_implicit_resolver( + tag, regexp, first=None, + Loader=None, Dumper=Dumper, +): """ Add an implicit scalar detector. If an implicit scalar value matches the given regexp, @@ -385,6 +399,7 @@ def to_yaml(cls, dumper, data): """ Convert a Python object to a representation node. """ - return dumper.represent_yaml_object(cls.yaml_tag, data, cls, - flow_style=cls.yaml_flow_style) - + return dumper.represent_yaml_object( + cls.yaml_tag, data, cls, + flow_style=cls.yaml_flow_style, + ) diff --git a/addon/globalPlugins/MathCAT/yaml/composer.py b/addon/globalPlugins/MathCAT/yaml/composer.py index 6d15cb40..1f389f41 100644 --- a/addon/globalPlugins/MathCAT/yaml/composer.py +++ b/addon/globalPlugins/MathCAT/yaml/composer.py @@ -38,9 +38,11 @@ def get_single_node(self): # Ensure that the stream contains no more documents. if not self.check_event(StreamEndEvent): event = self.get_event() - raise ComposerError("expected a single document in the stream", - document.start_mark, "but found another document", - event.start_mark) + raise ComposerError( + "expected a single document in the stream", + document.start_mark, "but found another document", + event.start_mark, + ) # Drop the STREAM-END event. self.get_event() @@ -65,16 +67,20 @@ def compose_node(self, parent, index): event = self.get_event() anchor = event.anchor if anchor not in self.anchors: - raise ComposerError(None, None, "found undefined alias %r" - % anchor, event.start_mark) + raise ComposerError( + None, None, "found undefined alias %r" + % anchor, event.start_mark, + ) return self.anchors[anchor] event = self.peek_event() anchor = event.anchor if anchor is not None: if anchor in self.anchors: - raise ComposerError("found duplicate anchor %r; first occurrence" - % anchor, self.anchors[anchor].start_mark, - "second occurrence", event.start_mark) + raise ComposerError( + "found duplicate anchor %r; first occurrence" + % anchor, self.anchors[anchor].start_mark, + "second occurrence", event.start_mark, + ) self.descend_resolver(parent, index) if self.check_event(ScalarEvent): node = self.compose_scalar_node(anchor) @@ -90,8 +96,10 @@ def compose_scalar_node(self, anchor): tag = event.tag if tag is None or tag == '!': tag = self.resolve(ScalarNode, event.value, event.implicit) - node = ScalarNode(tag, event.value, - event.start_mark, event.end_mark, style=event.style) + node = ScalarNode( + tag, event.value, + event.start_mark, event.end_mark, style=event.style, + ) if anchor is not None: self.anchors[anchor] = node return node @@ -101,9 +109,11 @@ def compose_sequence_node(self, anchor): tag = start_event.tag if tag is None or tag == '!': tag = self.resolve(SequenceNode, None, start_event.implicit) - node = SequenceNode(tag, [], - start_event.start_mark, None, - flow_style=start_event.flow_style) + node = SequenceNode( + tag, [], + start_event.start_mark, None, + flow_style=start_event.flow_style, + ) if anchor is not None: self.anchors[anchor] = node index = 0 @@ -119,9 +129,11 @@ def compose_mapping_node(self, anchor): tag = start_event.tag if tag is None or tag == '!': tag = self.resolve(MappingNode, None, start_event.implicit) - node = MappingNode(tag, [], - start_event.start_mark, None, - flow_style=start_event.flow_style) + node = MappingNode( + tag, [], + start_event.start_mark, None, + flow_style=start_event.flow_style, + ) if anchor is not None: self.anchors[anchor] = node while not self.check_event(MappingEndEvent): @@ -136,4 +148,3 @@ def compose_mapping_node(self, anchor): end_event = self.get_event() node.end_mark = end_event.end_mark return node - diff --git a/addon/globalPlugins/MathCAT/yaml/constructor.py b/addon/globalPlugins/MathCAT/yaml/constructor.py index 619acd30..5bb98137 100644 --- a/addon/globalPlugins/MathCAT/yaml/constructor.py +++ b/addon/globalPlugins/MathCAT/yaml/constructor.py @@ -5,7 +5,7 @@ 'FullConstructor', 'UnsafeConstructor', 'Constructor', - 'ConstructorError' + 'ConstructorError', ] from .error import * @@ -36,8 +36,10 @@ def check_state_key(self, key): object, to prevent user-controlled methods from being called during deserialization""" if self.get_state_keys_blacklist_regexp().match(key): - raise ConstructorError(None, None, - "blacklisted key '%s' in instance state found" % (key,), None) + raise ConstructorError( + None, None, + "blacklisted key '%s' in instance state found" % (key,), None, + ) def get_data(self): # Construct and return the next document. @@ -71,8 +73,10 @@ def construct_object(self, node, deep=False): old_deep = self.deep_construct self.deep_construct = True if node in self.recursive_objects: - raise ConstructorError(None, None, - "found unconstructable recursive node", node.start_mark) + raise ConstructorError( + None, None, + "found unconstructable recursive node", node.start_mark, + ) self.recursive_objects[node] = None constructor = None tag_suffix = None @@ -116,39 +120,51 @@ def construct_object(self, node, deep=False): def construct_scalar(self, node): if not isinstance(node, ScalarNode): - raise ConstructorError(None, None, - "expected a scalar node, but found %s" % node.id, - node.start_mark) + raise ConstructorError( + None, None, + "expected a scalar node, but found %s" % node.id, + node.start_mark, + ) return node.value def construct_sequence(self, node, deep=False): if not isinstance(node, SequenceNode): - raise ConstructorError(None, None, - "expected a sequence node, but found %s" % node.id, - node.start_mark) - return [self.construct_object(child, deep=deep) - for child in node.value] + raise ConstructorError( + None, None, + "expected a sequence node, but found %s" % node.id, + node.start_mark, + ) + return [ + self.construct_object(child, deep=deep) + for child in node.value + ] def construct_mapping(self, node, deep=False): if not isinstance(node, MappingNode): - raise ConstructorError(None, None, - "expected a mapping node, but found %s" % node.id, - node.start_mark) + raise ConstructorError( + None, None, + "expected a mapping node, but found %s" % node.id, + node.start_mark, + ) mapping = {} for key_node, value_node in node.value: key = self.construct_object(key_node, deep=deep) if not isinstance(key, collections.abc.Hashable): - raise ConstructorError("while constructing a mapping", node.start_mark, - "found unhashable key", key_node.start_mark) + raise ConstructorError( + "while constructing a mapping", node.start_mark, + "found unhashable key", key_node.start_mark, + ) value = self.construct_object(value_node, deep=deep) mapping[key] = value return mapping def construct_pairs(self, node, deep=False): if not isinstance(node, MappingNode): - raise ConstructorError(None, None, - "expected a mapping node, but found %s" % node.id, - node.start_mark) + raise ConstructorError( + None, None, + "expected a mapping node, but found %s" % node.id, + node.start_mark, + ) pairs = [] for key_node, value_node in node.value: key = self.construct_object(key_node, deep=deep) @@ -191,19 +207,23 @@ def flatten_mapping(self, node): submerge = [] for subnode in value_node.value: if not isinstance(subnode, MappingNode): - raise ConstructorError("while constructing a mapping", - node.start_mark, - "expected a mapping for merging, but found %s" - % subnode.id, subnode.start_mark) + raise ConstructorError( + "while constructing a mapping", + node.start_mark, + "expected a mapping for merging, but found %s" + % subnode.id, subnode.start_mark, + ) self.flatten_mapping(subnode) submerge.append(subnode.value) submerge.reverse() for value in submerge: merge.extend(value) else: - raise ConstructorError("while constructing a mapping", node.start_mark, - "expected a mapping or list of mappings for merging, but found %s" - % value_node.id, value_node.start_mark) + raise ConstructorError( + "while constructing a mapping", node.start_mark, + "expected a mapping or list of mappings for merging, but found %s" + % value_node.id, value_node.start_mark, + ) elif key_node.tag == 'tag:yaml.org,2002:value': key_node.tag = 'tag:yaml.org,2002:str' index += 1 @@ -295,17 +315,21 @@ def construct_yaml_binary(self, node): try: value = self.construct_scalar(node).encode('ascii') except UnicodeEncodeError as exc: - raise ConstructorError(None, None, - "failed to convert base64 data into ascii: %s" % exc, - node.start_mark) + raise ConstructorError( + None, None, + "failed to convert base64 data into ascii: %s" % exc, + node.start_mark, + ) try: if hasattr(base64, 'decodebytes'): return base64.decodebytes(value) else: return base64.decodestring(value) except binascii.Error as exc: - raise ConstructorError(None, None, - "failed to decode base64 data: %s" % exc, node.start_mark) + raise ConstructorError( + None, None, + "failed to decode base64 data: %s" % exc, node.start_mark, + ) timestamp_regexp = re.compile( r'''^(?P[0-9][0-9][0-9][0-9]) @@ -317,7 +341,8 @@ def construct_yaml_binary(self, node): :(?P[0-9][0-9]) (?:\.(?P[0-9]*))? (?:[ \t]*(?PZ|(?P[-+])(?P[0-9][0-9]?) - (?::(?P[0-9][0-9]))?))?)?$''', re.X) + (?::(?P[0-9][0-9]))?))?)?$''', re.X, + ) def construct_yaml_timestamp(self, node): value = self.construct_scalar(node) @@ -347,8 +372,10 @@ def construct_yaml_timestamp(self, node): tzinfo = datetime.timezone(delta) elif values['tz']: tzinfo = datetime.timezone.utc - return datetime.datetime(year, month, day, hour, minute, second, fraction, - tzinfo=tzinfo) + return datetime.datetime( + year, month, day, hour, minute, second, fraction, + tzinfo=tzinfo, + ) def construct_yaml_omap(self, node): # Note: we do not check for duplicate keys, because it's too @@ -356,17 +383,23 @@ def construct_yaml_omap(self, node): omap = [] yield omap if not isinstance(node, SequenceNode): - raise ConstructorError("while constructing an ordered map", node.start_mark, - "expected a sequence, but found %s" % node.id, node.start_mark) + raise ConstructorError( + "while constructing an ordered map", node.start_mark, + "expected a sequence, but found %s" % node.id, node.start_mark, + ) for subnode in node.value: if not isinstance(subnode, MappingNode): - raise ConstructorError("while constructing an ordered map", node.start_mark, - "expected a mapping of length 1, but found %s" % subnode.id, - subnode.start_mark) + raise ConstructorError( + "while constructing an ordered map", node.start_mark, + "expected a mapping of length 1, but found %s" % subnode.id, + subnode.start_mark, + ) if len(subnode.value) != 1: - raise ConstructorError("while constructing an ordered map", node.start_mark, - "expected a single mapping item, but found %d items" % len(subnode.value), - subnode.start_mark) + raise ConstructorError( + "while constructing an ordered map", node.start_mark, + "expected a single mapping item, but found %d items" % len(subnode.value), + subnode.start_mark, + ) key_node, value_node = subnode.value[0] key = self.construct_object(key_node) value = self.construct_object(value_node) @@ -377,17 +410,23 @@ def construct_yaml_pairs(self, node): pairs = [] yield pairs if not isinstance(node, SequenceNode): - raise ConstructorError("while constructing pairs", node.start_mark, - "expected a sequence, but found %s" % node.id, node.start_mark) + raise ConstructorError( + "while constructing pairs", node.start_mark, + "expected a sequence, but found %s" % node.id, node.start_mark, + ) for subnode in node.value: if not isinstance(subnode, MappingNode): - raise ConstructorError("while constructing pairs", node.start_mark, - "expected a mapping of length 1, but found %s" % subnode.id, - subnode.start_mark) + raise ConstructorError( + "while constructing pairs", node.start_mark, + "expected a mapping of length 1, but found %s" % subnode.id, + subnode.start_mark, + ) if len(subnode.value) != 1: - raise ConstructorError("while constructing pairs", node.start_mark, - "expected a single mapping item, but found %d items" % len(subnode.value), - subnode.start_mark) + raise ConstructorError( + "while constructing pairs", node.start_mark, + "expected a single mapping item, but found %d items" % len(subnode.value), + subnode.start_mark, + ) key_node, value_node = subnode.value[0] key = self.construct_object(key_node) value = self.construct_object(value_node) @@ -424,60 +463,76 @@ def construct_yaml_object(self, node, cls): data.__dict__.update(state) def construct_undefined(self, node): - raise ConstructorError(None, None, - "could not determine a constructor for the tag %r" % node.tag, - node.start_mark) + raise ConstructorError( + None, None, + "could not determine a constructor for the tag %r" % node.tag, + node.start_mark, + ) SafeConstructor.add_constructor( 'tag:yaml.org,2002:null', - SafeConstructor.construct_yaml_null) + SafeConstructor.construct_yaml_null, +) SafeConstructor.add_constructor( 'tag:yaml.org,2002:bool', - SafeConstructor.construct_yaml_bool) + SafeConstructor.construct_yaml_bool, +) SafeConstructor.add_constructor( 'tag:yaml.org,2002:int', - SafeConstructor.construct_yaml_int) + SafeConstructor.construct_yaml_int, +) SafeConstructor.add_constructor( 'tag:yaml.org,2002:float', - SafeConstructor.construct_yaml_float) + SafeConstructor.construct_yaml_float, +) SafeConstructor.add_constructor( 'tag:yaml.org,2002:binary', - SafeConstructor.construct_yaml_binary) + SafeConstructor.construct_yaml_binary, +) SafeConstructor.add_constructor( 'tag:yaml.org,2002:timestamp', - SafeConstructor.construct_yaml_timestamp) + SafeConstructor.construct_yaml_timestamp, +) SafeConstructor.add_constructor( 'tag:yaml.org,2002:omap', - SafeConstructor.construct_yaml_omap) + SafeConstructor.construct_yaml_omap, +) SafeConstructor.add_constructor( 'tag:yaml.org,2002:pairs', - SafeConstructor.construct_yaml_pairs) + SafeConstructor.construct_yaml_pairs, +) SafeConstructor.add_constructor( 'tag:yaml.org,2002:set', - SafeConstructor.construct_yaml_set) + SafeConstructor.construct_yaml_set, +) SafeConstructor.add_constructor( 'tag:yaml.org,2002:str', - SafeConstructor.construct_yaml_str) + SafeConstructor.construct_yaml_str, +) SafeConstructor.add_constructor( 'tag:yaml.org,2002:seq', - SafeConstructor.construct_yaml_seq) + SafeConstructor.construct_yaml_seq, +) SafeConstructor.add_constructor( 'tag:yaml.org,2002:map', - SafeConstructor.construct_yaml_map) + SafeConstructor.construct_yaml_map, +) -SafeConstructor.add_constructor(None, - SafeConstructor.construct_undefined) +SafeConstructor.add_constructor( + None, + SafeConstructor.construct_undefined, +) class FullConstructor(SafeConstructor): # 'extend' is blacklisted because it is used by @@ -501,17 +556,21 @@ def construct_python_bytes(self, node): try: value = self.construct_scalar(node).encode('ascii') except UnicodeEncodeError as exc: - raise ConstructorError(None, None, - "failed to convert base64 data into ascii: %s" % exc, - node.start_mark) + raise ConstructorError( + None, None, + "failed to convert base64 data into ascii: %s" % exc, + node.start_mark, + ) try: if hasattr(base64, 'decodebytes'): return base64.decodebytes(value) else: return base64.decodestring(value) except binascii.Error as exc: - raise ConstructorError(None, None, - "failed to decode base64 data: %s" % exc, node.start_mark) + raise ConstructorError( + None, None, + "failed to decode base64 data: %s" % exc, node.start_mark, + ) def construct_python_long(self, node): return self.construct_yaml_int(node) @@ -524,23 +583,31 @@ def construct_python_tuple(self, node): def find_python_module(self, name, mark, unsafe=False): if not name: - raise ConstructorError("while constructing a Python module", mark, - "expected non-empty name appended to the tag", mark) + raise ConstructorError( + "while constructing a Python module", mark, + "expected non-empty name appended to the tag", mark, + ) if unsafe: try: __import__(name) except ImportError as exc: - raise ConstructorError("while constructing a Python module", mark, - "cannot find module %r (%s)" % (name, exc), mark) + raise ConstructorError( + "while constructing a Python module", mark, + "cannot find module %r (%s)" % (name, exc), mark, + ) if name not in sys.modules: - raise ConstructorError("while constructing a Python module", mark, - "module %r is not imported" % name, mark) + raise ConstructorError( + "while constructing a Python module", mark, + "module %r is not imported" % name, mark, + ) return sys.modules[name] def find_python_name(self, name, mark, unsafe=False): if not name: - raise ConstructorError("while constructing a Python object", mark, - "expected non-empty name appended to the tag", mark) + raise ConstructorError( + "while constructing a Python object", mark, + "expected non-empty name appended to the tag", mark, + ) if '.' in name: module_name, object_name = name.rsplit('.', 1) else: @@ -550,43 +617,57 @@ def find_python_name(self, name, mark, unsafe=False): try: __import__(module_name) except ImportError as exc: - raise ConstructorError("while constructing a Python object", mark, - "cannot find module %r (%s)" % (module_name, exc), mark) + raise ConstructorError( + "while constructing a Python object", mark, + "cannot find module %r (%s)" % (module_name, exc), mark, + ) if module_name not in sys.modules: - raise ConstructorError("while constructing a Python object", mark, - "module %r is not imported" % module_name, mark) + raise ConstructorError( + "while constructing a Python object", mark, + "module %r is not imported" % module_name, mark, + ) module = sys.modules[module_name] if not hasattr(module, object_name): - raise ConstructorError("while constructing a Python object", mark, - "cannot find %r in the module %r" - % (object_name, module.__name__), mark) + raise ConstructorError( + "while constructing a Python object", mark, + "cannot find %r in the module %r" + % (object_name, module.__name__), mark, + ) return getattr(module, object_name) def construct_python_name(self, suffix, node): value = self.construct_scalar(node) if value: - raise ConstructorError("while constructing a Python name", node.start_mark, - "expected the empty value, but found %r" % value, node.start_mark) + raise ConstructorError( + "while constructing a Python name", node.start_mark, + "expected the empty value, but found %r" % value, node.start_mark, + ) return self.find_python_name(suffix, node.start_mark) def construct_python_module(self, suffix, node): value = self.construct_scalar(node) if value: - raise ConstructorError("while constructing a Python module", node.start_mark, - "expected the empty value, but found %r" % value, node.start_mark) + raise ConstructorError( + "while constructing a Python module", node.start_mark, + "expected the empty value, but found %r" % value, node.start_mark, + ) return self.find_python_module(suffix, node.start_mark) - def make_python_instance(self, suffix, node, - args=None, kwds=None, newobj=False, unsafe=False): + def make_python_instance( + self, suffix, node, + args=None, kwds=None, newobj=False, unsafe=False, + ): if not args: args = [] if not kwds: kwds = {} cls = self.find_python_name(suffix, node.start_mark) if not (unsafe or isinstance(cls, type)): - raise ConstructorError("while constructing a Python instance", node.start_mark, - "expected a class, but found %r" % type(cls), - node.start_mark) + raise ConstructorError( + "while constructing a Python instance", node.start_mark, + "expected a class, but found %r" % type(cls), + node.start_mark, + ) if newobj and isinstance(cls, type): return cls.__new__(cls, *args, **kwds) else: @@ -660,55 +741,68 @@ def construct_python_object_new(self, suffix, node): FullConstructor.add_constructor( 'tag:yaml.org,2002:python/none', - FullConstructor.construct_yaml_null) + FullConstructor.construct_yaml_null, +) FullConstructor.add_constructor( 'tag:yaml.org,2002:python/bool', - FullConstructor.construct_yaml_bool) + FullConstructor.construct_yaml_bool, +) FullConstructor.add_constructor( 'tag:yaml.org,2002:python/str', - FullConstructor.construct_python_str) + FullConstructor.construct_python_str, +) FullConstructor.add_constructor( 'tag:yaml.org,2002:python/unicode', - FullConstructor.construct_python_unicode) + FullConstructor.construct_python_unicode, +) FullConstructor.add_constructor( 'tag:yaml.org,2002:python/bytes', - FullConstructor.construct_python_bytes) + FullConstructor.construct_python_bytes, +) FullConstructor.add_constructor( 'tag:yaml.org,2002:python/int', - FullConstructor.construct_yaml_int) + FullConstructor.construct_yaml_int, +) FullConstructor.add_constructor( 'tag:yaml.org,2002:python/long', - FullConstructor.construct_python_long) + FullConstructor.construct_python_long, +) FullConstructor.add_constructor( 'tag:yaml.org,2002:python/float', - FullConstructor.construct_yaml_float) + FullConstructor.construct_yaml_float, +) FullConstructor.add_constructor( 'tag:yaml.org,2002:python/complex', - FullConstructor.construct_python_complex) + FullConstructor.construct_python_complex, +) FullConstructor.add_constructor( 'tag:yaml.org,2002:python/list', - FullConstructor.construct_yaml_seq) + FullConstructor.construct_yaml_seq, +) FullConstructor.add_constructor( 'tag:yaml.org,2002:python/tuple', - FullConstructor.construct_python_tuple) + FullConstructor.construct_python_tuple, +) FullConstructor.add_constructor( 'tag:yaml.org,2002:python/dict', - FullConstructor.construct_yaml_map) + FullConstructor.construct_yaml_map, +) FullConstructor.add_multi_constructor( 'tag:yaml.org,2002:python/name:', - FullConstructor.construct_python_name) + FullConstructor.construct_python_name, +) class UnsafeConstructor(FullConstructor): @@ -720,27 +814,33 @@ def find_python_name(self, name, mark): def make_python_instance(self, suffix, node, args=None, kwds=None, newobj=False): return super(UnsafeConstructor, self).make_python_instance( - suffix, node, args, kwds, newobj, unsafe=True) + suffix, node, args, kwds, newobj, unsafe=True, + ) def set_python_instance_state(self, instance, state): return super(UnsafeConstructor, self).set_python_instance_state( - instance, state, unsafe=True) + instance, state, unsafe=True, + ) UnsafeConstructor.add_multi_constructor( 'tag:yaml.org,2002:python/module:', - UnsafeConstructor.construct_python_module) + UnsafeConstructor.construct_python_module, +) UnsafeConstructor.add_multi_constructor( 'tag:yaml.org,2002:python/object:', - UnsafeConstructor.construct_python_object) + UnsafeConstructor.construct_python_object, +) UnsafeConstructor.add_multi_constructor( 'tag:yaml.org,2002:python/object/new:', - UnsafeConstructor.construct_python_object_new) + UnsafeConstructor.construct_python_object_new, +) UnsafeConstructor.add_multi_constructor( 'tag:yaml.org,2002:python/object/apply:', - UnsafeConstructor.construct_python_object_apply) + UnsafeConstructor.construct_python_object_apply, +) # Constructor is same as UnsafeConstructor. Need to leave this in place in case # people have extended it directly. diff --git a/addon/globalPlugins/MathCAT/yaml/cyaml.py b/addon/globalPlugins/MathCAT/yaml/cyaml.py index 0c213458..946d60cc 100644 --- a/addon/globalPlugins/MathCAT/yaml/cyaml.py +++ b/addon/globalPlugins/MathCAT/yaml/cyaml.py @@ -1,7 +1,7 @@ __all__ = [ 'CBaseLoader', 'CSafeLoader', 'CFullLoader', 'CUnsafeLoader', 'CLoader', - 'CBaseDumper', 'CSafeDumper', 'CDumper' + 'CBaseDumper', 'CSafeDumper', 'CDumper', ] from yaml._yaml import CParser, CEmitter @@ -50,52 +50,69 @@ def __init__(self, stream): class CBaseDumper(CEmitter, BaseRepresenter, BaseResolver): - def __init__(self, stream, - default_style=None, default_flow_style=False, - canonical=None, indent=None, width=None, - allow_unicode=None, line_break=None, - encoding=None, explicit_start=None, explicit_end=None, - version=None, tags=None, sort_keys=True): - CEmitter.__init__(self, stream, canonical=canonical, - indent=indent, width=width, encoding=encoding, - allow_unicode=allow_unicode, line_break=line_break, - explicit_start=explicit_start, explicit_end=explicit_end, - version=version, tags=tags) - Representer.__init__(self, default_style=default_style, - default_flow_style=default_flow_style, sort_keys=sort_keys) + def __init__( + self, stream, + default_style=None, default_flow_style=False, + canonical=None, indent=None, width=None, + allow_unicode=None, line_break=None, + encoding=None, explicit_start=None, explicit_end=None, + version=None, tags=None, sort_keys=True, + ): + CEmitter.__init__( + self, stream, canonical=canonical, + indent=indent, width=width, encoding=encoding, + allow_unicode=allow_unicode, line_break=line_break, + explicit_start=explicit_start, explicit_end=explicit_end, + version=version, tags=tags, + ) + Representer.__init__( + self, default_style=default_style, + default_flow_style=default_flow_style, sort_keys=sort_keys, + ) Resolver.__init__(self) class CSafeDumper(CEmitter, SafeRepresenter, Resolver): - def __init__(self, stream, - default_style=None, default_flow_style=False, - canonical=None, indent=None, width=None, - allow_unicode=None, line_break=None, - encoding=None, explicit_start=None, explicit_end=None, - version=None, tags=None, sort_keys=True): - CEmitter.__init__(self, stream, canonical=canonical, - indent=indent, width=width, encoding=encoding, - allow_unicode=allow_unicode, line_break=line_break, - explicit_start=explicit_start, explicit_end=explicit_end, - version=version, tags=tags) - SafeRepresenter.__init__(self, default_style=default_style, - default_flow_style=default_flow_style, sort_keys=sort_keys) + def __init__( + self, stream, + default_style=None, default_flow_style=False, + canonical=None, indent=None, width=None, + allow_unicode=None, line_break=None, + encoding=None, explicit_start=None, explicit_end=None, + version=None, tags=None, sort_keys=True, + ): + CEmitter.__init__( + self, stream, canonical=canonical, + indent=indent, width=width, encoding=encoding, + allow_unicode=allow_unicode, line_break=line_break, + explicit_start=explicit_start, explicit_end=explicit_end, + version=version, tags=tags, + ) + SafeRepresenter.__init__( + self, default_style=default_style, + default_flow_style=default_flow_style, sort_keys=sort_keys, + ) Resolver.__init__(self) class CDumper(CEmitter, Serializer, Representer, Resolver): - def __init__(self, stream, - default_style=None, default_flow_style=False, - canonical=None, indent=None, width=None, - allow_unicode=None, line_break=None, - encoding=None, explicit_start=None, explicit_end=None, - version=None, tags=None, sort_keys=True): - CEmitter.__init__(self, stream, canonical=canonical, - indent=indent, width=width, encoding=encoding, - allow_unicode=allow_unicode, line_break=line_break, - explicit_start=explicit_start, explicit_end=explicit_end, - version=version, tags=tags) - Representer.__init__(self, default_style=default_style, - default_flow_style=default_flow_style, sort_keys=sort_keys) + def __init__( + self, stream, + default_style=None, default_flow_style=False, + canonical=None, indent=None, width=None, + allow_unicode=None, line_break=None, + encoding=None, explicit_start=None, explicit_end=None, + version=None, tags=None, sort_keys=True, + ): + CEmitter.__init__( + self, stream, canonical=canonical, + indent=indent, width=width, encoding=encoding, + allow_unicode=allow_unicode, line_break=line_break, + explicit_start=explicit_start, explicit_end=explicit_end, + version=version, tags=tags, + ) + Representer.__init__( + self, default_style=default_style, + default_flow_style=default_flow_style, sort_keys=sort_keys, + ) Resolver.__init__(self) - diff --git a/addon/globalPlugins/MathCAT/yaml/dumper.py b/addon/globalPlugins/MathCAT/yaml/dumper.py index 6aadba55..585f8f4f 100644 --- a/addon/globalPlugins/MathCAT/yaml/dumper.py +++ b/addon/globalPlugins/MathCAT/yaml/dumper.py @@ -8,55 +8,78 @@ class BaseDumper(Emitter, Serializer, BaseRepresenter, BaseResolver): - def __init__(self, stream, - default_style=None, default_flow_style=False, - canonical=None, indent=None, width=None, - allow_unicode=None, line_break=None, - encoding=None, explicit_start=None, explicit_end=None, - version=None, tags=None, sort_keys=True): - Emitter.__init__(self, stream, canonical=canonical, - indent=indent, width=width, - allow_unicode=allow_unicode, line_break=line_break) - Serializer.__init__(self, encoding=encoding, - explicit_start=explicit_start, explicit_end=explicit_end, - version=version, tags=tags) - Representer.__init__(self, default_style=default_style, - default_flow_style=default_flow_style, sort_keys=sort_keys) + def __init__( + self, stream, + default_style=None, default_flow_style=False, + canonical=None, indent=None, width=None, + allow_unicode=None, line_break=None, + encoding=None, explicit_start=None, explicit_end=None, + version=None, tags=None, sort_keys=True, + ): + Emitter.__init__( + self, stream, canonical=canonical, + indent=indent, width=width, + allow_unicode=allow_unicode, line_break=line_break, + ) + Serializer.__init__( + self, encoding=encoding, + explicit_start=explicit_start, explicit_end=explicit_end, + version=version, tags=tags, + ) + Representer.__init__( + self, default_style=default_style, + default_flow_style=default_flow_style, sort_keys=sort_keys, + ) Resolver.__init__(self) class SafeDumper(Emitter, Serializer, SafeRepresenter, Resolver): - def __init__(self, stream, - default_style=None, default_flow_style=False, - canonical=None, indent=None, width=None, - allow_unicode=None, line_break=None, - encoding=None, explicit_start=None, explicit_end=None, - version=None, tags=None, sort_keys=True): - Emitter.__init__(self, stream, canonical=canonical, - indent=indent, width=width, - allow_unicode=allow_unicode, line_break=line_break) - Serializer.__init__(self, encoding=encoding, - explicit_start=explicit_start, explicit_end=explicit_end, - version=version, tags=tags) - SafeRepresenter.__init__(self, default_style=default_style, - default_flow_style=default_flow_style, sort_keys=sort_keys) + def __init__( + self, stream, + default_style=None, default_flow_style=False, + canonical=None, indent=None, width=None, + allow_unicode=None, line_break=None, + encoding=None, explicit_start=None, explicit_end=None, + version=None, tags=None, sort_keys=True, + ): + Emitter.__init__( + self, stream, canonical=canonical, + indent=indent, width=width, + allow_unicode=allow_unicode, line_break=line_break, + ) + Serializer.__init__( + self, encoding=encoding, + explicit_start=explicit_start, explicit_end=explicit_end, + version=version, tags=tags, + ) + SafeRepresenter.__init__( + self, default_style=default_style, + default_flow_style=default_flow_style, sort_keys=sort_keys, + ) Resolver.__init__(self) class Dumper(Emitter, Serializer, Representer, Resolver): - def __init__(self, stream, - default_style=None, default_flow_style=False, - canonical=None, indent=None, width=None, - allow_unicode=None, line_break=None, - encoding=None, explicit_start=None, explicit_end=None, - version=None, tags=None, sort_keys=True): - Emitter.__init__(self, stream, canonical=canonical, - indent=indent, width=width, - allow_unicode=allow_unicode, line_break=line_break) - Serializer.__init__(self, encoding=encoding, - explicit_start=explicit_start, explicit_end=explicit_end, - version=version, tags=tags) - Representer.__init__(self, default_style=default_style, - default_flow_style=default_flow_style, sort_keys=sort_keys) + def __init__( + self, stream, + default_style=None, default_flow_style=False, + canonical=None, indent=None, width=None, + allow_unicode=None, line_break=None, + encoding=None, explicit_start=None, explicit_end=None, + version=None, tags=None, sort_keys=True, + ): + Emitter.__init__( + self, stream, canonical=canonical, + indent=indent, width=width, + allow_unicode=allow_unicode, line_break=line_break, + ) + Serializer.__init__( + self, encoding=encoding, + explicit_start=explicit_start, explicit_end=explicit_end, + version=version, tags=tags, + ) + Representer.__init__( + self, default_style=default_style, + default_flow_style=default_flow_style, sort_keys=sort_keys, + ) Resolver.__init__(self) - diff --git a/addon/globalPlugins/MathCAT/yaml/emitter.py b/addon/globalPlugins/MathCAT/yaml/emitter.py index a664d011..a5b73657 100644 --- a/addon/globalPlugins/MathCAT/yaml/emitter.py +++ b/addon/globalPlugins/MathCAT/yaml/emitter.py @@ -15,10 +15,12 @@ class EmitterError(YAMLError): pass class ScalarAnalysis: - def __init__(self, scalar, empty, multiline, - allow_flow_plain, allow_block_plain, - allow_single_quoted, allow_double_quoted, - allow_block): + def __init__( + self, scalar, empty, multiline, + allow_flow_plain, allow_block_plain, + allow_single_quoted, allow_double_quoted, + allow_block, + ): self.scalar = scalar self.empty = empty self.multiline = multiline @@ -35,8 +37,10 @@ class Emitter: 'tag:yaml.org,2002:' : '!!', } - def __init__(self, stream, canonical=None, indent=None, width=None, - allow_unicode=None, line_break=None): + def __init__( + self, stream, canonical=None, indent=None, width=None, + allow_unicode=None, line_break=None, + ): # The stream should have the methods `write` and possibly `flush`. self.stream = stream @@ -164,8 +168,10 @@ def expect_stream_start(self): self.write_stream_start() self.state = self.expect_first_document_start else: - raise EmitterError("expected StreamStartEvent, but got %s" - % self.event) + raise EmitterError( + "expected StreamStartEvent, but got %s" + % self.event, + ) def expect_nothing(self): raise EmitterError("expected nothing, but got %s" % self.event) @@ -192,9 +198,11 @@ def expect_document_start(self, first=False): handle_text = self.prepare_tag_handle(handle) prefix_text = self.prepare_tag_prefix(prefix) self.write_tag_directive(handle_text, prefix_text) - implicit = (first and not self.event.explicit and not self.canonical - and not self.event.version and not self.event.tags - and not self.check_empty_document()) + implicit = ( + first and not self.event.explicit and not self.canonical + and not self.event.version and not self.event.tags + and not self.check_empty_document() + ) if not implicit: self.write_indent() self.write_indicator('---', True) @@ -208,8 +216,10 @@ def expect_document_start(self, first=False): self.write_stream_end() self.state = self.expect_nothing else: - raise EmitterError("expected DocumentStartEvent, but got %s" - % self.event) + raise EmitterError( + "expected DocumentStartEvent, but got %s" + % self.event, + ) def expect_document_end(self): if isinstance(self.event, DocumentEndEvent): @@ -220,8 +230,10 @@ def expect_document_end(self): self.flush_stream() self.state = self.expect_document_start else: - raise EmitterError("expected DocumentEndEvent, but got %s" - % self.event) + raise EmitterError( + "expected DocumentEndEvent, but got %s" + % self.event, + ) def expect_document_root(self): self.states.append(self.expect_document_end) @@ -229,8 +241,10 @@ def expect_document_root(self): # Node handlers. - def expect_node(self, root=False, sequence=False, mapping=False, - simple_key=False): + def expect_node( + self, root=False, sequence=False, mapping=False, + simple_key=False, + ): self.root_context = root self.sequence_context = sequence self.mapping_context = mapping @@ -420,19 +434,25 @@ def expect_block_mapping_value(self): # Checkers. def check_empty_sequence(self): - return (isinstance(self.event, SequenceStartEvent) and self.events - and isinstance(self.events[0], SequenceEndEvent)) + return ( + isinstance(self.event, SequenceStartEvent) and self.events + and isinstance(self.events[0], SequenceEndEvent) + ) def check_empty_mapping(self): - return (isinstance(self.event, MappingStartEvent) and self.events - and isinstance(self.events[0], MappingEndEvent)) + return ( + isinstance(self.event, MappingStartEvent) and self.events + and isinstance(self.events[0], MappingEndEvent) + ) def check_empty_document(self): if not isinstance(self.event, DocumentStartEvent) or not self.events: return False event = self.events[0] - return (isinstance(event, ScalarEvent) and event.anchor is None - and event.tag is None and event.implicit and event.value == '') + return ( + isinstance(event, ScalarEvent) and event.anchor is None + and event.tag is None and event.implicit and event.value == '' + ) def check_simple_key(self): length = 0 @@ -449,10 +469,16 @@ def check_simple_key(self): if self.analysis is None: self.analysis = self.analyze_scalar(self.event.value) length += len(self.analysis.scalar) - return (length < 128 and (isinstance(self.event, AliasEvent) - or (isinstance(self.event, ScalarEvent) - and not self.analysis.empty and not self.analysis.multiline) - or self.check_empty_sequence() or self.check_empty_mapping())) + return ( + length < 128 and ( + isinstance(self.event, AliasEvent) + or ( + isinstance(self.event, ScalarEvent) + and not self.analysis.empty and not self.analysis.multiline + ) + or self.check_empty_sequence() or self.check_empty_mapping() + ) + ) # Anchor, Tag, and Scalar processors. @@ -497,18 +523,28 @@ def choose_scalar_style(self): if self.event.style == '"' or self.canonical: return '"' if not self.event.style and self.event.implicit[0]: - if (not (self.simple_key_context and - (self.analysis.empty or self.analysis.multiline)) - and (self.flow_level and self.analysis.allow_flow_plain - or (not self.flow_level and self.analysis.allow_block_plain))): + if ( + not ( + self.simple_key_context and + (self.analysis.empty or self.analysis.multiline) + ) + and ( + self.flow_level and self.analysis.allow_flow_plain + or (not self.flow_level and self.analysis.allow_block_plain) + ) + ): return '' if self.event.style and self.event.style in '|>': - if (not self.flow_level and not self.simple_key_context - and self.analysis.allow_block): + if ( + not self.flow_level and not self.simple_key_context + and self.analysis.allow_block + ): return self.event.style if not self.event.style or self.event.style == '\'': - if (self.analysis.allow_single_quoted and - not (self.simple_key_context and self.analysis.multiline)): + if ( + self.analysis.allow_single_quoted and + not (self.simple_key_context and self.analysis.multiline) + ): return '\'' return '"' @@ -548,10 +584,14 @@ def prepare_tag_handle(self, handle): if handle[0] != '!' or handle[-1] != '!': raise EmitterError("tag handle must start and end with '!': %r" % handle) for ch in handle[1:-1]: - if not ('0' <= ch <= '9' or 'A' <= ch <= 'Z' or 'a' <= ch <= 'z' \ - or ch in '-_'): - raise EmitterError("invalid character %r in the tag handle: %r" - % (ch, handle)) + if not ( + '0' <= ch <= '9' or 'A' <= ch <= 'Z' or 'a' <= ch <= 'z' \ + or ch in '-_' + ): + raise EmitterError( + "invalid character %r in the tag handle: %r" + % (ch, handle), + ) return handle def prepare_tag_prefix(self, prefix): @@ -617,20 +657,26 @@ def prepare_anchor(self, anchor): if not anchor: raise EmitterError("anchor must not be empty") for ch in anchor: - if not ('0' <= ch <= '9' or 'A' <= ch <= 'Z' or 'a' <= ch <= 'z' \ - or ch in '-_'): - raise EmitterError("invalid character %r in the anchor: %r" - % (ch, anchor)) + if not ( + '0' <= ch <= '9' or 'A' <= ch <= 'Z' or 'a' <= ch <= 'z' \ + or ch in '-_' + ): + raise EmitterError( + "invalid character %r in the anchor: %r" + % (ch, anchor), + ) return anchor def analyze_scalar(self, scalar): # Empty scalar is a special case. if not scalar: - return ScalarAnalysis(scalar=scalar, empty=True, multiline=False, - allow_flow_plain=False, allow_block_plain=True, - allow_single_quoted=True, allow_double_quoted=True, - allow_block=False) + return ScalarAnalysis( + scalar=scalar, empty=True, multiline=False, + allow_flow_plain=False, allow_block_plain=True, + allow_single_quoted=True, allow_double_quoted=True, + allow_block=False, + ) # Indicators and special characters. block_indicators = False @@ -655,8 +701,10 @@ def analyze_scalar(self, scalar): preceded_by_whitespace = True # Last character or followed by a whitespace. - followed_by_whitespace = (len(scalar) == 1 or - scalar[1] in '\0 \t\r\n\x85\u2028\u2029') + followed_by_whitespace = ( + len(scalar) == 1 or + scalar[1] in '\0 \t\r\n\x85\u2028\u2029' + ) # The previous character is a space. previous_space = False @@ -697,9 +745,11 @@ def analyze_scalar(self, scalar): if ch in '\n\x85\u2028\u2029': line_breaks = True if not (ch == '\n' or '\x20' <= ch <= '\x7E'): - if (ch == '\x85' or '\xA0' <= ch <= '\uD7FF' - or '\uE000' <= ch <= '\uFFFD' - or '\U00010000' <= ch < '\U0010ffff') and ch != '\uFEFF': + if ( + ch == '\x85' or '\xA0' <= ch <= '\uD7FF' + or '\uE000' <= ch <= '\uFFFD' + or '\U00010000' <= ch < '\U0010ffff' + ) and ch != '\uFEFF': unicode_characters = True if not self.allow_unicode: special_characters = True @@ -732,8 +782,10 @@ def analyze_scalar(self, scalar): # Prepare for the next character. index += 1 preceded_by_whitespace = (ch in '\0 \t\r\n\x85\u2028\u2029') - followed_by_whitespace = (index+1 >= len(scalar) or - scalar[index+1] in '\0 \t\r\n\x85\u2028\u2029') + followed_by_whitespace = ( + index+1 >= len(scalar) or + scalar[index+1] in '\0 \t\r\n\x85\u2028\u2029' + ) # Let's decide what styles are allowed. allow_flow_plain = True @@ -743,8 +795,10 @@ def analyze_scalar(self, scalar): allow_block = True # Leading and trailing whitespaces are bad for plain scalars. - if (leading_space or leading_break - or trailing_space or trailing_break): + if ( + leading_space or leading_break + or trailing_space or trailing_break + ): allow_flow_plain = allow_block_plain = False # We do not permit trailing spaces for block scalars. @@ -775,13 +829,15 @@ def analyze_scalar(self, scalar): if block_indicators: allow_block_plain = False - return ScalarAnalysis(scalar=scalar, - empty=False, multiline=line_breaks, - allow_flow_plain=allow_flow_plain, - allow_block_plain=allow_block_plain, - allow_single_quoted=allow_single_quoted, - allow_double_quoted=allow_double_quoted, - allow_block=allow_block) + return ScalarAnalysis( + scalar=scalar, + empty=False, multiline=line_breaks, + allow_flow_plain=allow_flow_plain, + allow_block_plain=allow_block_plain, + allow_single_quoted=allow_single_quoted, + allow_double_quoted=allow_double_quoted, + allow_block=allow_block, + ) # Writers. @@ -797,8 +853,10 @@ def write_stream_start(self): def write_stream_end(self): self.flush_stream() - def write_indicator(self, indicator, need_whitespace, - whitespace=False, indention=False): + def write_indicator( + self, indicator, need_whitespace, + whitespace=False, indention=False, + ): if self.whitespace or not need_whitespace: data = indicator else: @@ -931,10 +989,16 @@ def write_double_quoted(self, text, split=True): if end < len(text): ch = text[end] if ch is None or ch in '"\\\x85\u2028\u2029\uFEFF' \ - or not ('\x20' <= ch <= '\x7E' - or (self.allow_unicode - and ('\xA0' <= ch <= '\uD7FF' - or '\uE000' <= ch <= '\uFFFD'))): + or not ( + '\x20' <= ch <= '\x7E' + or ( + self.allow_unicode + and ( + '\xA0' <= ch <= '\uD7FF' + or '\uE000' <= ch <= '\uFFFD' + ) + ) + ): if start < end: data = text[start:end] self.column += len(data) diff --git a/addon/globalPlugins/MathCAT/yaml/error.py b/addon/globalPlugins/MathCAT/yaml/error.py index b796b4dc..4e203a6e 100644 --- a/addon/globalPlugins/MathCAT/yaml/error.py +++ b/addon/globalPlugins/MathCAT/yaml/error.py @@ -47,8 +47,10 @@ class YAMLError(Exception): class MarkedYAMLError(YAMLError): - def __init__(self, context=None, context_mark=None, - problem=None, problem_mark=None, note=None): + def __init__( + self, context=None, context_mark=None, + problem=None, problem_mark=None, note=None, + ): self.context = context self.context_mark = context_mark self.problem = problem @@ -60,10 +62,12 @@ def __str__(self): if self.context is not None: lines.append(self.context) if self.context_mark is not None \ - and (self.problem is None or self.problem_mark is None - or self.context_mark.name != self.problem_mark.name - or self.context_mark.line != self.problem_mark.line - or self.context_mark.column != self.problem_mark.column): + and ( + self.problem is None or self.problem_mark is None + or self.context_mark.name != self.problem_mark.name + or self.context_mark.line != self.problem_mark.line + or self.context_mark.column != self.problem_mark.column + ): lines.append(str(self.context_mark)) if self.problem is not None: lines.append(self.problem) @@ -72,4 +76,3 @@ def __str__(self): if self.note is not None: lines.append(self.note) return '\n'.join(lines) - diff --git a/addon/globalPlugins/MathCAT/yaml/events.py b/addon/globalPlugins/MathCAT/yaml/events.py index f79ad389..ce211528 100644 --- a/addon/globalPlugins/MathCAT/yaml/events.py +++ b/addon/globalPlugins/MathCAT/yaml/events.py @@ -6,10 +6,14 @@ def __init__(self, start_mark=None, end_mark=None): self.start_mark = start_mark self.end_mark = end_mark def __repr__(self): - attributes = [key for key in ['anchor', 'tag', 'implicit', 'value'] - if hasattr(self, key)] - arguments = ', '.join(['%s=%r' % (key, getattr(self, key)) - for key in attributes]) + attributes = [ + key for key in ['anchor', 'tag', 'implicit', 'value'] + if hasattr(self, key) + ] + arguments = ', '.join([ + '%s=%r' % (key, getattr(self, key)) + for key in attributes + ]) return '%s(%s)' % (self.__class__.__name__, arguments) class NodeEvent(Event): @@ -19,8 +23,10 @@ def __init__(self, anchor, start_mark=None, end_mark=None): self.end_mark = end_mark class CollectionStartEvent(NodeEvent): - def __init__(self, anchor, tag, implicit, start_mark=None, end_mark=None, - flow_style=None): + def __init__( + self, anchor, tag, implicit, start_mark=None, end_mark=None, + flow_style=None, + ): self.anchor = anchor self.tag = tag self.implicit = implicit @@ -43,8 +49,10 @@ class StreamEndEvent(Event): pass class DocumentStartEvent(Event): - def __init__(self, start_mark=None, end_mark=None, - explicit=None, version=None, tags=None): + def __init__( + self, start_mark=None, end_mark=None, + explicit=None, version=None, tags=None, + ): self.start_mark = start_mark self.end_mark = end_mark self.explicit = explicit @@ -52,8 +60,10 @@ def __init__(self, start_mark=None, end_mark=None, self.tags = tags class DocumentEndEvent(Event): - def __init__(self, start_mark=None, end_mark=None, - explicit=None): + def __init__( + self, start_mark=None, end_mark=None, + explicit=None, + ): self.start_mark = start_mark self.end_mark = end_mark self.explicit = explicit @@ -62,8 +72,10 @@ class AliasEvent(NodeEvent): pass class ScalarEvent(NodeEvent): - def __init__(self, anchor, tag, implicit, value, - start_mark=None, end_mark=None, style=None): + def __init__( + self, anchor, tag, implicit, value, + start_mark=None, end_mark=None, style=None, + ): self.anchor = anchor self.tag = tag self.implicit = implicit @@ -83,4 +95,3 @@ class MappingStartEvent(CollectionStartEvent): class MappingEndEvent(CollectionEndEvent): pass - diff --git a/addon/globalPlugins/MathCAT/yaml/nodes.py b/addon/globalPlugins/MathCAT/yaml/nodes.py index c4f070c4..3ce5469c 100644 --- a/addon/globalPlugins/MathCAT/yaml/nodes.py +++ b/addon/globalPlugins/MathCAT/yaml/nodes.py @@ -24,8 +24,10 @@ def __repr__(self): class ScalarNode(Node): id = 'scalar' - def __init__(self, tag, value, - start_mark=None, end_mark=None, style=None): + def __init__( + self, tag, value, + start_mark=None, end_mark=None, style=None, + ): self.tag = tag self.value = value self.start_mark = start_mark @@ -33,8 +35,10 @@ def __init__(self, tag, value, self.style = style class CollectionNode(Node): - def __init__(self, tag, value, - start_mark=None, end_mark=None, flow_style=None): + def __init__( + self, tag, value, + start_mark=None, end_mark=None, flow_style=None, + ): self.tag = tag self.value = value self.start_mark = start_mark @@ -46,4 +50,3 @@ class SequenceNode(CollectionNode): class MappingNode(CollectionNode): id = 'mapping' - diff --git a/addon/globalPlugins/MathCAT/yaml/parser.py b/addon/globalPlugins/MathCAT/yaml/parser.py index 13a5995d..afd8fb3f 100644 --- a/addon/globalPlugins/MathCAT/yaml/parser.py +++ b/addon/globalPlugins/MathCAT/yaml/parser.py @@ -128,8 +128,10 @@ def parse_stream_start(self): # Parse the stream start. token = self.get_token() - event = StreamStartEvent(token.start_mark, token.end_mark, - encoding=token.encoding) + event = StreamStartEvent( + token.start_mark, token.end_mark, + encoding=token.encoding, + ) # Prepare the next state. self.state = self.parse_implicit_document_start @@ -139,13 +141,17 @@ def parse_stream_start(self): def parse_implicit_document_start(self): # Parse an implicit document. - if not self.check_token(DirectiveToken, DocumentStartToken, - StreamEndToken): + if not self.check_token( + DirectiveToken, DocumentStartToken, + StreamEndToken, + ): self.tag_handles = self.DEFAULT_TAGS token = self.peek_token() start_mark = end_mark = token.start_mark - event = DocumentStartEvent(start_mark, end_mark, - explicit=False) + event = DocumentStartEvent( + start_mark, end_mark, + explicit=False, + ) # Prepare the next state. self.states.append(self.parse_document_end) @@ -168,14 +174,18 @@ def parse_document_start(self): start_mark = token.start_mark version, tags = self.process_directives() if not self.check_token(DocumentStartToken): - raise ParserError(None, None, - "expected '', but found %r" - % self.peek_token().id, - self.peek_token().start_mark) + raise ParserError( + None, None, + "expected '', but found %r" + % self.peek_token().id, + self.peek_token().start_mark, + ) token = self.get_token() end_mark = token.end_mark - event = DocumentStartEvent(start_mark, end_mark, - explicit=True, version=version, tags=tags) + event = DocumentStartEvent( + start_mark, end_mark, + explicit=True, version=version, tags=tags, + ) self.states.append(self.parse_document_end) self.state = self.parse_document_content else: @@ -197,8 +207,10 @@ def parse_document_end(self): token = self.get_token() end_mark = token.end_mark explicit = True - event = DocumentEndEvent(start_mark, end_mark, - explicit=explicit) + event = DocumentEndEvent( + start_mark, end_mark, + explicit=explicit, + ) # Prepare the next state. self.state = self.parse_document_start @@ -206,8 +218,10 @@ def parse_document_end(self): return event def parse_document_content(self): - if self.check_token(DirectiveToken, - DocumentStartToken, DocumentEndToken, StreamEndToken): + if self.check_token( + DirectiveToken, + DocumentStartToken, DocumentEndToken, StreamEndToken, + ): event = self.process_empty_scalar(self.peek_token().start_mark) self.state = self.states.pop() return event @@ -221,20 +235,26 @@ def process_directives(self): token = self.get_token() if token.name == 'YAML': if self.yaml_version is not None: - raise ParserError(None, None, - "found duplicate YAML directive", token.start_mark) + raise ParserError( + None, None, + "found duplicate YAML directive", token.start_mark, + ) major, minor = token.value if major != 1: - raise ParserError(None, None, - "found incompatible YAML document (version 1.* is required)", - token.start_mark) + raise ParserError( + None, None, + "found incompatible YAML document (version 1.* is required)", + token.start_mark, + ) self.yaml_version = token.value elif token.name == 'TAG': handle, prefix = token.value if handle in self.tag_handles: - raise ParserError(None, None, - "duplicate tag handle %r" % handle, - token.start_mark) + raise ParserError( + None, None, + "duplicate tag handle %r" % handle, + token.start_mark, + ) self.tag_handles[handle] = prefix if self.tag_handles: value = self.yaml_version, self.tag_handles.copy() @@ -302,9 +322,11 @@ def parse_node(self, block=False, indentless_sequence=False): handle, suffix = tag if handle is not None: if handle not in self.tag_handles: - raise ParserError("while parsing a node", start_mark, - "found undefined tag handle %r" % handle, - tag_mark) + raise ParserError( + "while parsing a node", start_mark, + "found undefined tag handle %r" % handle, + tag_mark, + ) tag = self.tag_handles[handle]+suffix else: tag = suffix @@ -318,8 +340,10 @@ def parse_node(self, block=False, indentless_sequence=False): implicit = (tag is None or tag == '!') if indentless_sequence and self.check_token(BlockEntryToken): end_mark = self.peek_token().end_mark - event = SequenceStartEvent(anchor, tag, implicit, - start_mark, end_mark) + event = SequenceStartEvent( + anchor, tag, implicit, + start_mark, end_mark, + ) self.state = self.parse_indentless_sequence_entry else: if self.check_token(ScalarToken): @@ -331,34 +355,46 @@ def parse_node(self, block=False, indentless_sequence=False): implicit = (False, True) else: implicit = (False, False) - event = ScalarEvent(anchor, tag, implicit, token.value, - start_mark, end_mark, style=token.style) + event = ScalarEvent( + anchor, tag, implicit, token.value, + start_mark, end_mark, style=token.style, + ) self.state = self.states.pop() elif self.check_token(FlowSequenceStartToken): end_mark = self.peek_token().end_mark - event = SequenceStartEvent(anchor, tag, implicit, - start_mark, end_mark, flow_style=True) + event = SequenceStartEvent( + anchor, tag, implicit, + start_mark, end_mark, flow_style=True, + ) self.state = self.parse_flow_sequence_first_entry elif self.check_token(FlowMappingStartToken): end_mark = self.peek_token().end_mark - event = MappingStartEvent(anchor, tag, implicit, - start_mark, end_mark, flow_style=True) + event = MappingStartEvent( + anchor, tag, implicit, + start_mark, end_mark, flow_style=True, + ) self.state = self.parse_flow_mapping_first_key elif block and self.check_token(BlockSequenceStartToken): end_mark = self.peek_token().start_mark - event = SequenceStartEvent(anchor, tag, implicit, - start_mark, end_mark, flow_style=False) + event = SequenceStartEvent( + anchor, tag, implicit, + start_mark, end_mark, flow_style=False, + ) self.state = self.parse_block_sequence_first_entry elif block and self.check_token(BlockMappingStartToken): end_mark = self.peek_token().start_mark - event = MappingStartEvent(anchor, tag, implicit, - start_mark, end_mark, flow_style=False) + event = MappingStartEvent( + anchor, tag, implicit, + start_mark, end_mark, flow_style=False, + ) self.state = self.parse_block_mapping_first_key elif anchor is not None or tag is not None: # Empty scalars are allowed even if a tag or an anchor is # specified. - event = ScalarEvent(anchor, tag, (implicit, False), '', - start_mark, end_mark) + event = ScalarEvent( + anchor, tag, (implicit, False), '', + start_mark, end_mark, + ) self.state = self.states.pop() else: if block: @@ -366,9 +402,11 @@ def parse_node(self, block=False, indentless_sequence=False): else: node = 'flow' token = self.peek_token() - raise ParserError("while parsing a %s node" % node, start_mark, - "expected the node content, but found %r" % token.id, - token.start_mark) + raise ParserError( + "while parsing a %s node" % node, start_mark, + "expected the node content, but found %r" % token.id, + token.start_mark, + ) return event # block_sequence ::= BLOCK-SEQUENCE-START (BLOCK-ENTRY block_node?)* BLOCK-END @@ -389,8 +427,10 @@ def parse_block_sequence_entry(self): return self.process_empty_scalar(token.end_mark) if not self.check_token(BlockEndToken): token = self.peek_token() - raise ParserError("while parsing a block collection", self.marks[-1], - "expected , but found %r" % token.id, token.start_mark) + raise ParserError( + "while parsing a block collection", self.marks[-1], + "expected , but found %r" % token.id, token.start_mark, + ) token = self.get_token() event = SequenceEndEvent(token.start_mark, token.end_mark) self.state = self.states.pop() @@ -402,8 +442,10 @@ def parse_block_sequence_entry(self): def parse_indentless_sequence_entry(self): if self.check_token(BlockEntryToken): token = self.get_token() - if not self.check_token(BlockEntryToken, - KeyToken, ValueToken, BlockEndToken): + if not self.check_token( + BlockEntryToken, + KeyToken, ValueToken, BlockEndToken, + ): self.states.append(self.parse_indentless_sequence_entry) return self.parse_block_node() else: @@ -435,8 +477,10 @@ def parse_block_mapping_key(self): return self.process_empty_scalar(token.end_mark) if not self.check_token(BlockEndToken): token = self.peek_token() - raise ParserError("while parsing a block mapping", self.marks[-1], - "expected , but found %r" % token.id, token.start_mark) + raise ParserError( + "while parsing a block mapping", self.marks[-1], + "expected , but found %r" % token.id, token.start_mark, + ) token = self.get_token() event = MappingEndEvent(token.start_mark, token.end_mark) self.state = self.states.pop() @@ -480,14 +524,18 @@ def parse_flow_sequence_entry(self, first=False): self.get_token() else: token = self.peek_token() - raise ParserError("while parsing a flow sequence", self.marks[-1], - "expected ',' or ']', but got %r" % token.id, token.start_mark) - + raise ParserError( + "while parsing a flow sequence", self.marks[-1], + "expected ',' or ']', but got %r" % token.id, token.start_mark, + ) + if self.check_token(KeyToken): token = self.peek_token() - event = MappingStartEvent(None, None, True, - token.start_mark, token.end_mark, - flow_style=True) + event = MappingStartEvent( + None, None, True, + token.start_mark, token.end_mark, + flow_style=True, + ) self.state = self.parse_flow_sequence_entry_mapping_key return event elif not self.check_token(FlowSequenceEndToken): @@ -501,8 +549,10 @@ def parse_flow_sequence_entry(self, first=False): def parse_flow_sequence_entry_mapping_key(self): token = self.get_token() - if not self.check_token(ValueToken, - FlowEntryToken, FlowSequenceEndToken): + if not self.check_token( + ValueToken, + FlowEntryToken, FlowSequenceEndToken, + ): self.states.append(self.parse_flow_sequence_entry_mapping_value) return self.parse_flow_node() else: @@ -546,12 +596,16 @@ def parse_flow_mapping_key(self, first=False): self.get_token() else: token = self.peek_token() - raise ParserError("while parsing a flow mapping", self.marks[-1], - "expected ',' or '}', but got %r" % token.id, token.start_mark) + raise ParserError( + "while parsing a flow mapping", self.marks[-1], + "expected ',' or '}', but got %r" % token.id, token.start_mark, + ) if self.check_token(KeyToken): token = self.get_token() - if not self.check_token(ValueToken, - FlowEntryToken, FlowMappingEndToken): + if not self.check_token( + ValueToken, + FlowEntryToken, FlowMappingEndToken, + ): self.states.append(self.parse_flow_mapping_value) return self.parse_flow_node() else: @@ -586,4 +640,3 @@ def parse_flow_mapping_empty_value(self): def process_empty_scalar(self, mark): return ScalarEvent(None, None, (True, False), '', mark, mark) - diff --git a/addon/globalPlugins/MathCAT/yaml/reader.py b/addon/globalPlugins/MathCAT/yaml/reader.py index 774b0219..5509369c 100644 --- a/addon/globalPlugins/MathCAT/yaml/reader.py +++ b/addon/globalPlugins/MathCAT/yaml/reader.py @@ -34,13 +34,17 @@ def __str__(self): if isinstance(self.character, bytes): return "'%s' codec can't decode byte #x%02x: %s\n" \ " in \"%s\", position %d" \ - % (self.encoding, ord(self.character), self.reason, - self.name, self.position) + % ( + self.encoding, ord(self.character), self.reason, + self.name, self.position, + ) else: return "unacceptable character #x%04x: %s\n" \ " in \"%s\", position %d" \ - % (self.character, self.reason, - self.name, self.position) + % ( + self.character, self.reason, + self.name, self.position, + ) class Reader(object): # Reader: @@ -113,11 +117,15 @@ def forward(self, length=1): def get_mark(self): if self.stream is None: - return Mark(self.name, self.index, self.line, self.column, - self.buffer, self.pointer) + return Mark( + self.name, self.index, self.line, self.column, + self.buffer, self.pointer, + ) else: - return Mark(self.name, self.index, self.line, self.column, - None, None) + return Mark( + self.name, self.index, self.line, self.column, + None, None, + ) def determine_encoding(self): while not self.eof and (self.raw_buffer is None or len(self.raw_buffer) < 2): @@ -140,8 +148,10 @@ def check_printable(self, data): if match: character = match.group() position = self.index+(len(self.buffer)-self.pointer)+match.start() - raise ReaderError(self.name, position, ord(character), - 'unicode', "special characters are not allowed") + raise ReaderError( + self.name, position, ord(character), + 'unicode', "special characters are not allowed", + ) def update(self, length): if self.raw_buffer is None: @@ -153,16 +163,20 @@ def update(self, length): self.update_raw() if self.raw_decode is not None: try: - data, converted = self.raw_decode(self.raw_buffer, - 'strict', self.eof) + data, converted = self.raw_decode( + self.raw_buffer, + 'strict', self.eof, + ) except UnicodeDecodeError as exc: character = self.raw_buffer[exc.start] if self.stream is not None: position = self.stream_pointer-len(self.raw_buffer)+exc.start else: position = exc.start - raise ReaderError(self.name, position, character, - exc.encoding, exc.reason) + raise ReaderError( + self.name, position, character, + exc.encoding, exc.reason, + ) else: data = self.raw_buffer converted = len(data) diff --git a/addon/globalPlugins/MathCAT/yaml/representer.py b/addon/globalPlugins/MathCAT/yaml/representer.py index 808ca06d..3f6b7dda 100644 --- a/addon/globalPlugins/MathCAT/yaml/representer.py +++ b/addon/globalPlugins/MathCAT/yaml/representer.py @@ -1,6 +1,8 @@ -__all__ = ['BaseRepresenter', 'SafeRepresenter', 'Representer', - 'RepresenterError'] +__all__ = [ + 'BaseRepresenter', 'SafeRepresenter', 'Representer', + 'RepresenterError', +] from .error import * from .nodes import * @@ -230,44 +232,70 @@ def represent_yaml_object(self, tag, data, cls, flow_style=None): def represent_undefined(self, data): raise RepresenterError("cannot represent an object", data) -SafeRepresenter.add_representer(type(None), - SafeRepresenter.represent_none) - -SafeRepresenter.add_representer(str, - SafeRepresenter.represent_str) - -SafeRepresenter.add_representer(bytes, - SafeRepresenter.represent_binary) - -SafeRepresenter.add_representer(bool, - SafeRepresenter.represent_bool) - -SafeRepresenter.add_representer(int, - SafeRepresenter.represent_int) - -SafeRepresenter.add_representer(float, - SafeRepresenter.represent_float) - -SafeRepresenter.add_representer(list, - SafeRepresenter.represent_list) - -SafeRepresenter.add_representer(tuple, - SafeRepresenter.represent_list) - -SafeRepresenter.add_representer(dict, - SafeRepresenter.represent_dict) - -SafeRepresenter.add_representer(set, - SafeRepresenter.represent_set) - -SafeRepresenter.add_representer(datetime.date, - SafeRepresenter.represent_date) - -SafeRepresenter.add_representer(datetime.datetime, - SafeRepresenter.represent_datetime) - -SafeRepresenter.add_representer(None, - SafeRepresenter.represent_undefined) +SafeRepresenter.add_representer( + type(None), + SafeRepresenter.represent_none, +) + +SafeRepresenter.add_representer( + str, + SafeRepresenter.represent_str, +) + +SafeRepresenter.add_representer( + bytes, + SafeRepresenter.represent_binary, +) + +SafeRepresenter.add_representer( + bool, + SafeRepresenter.represent_bool, +) + +SafeRepresenter.add_representer( + int, + SafeRepresenter.represent_int, +) + +SafeRepresenter.add_representer( + float, + SafeRepresenter.represent_float, +) + +SafeRepresenter.add_representer( + list, + SafeRepresenter.represent_list, +) + +SafeRepresenter.add_representer( + tuple, + SafeRepresenter.represent_list, +) + +SafeRepresenter.add_representer( + dict, + SafeRepresenter.represent_dict, +) + +SafeRepresenter.add_representer( + set, + SafeRepresenter.represent_set, +) + +SafeRepresenter.add_representer( + datetime.date, + SafeRepresenter.represent_date, +) + +SafeRepresenter.add_representer( + datetime.datetime, + SafeRepresenter.represent_datetime, +) + +SafeRepresenter.add_representer( + None, + SafeRepresenter.represent_undefined, +) class Representer(SafeRepresenter): @@ -291,7 +319,8 @@ def represent_name(self, data): def represent_module(self, data): return self.represent_scalar( - 'tag:yaml.org,2002:python/module:'+data.__name__, '') + 'tag:yaml.org,2002:python/module:'+data.__name__, '', + ) def represent_object(self, data): # We use __reduce__ API to save the data. data.__reduce__ returns @@ -340,7 +369,8 @@ def represent_object(self, data): if not args and not listitems and not dictitems \ and isinstance(state, dict) and newobj: return self.represent_mapping( - 'tag:yaml.org,2002:python/object:'+function_name, state) + 'tag:yaml.org,2002:python/object:'+function_name, state, + ) if not listitems and not dictitems \ and isinstance(state, dict) and not state: return self.represent_sequence(tag+function_name, args) @@ -363,27 +393,42 @@ def represent_ordered_dict(self, data): items = [[key, value] for key, value in data.items()] return self.represent_sequence(tag, [items]) -Representer.add_representer(complex, - Representer.represent_complex) - -Representer.add_representer(tuple, - Representer.represent_tuple) - -Representer.add_multi_representer(type, - Representer.represent_name) - -Representer.add_representer(collections.OrderedDict, - Representer.represent_ordered_dict) - -Representer.add_representer(types.FunctionType, - Representer.represent_name) - -Representer.add_representer(types.BuiltinFunctionType, - Representer.represent_name) - -Representer.add_representer(types.ModuleType, - Representer.represent_module) - -Representer.add_multi_representer(object, - Representer.represent_object) - +Representer.add_representer( + complex, + Representer.represent_complex, +) + +Representer.add_representer( + tuple, + Representer.represent_tuple, +) + +Representer.add_multi_representer( + type, + Representer.represent_name, +) + +Representer.add_representer( + collections.OrderedDict, + Representer.represent_ordered_dict, +) + +Representer.add_representer( + types.FunctionType, + Representer.represent_name, +) + +Representer.add_representer( + types.BuiltinFunctionType, + Representer.represent_name, +) + +Representer.add_representer( + types.ModuleType, + Representer.represent_module, +) + +Representer.add_multi_representer( + object, + Representer.represent_object, +) diff --git a/addon/globalPlugins/MathCAT/yaml/resolver.py b/addon/globalPlugins/MathCAT/yaml/resolver.py index 3522bdaa..4b255b71 100644 --- a/addon/globalPlugins/MathCAT/yaml/resolver.py +++ b/addon/globalPlugins/MathCAT/yaml/resolver.py @@ -96,8 +96,10 @@ def descend_resolver(self, current_node, current_index): if current_node: depth = len(self.resolver_prefix_paths) for path, kind in self.resolver_prefix_paths[-1]: - if self.check_resolver_prefix(depth, path, kind, - current_node, current_index): + if self.check_resolver_prefix( + depth, path, kind, + current_node, current_index, + ): if len(path) > depth: prefix_paths.append((path, kind)) else: @@ -117,8 +119,10 @@ def ascend_resolver(self): self.resolver_exact_paths.pop() self.resolver_prefix_paths.pop() - def check_resolver_prefix(self, depth, path, kind, - current_node, current_index): + def check_resolver_prefix( + self, depth, path, kind, + current_node, current_index, + ): node_check, index_check = path[depth-1] if isinstance(node_check, str): if current_node.tag != node_check: @@ -132,8 +136,10 @@ def check_resolver_prefix(self, depth, path, kind, and current_index is None: return if isinstance(index_check, str): - if not (isinstance(current_index, ScalarNode) - and index_check == current_index.value): + if not ( + isinstance(current_index, ScalarNode) + and index_check == current_index.value + ): return elif isinstance(index_check, int) and not isinstance(index_check, bool): if index_check != current_index: @@ -169,59 +175,76 @@ class Resolver(BaseResolver): Resolver.add_implicit_resolver( 'tag:yaml.org,2002:bool', - re.compile(r'''^(?:yes|Yes|YES|no|No|NO + re.compile( + r'''^(?:yes|Yes|YES|no|No|NO |true|True|TRUE|false|False|FALSE - |on|On|ON|off|Off|OFF)$''', re.X), - list('yYnNtTfFoO')) + |on|On|ON|off|Off|OFF)$''', re.X, + ), + list('yYnNtTfFoO'), +) Resolver.add_implicit_resolver( 'tag:yaml.org,2002:float', - re.compile(r'''^(?:[-+]?(?:[0-9][0-9_]*)\.[0-9_]*(?:[eE][-+][0-9]+)? + re.compile( + r'''^(?:[-+]?(?:[0-9][0-9_]*)\.[0-9_]*(?:[eE][-+][0-9]+)? |\.[0-9][0-9_]*(?:[eE][-+][0-9]+)? |[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+\.[0-9_]* |[-+]?\.(?:inf|Inf|INF) - |\.(?:nan|NaN|NAN))$''', re.X), - list('-+0123456789.')) + |\.(?:nan|NaN|NAN))$''', re.X, + ), + list('-+0123456789.'), +) Resolver.add_implicit_resolver( 'tag:yaml.org,2002:int', - re.compile(r'''^(?:[-+]?0b[0-1_]+ + re.compile( + r'''^(?:[-+]?0b[0-1_]+ |[-+]?0[0-7_]+ |[-+]?(?:0|[1-9][0-9_]*) |[-+]?0x[0-9a-fA-F_]+ - |[-+]?[1-9][0-9_]*(?::[0-5]?[0-9])+)$''', re.X), - list('-+0123456789')) + |[-+]?[1-9][0-9_]*(?::[0-5]?[0-9])+)$''', re.X, + ), + list('-+0123456789'), +) Resolver.add_implicit_resolver( 'tag:yaml.org,2002:merge', re.compile(r'^(?:<<)$'), - ['<']) + ['<'], +) Resolver.add_implicit_resolver( 'tag:yaml.org,2002:null', - re.compile(r'''^(?: ~ + re.compile( + r'''^(?: ~ |null|Null|NULL - | )$''', re.X), - ['~', 'n', 'N', '']) + | )$''', re.X, + ), + ['~', 'n', 'N', ''], +) Resolver.add_implicit_resolver( 'tag:yaml.org,2002:timestamp', - re.compile(r'''^(?:[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9] + re.compile( + r'''^(?:[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9] |[0-9][0-9][0-9][0-9] -[0-9][0-9]? -[0-9][0-9]? (?:[Tt]|[ \t]+)[0-9][0-9]? :[0-9][0-9] :[0-9][0-9] (?:\.[0-9]*)? - (?:[ \t]*(?:Z|[-+][0-9][0-9]?(?::[0-9][0-9])?))?)$''', re.X), - list('0123456789')) + (?:[ \t]*(?:Z|[-+][0-9][0-9]?(?::[0-9][0-9])?))?)$''', re.X, + ), + list('0123456789'), +) Resolver.add_implicit_resolver( 'tag:yaml.org,2002:value', re.compile(r'^(?:=)$'), - ['=']) + ['='], +) # The following resolver is only for documentation purposes. It cannot work # because plain scalars cannot start with '!', '&', or '*'. Resolver.add_implicit_resolver( 'tag:yaml.org,2002:yaml', re.compile(r'^(?:!|&|\*)$'), - list('!&*')) - + list('!&*'), +) diff --git a/addon/globalPlugins/MathCAT/yaml/scanner.py b/addon/globalPlugins/MathCAT/yaml/scanner.py index de925b07..39e2791a 100644 --- a/addon/globalPlugins/MathCAT/yaml/scanner.py +++ b/addon/globalPlugins/MathCAT/yaml/scanner.py @@ -255,9 +255,11 @@ def fetch_more_tokens(self): return self.fetch_plain() # No? It's an error. Let's produce a nice error message. - raise ScannerError("while scanning for the next token", None, - "found character %r that cannot start any token" % ch, - self.get_mark()) + raise ScannerError( + "while scanning for the next token", None, + "found character %r that cannot start any token" % ch, + self.get_mark(), + ) # Simple keys treatment. @@ -288,8 +290,10 @@ def stale_possible_simple_keys(self): if key.line != self.line \ or self.index-key.index > 1024: if key.required: - raise ScannerError("while scanning a simple key", key.mark, - "could not find expected ':'", self.get_mark()) + raise ScannerError( + "while scanning a simple key", key.mark, + "could not find expected ':'", self.get_mark(), + ) del self.possible_simple_keys[level] def save_possible_simple_key(self): @@ -305,18 +309,22 @@ def save_possible_simple_key(self): if self.allow_simple_key: self.remove_possible_simple_key() token_number = self.tokens_taken+len(self.tokens) - key = SimpleKey(token_number, required, - self.index, self.line, self.column, self.get_mark()) + key = SimpleKey( + token_number, required, + self.index, self.line, self.column, self.get_mark(), + ) self.possible_simple_keys[self.flow_level] = key def remove_possible_simple_key(self): # Remove the saved possible key position at the current flow level. if self.flow_level in self.possible_simple_keys: key = self.possible_simple_keys[self.flow_level] - + if key.required: - raise ScannerError("while scanning a simple key", key.mark, - "could not find expected ':'", self.get_mark()) + raise ScannerError( + "while scanning a simple key", key.mark, + "could not find expected ':'", self.get_mark(), + ) del self.possible_simple_keys[self.flow_level] @@ -362,11 +370,15 @@ def fetch_stream_start(self): # Read the token. mark = self.get_mark() - + # Add STREAM-START. - self.tokens.append(StreamStartToken(mark, mark, - encoding=self.encoding)) - + self.tokens.append( + StreamStartToken( + mark, mark, + encoding=self.encoding, + ), + ) + def fetch_stream_end(self): @@ -380,7 +392,7 @@ def fetch_stream_end(self): # Read the token. mark = self.get_mark() - + # Add STREAM-END. self.tokens.append(StreamEndToken(mark, mark)) @@ -388,7 +400,7 @@ def fetch_stream_end(self): self.done = True def fetch_directive(self): - + # Set the current indentation to -1. self.unwind_indent(-1) @@ -488,9 +500,11 @@ def fetch_block_entry(self): # Are we allowed to start a new entry? if not self.allow_simple_key: - raise ScannerError(None, None, - "sequence entries are not allowed here", - self.get_mark()) + raise ScannerError( + None, None, + "sequence entries are not allowed here", + self.get_mark(), + ) # We may need to add BLOCK-SEQUENCE-START. if self.add_indent(self.column): @@ -515,15 +529,17 @@ def fetch_block_entry(self): self.tokens.append(BlockEntryToken(start_mark, end_mark)) def fetch_key(self): - + # Block context needs additional checks. if not self.flow_level: # Are we allowed to start a key (not necessary a simple)? if not self.allow_simple_key: - raise ScannerError(None, None, - "mapping keys are not allowed here", - self.get_mark()) + raise ScannerError( + None, None, + "mapping keys are not allowed here", + self.get_mark(), + ) # We may need to add BLOCK-MAPPING-START. if self.add_indent(self.column): @@ -550,22 +566,26 @@ def fetch_value(self): # Add KEY. key = self.possible_simple_keys[self.flow_level] del self.possible_simple_keys[self.flow_level] - self.tokens.insert(key.token_number-self.tokens_taken, - KeyToken(key.mark, key.mark)) + self.tokens.insert( + key.token_number-self.tokens_taken, + KeyToken(key.mark, key.mark), + ) # If this key starts a new block mapping, we need to add # BLOCK-MAPPING-START. if not self.flow_level: if self.add_indent(key.column): - self.tokens.insert(key.token_number-self.tokens_taken, - BlockMappingStartToken(key.mark, key.mark)) + self.tokens.insert( + key.token_number-self.tokens_taken, + BlockMappingStartToken(key.mark, key.mark), + ) # There cannot be two simple keys one after another. self.allow_simple_key = False # It must be a part of a complex key. else: - + # Block context needs additional checks. # (Do we really need them? They will be caught by the parser # anyway.) @@ -574,9 +594,11 @@ def fetch_value(self): # We are allowed to start a complex value if and only if # we can start a simple key. if not self.allow_simple_key: - raise ScannerError(None, None, - "mapping values are not allowed here", - self.get_mark()) + raise ScannerError( + None, None, + "mapping values are not allowed here", + self.get_mark(), + ) # If this value starts a new block mapping, we need to add # BLOCK-MAPPING-START. It will be detected as an error later by @@ -744,8 +766,10 @@ def check_plain(self): # independent. ch = self.peek() return ch not in '\0 \t\r\n\x85\u2028\u2029-?:,[]{}#&*!|>\'\"%@`' \ - or (self.peek(1) not in '\0 \t\r\n\x85\u2028\u2029' - and (ch == '-' or (not self.flow_level and ch in '?:'))) + or ( + self.peek(1) not in '\0 \t\r\n\x85\u2028\u2029' + and (ch == '-' or (not self.flow_level and ch in '?:')) + ) # Scanners. @@ -812,16 +836,20 @@ def scan_directive_name(self, start_mark): length += 1 ch = self.peek(length) if not length: - raise ScannerError("while scanning a directive", start_mark, - "expected alphabetic or numeric character, but found %r" - % ch, self.get_mark()) + raise ScannerError( + "while scanning a directive", start_mark, + "expected alphabetic or numeric character, but found %r" + % ch, self.get_mark(), + ) value = self.prefix(length) self.forward(length) ch = self.peek() if ch not in '\0 \r\n\x85\u2028\u2029': - raise ScannerError("while scanning a directive", start_mark, - "expected alphabetic or numeric character, but found %r" - % ch, self.get_mark()) + raise ScannerError( + "while scanning a directive", start_mark, + "expected alphabetic or numeric character, but found %r" + % ch, self.get_mark(), + ) return value def scan_yaml_directive_value(self, start_mark): @@ -830,23 +858,29 @@ def scan_yaml_directive_value(self, start_mark): self.forward() major = self.scan_yaml_directive_number(start_mark) if self.peek() != '.': - raise ScannerError("while scanning a directive", start_mark, - "expected a digit or '.', but found %r" % self.peek(), - self.get_mark()) + raise ScannerError( + "while scanning a directive", start_mark, + "expected a digit or '.', but found %r" % self.peek(), + self.get_mark(), + ) self.forward() minor = self.scan_yaml_directive_number(start_mark) if self.peek() not in '\0 \r\n\x85\u2028\u2029': - raise ScannerError("while scanning a directive", start_mark, - "expected a digit or ' ', but found %r" % self.peek(), - self.get_mark()) + raise ScannerError( + "while scanning a directive", start_mark, + "expected a digit or ' ', but found %r" % self.peek(), + self.get_mark(), + ) return (major, minor) def scan_yaml_directive_number(self, start_mark): # See the specification for details. ch = self.peek() if not ('0' <= ch <= '9'): - raise ScannerError("while scanning a directive", start_mark, - "expected a digit, but found %r" % ch, self.get_mark()) + raise ScannerError( + "while scanning a directive", start_mark, + "expected a digit, but found %r" % ch, self.get_mark(), + ) length = 0 while '0' <= self.peek(length) <= '9': length += 1 @@ -869,8 +903,10 @@ def scan_tag_directive_handle(self, start_mark): value = self.scan_tag_handle('directive', start_mark) ch = self.peek() if ch != ' ': - raise ScannerError("while scanning a directive", start_mark, - "expected ' ', but found %r" % ch, self.get_mark()) + raise ScannerError( + "while scanning a directive", start_mark, + "expected ' ', but found %r" % ch, self.get_mark(), + ) return value def scan_tag_directive_prefix(self, start_mark): @@ -878,8 +914,10 @@ def scan_tag_directive_prefix(self, start_mark): value = self.scan_tag_uri('directive', start_mark) ch = self.peek() if ch not in '\0 \r\n\x85\u2028\u2029': - raise ScannerError("while scanning a directive", start_mark, - "expected ' ', but found %r" % ch, self.get_mark()) + raise ScannerError( + "while scanning a directive", start_mark, + "expected ' ', but found %r" % ch, self.get_mark(), + ) return value def scan_directive_ignored_line(self, start_mark): @@ -891,9 +929,11 @@ def scan_directive_ignored_line(self, start_mark): self.forward() ch = self.peek() if ch not in '\0\r\n\x85\u2028\u2029': - raise ScannerError("while scanning a directive", start_mark, - "expected a comment or a line break, but found %r" - % ch, self.get_mark()) + raise ScannerError( + "while scanning a directive", start_mark, + "expected a comment or a line break, but found %r" + % ch, self.get_mark(), + ) self.scan_line_break() def scan_anchor(self, TokenClass): @@ -919,16 +959,20 @@ def scan_anchor(self, TokenClass): length += 1 ch = self.peek(length) if not length: - raise ScannerError("while scanning an %s" % name, start_mark, - "expected alphabetic or numeric character, but found %r" - % ch, self.get_mark()) + raise ScannerError( + "while scanning an %s" % name, start_mark, + "expected alphabetic or numeric character, but found %r" + % ch, self.get_mark(), + ) value = self.prefix(length) self.forward(length) ch = self.peek() if ch not in '\0 \t\r\n\x85\u2028\u2029?:,]}%@`': - raise ScannerError("while scanning an %s" % name, start_mark, - "expected alphabetic or numeric character, but found %r" - % ch, self.get_mark()) + raise ScannerError( + "while scanning an %s" % name, start_mark, + "expected alphabetic or numeric character, but found %r" + % ch, self.get_mark(), + ) end_mark = self.get_mark() return TokenClass(value, start_mark, end_mark) @@ -941,9 +985,11 @@ def scan_tag(self): self.forward(2) suffix = self.scan_tag_uri('tag', start_mark) if self.peek() != '>': - raise ScannerError("while parsing a tag", start_mark, - "expected '>', but found %r" % self.peek(), - self.get_mark()) + raise ScannerError( + "while parsing a tag", start_mark, + "expected '>', but found %r" % self.peek(), + self.get_mark(), + ) self.forward() elif ch in '\0 \t\r\n\x85\u2028\u2029': handle = None @@ -967,8 +1013,10 @@ def scan_tag(self): suffix = self.scan_tag_uri('tag', start_mark) ch = self.peek() if ch not in '\0 \r\n\x85\u2028\u2029': - raise ScannerError("while scanning a tag", start_mark, - "expected ' ', but found %r" % ch, self.get_mark()) + raise ScannerError( + "while scanning a tag", start_mark, + "expected ' ', but found %r" % ch, self.get_mark(), + ) value = (handle, suffix) end_mark = self.get_mark() return TagToken(value, start_mark, end_mark) @@ -1017,14 +1065,14 @@ def scan_block_scalar(self, style): # Unfortunately, folding rules are ambiguous. # # This is the folding according to the specification: - + if folded and line_break == '\n' \ and leading_non_space and self.peek() not in ' \t': if not breaks: chunks.append(' ') else: chunks.append(line_break) - + # This is Clark Evans's interpretation (also in the spec # examples): # @@ -1046,8 +1094,10 @@ def scan_block_scalar(self, style): chunks.extend(breaks) # We are done. - return ScalarToken(''.join(chunks), False, start_mark, end_mark, - style) + return ScalarToken( + ''.join(chunks), False, start_mark, end_mark, + style, + ) def scan_block_scalar_indicators(self, start_mark): # See the specification for details. @@ -1064,16 +1114,20 @@ def scan_block_scalar_indicators(self, start_mark): if ch in '0123456789': increment = int(ch) if increment == 0: - raise ScannerError("while scanning a block scalar", start_mark, - "expected indentation indicator in the range 1-9, but found 0", - self.get_mark()) + raise ScannerError( + "while scanning a block scalar", start_mark, + "expected indentation indicator in the range 1-9, but found 0", + self.get_mark(), + ) self.forward() elif ch in '0123456789': increment = int(ch) if increment == 0: - raise ScannerError("while scanning a block scalar", start_mark, - "expected indentation indicator in the range 1-9, but found 0", - self.get_mark()) + raise ScannerError( + "while scanning a block scalar", start_mark, + "expected indentation indicator in the range 1-9, but found 0", + self.get_mark(), + ) self.forward() ch = self.peek() if ch in '+-': @@ -1084,9 +1138,11 @@ def scan_block_scalar_indicators(self, start_mark): self.forward() ch = self.peek() if ch not in '\0 \r\n\x85\u2028\u2029': - raise ScannerError("while scanning a block scalar", start_mark, - "expected chomping or indentation indicators, but found %r" - % ch, self.get_mark()) + raise ScannerError( + "while scanning a block scalar", start_mark, + "expected chomping or indentation indicators, but found %r" + % ch, self.get_mark(), + ) return chomping, increment def scan_block_scalar_ignored_line(self, start_mark): @@ -1098,9 +1154,11 @@ def scan_block_scalar_ignored_line(self, start_mark): self.forward() ch = self.peek() if ch not in '\0\r\n\x85\u2028\u2029': - raise ScannerError("while scanning a block scalar", start_mark, - "expected a comment or a line break, but found %r" % ch, - self.get_mark()) + raise ScannerError( + "while scanning a block scalar", start_mark, + "expected a comment or a line break, but found %r" % ch, + self.get_mark(), + ) self.scan_line_break() def scan_block_scalar_indentation(self): @@ -1152,8 +1210,10 @@ def scan_flow_scalar(self, style): chunks.extend(self.scan_flow_scalar_non_spaces(double, start_mark)) self.forward() end_mark = self.get_mark() - return ScalarToken(''.join(chunks), False, start_mark, end_mark, - style) + return ScalarToken( + ''.join(chunks), False, start_mark, end_mark, + style, + ) ESCAPE_REPLACEMENTS = { '0': '\0', @@ -1210,9 +1270,11 @@ def scan_flow_scalar_non_spaces(self, double, start_mark): self.forward() for k in range(length): if self.peek(k) not in '0123456789ABCDEFabcdef': - raise ScannerError("while scanning a double-quoted scalar", start_mark, - "expected escape sequence of %d hexadecimal numbers, but found %r" % - (length, self.peek(k)), self.get_mark()) + raise ScannerError( + "while scanning a double-quoted scalar", start_mark, + "expected escape sequence of %d hexadecimal numbers, but found %r" % + (length, self.peek(k)), self.get_mark(), + ) code = int(self.prefix(length), 16) chunks.append(chr(code)) self.forward(length) @@ -1220,8 +1282,10 @@ def scan_flow_scalar_non_spaces(self, double, start_mark): self.scan_line_break() chunks.extend(self.scan_flow_scalar_breaks(double, start_mark)) else: - raise ScannerError("while scanning a double-quoted scalar", start_mark, - "found unknown escape character %r" % ch, self.get_mark()) + raise ScannerError( + "while scanning a double-quoted scalar", start_mark, + "found unknown escape character %r" % ch, self.get_mark(), + ) else: return chunks @@ -1235,8 +1299,10 @@ def scan_flow_scalar_spaces(self, double, start_mark): self.forward(length) ch = self.peek() if ch == '\0': - raise ScannerError("while scanning a quoted scalar", start_mark, - "found unexpected end of stream", self.get_mark()) + raise ScannerError( + "while scanning a quoted scalar", start_mark, + "found unexpected end of stream", self.get_mark(), + ) elif ch in '\r\n\x85\u2028\u2029': line_break = self.scan_line_break() breaks = self.scan_flow_scalar_breaks(double, start_mark) @@ -1258,8 +1324,10 @@ def scan_flow_scalar_breaks(self, double, start_mark): prefix = self.prefix(3) if (prefix == '---' or prefix == '...') \ and self.peek(3) in '\0 \t\r\n\x85\u2028\u2029': - raise ScannerError("while scanning a quoted scalar", start_mark, - "found unexpected document separator", self.get_mark()) + raise ScannerError( + "while scanning a quoted scalar", start_mark, + "found unexpected document separator", self.get_mark(), + ) while self.peek() in ' \t': self.forward() if self.peek() in '\r\n\x85\u2028\u2029': @@ -1289,9 +1357,11 @@ def scan_plain(self): while True: ch = self.peek(length) if ch in '\0 \t\r\n\x85\u2028\u2029' \ - or (ch == ':' and - self.peek(length+1) in '\0 \t\r\n\x85\u2028\u2029' - + (u',[]{}' if self.flow_level else u''))\ + or ( + ch == ':' and + self.peek(length+1) in '\0 \t\r\n\x85\u2028\u2029' + + (u',[]{}' if self.flow_level else u'') + )\ or (self.flow_level and ch in ',?[]{}'): break length += 1 @@ -1351,8 +1421,10 @@ def scan_tag_handle(self, name, start_mark): # tag handles. I have allowed it anyway. ch = self.peek() if ch != '!': - raise ScannerError("while scanning a %s" % name, start_mark, - "expected '!', but found %r" % ch, self.get_mark()) + raise ScannerError( + "while scanning a %s" % name, start_mark, + "expected '!', but found %r" % ch, self.get_mark(), + ) length = 1 ch = self.peek(length) if ch != ' ': @@ -1362,8 +1434,10 @@ def scan_tag_handle(self, name, start_mark): ch = self.peek(length) if ch != '!': self.forward(length) - raise ScannerError("while scanning a %s" % name, start_mark, - "expected '!', but found %r" % ch, self.get_mark()) + raise ScannerError( + "while scanning a %s" % name, start_mark, + "expected '!', but found %r" % ch, self.get_mark(), + ) length += 1 value = self.prefix(length) self.forward(length) @@ -1390,8 +1464,10 @@ def scan_tag_uri(self, name, start_mark): self.forward(length) length = 0 if not chunks: - raise ScannerError("while parsing a %s" % name, start_mark, - "expected URI, but found %r" % ch, self.get_mark()) + raise ScannerError( + "while parsing a %s" % name, start_mark, + "expected URI, but found %r" % ch, self.get_mark(), + ) return ''.join(chunks) def scan_uri_escapes(self, name, start_mark): @@ -1402,9 +1478,11 @@ def scan_uri_escapes(self, name, start_mark): self.forward() for k in range(2): if self.peek(k) not in '0123456789ABCDEFabcdef': - raise ScannerError("while scanning a %s" % name, start_mark, - "expected URI escape sequence of 2 hexadecimal numbers, but found %r" - % self.peek(k), self.get_mark()) + raise ScannerError( + "while scanning a %s" % name, start_mark, + "expected URI escape sequence of 2 hexadecimal numbers, but found %r" + % self.peek(k), self.get_mark(), + ) codes.append(int(self.prefix(2), 16)) self.forward(2) try: diff --git a/addon/globalPlugins/MathCAT/yaml/serializer.py b/addon/globalPlugins/MathCAT/yaml/serializer.py index fe911e67..1962fb16 100644 --- a/addon/globalPlugins/MathCAT/yaml/serializer.py +++ b/addon/globalPlugins/MathCAT/yaml/serializer.py @@ -12,8 +12,10 @@ class Serializer: ANCHOR_TEMPLATE = 'id%03d' - def __init__(self, encoding=None, - explicit_start=None, explicit_end=None, version=None, tags=None): + def __init__( + self, encoding=None, + explicit_start=None, explicit_end=None, version=None, tags=None, + ): self.use_encoding = encoding self.use_explicit_start = explicit_start self.use_explicit_end = explicit_end @@ -48,8 +50,12 @@ def serialize(self, node): raise SerializerError("serializer is not opened") elif self.closed: raise SerializerError("serializer is closed") - self.emit(DocumentStartEvent(explicit=self.use_explicit_start, - version=self.use_version, tags=self.use_tags)) + self.emit( + DocumentStartEvent( + explicit=self.use_explicit_start, + version=self.use_version, tags=self.use_tags, + ), + ) self.anchor_node(node) self.serialize_node(node, None, None) self.emit(DocumentEndEvent(explicit=self.use_explicit_end)) @@ -86,26 +92,41 @@ def serialize_node(self, node, parent, index): detected_tag = self.resolve(ScalarNode, node.value, (True, False)) default_tag = self.resolve(ScalarNode, node.value, (False, True)) implicit = (node.tag == detected_tag), (node.tag == default_tag) - self.emit(ScalarEvent(alias, node.tag, implicit, node.value, - style=node.style)) + self.emit( + ScalarEvent( + alias, node.tag, implicit, node.value, + style=node.style, + ), + ) elif isinstance(node, SequenceNode): - implicit = (node.tag - == self.resolve(SequenceNode, node.value, True)) - self.emit(SequenceStartEvent(alias, node.tag, implicit, - flow_style=node.flow_style)) + implicit = ( + node.tag + == self.resolve(SequenceNode, node.value, True) + ) + self.emit( + SequenceStartEvent( + alias, node.tag, implicit, + flow_style=node.flow_style, + ), + ) index = 0 for item in node.value: self.serialize_node(item, node, index) index += 1 self.emit(SequenceEndEvent()) elif isinstance(node, MappingNode): - implicit = (node.tag - == self.resolve(MappingNode, node.value, True)) - self.emit(MappingStartEvent(alias, node.tag, implicit, - flow_style=node.flow_style)) + implicit = ( + node.tag + == self.resolve(MappingNode, node.value, True) + ) + self.emit( + MappingStartEvent( + alias, node.tag, implicit, + flow_style=node.flow_style, + ), + ) for key, value in node.value: self.serialize_node(key, node, None) self.serialize_node(value, node, key) self.emit(MappingEndEvent()) self.ascend_resolver() - diff --git a/addon/globalPlugins/MathCAT/yaml/tokens.py b/addon/globalPlugins/MathCAT/yaml/tokens.py index 4d0b48a3..f3111adf 100644 --- a/addon/globalPlugins/MathCAT/yaml/tokens.py +++ b/addon/globalPlugins/MathCAT/yaml/tokens.py @@ -4,11 +4,15 @@ def __init__(self, start_mark, end_mark): self.start_mark = start_mark self.end_mark = end_mark def __repr__(self): - attributes = [key for key in self.__dict__ - if not key.endswith('_mark')] + attributes = [ + key for key in self.__dict__ + if not key.endswith('_mark') + ] attributes.sort() - arguments = ', '.join(['%s=%r' % (key, getattr(self, key)) - for key in attributes]) + arguments = ', '.join([ + '%s=%r' % (key, getattr(self, key)) + for key in attributes + ]) return '%s(%s)' % (self.__class__.__name__, arguments) #class BOMToken(Token): @@ -30,8 +34,10 @@ class DocumentEndToken(Token): class StreamStartToken(Token): id = '' - def __init__(self, start_mark=None, end_mark=None, - encoding=None): + def __init__( + self, start_mark=None, end_mark=None, + encoding=None, + ): self.start_mark = start_mark self.end_mark = end_mark self.encoding = encoding @@ -101,4 +107,3 @@ def __init__(self, value, plain, start_mark, end_mark, style=None): self.start_mark = start_mark self.end_mark = end_mark self.style = style - diff --git a/addon/locale/pt_BR/LC_MESSAGES/nvda.po b/addon/locale/pt_BR/LC_MESSAGES/nvda.po new file mode 100644 index 00000000..ca89730f --- /dev/null +++ b/addon/locale/pt_BR/LC_MESSAGES/nvda.po @@ -0,0 +1,352 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2024-2025 NVDA Contributors. +# This file is distributed under the same license as the MathCAT package. +# Josevan Barbosa Fernandes , 2024-2025. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: MathCAT 0.3.3\n" +"Report-Msgid-Bugs-To: nvda-translations@groups.io\n" +"POT-Creation-Date: 2023-08-14 03:25+0000\n" +"PO-Revision-Date: 2025-05-10 17:54-0300\n" +"Last-Translator: Josevan Barbosa Fernandes \n" +"Language-Team: NVDA Brazilian Portuguese translation team (Equipe de " +"tradução do NVDA para Português Brasileiro) \n" +"Language: pt_BR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 3.6\n" + +#. Translators: this message directs users to look in the log file +msgid "Error in starting navigation of math: see NVDA error log for details" +msgstr "" +"Erro ao iniciar a navegação de matemática: consulte o registro de erros do " +"NVDA para obter detalhes" + +#. Translators: this message directs users to look in the log file +msgid "Error in brailling math: see NVDA error log for details" +msgstr "" +"Erro no brailling math: consulte o registro de erros do NVDA para obter " +"detalhes" + +#. Translators: this message directs users to look in the log file +msgid "Error in navigating math: see NVDA error log for details" +msgstr "" +"Erro na navegação de matemática: consulte o registro de erros do NVDA para " +"obter detalhes" + +#. Translators: Message to be announced during Keyboard Help +msgid "Copy navigation focus to clipboard" +msgstr "Copie o foco de navegação para a área de transferência" + +#. Translators: Name of the section in "Input gestures" dialog. +msgid "Clipboard" +msgstr "Área de transferência" + +#. Translators: copy to clipboard +msgid "copy as " +msgstr "compiar como " + +#. Translators: this message directs users to look in the log file +msgid "unable to copy math: see NVDA error log for details" +msgstr "" +"incapaz de copiar matemática : consulte o registro de erros do NVDA para " +"obter detalhes" + +#. Translators: this message directs users to look in the log file +msgid "MathCAT initialization failed: see NVDA error log for details" +msgstr "" +"Falha na inicialização do MathCAT: consulte o registro de erros do NVDA para " +"obter detalhes" + +#. Translators: this message directs users to look in the log file +msgid "Illegal MathML found: see NVDA error log for details" +msgstr "" +"MathML ilegal encontrado: consulte o registro de erros do NVDA para obter " +"detalhes" + +#. Translators: this message directs users to look in the log file +msgid "Error in speaking math: see NVDA error log for details" +msgstr "" +"Erro ao falar matemática: consulte o registro de erros do NVDA para obter " +"detalhes" + +#. Translators: menu item -- use the language of the voice chosen in the NVDA speech settings dialog +#. "Auto" == "Automatic" -- other items in menu are "English (en)", etc., so this matches that style +msgid "Use Voice's Language (Auto)" +msgstr "Usar o idioma da voz (automático)" + +#. Translators: this is a test string that is spoken. Only translate "the square root of x squared plus y squared" +msgid "" +"the square root of x squared plus y squared" +msgstr "" + +#. Translators: this is a test string that is spoken. Only translate "the fraction with numerator" +#. and other parts NOT inside '<.../>', +#, python-brace-format +msgid "" +"the fraction with numerator x to the n -th power plus 1 and " +"denominator x to the " +"n -" +"thpower minus 1 " +"end fraction " +msgstr "" + +#. Translators: title for MathCAT preferences dialog +msgid "MathCAT Preferences" +msgstr "Preferências do MathCAT" + +#. Translators: A heading that labels three navigation pane tab names in the MathCAT dialog +msgid "Categories:" +msgstr "Categorias:" + +#. Translators: these are navigation pane headings for the MathCAT preferences dialog under the title "Categories" +msgid "Speech" +msgstr "Fala" + +#. Translators: these are navigation pane headings for the MathCAT preferences dialog under the title "Categories" +msgid "Navigation" +msgstr "Navegação" + +#. Translators: these are navigation pane headings for the MathCAT preferences dialog under the title "Categories" +msgid "Braille" +msgstr "Braille" + +#. Translators: this is the text label for whom to target the speech for (options are below) +msgid "Generate speech for:" +msgstr "Gerar fala para:" + +#. Translators: these are the categories of impairments that MathCAT supports +#. Translators: Learning disabilities includes dyslexia and ADHD +msgid "Learning disabilities" +msgstr "Dificuldades de aprendizagem" + +#. Translators: target people who are blind +msgid "Blindness" +msgstr "Cegueira" + +#. Translators: target people who have low vision +msgid "Low vision" +msgstr "Baixa visão" + +#. Translators: label for pull down allowing users to choose the speech language for math +msgid "Language:" +msgstr "Idioma:" + +#. Translators: label for pull down to specify what character to use in numbers as the decimal separator +msgid "Decimal separator for numbers:" +msgstr "" + +#. Translators: options for decimal separator -- "Auto" = automatically pick the choice based on the language +msgid "Auto" +msgstr "" + +#. Translators: options for decimal separator -- "Custom" = user sets it +#. Currently there is no UI for how it is done yet, but eventually there will be a dialog that pops up to set it +msgid "Custom" +msgstr "Personalizado" + +#. Translators: label for pull down allowing users to choose the "style" (version, rules) of speech for math +msgid "Speech style:" +msgstr "Estilo de fala:" + +#. Translators: label for pull down to specify how verbose/terse the speech should be +msgid "Speech verbosity:" +msgstr "Verbosidade de fala:" + +#. Translators: options for speech verbosity -- "terse" = use less words +#. Translators: options for navigation verbosity -- "terse" = use less words +msgid "Terse" +msgstr "Fraco" + +#. Translators: options for speech verbosity -- "medium" = try to be nether too terse nor too verbose words +#. Translators: options for navigation verbosity -- "medium" = try to be nether too terse nor too verbose words +msgid "Medium" +msgstr "Médio" + +#. Translators: options for speech verbosity -- "verbose" = use more words +#. Translators: options for navigation verbosity -- "verbose" = use more words +msgid "Verbose" +msgstr "Verboso" + +#. Translators: label for slider that specifies a percentage of the normal speech rate that should be used for math +msgid "Relative speech rate:" +msgstr "Taxa de fala relativa:" + +#. Translators: label for slider that specifies relative factor to increase or decrease pauses in the math speech +msgid "Pause factor:" +msgstr "Fator de pausa:" + +#. Translators: label for check box controling a beep sound +msgid "Make a sound when starting/ending math speech" +msgstr "Fazer um som ao iniciar/terminar a fala em matemática" + +#. Translators: label for pull down to specify a subject area (Geometry, Calculus, ...) +msgid "Subject area to be used when it cannot be determined automatically:" +msgstr "" +"Área de assunto a ser usada quando não puder ser determinada automaticamente:" + +#. Translators: a generic (non-specific) math subject area +msgid "General" +msgstr "Geral" + +#. Translators: label for pull down to specify how verbose/terse the speech should be +msgid "Speech for chemical formulas:" +msgstr "Fala para fórmulas químicas:" + +#. Translators: values for chemistry options with example speech in parenthesis +msgid "Spell it out (H 2 O)" +msgstr "Soletre (H 2 O)" + +#. Translators: values for chemistry options with example speech in parenthesis (never interpret as chemistry) +msgid "Off (H sub 2 O)" +msgstr "" + +#. Translators: label for pull down to specify one of three modes use to navigate math expressions +msgid "Navigation mode to use when beginning to navigate an equation:" +msgstr "Modo de navegação a ser usado ao começar a navegar em uma equação:" + +#. Translators: names of different modes of navigation. "Enhanced" mode understands math structure +msgid "Enhanced" +msgstr "Aprimorado" + +#. Translators: "Simple" walks by character expect for things like fractions, roots, and scripts +msgid "Simple" +msgstr "Simples" + +#. Translators: "Character" moves around by character, automatically moving into fractions, etc +msgid "Character" +msgstr "Caractere" + +#. Translators: label for checkbox that controls whether any changes to the navigation mode should be preserved +msgid "Reset navigation mode on entry to an expression" +msgstr "Redefinir o modo de navegação ao entrar em uma expressão" + +#. Translators: label for pull down to specify whether the expression is spoken or described (an overview) +msgid "Navigation speech to use when beginning to navigate an equation:" +msgstr "Fala navegação a ser usado ao começar a navegar em uma equação:" + +#. Translators: "Speak" the expression after moving to it +msgid "Speak" +msgstr "Fala" + +#. Translators: "Describe" the expression after moving to it ("overview is a synonym") +msgid "Describe/overview" +msgstr "Descrição/visão geral" + +#. Translators: label for checkbox that controls whether any changes to the speak vs overview reading should be ignored +msgid "Reset navigation speech on entry to an expression" +msgstr "Redefinir a fala de navegação ao entrar em uma expressão" + +#. Translators: label for checkbox that controls whether arrow keys move out of fractions, etc., +#. or whether you have to manually back out of the fraction, etc. +msgid "Automatic zoom out of 2D notations" +msgstr "Redução automática do zoom de notações 2D" + +#. Translators: label for pull down to specify whether you want a terse or verbose reading of navigation commands +msgid "Speech amount for navigation:" +msgstr "Quantidade de fala para navegação:" + +#. Translators: label for pull down to specify how math will be copied to the clipboard +msgid "Copy math as:" +msgstr "Copiar matemática como:" + +#. Translators: options for Copy expression to clipboard as -- "MathML" +msgid "MathML" +msgstr "" + +#. Translators: options for Copy expression to clipboard as -- "LaTeX" +msgid "LaTeX" +msgstr "" + +#. Translators: options for Copy expression to clipboard as -- "ASCIIMath" +msgid "ASCIIMath" +msgstr "" + +#. Translators: label for pull down to specify which braille code to use +msgid "Braille math code for refreshable displays:" +msgstr "Código matemático em Braille para telas atualizáveis:" + +#. Translators: label for pull down to specify how braille dots should be modified when navigating/selecting subexprs +msgid "Highlight with dots 7 && 8 the current nav node:" +msgstr "" + +#. Translators: options for using dots 7 and 8: +#. Translators: "off" -- don't highlight +msgid "Off" +msgstr "Desligado" + +#. Translators: "First character" -- only the first character of the current navigation node uses dots 7 & 8 +msgid "First character" +msgstr "Primeiro caractere" + +#. Translators: "Endpoints" -- only the first and last character of the current navigation node uses dots 7 & 8 +msgid "Endpoints" +msgstr "Pontos finais" + +#. Translators: "All" -- all the characters for the current navigation node use dots 7 & 8 +msgid "All" +msgstr "Todo" + +#. Translators: dialog "ok" button +msgid "OK" +msgstr "Ok" + +#. Translators: dialog "cancel" button +msgid "Cancel" +msgstr "Cancelar" + +#. Translators: dialog "apply" button +msgid "Apply" +msgstr "Aplicar" + +#. Translators: button to reset all the preferences to their default values +msgid "Reset to defaults" +msgstr "Redefinir para os padrões" + +#. Translators: button to bring up a help page +msgid "Help" +msgstr "Ajuda" + +#. Translators: this show up in the NVDA preferences dialog. It opens the MathCAT preferences dialog +msgid "&MathCAT Settings..." +msgstr "Configurações do &MathCAT..." + +#. Add-on summary, usually the user visible name of the addon. +#. Translators: Summary for this add-on +#. to be shown on installation and add-on information found in Add-ons Manager. +msgid "MathCAT: speech and braille from MathML" +msgstr "" + +#. Translators: Long description to be shown for this add-on on add-on information from add-ons manager +msgid "" +"MathCAT is a replacement for MathPlayer which has been discontinued.\n" +" It provides speech and braille support, and also supports " +"MathPlayer's three modes of navigation.\n" +" The speech quality is not quite as good as MathPlayer's speech yet,\n" +" but the braille support is much better and includes support for " +"Nemeth, UEB Technical, CMU (Spanish/Portuguese),\n" +" and Vietnamese braille code standards. Translations to Chinese " +"(Traditional), Indonesian, Spanish, and Vietnamese exist\n" +" and other translations are in progress." +msgstr "" +"O MathCAT é um substituto para o MathPlayer, que foi descontinuado.\n" +" Ele oferece suporte a fala e braile e também suporta os três modos " +"de navegação do MathPlayer.\n" +" A qualidade da fala ainda não é tão boa quanto a do MathPlayer,\n" +" mas o suporte a braile é muito melhor e inclui suporte aos padrões de " +"código braile Nemeth, UEB Technical, CMU (espanhol/português),\n" +" e vietnamita. Existem traduções para chinês (tradicional), indonésio, " +"espanhol e vietnamita\n" +" e outras traduções estão em andamento." diff --git a/build.rs b/build.rs index 6e40b59f..ac4f5757 100644 --- a/build.rs +++ b/build.rs @@ -13,4 +13,4 @@ fn main() { let mut zip_archive = ZipArchive::new(archive).unwrap(); zip_archive.extract(&location).expect("Zip extraction failed"); -} \ No newline at end of file +} diff --git a/buildVars.py b/buildVars.py index 96669070..06b82b1e 100644 --- a/buildVars.py +++ b/buildVars.py @@ -10,49 +10,49 @@ # To avoid initializing translations in this module we simply roll our own "fake" `_` function # which returns whatever is given to it as an argument. def _(arg): - return arg + return arg # Add-on information variables addon_info = { - # add-on Name/identifier, internal for NVDA - "addon_name": "MathCAT", - # Add-on summary, usually the user visible name of the addon. - # Translators: Summary for this add-on - # to be shown on installation and add-on information found in Add-ons Manager. - "addon_summary": _("MathCAT: speech and braille from MathML"), - # Add-on description - "addon_description": _( - # Translators: Long description to be shown for this add-on on add-on information from add-ons manager - """MathCAT is a replacement for MathPlayer which has been discontinued. + # add-on Name/identifier, internal for NVDA + "addon_name": "MathCAT", + # Add-on summary, usually the user visible name of the addon. + # Translators: Summary for this add-on + # to be shown on installation and add-on information found in Add-ons Manager. + "addon_summary": _("MathCAT: speech and braille from MathML"), + # Add-on description + "addon_description": _( + # Translators: Long description to be shown for this add-on on add-on information from add-ons manager + """MathCAT is a replacement for MathPlayer which has been discontinued. It provides speech and braille support, and also supports MathPlayer's three modes of navigation. The speech quality is not quite as good as MathPlayer's speech yet, but the braille support is much better and includes support for Nemeth, UEB Technical, CMU (Spanish/Portuguese), and Vietnamese braille code standards. Translations to Chinese (Traditional), Indonesian, Spanish, and Vietnamese exist - and other translations are in progress.""" - ), - # version - "addon_version": "0.6.9", - # Author(s) - "addon_author": "Neil Soiffer ", - # URL for the add-on documentation support - "addon_url": "https://nsoiffer.github.io/MathCAT/", - # URL for the add-on repository where the source code can be found - "addon_sourceURL": "https://github.com/NSoiffer/MathCATForPython", - # Documentation file name - "addon_docFileName": "readme.html", - # Minimum NVDA version supported (e.g. "2018.3.0", minor version is optional) - "addon_minimumNVDAVersion": "2024.1", - # Last NVDA version supported/tested (e.g. "2018.4.0", ideally more recent than minimum version) - "addon_lastTestedNVDAVersion": "2025.1", - # Add-on update channel (default is None, denoting stable releases, - # and for development releases, use "dev".) - # Do not change unless you know what you are doing! - "addon_updateChannel": "dev", - # Add-on license such as GPL 2 - "addon_license": "MIT and GPL 2", - # URL for the license document the ad-on is licensed under - "addon_licenseURL": "https://raw.githubusercontent.com/NSoiffer/MathCAT/main/LICENSE", + and other translations are in progress.""", + ), + # version + "addon_version": "0.6.9", + # Author(s) + "addon_author": "Neil Soiffer ", + # URL for the add-on documentation support + "addon_url": "https://nsoiffer.github.io/MathCAT/", + # URL for the add-on repository where the source code can be found + "addon_sourceURL": "https://github.com/NSoiffer/MathCATForPython", + # Documentation file name + "addon_docFileName": "readme.html", + # Minimum NVDA version supported (e.g. "2018.3.0", minor version is optional) + "addon_minimumNVDAVersion": "2024.1", + # Last NVDA version supported/tested (e.g. "2018.4.0", ideally more recent than minimum version) + "addon_lastTestedNVDAVersion": "2025.1", + # Add-on update channel (default is None, denoting stable releases, + # and for development releases, use "dev".) + # Do not change unless you know what you are doing! + "addon_updateChannel": "dev", + # Add-on license such as GPL 2 + "addon_license": "MIT and GPL 2", + # URL for the license document the ad-on is licensed under + "addon_licenseURL": "https://raw.githubusercontent.com/NSoiffer/MathCAT/main/LICENSE", } # Define the python files that are the sources of your add-on. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..7dae399e --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,210 @@ +[build-system] +requires = ["setuptools~=72.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "MathCATForPython" +dynamic = ["version"] +description = "Math Capable Assistive Technology (MathCAT) Python bindings and addon for NVDA" +maintainers = [ + {name = "Neil Soiffer", email = "soiffer@alum.nit.edu"}, +] +requires-python = ">=3.11,<3.12" +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: End Users/Desktop", + "License :: OSI Approved :: GNU General Public License v2", + "Operating System :: Microsoft :: Windows", + "Programming Language :: Python :: 3", + "Topic :: Accessibility", +] +readme = "readme.md" +license = {file = "LICENSE"} +dependencies = [ + "wxPython==4.2.2", +] + +[project.urls] +Repository = "http://github.com/NSoiffer/MathCATForPython" + +[tool.ruff] +line-length = 110 + +builtins = [ + # translation lookup + "_", + # translation lookup + "ngettext", + # translation lookup + "pgettext", + # translation lookup + "npgettext", +] + +include = [ + "*.py", + "sconstruct", +] + +exclude = [ + ".git", + "__pycache__", + ".venv", + "./addon/globalPlugins/MathCAT/yaml/*", +] + +[tool.ruff.format] +indent-style = "tab" +line-ending = "lf" + +[tool.ruff.lint.mccabe] +max-complexity = 15 + +[tool.ruff.lint] +ignore = [ + # indentation contains tabs + "W191", +] +logger-objects = ["logHandler.log"] + +[tool.ruff.lint.per-file-ignores] +# sconscripts contains many inbuilt functions not recognised by the lint, +# so ignore F821. +"sconstruct" = ["F821"] + +[tool.pyright] +venvPath = ".venv" +venv = "." +pythonPlatform = "Windows" +typeCheckingMode = "strict" + +include = [ + "**/*.py", +] + +exclude = [ + "sconstruct", + ".git", + "__pycache__", + ".venv", + # When excluding concrete paths relative to a directory, + # not matching multiple folders by name e.g. `__pycache__`, + # paths are relative to the configuration file. +] + +# Tell pyright where to load python code from +extraPaths = [ + "./addon", +] + +# General config +analyzeUnannotatedFunctions = true +deprecateTypingAliases = true + +# Stricter typing +strictParameterNoneValue = true +strictListInference = true +strictDictionaryInference = true +strictSetInference = true + +# Compliant rules +reportAssertAlwaysTrue = true +reportAssertTypeFailure = true +reportDuplicateImport = true +reportIncompleteStub = true +reportInconsistentOverload = true +reportInconsistentConstructor = true +reportInvalidStringEscapeSequence = true +reportInvalidStubStatement = true +reportInvalidTypeVarUse = true +reportMatchNotExhaustive = true +reportMissingModuleSource = true +reportMissingImports = false # temporarily setting to false for MathCAT integration +reportNoOverloadImplementation = true +reportOptionalContextManager = true +reportOverlappingOverload = true +reportPrivateImportUsage = true +reportPropertyTypeMismatch = true +reportSelfClsParameterName = true +reportShadowedImports = true +reportTypeCommentUsage = true +reportTypedDictNotRequiredAccess = true +reportUndefinedVariable = true +reportUnusedExpression = true +reportUnboundVariable = true +reportUnhashable = true +reportUnnecessaryCast = true +reportUnnecessaryContains = true +reportUnnecessaryTypeIgnoreComment = true +reportUntypedClassDecorator = true +reportUntypedFunctionDecorator = true +reportUnusedClass = true +reportUnusedCoroutine = true +reportUnusedExcept = true + +# Should switch to true when possible +reportDeprecated = false + +# Can be enabled by generating type stubs for modules via pyright CLI +reportMissingTypeStubs = false + +reportUnsupportedDunderAll = false +reportAbstractUsage = false +reportUntypedBaseClass = false +reportOptionalIterable = false +reportCallInDefaultInitializer = false +reportInvalidTypeArguments = false +reportUntypedNamedTuple = false +reportRedeclaration = false +reportOptionalCall = false +reportConstantRedefinition = false +reportWildcardImportFromLibrary = false +reportIncompatibleVariableOverride = false +reportInvalidTypeForm = false +reportGeneralTypeIssues = false +reportOptionalOperand = false +reportUnnecessaryComparison = false +reportFunctionMemberAccess = false +reportUnnecessaryIsInstance = false +reportUnusedFunction = false +reportImportCycles = false +reportUnusedImport = false +reportUnusedVariable = false +reportOperatorIssue = false +reportAssignmentType = false +reportReturnType = false +reportPossiblyUnboundVariable = false +reportMissingSuperCall = false +reportUninitializedInstanceVariable = false +reportUnknownLambdaType = false +reportMissingTypeArgument = false +reportImplicitStringConcatenation = false +reportIncompatibleMethodOverride = false +reportPrivateUsage = false +reportUnusedCallResult = false +reportOptionalSubscript = false +reportCallIssue = false +reportOptionalMemberAccess = false +reportImplicitOverride = false +reportIndexIssue = false +reportAttributeAccessIssue = false +reportArgumentType = false +reportUnknownParameterType = false +reportMissingParameterType = false +reportUnknownVariableType = false +reportUnknownArgumentType = false +reportUnknownMemberType = false + +[dependency-groups] +dev = [ + "SCons==4.8.1", + "setuptools~=72.0", +] +lint = [ + "ruff==0.8.1", + "pre-commit==4.0.1", + "pyright==1.1.396", +] + +[tool.setuptools.dynamic] +version = {attr = "buildVersion.version_detailed"} diff --git a/pyrightconfig.json b/pyrightconfig.json deleted file mode 100644 index 571cd01a..00000000 --- a/pyrightconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "typeCheckingMode": "basic", - "reportMissingImports" : false, - "include": [ - "addon/globalPlugins/MathCAT" - ], - "exclude": [ - "**/__pycache__", - "addon/globalPlugins/MathCAT/yaml" - ], -} \ No newline at end of file diff --git a/readme.md b/readme.md index d9727161..476c0594 100644 --- a/readme.md +++ b/readme.md @@ -62,7 +62,7 @@ Lots of changes because it has been a while since the last official release. * Added inference for cross-product and dot-product * Added inference for div, grad, and curl * Added special speech for zero, identity, and diagonal matrices in English -* Be more restrictive when inferring a table +* Be more restrictive when inferring a table * Changed speech for the general cases of `mover` and `munder` from "modified x with y above it" to "quantity x with y above it" * Improved rule for {} so that it isn't always spoken as "set of ...". It could just be bracketing chars. * Tweaked the speech for ∈ inside of a set so that the word "is" is dropped when part of a set -- "the set of all x is an element of ..." sounds poor. @@ -147,7 +147,7 @@ Lots of changes because it has been a while since the last official release. * Fixed language switching when voice changes and MathCAT language is "Auto" * Added more checks for $Impairments to improve reading when it is not set for those who are blind * Nemeth: fix for "~" when it isn't part of an mrow -* UEB: character additions, "~" spacing fix if prefix, xor fix, +* UEB: character additions, "~" spacing fix if prefix, xor fix, * MathML cleanup for accented vowels (mainly for Vietnamese) * Major rewrite of preference reading/updating code with big speedup -- added `CheckRuleFiles` pref to control which files are checked for updates * Added two new interface calls -- enables setting the navigaton location from the braille cursor (not part of MathCAT addon yet) @@ -212,4 +212,3 @@ Note: there is now an option to get Vietnam's braille standard as braille output * A preference setting to control the duration of pausing (works with changes to relative speech rate for math) * Support to recognize chemistry notation and speak it appropriately * Translations to Indonesian and Vietnamese - diff --git a/site_scons/site_tools/gettexttool/__init__.py b/site_scons/site_tools/gettexttool/__init__.py index 81099f59..3e8af277 100644 --- a/site_scons/site_tools/gettexttool/__init__.py +++ b/site_scons/site_tools/gettexttool/__init__.py @@ -1,4 +1,4 @@ -""" This tool allows generation of gettext .mo compiled files, pot files from source code files +"""This tool allows generation of gettext .mo compiled files, pot files from source code files and pot files for merging. Three new builders are added into the constructed environment: @@ -15,44 +15,46 @@ """ + from SCons.Action import Action def exists(env): - return True + return True XGETTEXT_COMMON_ARGS = ( - "--msgid-bugs-address='$gettext_package_bugs_address' " - "--package-name='$gettext_package_name' " - "--package-version='$gettext_package_version' " - "--keyword=pgettext:1c,2 " - "-c -o $TARGET $SOURCES" + "--msgid-bugs-address='$gettext_package_bugs_address' " + "--package-name='$gettext_package_name' " + "--package-version='$gettext_package_version' " + "--keyword=pgettext:1c,2 " + "-c -o $TARGET $SOURCES" ) def generate(env): - env.SetDefault(gettext_package_bugs_address="example@example.com") - env.SetDefault(gettext_package_name="") - env.SetDefault(gettext_package_version="") - - env["BUILDERS"]["gettextMoFile"] = env.Builder( - action=Action("msgfmt -o $TARGET $SOURCE", "Compiling translation $SOURCE"), - suffix=".mo", - src_suffix=".po", - ) - - env["BUILDERS"]["gettextPotFile"] = env.Builder( - action=Action( - "xgettext " + XGETTEXT_COMMON_ARGS, "Generating pot file $TARGET" - ), - suffix=".pot", - ) - - env["BUILDERS"]["gettextMergePotFile"] = env.Builder( - action=Action( - "xgettext " + "--omit-header --no-location " + XGETTEXT_COMMON_ARGS, - "Generating pot file $TARGET", - ), - suffix=".pot", - ) + env.SetDefault(gettext_package_bugs_address="example@example.com") + env.SetDefault(gettext_package_name="") + env.SetDefault(gettext_package_version="") + + env["BUILDERS"]["gettextMoFile"] = env.Builder( + action=Action("msgfmt -o $TARGET $SOURCE", "Compiling translation $SOURCE"), + suffix=".mo", + src_suffix=".po", + ) + + env["BUILDERS"]["gettextPotFile"] = env.Builder( + action=Action( + "xgettext " + XGETTEXT_COMMON_ARGS, + "Generating pot file $TARGET", + ), + suffix=".pot", + ) + + env["BUILDERS"]["gettextMergePotFile"] = env.Builder( + action=Action( + "xgettext " + "--omit-header --no-location " + XGETTEXT_COMMON_ARGS, + "Generating pot file $TARGET", + ), + suffix=".pot", + ) diff --git a/src/lib.rs b/src/lib.rs index d02b74ad..e53a7fa3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,7 +2,7 @@ //! 1. whatever preferences the AT needs to set, it is done with calls to [`SetPreference`]. //! 2. the MathML is sent over via [`SetMathML`]. //! 3. AT calls to get the speech [`GetSpokenText`] and calls [`GetBraille`] to get the (Unicode) braille. -//! +//! //! Navigation can be done via calls to either: //! * [`DoNavigateKeyPress`] (takes key events as input) //! * [`DoNavigateCommand`] (takes the commands the key events internally map to) @@ -14,7 +14,7 @@ //! * [`GetNavigationMathML`] -- returns a string representing the MathML for the selected node //! Note: a second integer is returned. This is the offset in characters for a leaf node. //! This is needed when navigating by character for multi-symbol leaf nodes such as "sin" and "1234" -//! +//! //! It is also possible to find out what preferences are currently set by calling [`GetPreference`] //! //! AT can pass key strokes to allow a user to navigate the MathML by calling [`DoNavigateKeyPress`]; the speech is returned. @@ -97,7 +97,7 @@ pub fn GetPreference(_py: Python, name: String) -> PyResult { #[pyfunction] /// Get the braille associated with the MathML node with a given id (MathML set by `SetMathML`]). /// An empty string can be used to return the braille associated with the entire expression. -/// +/// /// The braille returned depends upon the preference for braille output. pub fn GetBraille(_py: Python, nav_node_id: String) -> PyResult { return convert_error( get_braille(nav_node_id) ); @@ -106,7 +106,7 @@ pub fn GetBraille(_py: Python, nav_node_id: String) -> PyResult { #[pyfunction] /// Get the braille associated with the MathML node with a given id (MathML set by `SetMathML`]). /// An empty string can be used to return the braille associated with the entire expression. -/// +/// /// The braille returned depends upon the preference for braille output. pub fn GetNavigationBraille(_py: Python) -> PyResult { return convert_error( get_navigation_braille() ); @@ -124,17 +124,17 @@ pub fn DoNavigateKeyPress(_py: Python, key: usize, shift_key: bool, control_key: /// Given a command, the current node is moved accordingly (or value reported in some cases). /// /// The spoken text for the new current node is returned. -/// +/// /// The list of legal commands are: -/// "MovePrevious", "MoveNext", "MoveStart", "MoveEnd", "MoveLineStart", "MoveLineEnd", -/// "MoveCellPrevious", "MoveCellNext", "MoveCellUp", "MoveCellDown", "MoveColumnStart", "MoveColumnEnd", -/// "ZoomIn", "ZoomOut", "ZoomOutAll", "ZoomInAll", -/// "MoveLastLocation", -/// "ReadPrevious", "ReadNext", "ReadCurrent", "ReadCellCurrent", "ReadStart", "ReadEnd", "ReadLineStart", "ReadLineEnd", -/// "DescribePrevious", "DescribeNext", "DescribeCurrent", -/// "WhereAmI", "WhereAmIAll", -/// "ToggleZoomLockUp", "ToggleZoomLockDown", "ToggleSpeakMode", -/// "Exit", +/// "MovePrevious", "MoveNext", "MoveStart", "MoveEnd", "MoveLineStart", "MoveLineEnd", +/// "MoveCellPrevious", "MoveCellNext", "MoveCellUp", "MoveCellDown", "MoveColumnStart", "MoveColumnEnd", +/// "ZoomIn", "ZoomOut", "ZoomOutAll", "ZoomInAll", +/// "MoveLastLocation", +/// "ReadPrevious", "ReadNext", "ReadCurrent", "ReadCellCurrent", "ReadStart", "ReadEnd", "ReadLineStart", "ReadLineEnd", +/// "DescribePrevious", "DescribeNext", "DescribeCurrent", +/// "WhereAmI", "WhereAmIAll", +/// "ToggleZoomLockUp", "ToggleZoomLockDown", "ToggleSpeakMode", +/// "Exit", /// "MoveTo0","MoveTo1","MoveTo2","MoveTo3","MoveTo4","MoveTo5","MoveTo6","MoveTo7","MoveTo8","MoveTo9", /// "Read0","Read1","Read2","Read3","Read4","Read5","Read6","Read7","Read8","Read9", /// "Describe0","Describe1","Describe2","Describe3","Describe4","Describe5","Describe6","Describe7","Describe8","Describe9", @@ -192,4 +192,4 @@ mod py_tests { Err(e) => panic!("Error remains {}", e.to_string()), } } -} \ No newline at end of file +}