Repository: nvaccess/nvda

Issue:
Can't read status bar in File Explorer (Windows 10)
In File Explorer (WINDOWS+E), pressing NVDA+end (laptop: NVDA+shift+end) reads "No status line found", even if the status line is enabled (it's possible to disable it in the folder options.

Expected behaviour, read the status bar, for instance, "30 items, 1 selected, details view button, large icons view button".




Discussion:
Hi, there is an issue that’s on file that deals with this problem. Note that this isn’t specific to Windows 10, as this exists in other versions as well. Thanks.
I was sure this had come up before but I couldn't find an issue when I created this one.  I wasn't sure whether it was Windows 10 specific or not.


CC @MichaelDCurran, @JCsTeh, I forgot which issue number was where we talked about tweaking api.getStatusBar(). Thanks.
Perhaps relevant:
* Unable to find status bar in Visual Studio Ref bug #5618
* Unable to find status bar in Office aplications #649
* appModules should have a way to override the logic to retrieve status bar text in api.getStatusBar #2125
* Status bar handling does not work with Java Access Bridge applications #5674
In https://github.com/nvaccess/nvda/issues/6842#issuecomment-410670910 @ruifontes proposed the following appmodule as a fix. This can probably be adapted into a pull request to fix this issue.
[explorer.py.txt](https://github.com/nvaccess/nvda/files/2268565/explorer.py.txt)


@ruifontes are you planing to raise a pull request with the las version of the NVDA Addon?
No, I am yet gaving my first steps in GitHub...
If you, or someone else, can do it, better!


In my view we  cannot proceed with this until #4640 is implemented. The problem with the current approach is the fact that the entire script for reading status bar is duplicated, therefore any updates to it needs to be performed in the Explorer appmodule in addition to the globalCommands.py
Hi,

Now that #4640 is a reality, let's proceed with solution design. The solution is actually simpler than what Rui proposed earlier. There are two possible solutions:

1. Screen coordinates: if it is determined that File Explorer status bar text is located 3 percent to the right of the bottom-left corner (absolutely 3 percent to the right regardless of screen orientation, resolution, and DPI), then Rui's solution should work. I doubt that's the case, as things can change depending on the three factors noted above. This solution has a flaw: what if wrong item gets announced, such as when all windows are minimized?
2. UIA and overlay class: define an overlay class for status bar that handles status bar text correctly (overriding get_name), then locate this status bar via UIA tree traversal (IUIAutomation::CreatePropertyCondition/ElementFromHandle). For reference, UIA automation ID for the status bar object is "StatusBarModuleInner". This option raises concerns about performance and responsiveness.

I advise exploring both options and choosing the one that works across all of the following scenarios:

* Windows versions
* Screen resolution and orientation
* High DPI
* Window is maximized and restored
* Regardless of whether an item is selected or not
* Opening hidden operating system folders

I'm personally in favor of option 2 because it can work better across screen resolutions, orientation, and DPI setting.

Thanks.
Thanks to @josephsl hints, I could prototype option 2 with what seems at first glance running with decent performance.
However, complying with the current `api.getStatusBarText` implementation requires overlaying the status bar children, which in turns requires to look up the object parent in `chooseNVDAObjectOverlayClasses`.
The prototype works fine, but I've already encountered in other situations corner-cases where this parent lookup might be risky (freezing NVDA in some circumstances I could not well determine).
@josephsl, can you strongly confirm looking up the parent object role in this very context is 100% sure during `chooseNVDAObjectOverlayClasses`?

Furthermore, the status bar itself and two of its potentially three children in latest Windows 10 have names that are simply reformulations of their role's label. This info is of no use, but other/future implementation may differ. Thus I'm a bit concerned by dropping the names altogether in the overlays.

The safest solution IMHO would be to allow the app module to also provide a `getStatusBarText` implementation.
This way, `chooseNVDAObjectOverlayClasses` would not be penalized by extraneous tests, the `report_statusBar` global command would still move the navigator object to the status bar for eventual interaction, and the announced text would be much more informative, eg. stating "No elements selected" when the selected element counter is missing.
This approach would also ease customizing the announce depending on the Windows version if need be.

Resolving patch:
diff --git a/source/api.py b/source/api.py
--- a/source/api.py
+++ b/source/api.py
@@ -1,5 +1,5 @@
 # A part of NonVisual Desktop Access (NVDA)
-# Copyright (C) 2006-2020 NV Access Limited, James Teh, Michael Curran, Peter Vagner, Derek Riemer,
+# Copyright (C) 2006-2021 NV Access Limited, James Teh, Michael Curran, Peter Vagner, Derek Riemer,
 # Davy Kager, Babbage B.V., Leonard de Ruijter, Joseph Lee, Accessolutions, Julien Cochuyt
 # This file may be used under the terms of the GNU General Public License, version 2 or later.
 # For more details see: https://www.gnu.org/licenses/gpl-2.0.html
@@ -374,6 +374,10 @@ def getStatusBarText(obj):
 	@return: The status bar text.
 	@rtype: str
 	"""
+	try:
+		return obj.appModule.getStatusBarText(obj)
+	except NotImplementedError:
+		pass
 	text = obj.name or ""
 	if text:
 		text += " "
diff --git a/source/appModuleHandler.py b/source/appModuleHandler.py
--- a/source/appModuleHandler.py
+++ b/source/appModuleHandler.py
@@ -1,7 +1,7 @@
 # -*- coding: UTF-8 -*-
 # A part of NonVisual Desktop Access (NVDA)
-# Copyright (C) 2006-2019 NV Access Limited, Peter Vágner, Aleksey Sadovoy, Patrick Zajda, Joseph Lee,
-# Babbage B.V., Mozilla Corporation
+# Copyright (C) 2006-2021 NV Access Limited, Peter Vágner, Aleksey Sadovoy, Patrick Zajda, Joseph Lee,
+# Babbage B.V., Mozilla Corporation, Julien Cochuyt
 # This file is covered by the GNU General Public License.
 # See the file COPYING for more details.
 
@@ -599,6 +599,13 @@ def _get_statusBar(self):
 		"""
 		raise NotImplementedError()
 
+	def getStatusBarText(self, obj: NVDAObjects.NVDAObject) -> str:
+		"""Get the text from the given status bar.
+		If C{NotImplementedError} is raised, L{api.getStatusBarText} will resort to
+		retrieve the name of the status bar and the names and values of all of its children.
+		"""
+		raise NotImplementedError()
+
 	def _get_statusBarTextInfo(self):
 		"""Retrieve a L{TextInfo} positioned at the status bar of the application.
 		This is used by L{GlobalCommands.script_reportStatusLine} in cases where
@@ -609,6 +616,7 @@ def _get_statusBarTextInfo(self):
 		"""
 		raise NotImplementedError()
 
+
 class AppProfileTrigger(config.ProfileTrigger):
 	"""A configuration profile trigger for when a particular application has focus.
 	"""
diff --git a/source/appModules/explorer.py b/source/appModules/explorer.py
--- a/source/appModules/explorer.py
+++ b/source/appModules/explorer.py
@@ -360,6 +360,90 @@ def chooseNVDAObjectOverlayClasses(self, obj, clsList):
 			elif uiaClassName == "UIProperty" and role == controlTypes.Role.EDITABLETEXT:
 				clsList.insert(0, UIProperty)
 
+	def _get_statusBar(self):
+		foreground = api.getForegroundObject()
+		if not isinstance(foreground, UIA) or not foreground.windowClassName == "CabinetWClass":
+			# This is not the file explorer window. Resort to standard behavior.
+			raise NotImplementedError
+		import UIAHandler
+		clientObject = UIAHandler.handler.clientObject
+		condition = clientObject.createPropertyCondition(
+			UIAHandler.UIA_ControlTypePropertyId,
+			UIAHandler.UIA_StatusBarControlTypeId
+		)
+		walker = clientObject.createTreeWalker(condition)
+		try:
+			element = walker.getFirstChildElement(foreground.UIAElement)
+		except COMError:
+			# We could not find the expected object. Resort to standard behavior.
+			raise NotImplementedError()
+		element = element.buildUpdatedCache(UIAHandler.handler.baseCacheRequest)
+		statusBar = UIA(UIAElement=element)
+		return statusBar
+
+	@staticmethod
+	def _getStatusBarTextWin7(obj) -> str:
+		"""For status bar in Windows 7 Windows Explorer we're interested only in the name of the first child
+		the rest are either empty or contain garbage."""
+		if obj.firstChild and obj.firstChild.name:
+			return obj.firstChild.name
+		raise NotImplementedError
+
+	@staticmethod
+	def _getStatusBarTextPostWin7(obj) -> str:
+		# The expected status bar, as of Windows 10 20H2 at least, contains:
+		#  - A grouping with a single static text child presenting the total number of elements
+		#  - Optionally, a grouping with a single static text child presenting the number of
+		#    selected elements and their total size, missing if no element is selected.
+		#  - A grouping with two radio buttons to control the display mode.
+		parts = []
+		for index, child in enumerate(obj.children):
+			if (
+				child.role == controlTypes.Role.GROUPING
+				and child.childCount == 1
+				and child.firstChild.role == controlTypes.Role.STATICTEXT
+			):
+				parts.append(child.firstChild.name)
+			elif (
+				child.role == controlTypes.Role.GROUPING
+				and child.childCount > 1
+				and not any(
+					grandChild for grandChild in child.children
+					if grandChild.role != controlTypes.Role.RADIOBUTTON
+				)
+			):
+				selected = next(iter(
+					grandChild for grandChild in child.children
+					if controlTypes.State.CHECKED in grandChild.states
+				), None)
+				if selected is not None:
+					parts.append(" ".join(
+						[child.name]
+						+ ([selected.name] if selected is not None else [])
+					))
+			else:
+				# Unexpected child, try to retrieve something useful.
+				parts.append(" ".join(
+					chunk
+					for chunk in (child.name, child.value)
+					if chunk and isinstance(chunk, str) and not chunk.isspace()
+				))
+		if not parts:
+			# We couldn't retrieve anything. Resort to standard behavior.
+			raise NotImplementedError
+		return ", ".join(parts)
+
+	def getStatusBarText(self, obj) -> str:
+		if obj.windowClassName == "msctls_statusbar32":  # Windows 7
+			return self._getStatusBarTextWin7(obj)
+		if (
+			isinstance(obj, UIA) or obj.UIAElement.cachedClassname == "StatusBarModuleInner"
+		):  # Windows 8 or later
+			return self._getStatusBarTextPostWin7(obj)
+		else:
+			# This is not the file explorer status bar. Resort to standard behavior.
+			raise NotImplementedError
+
 	def event_NVDAObject_init(self, obj):
 		windowClass = obj.windowClassName
 		role = obj.role


Test patch: