forked from nvaccess/nvda
-
Notifications
You must be signed in to change notification settings - Fork 0
/
explorer.py
358 lines (297 loc) · 14.1 KB
/
explorer.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
# -*- 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.
"""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.
"""
from comtypes import COMError
import time
import appModuleHandler
import controlTypes
import winUser
import winVersion
import api
import speech
import eventHandler
import mouseHandler
from NVDAObjects.window import Window
from NVDAObjects.IAccessible import IAccessible, List
from NVDAObjects.UIA import UIA
from NVDAObjects.window.edit import RichEdit50, EditTextInfo
# Suppress incorrect Win 10 Task switching window focus
class MultitaskingViewFrameWindow(UIA):
shouldAllowUIAFocusEvent=False
# Suppress focus ancestry for task switching list items if alt is held down (alt+tab)
class MultitaskingViewFrameListItem(UIA):
def _get_container(self):
if winUser.getAsyncKeyState(winUser.VK_MENU)&32768:
return api.getDesktopObject()
else:
return super(MultitaskingViewFrameListItem,self).container
# Support for Win8 start screen search suggestions.
class SuggestionListItem(UIA):
def event_UIA_elementSelected(self):
speech.cancelSpeech()
api.setNavigatorObject(self, isFocus=True)
self.reportFocus()
super(SuggestionListItem,self).event_UIA_elementSelected()
# Windows 8 hack: Class to disable incorrect focus on windows 8 search box (containing the already correctly focused edit field)
class SearchBoxClient(IAccessible):
shouldAllowIAccessibleFocusEvent=False
# Class for menu items for Windows Places and Frequently used Programs (in start menu)
# Also used for desktop items
class SysListView32EmittingDuplicateFocusEvents(IAccessible):
# #474: When focus moves to these items, an extra focus is fired on the parent
# However NVDA redirects it to the real focus.
# But this means double focus events on the item, so filter the second one out
# #2988: Also seen when coming back to the Windows 7 desktop from different applications.
def _get_shouldAllowIAccessibleFocusEvent(self):
res = super().shouldAllowIAccessibleFocusEvent
if not res:
return False
focus = eventHandler.lastQueuedFocusObject
if type(focus)!=type(self) or (self.event_windowHandle,self.event_objectID,self.event_childID)!=(focus.event_windowHandle,focus.event_objectID,focus.event_childID):
return True
return False
class NotificationArea(IAccessible):
"""The Windows notification area, a.k.a. system tray.
"""
def event_gainFocus(self):
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
# 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.role == controlTypes.ROLE_TOOLBAR:
# Sometimes, the toolbar itself receives the focus instead of the focused child.
# However, the focused child still has the focused state.
for child in self.children:
if child.hasFocus:
# Redirect the focus to the focused child.
eventHandler.executeEvent("gainFocus", child)
return
# We've really landed on the toolbar itself.
# This was probably caused by moving the mouse out of the way in a previous focus event.
# This previous focus event is no longer useful, so cancel speech.
speech.cancelSpeech()
if eventHandler.isPendingEvents("gainFocus"):
return
super(NotificationArea, self).event_gainFocus()
class GridTileElement(UIA):
role=controlTypes.ROLE_TABLECELL
def _get_description(self):
name=self.name
descriptionStrings=[]
for child in self.children:
description=child.basicText
if not description or description==name: continue
descriptionStrings.append(description)
return " ".join(descriptionStrings)
return description
class GridListTileElement(UIA):
role=controlTypes.ROLE_TABLECELL
description=None
class GridGroup(UIA):
"""A group in the Windows 8 Start Menu.
"""
presentationType=UIA.presType_content
# Normally the name is the first tile which is rather redundant
# However some groups have custom header text which should be read instead
def _get_name(self):
child=self.firstChild
if isinstance(child,UIA):
try:
automationID=child.UIAElement.currentAutomationID
except COMError:
automationID=None
if automationID=="GridListGroupHeader":
return child.name
class ImmersiveLauncher(UIA):
# When the Windows 8 start screen opens, focus correctly goes to the first tile, but then incorrectly back to the root of the window.
# Ignore focus events on this object.
shouldAllowUIAFocusEvent=False
class StartButton(IAccessible):
"""For Windows 8.1 and 10 Start buttons to be recognized as proper buttons and to suppress selection announcement."""
role = controlTypes.ROLE_BUTTON
def _get_states(self):
# #5178: Selection announcement should be suppressed.
# Borrowed from Mozilla objects in NVDAObjects/IAccessible/Mozilla.py.
states = super(StartButton, self).states
states.discard(controlTypes.STATE_SELECTED)
return states
CHAR_LTR_MARK = u'\u200E'
CHAR_RTL_MARK = u'\u200F'
class UIProperty(UIA):
#Used for columns in Windows Explorer Details view.
#These can contain dates that include unwanted left-to-right and right-to-left indicator characters.
def _get_value(self):
value = super(UIProperty, self).value
if value is None:
return value
return value.replace(CHAR_LTR_MARK,'').replace(CHAR_RTL_MARK,'')
class ReadOnlyEditBox(IAccessible):
#Used for read-only edit boxes in a properties window.
#These can contain dates that include unwanted left-to-right and right-to-left indicator characters.
def _get_windowText(self):
windowText = super(ReadOnlyEditBox, self).windowText
if windowText is not None:
return windowText.replace(CHAR_LTR_MARK,'').replace(CHAR_RTL_MARK,'')
return windowText
class MetadataEditField(RichEdit50):
""" Used for metadata edit fields in Windows Explorer in Windows 7.
By default these fields would use ITextDocumentTextInfo ,
but to avoid Windows Explorer crashes we need to use EditTextInfo here. """
@classmethod
def _get_TextInfo(cls):
if ((winVersion.winVersion.major, winVersion.winVersion.minor) == (6, 1)):
cls.TextInfo = EditTextInfo
else:
cls.TextInfo = super().TextInfo
return cls.TextInfo
class AppModule(appModuleHandler.AppModule):
def chooseNVDAObjectOverlayClasses(self, obj, clsList):
windowClass = obj.windowClassName
role = obj.role
if windowClass in ("Search Box","UniversalSearchBand") and role==controlTypes.ROLE_PANE and isinstance(obj,IAccessible):
clsList.insert(0,SearchBoxClient)
return # Optimization: return early to avoid comparing class names and roles that will never match.
if windowClass == "ToolbarWindow32":
if role != controlTypes.ROLE_POPUPMENU:
try:
# The toolbar's immediate parent is its window object, so we need to go one further.
toolbarParent = obj.parent.parent
if role != controlTypes.ROLE_TOOLBAR:
# Toolbar item.
toolbarParent = toolbarParent.parent
except AttributeError:
toolbarParent = None
if toolbarParent and toolbarParent.windowClassName == "SysPager":
clsList.insert(0, NotificationArea)
return
if windowClass == "Edit" and controlTypes.STATE_READONLY in obj.states:
clsList.insert(0, ReadOnlyEditBox)
return # Optimization: return early to avoid comparing class names and roles that will never match.
if windowClass == "SysListView32":
if(
role == controlTypes.ROLE_MENUITEM
or(
role == controlTypes.ROLE_LISTITEM
and obj.simpleParent
and obj.simpleParent.simpleParent
and obj.simpleParent.simpleParent == api.getDesktopObject()
)
):
clsList.insert(0, SysListView32EmittingDuplicateFocusEvents)
return # Optimization: return early to avoid comparing class names and roles that will never match.
# #5178: Start button in Windows 8.1 and 10 should not have been a list in the first place.
if windowClass == "Start" and role in (controlTypes.ROLE_LIST, controlTypes.ROLE_BUTTON):
if role == controlTypes.ROLE_LIST:
clsList.remove(List)
clsList.insert(0, StartButton)
return # Optimization: return early to avoid comparing class names and roles that will never match.
if windowClass == 'RICHEDIT50W' and obj.windowControlID == 256:
clsList.insert(0, MetadataEditField)
return # Optimization: return early to avoid comparing class names and roles that will never match.
if isinstance(obj, UIA):
uiaClassName = obj.UIAElement.cachedClassName
if uiaClassName == "GridTileElement":
clsList.insert(0, GridTileElement)
elif uiaClassName == "GridListTileElement":
clsList.insert(0, GridListTileElement)
elif uiaClassName == "GridGroup":
clsList.insert(0, GridGroup)
elif uiaClassName == "ImmersiveLauncher" and role == controlTypes.ROLE_PANE:
clsList.insert(0, ImmersiveLauncher)
elif uiaClassName == "ListViewItem" and obj.UIAElement.cachedAutomationId.startswith('Suggestion_'):
clsList.insert(0, SuggestionListItem)
elif uiaClassName == "MultitaskingViewFrame" and role == controlTypes.ROLE_WINDOW:
clsList.insert(0, MultitaskingViewFrameWindow)
# Windows 10 task switch list
elif role == controlTypes.ROLE_LISTITEM and (
# RS4 and below we can match on a window class
windowClass == "MultitaskingViewFrame" or
# RS5 and above we must look for a particular UIA automationID on the list
isinstance(obj.parent,UIA) and obj.parent.UIAElement.cachedAutomationID=="SwitchItemListControl"
):
clsList.insert(0, MultitaskingViewFrameListItem)
elif uiaClassName == "UIProperty" and role == controlTypes.ROLE_EDITABLETEXT:
clsList.insert(0, UIProperty)
def event_NVDAObject_init(self, obj):
windowClass = obj.windowClassName
role = obj.role
if windowClass == "ToolbarWindow32" and role == controlTypes.ROLE_POPUPMENU:
parent = obj.parent
if parent and parent.windowClassName == "SysPager" and not (obj.windowStyle & 0x80):
# This is the menu for a group of icons on the task bar, which Windows stupidly names "Application".
obj.name = None
return
if windowClass == "#32768":
# Standard menu.
parent = obj.parent
if parent and not parent.parent:
# Context menu.
# We don't trust the names that Explorer gives to context menus, so better to have no name at all.
obj.name = None
return
if windowClass == "DV2ControlHost" and role == controlTypes.ROLE_PANE:
# Windows 7 start menu.
obj.presentationType=obj.presType_content
obj.isPresentableFocusAncestor = True
# In Windows 7, the description of this pane is extremely verbose help text, so nuke it.
obj.description = None
return
# The Address bar is embedded inside a progressbar, how strange.
# Lets hide that
if windowClass=="msctls_progress32" and winUser.getClassName(winUser.getAncestor(obj.windowHandle,winUser.GA_PARENT))=="Address Band Root":
obj.presentationType=obj.presType_layout
return
if windowClass == "DirectUIHWND" and role == controlTypes.ROLE_LIST:
if obj.parent and obj.parent.parent:
parent = obj.parent.parent.parent
if parent is not None and parent.windowClassName == "Desktop Search Open View":
# List containing search results in Windows 7 start menu.
# Its name is not useful so discard it.
obj.name = None
return
def event_gainFocus(self, obj, nextHandler):
wClass = obj.windowClassName
if wClass == "ToolbarWindow32" and obj.role == controlTypes.ROLE_MENUITEM and obj.parent.role == controlTypes.ROLE_MENUBAR and eventHandler.isPendingEvents("gainFocus"):
# When exiting a menu, Explorer fires focus on the top level menu item before it returns to the previous focus.
# Unfortunately, this focus event always occurs in a subsequent cycle, so the event limiter doesn't eliminate it.
# Therefore, if there is a pending focus event, don't bother handling this event.
return
if wClass in ("ForegroundStaging", "LauncherTipWnd", "ApplicationManager_DesktopShellWindow"):
# #5116: The Windows 10 Task View fires foreground/focus on this weird invisible window and foreground staging screen before and after it appears.
# This causes NVDA to report "unknown", so ignore it.
# We can't do this using shouldAllowIAccessibleFocusEvent because this isn't checked for foreground.
# #8137: also seen when opening quick link menu (Windows+X) on Windows 8 and later.
return
if wClass == "WorkerW" and obj.role == controlTypes.ROLE_PANE and obj.name is None:
# #6671: Never allow WorkerW thread to send gain focus event, as it causes 'pane" to be announced when minimizing windows or moving to desktop.
return
nextHandler()
def isGoodUIAWindow(self, hwnd):
# #9204: shell raises window open event for emoji panel in build 18305 and later.
if winVersion.isWin10(version=1903) and winUser.getClassName(hwnd) == "ApplicationFrameWindow":
return True
return False
def event_UIA_window_windowOpen(self, obj, nextHandler):
# Send UIA window open event to input app window.
if isinstance(obj, UIA) and obj.UIAElement.cachedClassName == "ApplicationFrameWindow":
inputPanelWindow = obj.firstChild
inputPanelAppName = (
# 19H2 and earlier
"windowsinternal_composableshell_experiences_textinput_inputapp",
# 20H1 and later
"textinputhost"
)
if inputPanelWindow and inputPanelWindow.appModule.appName in inputPanelAppName:
eventHandler.executeEvent("UIA_window_windowOpen", inputPanelWindow)
return
nextHandler()