Repository: nvaccess/nvda

Issue:
Tips in system tray
In system tray with report tips checked NVDA reads twice almost all of the icons. I would not like to disable tips because in other places it can be useful but here is very uncomfortable. If tip and object name are the same text I think that NVDA should not read the tip.


Discussion:
While this is low severity, it seems like it would be fairly easy to implement, I'll set this issue to priority 2. If there proves to be edge cases I haven't considered that cause this to be harder to implement then the issue may need to be deprioritised 
Normally, tool tips only appear when you move the mouse to an icon. The reason the tips appear in the system tray is that we have to move the mouse to the icon in order to work around a Windows focus bug.

When talking about comparing object names, the question is which objects we're comparing. Comparing the focus might make sense for the system tray case, but it doesn't necessarily make sense anywhere else. And if we compare against the mouse object, a tool tip might not get read because it has the same name, but the user really should know if the tool tip appears even if it's a duplicate. Thus, I don't agree that comparing names is a good approach here.

Instead, I think we need to special case the system tray somehow, but that is not simple to do, so I'm dropping this to a p3. I'm wondering whether this should possibly even be a p4; yes, the repetition is annoying, but you can easily ignore/silence it and it doesn't cause an efficiency problem because it is presented *after* other information, not before it.

Resolving patch:
diff --git a/source/appModules/explorer.py b/source/appModules/explorer.py
--- a/source/appModules/explorer.py
+++ b/source/appModules/explorer.py
@@ -1,9 +1,8 @@
 # -*- coding: UTF-8 -*-
-#appModules/explorer.py
-#A part of NonVisual Desktop Access (NVDA)
-#Copyright (C) 2006-2019 NV Access Limited, Joseph Lee, Łukasz Golonka
-#This file is covered by the GNU General Public License.
-#See the file COPYING for more details.
+# A part of NonVisual Desktop Access (NVDA)
+# Copyright (C) 2006-2020 NV Access Limited, Joseph Lee, Łukasz Golonka, Julien Cochuyt
+# This file is covered by the GNU General Public License.
+# See the file COPYING for more details.
 
 """App module for Windows Explorer (aka Windows shell and renamed to File Explorer in Windows 8).
 Provides workarounds for controls such as identifying Start button, notification area and others.
@@ -22,7 +21,10 @@
 from NVDAObjects.window import Window
 from NVDAObjects.IAccessible import IAccessible, List
 from NVDAObjects.UIA import UIA
+from NVDAObjects.behaviors import ToolTip
 from NVDAObjects.window.edit import RichEdit50, EditTextInfo
+import config
+
 
 # Suppress incorrect Win 10 Task switching window focus
 class MultitaskingViewFrameWindow(UIA):
@@ -74,16 +76,25 @@ def _get_shouldAllowIAccessibleFocusEvent(self):
 class NotificationArea(IAccessible):
 	"""The Windows notification area, a.k.a. system tray.
 	"""
+	lastKnownLocation = None
 
 	def event_gainFocus(self):
+		NotificationArea.lastKnownLocation = self.location
 		if mouseHandler.lastMouseEventTime < time.time() - 0.2:
 			# This focus change was not caused by a mouse event.
-			# If the mouse is on another toolbar control, the notification area toolbar will rudely
+			# If the mouse is on another systray control, the notification area toolbar will rudely
 			# bounce the focus back to the object under the mouse after a brief pause.
 			# Moving the mouse to the focus object isn't a good solution because
 			# sometimes, the focus can't be moved away from the object under the mouse.
 			# Therefore, move the mouse out of the way.
-			winUser.setCursorPos(0, 0)
+			if self.location:
+				systrayLeft, systrayTop, systrayWidth, systrayHeight = self.location
+				mouseLeft, mouseTop = winUser.getCursorPos()
+				if (
+					systrayLeft <= mouseLeft <= systrayLeft + systrayWidth
+					and systrayTop <= mouseTop <= systrayTop + systrayHeight
+				):
+					winUser.setCursorPos(0, 0)
 
 		if self.role == controlTypes.ROLE_TOOLBAR:
 			# Sometimes, the toolbar itself receives the focus instead of the focused child.
@@ -103,6 +114,49 @@ def event_gainFocus(self):
 		super(NotificationArea, self).event_gainFocus()
 
 
+class ExplorerToolTip(ToolTip):
+
+	def shouldReport(self):
+		# Avoid reporting systray tool-tips if their text equals the focused systray icon name (#6656)
+
+		# Don't bother checking if reporting of tool-tips is disabled
+		if not config.conf["presentation"]["reportTooltips"]:
+			return False
+
+		focus = api.getFocusObject()
+
+		# Report if either
+		#  - the mouse has just moved
+		#  - the focus is not in the systray
+		#  - we do not know (yet) where the systray is located
+		if (
+			mouseHandler.lastMouseEventTime >= time.time() - 0.2
+			or not isinstance(focus, NotificationArea)
+			or NotificationArea.lastKnownLocation is None
+		):
+			return True
+
+		# Report if the mouse is indeed located in the systray
+		systrayLeft, systrayTop, systrayWidth, systrayHeight = NotificationArea.lastKnownLocation
+		mouseLeft, mouseTop = winUser.getCursorPos()
+		if (
+			systrayLeft <= mouseLeft <= systrayLeft + systrayWidth
+			and systrayTop <= mouseTop <= systrayTop + systrayHeight
+		):
+			return True
+
+		# Report is the next are different
+		if focus.name != self.name:
+			return True
+
+		# Do not report otherwise
+		return False
+
+	def event_show(self):
+		if self.shouldReport():
+			super().event_show()
+
+
 class GridTileElement(UIA):
 
 	role=controlTypes.ROLE_TABLECELL
@@ -217,7 +271,11 @@ def chooseNVDAObjectOverlayClasses(self, obj, clsList):
 					toolbarParent = None
 				if toolbarParent and toolbarParent.windowClassName == "SysPager":
 					clsList.insert(0, NotificationArea)
-			return 
+			return
+
+		if obj.role == controlTypes.ROLE_TOOLTIP:
+			clsList.insert(0, ExplorerToolTip)
+			return
 
 		if windowClass == "Edit" and controlTypes.STATE_READONLY in obj.states:
 			clsList.insert(0, ReadOnlyEditBox)


Test patch: