Skip to content

Restore sentence as the default say all reading unit, with a setting to read by paragraph or line#20467

Open
LeonarddeR wants to merge 9 commits into
nvaccess:masterfrom
LeonarddeR:sayAllReadingUnit
Open

Restore sentence as the default say all reading unit, with a setting to read by paragraph or line#20467
LeonarddeR wants to merge 9 commits into
nvaccess:masterfrom
LeonarddeR:sayAllReadingUnit

Conversation

@LeonarddeR

@LeonarddeR LeonarddeR commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Link to issue number:

Related to #13420, #9179 and #13971
Fixes #13829 by making paragraph available as an additional unit, so say all can send whole paragraphs to the synthesizer.

Summary of the issue:

For rich text controls where UNIT_READINGCHUNK was previously mapped to line (ITextDocumentTextInfo, e.g. WordPad, NVDA's log viewer, and third-party apps such as Paperback), say all's caret/review position hardly updates when word wrap is off and a paragraph spans many visual lines — the position only advances once the whole (very long) paragraph line is read. This was introduced by #13971, which changed the reading unit from sentence to line for these controls to fix caret tracking in other apps (#13420, #9179).

This PR effectively reverts the default introduced by #13971: say all now reads by sentence again by default wherever sentence boundaries are supported, restoring smoother caret tracking in rich edit controls with long paragraphs. To avoid regressing the caret-tracking fix #13971 provided for apps/languages without usable sentence boundaries, the choice is no longer hardcoded: a new speech setting lets users switch back to line-based reading, re-enabling the behavior @lukaszgo1 introduced in #13971 where they need it. A paragraph unit is also available, which sends larger chunks of text to the synthesizer so it can determine sentence boundaries (and e.g. abbreviations like "Mr.") itself (#13829).

Description of user facing changes:

  • New "Say all reads by" combo box in the Speech settings panel, with three options:
  • Say all now reads by sentence by default in Rich Edit controls such as WordPad and NVDA's log viewer.

Description of developer facing changes:

  • Added SayAllReadingUnitFlag feature flag (config.featureFlagEnums) with a [speech] sayAllReadingUnit configSpec entry, behaviorOfDefault="sentence".
  • Added TextInfo.unit_readingChunk, resolving textInfos.UNIT_READINGCHUNK to UNIT_SENTENCE/UNIT_PARAGRAPH/UNIT_LINE based on the flag's calculated value; cached per core pump (_cache_unit_readingChunk) to avoid repeated config lookups in the say all loop.
  • Added TextInfo._resolveReadingChunkUnit, used by winword.py/edit.py expand/move to substitute the resolved unit for UNIT_READINGCHUNK before their static unit-map lookup; the now-unreachable UNIT_READINGCHUNK entries were removed from those maps.
  • OffsetsTextInfo._getReadingChunkOffsets dispatches self.unit_readingChunk through the existing _getUnitOffsets, falling back to line offsets if the resolved unit raises NotImplementedError.
  • UIA-based TextInfos are unaffected: UIAHandler.NVDAUnitsToUIAUnits hardcodes UNIT_READINGCHUNK to TextUnit_Line, since UIA's TextPattern has no sentence text unit. This means modern UIA Word (NVDAObjects/UIA/wordDocument.py) already reads by line for say all regardless of this flag (the paragraph option is likewise ignored there for now); only legacy Word Object Model Word (winword.py, wdSentence) is driven by the flag.

Description of development approach:

Introduced the reading unit as a feature flag rather than hardcoding a default, so the sentence/line choice from #13971 remains available per user preference instead of being an all-or-nothing global default. SayAllReadingUnitFlag avoids a module-level import textInfos in featureFlagEnums.py, since that module is imported during config package init, before languageHandler installs the _ gettext builtin, and textInfos transitively calls _() at import time.

Testing strategy:

  • Added unit tests for _getReadingChunkOffsets covering the sentence, paragraph, line, and unsupported-unit-falls-back-to-line cases.
  • Manually verified say all in WordPad and NVDA's log viewer with both settings.

Known issues with pull request:

This PR defaults the flag to sentence, effectively reverting #13971's default. If reviewers decide line should remain the default (per #13971's rationale for #13420/#9179), the fix is a one-line change: flip behaviorOfDefault in the sayAllReadingUnit configSpec entry (source/config/configSpec.py) from "sentence" to "line". Note this would also move Word Object Model Word (winword.py, legacy Word) to line-based reading — it currently reads by sentence unconditionally on master (wdSentence, unaffected by #13971) and is now driven by the same flag as WordPad/log viewer. I consider this acceptable: a single uniform default across all UNIT_READINGCHUNK-capable TextInfos is preferable to Word Object Model Word and rich-edit controls disagreeing on reading unit by default. Modern UIA Word is unaffected by the default either way — it already reads by line unconditionally (see developer facing changes).

Code Review Checklist:

  • Documentation:
    • Change log entry
    • User Documentation
    • Developer / Technical Documentation
    • Context sensitive help for GUI changes
  • Testing:
    • Unit tests
    • System (end to end) tests
    • Manual testing
  • UX of all users considered:
    • Speech
    • Braille
    • Low Vision
    • Different web browsers
    • Localization in other languages / culture than English
  • API is compatible with existing add-ons.
  • Security precautions taken.

LeonarddeR and others added 6 commits July 8, 2026 19:43
Add a SayAllReadingUnitFlag feature flag controlling the text unit
say all advances by (the reading chunk), with an explicit DEFAULT
whose behavior is sentence, plus the [speech] configSpec entry.

Enum values are inlined string literals ('sentence'/'line') equal to
textInfos.UNIT_SENTENCE/UNIT_LINE. A module-level 'import textInfos'
was avoided: featureFlagEnums is imported during config package init,
before languageHandler installs the '_' gettext builtin, and textInfos
pulls in controlTypes.formatFields which calls _() at module load,
which would crash startup.
…le TextInfos

Make each sentence-capable TextInfo resolve UNIT_READINGCHUNK to the
effective value of the sayAllReadingUnit feature flag (sentence or line),
keeping line as the automatic fallback where sentence is unsupported:

- textInfos/offsets.py: _getReadingChunkOffsets tries sentence offsets when
  the flag resolves to sentence, falling back to line on NotImplementedError.
- NVDAObjects/window/winword.py and edit.py: substitute the calculated unit
  for UNIT_READINGCHUNK before the static unit-map lookup in expand/move.
- NVDAObjects/IAccessible/MSHTML.py: replace the READINGCHUNK->SENTENCE
  remaps in expand/move with the calculated unit; preserve the existing
  LINE->SENTENCE behaviour in move.

Add unit tests for _getReadingChunkOffsets covering sentence, line, and the
NotImplementedError line fallback.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a FeatureFlagCombo for the speech.sayAllReadingUnit feature flag to
SpeechSettingsPanel, with context help binding and onSave wiring.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a userGuide entry (anchored SpeechSettingsSayAllReadingUnit for
context help) and a changes.md New Features entry.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ies, guard enum

- MSHTML: revert the READINGCHUNK resolution. IHTMLTxtRange::move cannot move
  by line (the original mapped both READINGCHUNK and LINE to sentence for that
  reason), so honouring a "line" flag value there made say all's move soft-fail
  and stop after the first chunk. Legacy MSHTML keeps reading by sentence.
- winword.py / edit.py: remove the now-unreachable UNIT_READINGCHUNK entries from
  the static unit maps (READINGCHUNK is resolved to sentence/line before the map
  lookup in expand/move).
- Add a unit test guarding that SayAllReadingUnitFlag's inlined "sentence"/"line"
  values stay equal to textInfos.UNIT_SENTENCE/UNIT_LINE.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@LeonarddeR

LeonarddeR commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator Author

Also mentioning @cary-rowen here, since he was particularly involved with #13420 debugging.
Personally I'm using Paperback a lot lately and it is a real thorn in my eye that for long paragraphs, caret doesn't follow per sentence. I tend to disable word wrap when studying because it makes skimming a load more difficult.
Would the solution filed here be acceptable for you?
I have considered filing an issue first, but given how efficient AI is these days, I figured that it would be more helpful to provide a prototype so we can test the implementation while discussing😉

- new SayAllReadingUnitFlag.PARAGRAPH ("Prefer paragraph"), resolved
  via unit_readingChunk with line fallback where unsupported
- hoist duplicated UNIT_READINGCHUNK resolution from edit/winword
  expand/move into TextInfo._resolveReadingChunkUnit
- _getReadingChunkOffsets reuses _getUnitOffsets dispatch
- cache unit_readingChunk per core pump to avoid config reads in the
  say all hot loop
"Prefer sentence" etc. read oddly after the "Say all reads by" label;
options now complete the label as a sentence. Paragraph and line need
no qualifier since they are always supported.
@LeonarddeR LeonarddeR changed the title Restore sentence as the default say all reading unit, with a setting to prefer line Restore sentence as the default say all reading unit, with a setting to read by paragraph or line Jul 10, 2026
@LeonarddeR LeonarddeR marked this pull request as ready for review July 10, 2026 19:23
@LeonarddeR LeonarddeR requested review from a team as code owners July 10, 2026 19:23

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR reintroduces sentence-based “Say All” as the default reading unit (where supported) and makes the reading-chunk unit user-configurable via a new Speech setting, including a paragraph option to allow larger chunks to be sent to synthesizers.

Changes:

  • Added a new “Say all reads by” Speech setting (feature flag-backed) with options for sentence (where possible), paragraph, and line.
  • Updated TextInfo implementations to resolve UNIT_READINGCHUNK dynamically based on the configured reading unit, including support in legacy Word (winword) and Rich Edit (ITextDocument).
  • Added unit tests for reading-chunk offsets resolution and fallback-to-line behavior when a unit isn’t implemented.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
user_docs/en/userGuide.md Documents the new Speech setting in the User Guide.
user_docs/en/changes.md Adds a changelog entry for the new setting / default behavior change.
tests/unit/test_textInfos.py Adds unit tests for reading-chunk offset behavior across configured units.
source/textInfos/offsets.py Resolves reading-chunk offsets via the configured unit with fallback to line when unsupported.
source/textInfos/init.py Introduces unit_readingChunk and unit resolution helper for UNIT_READINGCHUNK.
source/NVDAObjects/window/winword.py Resolves UNIT_READINGCHUNK before mapping units to Word units.
source/NVDAObjects/window/edit.py Resolves UNIT_READINGCHUNK before mapping units to TOM units.
source/config/configSpec.py Adds the new [speech] sayAllReadingUnit feature-flag config entry.

Comment on lines +571 to +574
try:
return self._getUnitOffsets(self.unit_readingChunk, offset)
except NotImplementedError:
return self._getLineOffsets(offset)
Comment thread user_docs/en/userGuide.md

This combo box lets you choose the unit of text that say all (continuous reading) advances by.
When set to "Sentence where possible", NVDA reads sentence by sentence in controls and documents that support sentence boundaries, and automatically falls back to reading by line where sentence boundaries are not supported.
When set to "Paragraph", NVDA reads paragraph by paragraph.
Comment thread user_docs/en/changes.md
* In modes that show a continuation mark, when a word is cut across rows, the last cell of the row now shows a continuation mark (braille dots 7-8) so it is clear that the word continues on the next row.
* The "At word or syllable boundaries" option uses hyphenation dictionaries to split long words at syllable boundaries when they do not fit on the display.
* Magnifier: A new unassigned command has been added to move the mouse cursor to the center of the magnified view. (#20127, @CyrilleB79)
* A new "Say all reads by" speech setting lets you choose whether say all reads by sentence, paragraph or line; say all now reads by sentence by default where supported. (#13420, #9179, #13971, @LeonarddeR)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Say all pauses in the wrong places

2 participants