Skip to content

Commit e8e80b2

Browse files
authored
Merge 2e2d5a7 into f534c9a
2 parents f534c9a + 2e2d5a7 commit e8e80b2

4 files changed

Lines changed: 57 additions & 17 deletions

File tree

source/IAccessibleHandler/__init__.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import re
77
import struct
8+
from typing import Optional, Tuple
89
import weakref
910
from ctypes import (
1011
wintypes,
@@ -422,22 +423,22 @@ def accNavigate(pacc, childID, direction):
422423
return None
423424

424425

425-
def winEventToNVDAEvent(eventID, window, objectID, childID, useCache=True):
426-
"""Tries to convert a win event ID to an NVDA event name, and instanciate or fetch an NVDAObject for
426+
def winEventToNVDAEvent(
427+
eventID: int,
428+
window: int,
429+
objectID: int,
430+
childID: int,
431+
useCache: bool = True
432+
) -> Optional[Tuple[str, NVDAObjects.IAccessible.IAccessible]]:
433+
"""Tries to convert a win event ID to an NVDA event name, and instantiate or fetch an NVDAObject for
427434
the win event parameters.
428435
@param eventID: the win event ID (type)
429-
@type eventID: integer
430436
@param window: the win event's window handle
431-
@type window: integer
432437
@param objectID: the win event's object ID
433-
@type objectID: integer
434438
@param childID: the win event's childID
435-
@type childID: the win event's childID
436439
@param useCache: C{True} to use the L{liveNVDAObjectTable} cache when
437440
retrieving an NVDAObject, C{False} if the cache should not be used.
438-
@type useCache: boolean
439441
@returns: the NVDA event name and the NVDAObject the event is for
440-
@rtype: tuple of string and L{NVDAObjects.IAccessible.IAccessible}
441442
"""
442443
if isMSAADebugLoggingEnabled():
443444
log.debug(

source/NVDAObjects/__init__.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -535,10 +535,12 @@ def _get_locationText(self):
535535
# Translators: Reports navigator object's dimensions (example output: object edges positioned 20 per cent from left edge of screen, 10 per cent from top edge of screen, width is 40 per cent of screen, height is 50 per cent of screen).
536536
return _("Object edges positioned {left:.1f} per cent from left edge of screen, {top:.1f} per cent from top edge of screen, width is {width:.1f} per cent of screen, height is {height:.1f} per cent of screen").format(left=percentFromLeft,top=percentFromTop,width=percentWidth,height=percentHeight)
537537

538-
def _get_parent(self):
538+
#: Typing information for auto-property: _get_parent
539+
parent: typing.Optional['NVDAObject']
540+
541+
def _get_parent(self) -> typing.Optional['NVDAObject']:
539542
"""Retrieves this object's parent (the object that contains this object).
540543
@return: the parent object if it exists else None.
541-
@rtype: L{NVDAObject} or None
542544
"""
543545
return None
544546

source/api.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,11 @@
2727

2828
#User functions
2929

30-
def getFocusObject():
30+
def getFocusObject() -> NVDAObjects.NVDAObject:
31+
"""
32+
Gets the current object with focus.
33+
@returns: the object with focus
3134
"""
32-
Gets the current object with focus.
33-
@returns: the object with focus
34-
@rtype: L{NVDAObjects.NVDAObject}
35-
"""
3635
return globalVars.focusObject
3736

3837
def getForegroundObject():

source/eventHandler.py

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
import config
2222
import winUser
2323
import extensionPoints
24+
import oleacc
25+
2426

2527
#Some dicts to store event counts by name and or obj
2628
_pendingEventCountsByName={}
@@ -175,22 +177,25 @@ def __init__(self, obj, reportDevInfo: bool):
175177
elif not hasattr(obj, WAS_GAIN_FOCUS_OBJ_ATTR_NAME):
176178
setattr(obj, WAS_GAIN_FOCUS_OBJ_ATTR_NAME, False)
177179

178-
def _checkIfValid(self):
180+
def _checkIfValid(self) -> bool:
179181
stillValid = (
180182
self.isLastFocusObj()
181183
or not self.previouslyHadFocus()
182184
or self.isAncestorOfCurrentFocus()
183185
# Ensure titles for dialogs gaining focus are reported, EG NVDA Find dialog
184186
or self.isForegroundObject()
187+
# Ensure menu items are reported when focus is gained to the menu start (see #12624).
188+
or self.isMenuItemOfCurrentFocus()
185189
)
186190
return stillValid
187191

188-
def _getDevInfo(self):
192+
def _getDevInfo(self) -> str:
189193
return (
190194
f"isLast: {self.isLastFocusObj()}"
191195
f", previouslyHad: {self.previouslyHadFocus()}"
192196
f", isAncestorOfCurrentFocus: {self.isAncestorOfCurrentFocus()}"
193197
f", is foreground obj {self.isForegroundObject()}"
198+
f", isMenuItemOfCurrentFocus: {self.isMenuItemOfCurrentFocus()}"
194199
)
195200

196201
def isLastFocusObj(self):
@@ -208,6 +213,39 @@ def isForegroundObject(self):
208213
foreground = api.getForegroundObject()
209214
return self._obj is foreground or self._obj == foreground
210215

216+
def isMenuItemOfCurrentFocus(self) -> bool:
217+
"""
218+
Checks if the current object is a menu item of the current focus.
219+
The only known case where this returns True is the following (see #12624):
220+
221+
When opening a submenu in certain applications (like Thunderbird 78.12),
222+
NVDA can process a menu start event after the first item in the menu is focused.
223+
The menu start event causes a focus event on the menu, taking NVDA's focus from the menu item.
224+
Additionally, the "menu" parent of the submenu item is not keyboard focusable, and is separate from
225+
the menu item which triggered the submenu.
226+
The object tree in this case (menu item > submenu (not keyboard focusable) > submenu item).
227+
The focus event order after activating the menu item's sub menu is (submenu item, submenu).
228+
"""
229+
from NVDAObjects import IAccessible
230+
lastFocus = api.getFocusObject()
231+
_isMenuItemOfCurrentFocus = (
232+
self._obj.parent
233+
and isinstance(self._obj, IAccessible.IAccessible)
234+
and isinstance(lastFocus, IAccessible.IAccessible)
235+
and self._obj.IAccessibleRole == oleacc.ROLE_SYSTEM_MENUITEM
236+
and lastFocus.IAccessibleRole == oleacc.ROLE_SYSTEM_MENUPOPUP
237+
and self._obj.parent == lastFocus
238+
)
239+
if _isMenuItemOfCurrentFocus:
240+
# Change this to log.error for easy debugging
241+
log.debugWarning(
242+
"This parent menu was not announced properly, "
243+
"and should have been focused before the submenu item.\n"
244+
f"Object info: {self._obj.devInfo}\n"
245+
f"Object parent info: {self._obj.parent.devInfo}\n"
246+
)
247+
return _isMenuItemOfCurrentFocus
248+
211249

212250
def _getFocusLossCancellableSpeechCommand(
213251
obj,

0 commit comments

Comments
 (0)