Repository: nvaccess/nvda

Issue:
Braille doesn't refresh when downloading update
This is a specific case of braille not refreshing when the foreground dialog refreshes. Usually these dialogs have static text or a progress bar that can't be focused but that is updated at intervals.
In the case of downloading NVDA updates, braille keeps showing "Remaining: 0:00:00". Same for the time left.


Discussion:
Isn't this a more generic issue (i.e. I recall braille not being updated 
on progress bars as well, but I could be wrong in that).

That is true, see #3258. But this is a scenario where it actually matters and can be reproduced.
It could also be a totally different issue, I'm not sure.
This is discussed in the last few comments of #3258. I'm going to close that issue in favour of this one, since #3258 is quite confusing now.

First, if a progress bar updates, braille does get updated if that progress bar is the current focus/review object (depending on tethering).

Regarding the update dialog, here are the relevant comments from #3258:

[@dkager wrote](https://github.com/nvaccess/nvda/issues/3258#issuecomment-155307439):

> Another area where braille isn't refreshed is the NVDA update progress dialog.
> ...
> for braille it makes sense to re-evaluate a dialog's contents when a progress bar changes. Scraping may be more accurate, but it seems safe to assume that an updated progress bar implies something else was also updated.

[@jcsteh wrote](https://github.com/nvaccess/nvda/issues/3258#issuecomment-155307440):

> Progress bars themselves don't appear in dialog text. In some cases, there can be other text associated with the progress bar (e.g. the elapsed/remaining times in the NVDA update dialog) and this is often included in dialog text. However, we have no way of knowing that this is the case. Fetching dialog text is very expensive from a performance point of view, so we want to be very sure it's necessary before we do it.

It's worth noting that we can't just recalculate part of dialog text at present; it's all or nothing.

That said, I guess it's reasonable to hope that there might be some text associated with the progress bar in a dialog. If a progress bar changes and has a dialog as a parent (or maybe we could check a small number of ancestors), I guess we could update the dialog's braille region, though we'd probably want to limit it to every 10% or every 5 seconds or something like that.
> That said, I guess it's reasonable to hope that there might be some text associated with the progress bar in a dialog. If a progress bar changes and has a dialog as a parent (or maybe we could check a small number of ancestors), I guess we could update the dialog's braille region, though we'd probably want to limit it to every 10% or every 5 seconds or something like that.

For the delay we could use whatever the spoken progress bar messages use.
One concern I have is that updating the dialog text should ideally preserve the panning of the braille display. Otherwise you get in a situation where you are frantically scrolling to read something while the updates keep bumping you back to the top.
Unless I'm reading this wrong, braille is already updated whenever a progress bar changes value, see `NVDAObjects.behaviors.ProgressBar.event_valueChange`.
But `braille.braillejHandler.handleUpdate` then only updates the region associated with that object rather than the whole buffer.

So should we introduce periodic updates for progress bars and then update the whole buffer if that object has a dialog as ancestor? Or do we update on every value change as is done now?
In other words, is updating less frequently only a performance consideration, or also an attempt not to refresh the braille display too often?
You're correct: it does update for progress bars. However, in this case,
there is no region for the progress bar at all, since you're not focused on
it/reviewing it, nor is it an ancestor of the focus. So, that update is
essentially a no-op.

You don't need (or want) to update the whole buffer if there's a dialog
ancestor. Just update braille for the dialog. That will cause dialog text
to be re-calculated.

Calculating dialog text is very expensive (it has to crawl the whole
dialog), so updating it too often would be a performance problem, hence the
suggestion to rate limit it. Some progress bars fire a huge number of value
changes.


Resolving patch:
diff --git a/source/braille.py b/source/braille.py
--- a/source/braille.py
+++ b/source/braille.py
@@ -10,6 +10,7 @@
 import pkgutil
 import ctypes.wintypes
 import threading
+import time
 import wx
 import louis
 import winKernel
@@ -1688,6 +1689,20 @@ def _doCursorMove(self, region):
 		elif self.buffer is self.messageBuffer and keyboardHandler.keyCounter>self._keyCountForLastMessage:
 			self._dismissMessage()
 
+	# #6862: The value change of a progress bar change often goes together with changes of other objects in the dialog,
+	# e.g. the time remaining. Therefore, update the dialog when a contained progress bar changes.
+	def _handleProgressBarUpdate(self, obj):
+		oldTime = getattr(self, "_lastProgressBarUpdateTime", None)
+		newTime = time.time()
+		if oldTime and newTime - oldTime < 1:
+			# Fetching dialog text is expensive, so update at most once a second.
+			return
+		self._lastProgressBarUpdateTime = newTime
+		for obj in reversed(api.getFocusAncestors()[:-1]):
+			if obj.role == controlTypes.ROLE_DIALOG:
+				self.handleUpdate(obj)
+				return
+
 	def handleUpdate(self, obj):
 		if not self.enabled:
 			return
@@ -1698,6 +1713,15 @@ def handleUpdate(self, obj):
 				break
 		else:
 			# No region for this object.
+			# There are some objects that require special update behavior even if they have no region.
+			# This only applies when tethered to focus, because tethering to review shows only one object at a time,
+			# which always has a braille region associated with it.
+			if self.tether != self.TETHER_FOCUS:
+				return
+			# Late import to avoid circular import.
+			from NVDAObjects import NVDAObject
+			if isinstance(obj, NVDAObject) and obj.role == controlTypes.ROLE_PROGRESSBAR and obj.isInForeground:
+				self._handleProgressBarUpdate(obj)
 			return
 		self.mainBuffer.saveWindow()
 		region.update()


Test patch: