Skip to content
Open
Show file tree
Hide file tree
Changes from 48 commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
5b411ab
Add ability to scroll automatically with braille
nvdaes Oct 21, 2025
ab1056e
Add ability to scroll automatically in braille
nvdaes Oct 21, 2025
098bedc
Pre-commit auto-fix
pre-commit-ci[bot] Oct 21, 2025
b997c66
Beep instead of speaking message when scrolling is enabled, to avoid …
nvdaes Oct 22, 2025
7e89c2c
Merge branch 'brailleScroll' of https://github.com/nvdaes/nvda into b…
nvdaes Oct 22, 2025
d380d88
Added edit box to set the interval in seconds for autoScroll
nvdaes Oct 22, 2025
01e4f87
Convert to milliseconds autoScrollTimeout
nvdaes Oct 22, 2025
79d83f9
Add combo box for autoScroll intervals
nvdaes Oct 24, 2025
c3c7e47
Merge branch 'master' into brailleScroll
nvdaes Oct 24, 2025
f033be7
Save autoScroll interval option
nvdaes Oct 24, 2025
5b6fca5
Pre-commit auto-fix
pre-commit-ci[bot] Oct 24, 2025
572f1af
Use option for autoScrollInterval in config
nvdaes Oct 24, 2025
e1e8b70
Add commands to increase and decrease autoScroll
nvdaes Oct 25, 2025
7ab4eb2
Pre-commit auto-fix
pre-commit-ci[bot] Oct 25, 2025
3d60cd1
Update coment
nvdaes Oct 25, 2025
43335b5
Merge remote-tracking branch 'origin/master' into brailleScroll
nvdaes Oct 25, 2025
6873bae
Use oneShot in autoScroll
nvdaes Oct 25, 2025
17d52c6
Fix pyright analysis locally
nvdaes Oct 25, 2025
3463e1d
Merge remote-tracking branch 'origin/master' into brailleScroll
nvdaes Oct 30, 2025
a1a748c
Remove combo box to select how to calculate the autoScroll interval
nvdaes Oct 30, 2025
22515bb
Pre-commit auto-fix
pre-commit-ci[bot] Oct 30, 2025
5a26e26
Fix max and min timeout
nvdaes Oct 30, 2025
96bab6d
Merge branch 'brailleScroll' of https://github.com/nvdaes/nvda into b…
nvdaes Oct 30, 2025
ca22023
Pre-commit auto-fix
pre-commit-ci[bot] Oct 30, 2025
6920874
Merge branch 'master' into brailleScroll
nvdaes Nov 17, 2025
59b1694
Merge branch 'master' into brailleScroll
nvdaes Nov 18, 2025
a1b816b
Update braille.py
nvdaes Nov 18, 2025
55cd1bb
Use autoScrollRate and timeout when appropriate
nvdaes Nov 18, 2025
2b4d750
Pre-commit auto-fix
pre-commit-ci[bot] Nov 18, 2025
619c02c
Update variables in globalCommands
nvdaes Nov 18, 2025
3dce11a
Merge branch 'brailleScroll' of https://github.com/nvdaes/nvda into b…
nvdaes Nov 18, 2025
e1acf80
Update braille
nvdaes Nov 18, 2025
a2c7611
Fixes
nvdaes Nov 18, 2025
a4e11b5
Pre-commit auto-fix
pre-commit-ci[bot] Nov 18, 2025
9032f60
Merge branch 'master' into brailleScroll
nvdaes Nov 20, 2025
ff6880d
Update user guide
nvdaes Nov 20, 2025
a23a05a
Rename option
nvdaes Nov 20, 2025
bc64da2
Update user guide
nvdaes Nov 20, 2025
3207953
Disable autoScroll when the session is locked
nvdaes Nov 20, 2025
00ab1ae
Update user guide
nvdaes Nov 20, 2025
3efcc0e
Update changelog
nvdaes Nov 20, 2025
f3a2041
Apply suggestions from code review
nvdaes Nov 21, 2025
0734bb6
Apply suggestions from code review
nvdaes Nov 25, 2025
a6a142a
Pre-commit auto-fix
pre-commit-ci[bot] Nov 25, 2025
367a301
Address code review
nvdaes Nov 25, 2025
dbcffc5
Pre-commit auto-fix
pre-commit-ci[bot] Nov 25, 2025
a8f2459
Improve toggling scroll handling message timeout
nvdaes Nov 25, 2025
f8630ea
Merge branch 'brailleScroll' of https://github.com/nvdaes/nvda into b…
nvdaes Nov 25, 2025
87f6ec8
Apply suggestions from code review
nvdaes Dec 5, 2025
3d754c3
Merge
nvdaes Dec 5, 2025
b3052eb
Restore liblouis
nvdaes Dec 5, 2025
72cc841
Increase maximum for scroll rate
nvdaes Dec 5, 2025
c19039d
Allow scrolling when braille follows speech
nvdaes Dec 5, 2025
8f5eb26
Merge branch 'master' into brailleScroll
nvdaes Dec 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 48 additions & 1 deletion source/braille.py
Original file line number Diff line number Diff line change
Expand Up @@ -1704,10 +1704,11 @@ def nextLine(self):
try:
dest.obj.turnPage()
except RuntimeError:
pass
handler.autoScroll(enable=False)
else:
dest = dest.obj.makeTextInfo(textInfos.POSITION_FIRST)
else: # no page turn support
handler.autoScroll(enable=False)
shouldCollapseToEnd = True
dest.collapse(shouldCollapseToEnd)
self._setCursor(dest)
Expand Down Expand Up @@ -2475,6 +2476,7 @@ def __init__(self):
self._cursorBlinkUp = True
self._cells = []
self._cursorBlinkTimer = None
self._autoScrollCallLater: wx.CallLater | None = None
config.post_configProfileSwitch.register(self.handlePostConfigProfileSwitch)
if config.conf["braille"]["tetherTo"] == TetherTo.AUTO.value:
self._tether = TetherTo.FOCUS.value
Expand Down Expand Up @@ -2510,6 +2512,7 @@ def terminate(self):
if self._cursorBlinkTimer:
self._cursorBlinkTimer.Stop()
self._cursorBlinkTimer = None
self.autoScroll(enable=False)
config.post_configProfileSwitch.unregister(self.handlePostConfigProfileSwitch)
post_secureDesktopStateChange.unregister(self._onSecureDesktopStateChanged)
post_sessionLockStateChanged.unregister(self._onSessionLockStateChanged)
Expand All @@ -2525,12 +2528,14 @@ def terminate(self):

