Skip to content

Commit

Permalink
Re-order focus events for menuitemcheckbox and menuitemradio (#16551)
Browse files Browse the repository at this point in the history
Fixes #14550

Summary of the issue:
NVDA does not correctly announce checkbox or radio button menuitems when first entering submenues in Firefox or Chrome, as its announcement of the menuitem is disrupted by its announcement of the containing menu or grouping.

Description of user facing changes
Checkbox and radio button menuitems are correctly read in Chrome and Firefox.

Description of development approach
Updated FocusLossCancellableSpeechCommand.isMenuItemOfCurrentFocus to:

Detect objects with role IA2_ROLE_CHECK_MENU_ITEM or IA2.IA2_ROLE_RADIO_MENU_ITEM as well as oleacc.ROLE_SYSTEM_MENUITEM; and
Traverse up the old focus's ancestors to find the ancestor menu, instead of assuming that it will be its parent, to support grouped menuitems.
  • Loading branch information
SaschaCowley authored May 23, 2024
1 parent f1110a2 commit 3390edd
Show file tree
Hide file tree
Showing 2 changed files with 38 additions and 19 deletions.
56 changes: 37 additions & 19 deletions source/eventHandler.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import oleacc
from utils.security import objectBelowLockScreenAndWindowsIsLocked
import winVersion
from comInterfaces import IAccessible2Lib as IA2

if typing.TYPE_CHECKING:
import NVDAObjects
Expand All @@ -49,9 +50,6 @@ def queueEvent(eventName,obj,**kwargs):
@param eventName: the name of the event type (e.g. 'gainFocus', 'nameChange')
@type eventName: string
"""
# Allow NVDAObjects to redirect focus events to another object of their choosing.
if eventName == "gainFocus" and obj.focusRedirect:
obj = obj.focusRedirect
_trackFocusObject(eventName, obj)
with _pendingEventCountsLock:
_pendingEventCountsByName[eventName]=_pendingEventCountsByName.get(eventName,0)+1
Expand Down Expand Up @@ -242,7 +240,7 @@ def isForegroundObject(self):
def isMenuItemOfCurrentFocus(self) -> bool:
"""
Checks if the current object is a menu item of the current focus.
The only known case where this returns True is the following (see #12624):
The only known case where this returns True is the following (see #12624, #14550):
When opening a submenu in certain applications (like Thunderbird 78.12),
NVDA can process a menu start event after the first item in the menu is focused.
Expand All @@ -253,24 +251,41 @@ def isMenuItemOfCurrentFocus(self) -> bool:
The focus event order after activating the menu item's sub menu is (submenu item, submenu).
"""
from NVDAObjects import IAccessible

lastFocus = api.getFocusObject()
_isMenuItemOfCurrentFocus = (
self._obj.parent
and isinstance(self._obj, IAccessible.IAccessible)

# This case can only occur when:
# 1. the old and new focus targets are instances of IAccessible; and
# 2. the old focus is a menuitem, menuitemradio or menuitemcheckbox; and
# 3. the new focus is a menu; and
# 4. the old focus has a parent.
if not (
isinstance(self._obj, IAccessible.IAccessible)
and isinstance(lastFocus, IAccessible.IAccessible)
and self._obj.IAccessibleRole == oleacc.ROLE_SYSTEM_MENUITEM
and lastFocus.IAccessibleRole == oleacc.ROLE_SYSTEM_MENUPOPUP
and self._obj.parent == lastFocus
)
if _isMenuItemOfCurrentFocus:
# Change this to log.error for easy debugging
log.debugWarning(
"This parent menu was not announced properly, "
"and should have been focused before the submenu item.\n"
f"Object info: {self._obj.devInfo}\n"
f"Object parent info: {self._obj.parent.devInfo}\n"
and self._obj.IAccessibleRole in (
oleacc.ROLE_SYSTEM_MENUITEM,
IA2.IA2_ROLE_CHECK_MENU_ITEM,
IA2.IA2_ROLE_RADIO_MENU_ITEM
)
return _isMenuItemOfCurrentFocus
and lastFocus.IAccessibleRole == oleacc.ROLE_SYSTEM_MENUPOPUP
and self._obj.parent
):
return False

# Check that the old focus is a descendant of the new focus.
ancestor = self._obj.parent
while ancestor is not None:
if ancestor == lastFocus:
log.debugWarning(
"This ancestor menu was not announced properly, and should have been focused before the submenu item.\n"
f"Object info: {self._obj.devInfo}\n"
f"Ancestor info: {ancestor.devInfo}"
)
return True

ancestor = ancestor.parent

return False


def _getFocusLossCancellableSpeechCommand(
Expand Down Expand Up @@ -306,6 +321,9 @@ def executeEvent(
try:
global _virtualDesktopName
isGainFocus = eventName == "gainFocus"
# Allow NVDAObjects to redirect focus events to another object of their choosing.
if isGainFocus and obj.focusRedirect:
obj = obj.focusRedirect
sleepMode = obj.sleepMode
# Handle possible virtual desktop name change event.
# More effective in Windows 10 Version 1903 and later.
Expand Down
1 change: 1 addition & 0 deletions user_docs/en/changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
* NVDA will no longer announce "clipboard history" twice when navigating through the emoji panel menu items. (#16532, @josephsl)
* NVDA will no longer cut off speech and braille when reviewing kaomojis and symbols in the emoji panel. (#16533, @josephsl)
* In applications using Java Access Bridge, NVDA will now correctly read the last blank line of a text instead of repeating the previous line. (#9376, @dmitrii-drobotov)
* NVDA will correctly announce radio and checkbox menuitems when first entering submenus in Google Chrome and Mozilla Firefox. (#14550)

### Changes for Developers

Expand Down

0 comments on commit 3390edd

Please sign in to comment.