Skip to content

Commit

Permalink
Support IAccessible2 labelled-by relation (#17437)
Browse files Browse the repository at this point in the history
* 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 "<console>", line 1, in <module>
      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
    <NVDAObjects.IAccessible.IAccessible object at 0x0BC55A70>
    >>> focus.labeledBy.name
    'Company:'
    >>> focus.labeledBy.role
    <Role.STATICTEXT: 7>

 ### 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.

<!-- Please keep the following -->
@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 feea4aa
    Author: Michael Weghorn <[email protected]>
    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 <[email protected]>
Co-authored-by: Sean Budd <[email protected]>
  • Loading branch information
3 people authored Dec 2, 2024
1 parent c451a4d commit 83fbd7d
Show file tree
Hide file tree
Showing 3 changed files with 15 additions and 5 deletions.
1 change: 1 addition & 0 deletions source/IAccessibleHandler/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,4 @@ class RelationType(str, enum.Enum):
CONTROLLER_FOR = "controllerFor"
ERROR = "error"
ERROR_FOR = "errorFor"
LABELLED_BY = "labelledBy"
14 changes: 10 additions & 4 deletions source/NVDAObjects/IAccessible/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
Optional,
Tuple,
Union,
List,
)

from comtypes.automation import IEnumVARIANT, VARIANT
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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]
Expand Down
5 changes: 4 additions & 1 deletion user_docs/en/changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down

0 comments on commit 83fbd7d

Please sign in to comment.