def _clearAll(self) -> None:
"""Clear the braille buffers and update the braille display."""
self.autoScroll(enable=False)
self.mainBuffer.clear()
if self.buffer is self.messageBuffer:
self._dismissMessage(False)
self.update()

def _onSecureDesktopStateChanged(self, isSecureDesktop: bool):
self.autoScroll(enable=False)
self.mainBuffer.clear()
if not easeOfAccess.isRegistered():
if isSecureDesktop:
Expand Down Expand Up @@ -2984,13 +2989,20 @@ def scrollForward(self):
self.buffer.scrollForward()
if self.buffer is self.messageBuffer:
self._resetMessageTimer()
if self._autoScrollCallLater:
# Reset the timer.
self._resetAutoScroll()

def scrollBack(self):
self.buffer.scrollBack()
if self.buffer is self.messageBuffer:
self._resetMessageTimer()
if self._autoScrollCallLater:
# Reset the timer.
self._resetAutoScroll()

def routeTo(self, windowPos):
self.autoScroll(enable=False)
self.buffer.routeTo(windowPos)
if self.buffer is self.messageBuffer:
self._dismissMessage()
Expand All @@ -3015,6 +3027,7 @@ def message(self, text):
):
return
_pre_showBrailleMessage.notify()
self.autoScroll(enable=False)
if self.buffer is self.messageBuffer:
self.buffer.clear()
else:
Expand Down Expand Up @@ -3055,6 +3068,39 @@ def _dismissMessage(self, shouldUpdate: bool = True):
self.update()
_post_dismissBrailleMessage.notify()

def autoScroll(self, enable: bool) -> None:
"""
Enable or disable automatic scroll.

:param enable: ``True`` if automatic scroll should be enabled, ``False`` otherwise.
"""

if not self.enabled or config.conf["braille"]["mode"] == BrailleMode.SPEECH_OUTPUT.value:
Copy link
Member

Choose a reason for hiding this comment

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

I'm a bit confused by this, wouldn't braille mode speech output be a major use case for this? i.e. being able to read the braille and hear the speech at roughly the same time?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, may be. I don't use the speech mode, so I didn't think about this. I'll test and add it.

return
if enable and self._autoScrollCallLater is None:
self._autoScrollCallLater = wx.CallLater(self._calculateAutoScrollTimeout(), self.scrollForward)
elif not enable and self._autoScrollCallLater is not None:
self._autoScrollCallLater.Stop()
self._autoScrollCallLater = None

def _calculateAutoScrollTimeout(self) -> int:
"""
Calculate the timeout for automatic scroll.

:return: The number of milliseconds to wait until the next scroll.
"""

autoScrollRate = config.conf["braille"]["autoScrollRate"]
return int((self.displaySize / autoScrollRate) * 1000)

def _resetAutoScroll(self) -> None:
"""
Reset autoScroll.
"""

self.autoScroll(enable=False)
self.autoScroll(enable=True)

