Repository: nvaccess/nvda

Issue:
Windows 10: NVDA does not report which desktop I am on when I switch amongst multiple desktops
NVDA does not report which desktop I am on when I switch among multiple desktops in windows 10 Professional. To duplicate this, do the following. From with windows 10:
1. Get to the desktop with windows+d or to the task bar with windows+m.
2. Hit windows+ctrl+d to create a new desktop. 
Note:
You will not be told that a new desktop has been created. I believe sighted users do not get a notification either but there is reference to the "show desktop" button appearing.
3. Use control+windows+right arrow to switch to the next desktop.

You will not hear any output from NVDA. I would expect NVDA to say something like desktop1, desktop 2 etc.
Note:
You can hit ctrl+windows+left arrow to switch to a previous desktop but NVDA will not speak which desktop I am on.

As regards the use case for such a notification:
1. I may not remember what state I left that desktop in therefore it is handy to know that the desktop has changed.
2. I may want to skip that desktop and go to another desktop so the notification will help with rapid navigation.

I am using windows 10 professional 64bit and NVDA 2015.4.



Discussion:
It's worth noting that sighted users don't really get any indication 
either, certainly not of the name of the desktop. My understanding is 
taht this annoys a lot of sighted users too. They can see the Taskbar 
change and the visible windows if any. So, if they had similar stuff 
open on two desktops, it'll be difficult for them to see what desktop 
they're on when they switch. Still, the argument that it's easier for a 
sighted user to see what's changed at a glance is valid.

Unfortunately, regardless of any principle/philosophical arguments, 
there doesn't appear to be any way in Windows to get the name of the 
current desktop, at least not that I've been able to find. Until there 
is, this is not possible.


Resolving patch:
diff --git a/source/NVDAObjects/IAccessible/__init__.py b/source/NVDAObjects/IAccessible/__init__.py
--- a/source/NVDAObjects/IAccessible/__init__.py
+++ b/source/NVDAObjects/IAccessible/__init__.py
@@ -33,6 +33,7 @@
 from NVDAObjects import NVDAObject, NVDAObjectTextInfo, InvalidNVDAObject
 import NVDAObjects.JAB
 import eventHandler
+import core
 from NVDAObjects.behaviors import ProgressBar, Dialog, EditableTextWithAutoSelectDetection, FocusableUnfocusableContainer, ToolTip, Notification
 from locationHelper import RectLTWH
 
@@ -1934,9 +1935,28 @@ def event_alert(self):
 				speech.speakObject(child,reason=controlTypes.REASON_FOCUS)
 			child=child.simpleNext
 
+class Desktop(IAccessible):
+	"""
+	An IAccessible overlay class for the Desktop (root of all windows on the system).
+	"""
+
+	# In the past, The Desktop NVDAObject was just a Window, not IAccessible.
+	# But now with the introduction of Virtual Desktops in Windows 10, Desktops can have names, thus why IAccessible is used.
+	# But, we still want to maintain compatibility with the existing tree structure, thus its role is still Window, and it has no parent, next or previous objects.
+	role=controlTypes.ROLE_WINDOW
+	parent=None
+	next=None
+	previous=None
+
+	def event_nameChange(self):
+		# Instruct eventHandler to detect and handle a possible desktop name change, if it has not already.
+		# It is slightly delayed just in case a focus event is already going to occur, which will handle the name change itself.
+		core.callLater(250,eventHandler.handlePossibleDesktopNameChange)
+
 ###class mappings
 
 _staticMap={
+	("#32769",oleacc.ROLE_SYSTEM_CLIENT):"Desktop",
 	("ReBarWindow32",oleacc.ROLE_SYSTEM_CLIENT):"ReBarWindow32Client",
 	("Static",oleacc.ROLE_SYSTEM_STATICTEXT):"StaticText",
 	("msctls_statusbar32",oleacc.ROLE_SYSTEM_STATICTEXT):"StaticText",
diff --git a/source/NVDAObjects/window/__init__.py b/source/NVDAObjects/window/__init__.py
--- a/source/NVDAObjects/window/__init__.py
+++ b/source/NVDAObjects/window/__init__.py
@@ -83,9 +83,6 @@ class Window(NVDAObject):
 	def getPossibleAPIClasses(cls,kwargs,relation=None):
 		windowHandle=kwargs['windowHandle']
 		windowClassName=winUser.getClassName(windowHandle)
-		#The desktop window should stay as a window
-		if windowClassName=="#32769":
-			return
 		#If this window has a ghost window its too dangerous to try any higher APIs 
 		if GhostWindowFromHungWindow and GhostWindowFromHungWindow(windowHandle):
 			return
diff --git a/source/eventHandler.py b/source/eventHandler.py
--- a/source/eventHandler.py
+++ b/source/eventHandler.py
@@ -5,6 +5,8 @@
 #Copyright (C) 2007-2017 NV Access Limited, Babbage B.V.
 
 import threading
+import winVersion
+import ui
 import queueHandler
 import api
 import speech
@@ -24,6 +26,22 @@
 _pendingEventCountsByNameAndObj={}
 # Needed to ensure updates are atomic, as these might be updated from multiple threads simultaneously.
 _pendingEventCountsLock=threading.RLock()
+_lastDesktopName=None
+
+def handlePossibleDesktopNameChange():
+	"""
+	Reports the new virtual desktop name if changed.
+	On Windows versions lower than Windows 10, this function does nothing.
+	"""
+	global _lastDesktopName
+	# Desktop switch reporting can only be supported on Windows 10 May 2019 Update and above,
+	# As previous Windows builds could experience delays and incorrect desktop names.
+	if winVersion.winVersion>=(10,0,18334):
+		import UIAHandler
+		newName=UIAHandler.handler.rootElement.currentName
+		if newName!=_lastDesktopName:
+			ui.message(newName)
+			_lastDesktopName=newName
 
 #: the last object queued for a gainFocus event. Useful for code running outside NVDA's core queue 
 lastQueuedFocusObject=None
@@ -172,6 +190,7 @@ def doPreGainFocus(obj,sleepMode=False):
 				newForeground=obj
 		api.setForegroundObject(newForeground)
 		executeEvent('foreground',newForeground)
+	handlePossibleDesktopNameChange()
 	if sleepMode: return True
 	#Fire focus entered events for all new ancestors of the focus if this is a gainFocus event
 	for parent in globalVars.focusAncestors[globalVars.focusDifferenceLevel:]:


Test patch: