From 83fbd7dc5005a6c3fdf78200982654a475b30a08 Mon Sep 17 00:00:00 2001 From: Michael Weghorn Date: Mon, 2 Dec 2024 03:14:57 +0100 Subject: [PATCH] Support IAccessible2 labelled-by relation (#17437) * Support IAccessible2 labelled-by relation ### Link to issue number: Fixes #17436 ### Summary of the issue: IAccessible2 has an IA2_RELATION_LABELLED_BY relation type, see https://accessibility.linuxfoundation.org/a11yspecs/ia2/docs/html/group__grp_relations.html#ga7bbace7ffb57476b75d621af2e27d1ff . However, that one was not taken into account by NVDA's "labeledBy" property for objects. This could could be seen for example with LibreOffice that implements handling of that IAccessible2 relation. Additionally, a `None` return value from IAccessibleHandler.accNavigate wasn't handled in `IAccessible._get_labeledBy`, triggering an error, e.g.: >>> focus.labeledBy Traceback (most recent call last): File "", line 1, in File "C:\tools\cygwin\home\user\development\git\nvda\source\baseObject.py", line 59, in __get__ return instance._getPropertyViaCache(self.fget) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "C:\tools\cygwin\home\user\development\git\nvda\source\baseObject.py", line 167, in _getPropertyViaCache val = getterMethod(self) ^^^^^^^^^^^^^^^^^^ File "C:\tools\cygwin\home\user\development\git\nvda\source\NVDAObjects\IAccessible\__init__.py", line 1167, in _get_labeledBy (pacc, accChild) = IAccessibleHandler.accNavigate( ^^^^^^^^^^^^^^^^ TypeError: cannot unpack non-iterable NoneType object ### Description of user facing changes In NVDA's Python console, retrieving the "labeledBy" property now works for objects in applications implementing the "labelled-by" IAccessible2 relation and no longer triggers an error. ### Description of development approach * Extend `IAccessibleHandler.RelationType` with the `LABELLED_BY` relation as defined in the IAccessible2 spec. * Use this relation in `IAccessible._get_labeledBy` before falling back to trying a custom Mozilla/Gecko specific NAVRELATION. * Add handling for the case where `IAccessibleHandler.accNavigate` for the custom Mozilla/Geck approach returns `None` to fix the error triggered otherwise when no relation is set. ### Testing strategy: 1. start NVDA 2. Start LibreOffice Writer 3. open the options dialog: "Tools" -> "Options", go to the "User Data" page 4. move focus to the "Company" text edit 5. open NVDA's Python console (Ctrl+Insert+Z) 6. print the object that the currently focused edit is labelled by, and it's accessible name and role: >>> focus.labeledBy >>> focus.labeledBy.name 'Company:' >>> focus.labeledBy.role ### Known issues with pull request: None ### Code Review Checklist: - [x] Documentation: - Change log entry - User Documentation - Developer / Technical Documentation - Context sensitive help for GUI changes - [x] Testing: - Unit tests - System (end to end) tests - Manual testing - [x] UX of all users considered: - Speech - Braille - Low Vision - Different web browsers - Localization in other languages / culture than English - [x] API is compatible with existing add-ons. - [x] Security precautions taken. @coderabbitai summary * Add type annotation * Move changelog entry to "Changes for Developers" section * Avoid type clash due to typing.List import Use qualified name. Otherwise the newly introduced from __future__ import annotations in commit feea4aa0aa0a6b12c33ee8dd70f22d76a2cc6d83 Author: Michael Weghorn Date: Wed Nov 27 09:12:38 2024 +0000 Add type annotation causes source/NVDAObjects/IAccessible/__init__.py:2391:7: F811 Redefinition of unused `List` from line 13 | 2391 | class List(IAccessible): | ^^^^ F811 2392 | def _get_role(self): 2393 | return controlTypes.Role.LIST | = help: Remove definition: `List` Found 1 error. * Apply suggestions from code review * Add comment why import is needed Without deferred evaluation (s. PEPs mentioned in the newly added comment): $ ./runlint.bat Ensuring NVDA Python virtual environment call ruff check --fix source\NVDAObjects\IAccessible\__init__.py:1165:30: F821 Undefined name `IAccessible` | 1163 | return True 1164 | 1165 | def _get_labeledBy(self) -> IAccessible | None: | ^^^^^^^^^^^ F821 1166 | label = self._getIA2RelationFirstTarget(IAccessibleHandler.RelationType.LABELLED_BY) 1167 | if label: | Found 1 error. Deactivating NVDA Python virtual environment * Apply suggestions from code review * Update source/NVDAObjects/IAccessible/__init__.py * Update source/NVDAObjects/IAccessible/__init__.py --------- Co-authored-by: Sean Budd Co-authored-by: Sean Budd --- source/IAccessibleHandler/types.py | 1 + source/NVDAObjects/IAccessible/__init__.py | 14 ++++++++++---- user_docs/en/changes.md | 5 ++++- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/source/IAccessibleHandler/types.py b/source/IAccessibleHandler/types.py index 2d9e81a39ba..ec4318d7252 100644 --- a/source/IAccessibleHandler/types.py +++ b/source/IAccessibleHandler/types.py @@ -29,3 +29,4 @@ class RelationType(str, enum.Enum): CONTROLLER_FOR = "controllerFor" ERROR = "error" ERROR_FOR = "errorFor" + LABELLED_BY = "labelledBy" diff --git a/source/NVDAObjects/IAccessible/__init__.py b/source/NVDAObjects/IAccessible/__init__.py index 2ab07700488..3e2cb96094e 100644 --- a/source/NVDAObjects/IAccessible/__init__.py +++ b/source/NVDAObjects/IAccessible/__init__.py @@ -9,7 +9,6 @@ Optional, Tuple, Union, - List, ) from comtypes.automation import IEnumVARIANT, VARIANT @@ -1162,13 +1161,20 @@ def isPointInObject(self, x, y): return False return True - def _get_labeledBy(self): + def _get_labeledBy(self) -> "IAccessible | None": + label = self._getIA2RelationFirstTarget(IAccessibleHandler.RelationType.LABELLED_BY) + if label: + return label + try: - (pacc, accChild) = IAccessibleHandler.accNavigate( + ret = IAccessibleHandler.accNavigate( self.IAccessibleObject, self.IAccessibleChildID, IAccessibleHandler.NAVRELATION_LABELLED_BY, ) + if not ret: + return None + (pacc, accChild) = ret obj = IAccessible(IAccessibleObject=pacc, IAccessibleChildID=accChild) return obj except COMError: @@ -1940,7 +1946,7 @@ def _get_detailsRelations(self) -> Tuple["IAccessible"]: # due to caching of baseObject.AutoPropertyObject, do not attempt to return a generator. return tuple(detailsRelsGen) - def _get_controllerFor(self) -> List[NVDAObject]: + def _get_controllerFor(self) -> list[NVDAObject]: control = self._getIA2RelationFirstTarget(IAccessibleHandler.RelationType.CONTROLLER_FOR) if control: return [control] diff --git a/user_docs/en/changes.md b/user_docs/en/changes.md index 8e10c9ca3dd..36af04e82f0 100644 --- a/user_docs/en/changes.md +++ b/user_docs/en/changes.md @@ -108,7 +108,10 @@ Add-ons will need to be re-tested and have their manifest updated. * Removed the requirement to indent function parameter lists by two tabs from NVDA's Coding Standards, to be compatible with modern automatic linting. (#17126, @XLTechie) * Added the [VS Code workspace configuration for NVDA](https://nvaccess.org/nvaccess/vscode-nvda) as a git submodule. (#17003) * In the `brailleTables` module, a `getDefaultTableForCurrentLang` function has been added (#17222, @nvdaes) -* Retrieving the `labeledBy` property now works for UIA elements supporting the corresponding `LabeledBy` UIA property. (#17442, @michaelweghorn) +* Retrieving the `labeledBy` property now works for: + * objects in applications implementing the `labelled-by` IAccessible2 relation. (#17436, @michaelweghorn) + * UIA elements supporting the corresponding `LabeledBy` UIA property. (#17442, @michaelweghorn) + #### API Breaking Changes