def handleGainFocus(self, obj: "NVDAObject", shouldAutoTether: bool = True) -> None:
if not self.enabled or config.conf["braille"]["mode"] == BrailleMode.SPEECH_OUTPUT.value:
return
Expand All @@ -3078,6 +3124,7 @@ def handleGainFocus(self, obj: "NVDAObject", shouldAutoTether: bool = True) -> N
)

def _doNewObject(self, regions):
self.autoScroll(enable=False)
self.mainBuffer.clear()
focusToHardLeftSet = False
for region in regions:
Expand Down
2 changes: 2 additions & 0 deletions source/config/configSpec.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@
showMessages = integer(0, 2, default=1)
# Timeout after the message will disappear from braille display
messageTimeout = integer(default=4, min=1, max=20)
# Rate for automatic scroll
autoScrollRate = integer(default=10, min=1, max=20)
Copy link
Member

Choose a reason for hiding this comment

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

is 20 different rates too limiting? perhaps the interval step should be 0.1 or 0.5?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, 20 seems too much. Let's use 15 cells/sec. Should we use a slider instead of a spin control?

Copy link
Member

Choose a reason for hiding this comment

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

I was thinking 20 is not enough? I think a spin control is good here

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Generally I read at 12 cells/sec or so. According to a rule used in Spain for captions, the recommended speed is 12 chars/sec or so too. I think that 20 is a lot, but may be that in some texts can be used.
We can use 30 if you want. When lines are short, I use the keystroke in the braille display to scroll forward manually.

Copy link
Member

Choose a reason for hiding this comment

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

My concern wasn't about raising it to 30, but instead allowing 10.5/sec or 10.1/sec. e.g. finer grain

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

OK, then 20 may be enough, but I need to investigate how to allow 0.1 interval using seconds. Perhaps milliseconds wikk be better, or using a slider. I don't know another samples of float numbers with spin controls.

Copy link
Member

Choose a reason for hiding this comment

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

You can use SetIncrement on a wx.SpintCtrl

Copy link
Member

Choose a reason for hiding this comment

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

I think an interval of 0.5 might be enough? We can always change it 0.1 later

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

OK. I'll try to use SetIncrement and a float value in the config. I'll work for this this weekend.

tetherTo = option("auto", "focus", "review", default="auto")
reviewRoutingMovesSystemCaret = featureFlag(\
optionsEnum="ReviewRoutingMovesSystemCaretFlag", behaviorOfDefault="NEVER")
Expand Down
50 changes: 50 additions & 0 deletions source/globalCommands.py
Original file line number Diff line number Diff line change
Expand Up @@ -826,6 +826,56 @@ def script_toggleReportSpellingErrorsInBraille(self, gesture: inputCore.InputGes
# Translators: Message presented when turning off reporting spelling errors in braille.
ui.message(_("Report spelling errors in braille off"))

@script(
# Translators: Input help mode message for command to toggle braille automatic scroll.
description=_("Toggles braille automatic scroll"),
category=SCRCAT_BRAILLE,
)
def script_toggleBrailleAutoScroll(self, gesture: inputCore.InputGesture):
if config.conf["braille"]["mode"] == BrailleMode.SPEECH_OUTPUT.value:
return
shouldEnableAutoScroll = braille.handler._autoScrollCallLater is None
if shouldEnableAutoScroll:
# Translators: Message reported when automatic scrolling has been enabled in braille.
ui.message(_("Automatic scrolling enabled"))
timeout = config.conf["braille"]["messageTimeout"] * 1000
core.callLater(timeout, braille.handler.autoScroll, shouldEnableAutoScroll)
else:
# Translators: Message reported when automatic scrolling has been disabled in braille.
ui.message(_("Automatic scrolling disabled"))

@script(
# Translators: Input help mode message for command to increase the rate for braille automatic scroll.
description=_("Increases the rate for braille automatic scroll"),
category=SCRCAT_BRAILLE,
)
def script_increaseBrailleAutoScrollRate(self, gesture: inputCore.InputGesture):
maxRate = int(
config.conf.getConfigValidation(
("braille", "autoScrollRate"),
).kwargs["max"],
)
if config.conf["braille"]["autoScrollRate"] < maxRate:
config.conf["braille"]["autoScrollRate"] += 1
rate = str(config.conf["braille"]["autoScrollRate"])
ui.message(rate)

@script(
# Translators: Input help mode message for command to decrease the rate for braille automatic scroll.
description=_("Decreases the rate for braille automatic scroll"),
category=SCRCAT_BRAILLE,
)
def script_decreaseBrailleAutoScrollRate(self, gesture: inputCore.InputGesture):
minRate = int(
config.conf.getConfigValidation(
("braille", "autoScrollRate"),
).kwargs["min"],
)
if config.conf["braille"]["autoScrollRate"] > minRate:
config.conf["braille"]["autoScrollRate"] -= 1
rate = str(config.conf["braille"]["autoScrollRate"])
ui.message(rate)

@script(
# Translators: Input help mode message for toggle report pages command.
description=_("Toggles on and off the reporting of pages"),
Expand Down
22 changes: 22 additions & 0 deletions source/gui/settingsDialogs.py
Original file line number Diff line number Diff line change
Expand Up @@ -5493,6 +5493,27 @@ def makeSettings(self, settingsSizer):
)
self.bindHelpEvent("BrailleSettingsInterruptSpeech", self.brailleInterruptSpeechCombo)

minRate = int(
config.conf.getConfigValidation(
("braille", "autoScrollRate"),
).kwargs["min"],
)
maxRate = int(
config.conf.getConfigValidation(
("braille", "autoScrollRate"),
).kwargs["max"],
)
# Translators: The label for a setting in braille settings to change the rate for autoscroll.
autoScrollRateText = _("Auto&matic scroll rate (cells/sec)")
self.autoScrollRateEdit = followCursorGroupHelper.addLabeledControl(
autoScrollRateText,
nvdaControls.SelectOnFocusSpinCtrl,
min=minRate,
max=maxRate,
initial=config.conf["braille"]["autoScrollRate"],
)
self.bindHelpEvent("BrailleAutoScrollRate", self.autoScrollRateEdit)

if gui._isDebug():
log.debug("Finished making settings, now at %.2f seconds from start" % (time.time() - startTime))

Expand Down Expand Up @@ -5523,6 +5544,7 @@ def onSave(self):
]
config.conf["braille"]["showMessages"] = self.showMessagesList.GetSelection()
config.conf["braille"]["messageTimeout"] = self.messageTimeoutEdit.GetValue()
config.conf["braille"]["autoScrollRate"] = self.autoScrollRateEdit.GetValue()
tetherChoice = [x.value for x in TetherTo][self.tetherList.GetSelection()]
if tetherChoice == TetherTo.AUTO.value:
config.conf["braille"]["tetherTo"] = TetherTo.AUTO.value
Expand Down
2 changes: 2 additions & 0 deletions user_docs/en/changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ An action has been added to view the full scan results on the VirusTotal website
* In browse mode, the number of items in a list is now reported in braille. (#7455, @nvdaes)
* Automatically reading the entire result after a successful recognition is now possible via a new option in the Windows OCR settings. (#19150, @Cary-rowen)
* Added support for reading math content by integrating MathCAT. (@RyanMcCleary, #18323)
* Added ability to scroll forward braille automatically (#18573, @nvdaes)

### Changes

Expand Down Expand Up @@ -95,6 +96,7 @@ On ARM64 machines with Windows 11, these ARM64EC libraries are loaded instead of
* NVDA is now licensed under "GPL-2 or later".
* In `braille.py`, the `FormattingMarker` class has a new `shouldBeUsed` method, to determine if the formatting marker key should be reported (#7608, @nvdaes)
* Added `api.fakeNVDAObjectClasses` set and `api.isFakeNVDAObject` function to identify fake NVDAObject instances. (#19168, @hwf1324)
* Added an `autoScroll` method to `braille.handler`. (#18573, nvdaes)

#### API Breaking Changes

Expand Down
12 changes: 12 additions & 0 deletions user_docs/en/userGuide.md
Original file line number Diff line number Diff line change
Expand Up @@ -2424,6 +2424,18 @@ Enabling this option will cause NVDA to speak lines or paragraphs reached using

To toggle this option from anywhere, please assign a custom gesture to "speakOnNavigatingByUnit" in the "Braille" section of the [Input Gestures dialog](#InputGestures).

##### Automatic Scroll Rate {#BrailleAutoScrollRate}

This option controls the rate of automatic braille display scrolling, measured in cells per second.
For example, with the default value of 10 cells/sec, if a braille display with 40 cells is used, the number of seconds between automatic scrolls will be 4.
If the display had 20 cells, each line of braille would be shown for 2 seconds.

While the automatic scroll option is enabled, you can still use the scroll back command to read previous contents again, and scroll forward, for example, to skip a blank line, or if the line been read is too short.

Automatic scrolling will be disabled if a routing key is pressed, if a message is presented in braille, if a new object is displayed, when entering a secure screen, or when the session is locked.

Commands can be assigned to toggle the automatic scroll option, and to increase or decrease the scroll rate, from the "Braille" section of the [Input Gestures dialog](#InputGestures).

##### Avoid splitting words when possible {#BrailleSettingsWordWrap}

If this is enabled, a word which is too large to fit at the end of the braille display will not be split.
Expand Down
Loading