Skip to content

Commit 6244b4a

Browse files
authored
Merge d62f8ac into f6977fe
2 parents f6977fe + d62f8ac commit 6244b4a

6 files changed

Lines changed: 436 additions & 108 deletions

File tree

source/NVDAObjects/IAccessible/__init__.py

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

66
import typing
77
from typing import (
8+
Generator,
89
Optional,
10+
Tuple,
911
Union,
1012
List,
1113
)
@@ -1523,7 +1525,7 @@ def _getIA2TargetsForRelationsOfType(
15231525
self,
15241526
relationType: "IAccessibleHandler.RelationType",
15251527
maxRelations: int = 1,
1526-
) -> typing.List[IUnknown]:
1528+
) -> Generator[IUnknown, None, None]:
15271529
"""Gets the target IAccessible (actually IUnknown; use QueryInterface or
15281530
normalizeIAccessible to resolve) for the relations with given type.
15291531
Allows escape of exception: COMError(-2147417836, 'Requested object does not exist.'),
@@ -1539,17 +1541,20 @@ def _getIA2TargetsForRelationsOfType(
15391541
raise ValueError
15401542
if not isinstance(acc, IA2.IAccessible2_2):
15411543
acc = acc.QueryInterface(IA2.IAccessible2_2)
1544+
15421545
targets, count = acc.relationTargetsOfType(
15431546
relationType.value,
1547+
# Bug in relationTargetsOfType, Chrome does not respect maxRelations param.
1548+
# https://crbug.com/1399184
15441549
maxRelations
15451550
)
1551+
log.debug(f"Got {count} relations, given maxRelations: {maxRelations}")
15461552
if count == 0:
1547-
return list()
1548-
relationsGen = (
1553+
return
1554+
yield from (
15491555
targets[i]
15501556
for i in range(min(maxRelations, count))
15511557
)
1552-
return list(relationsGen)
15531558

15541559
def _getIA2RelationFirstTarget(
15551560
self,
@@ -1568,17 +1573,18 @@ def _getIA2RelationFirstTarget(
15681573

15691574
try:
15701575
# rather than fetch all the relations and querying the type, do that in process for performance reasons
1571-
targets = self._getIA2TargetsForRelationsOfType(relationType, maxRelations=1)
1572-
if targets:
1573-
ia2Object = IAccessibleHandler.normalizeIAccessible(targets[0])
1576+
targetsGen = self._getIA2TargetsForRelationsOfType(relationType, maxRelations=1)
1577+
for target in targetsGen:
1578+
ia2Object = IAccessibleHandler.normalizeIAccessible(target)
1579+
# Just take the first.
15741580
return IAccessible(
15751581
IAccessibleObject=ia2Object,
15761582
IAccessibleChildID=0
15771583
)
15781584
except (NotImplementedError, COMError):
15791585
log.debugWarning("Unable to use _getIA2TargetsForRelationsOfType, fallback to _IA2Relations.")
15801586

1581-
# eg IA2_2 is not available, fall back to old approach
1587+
# IA2_2 is not available, fall back to old approach
15821588
try:
15831589
for relation in self._IA2Relations:
15841590
if relation.relationType == relationType:
@@ -1594,21 +1600,74 @@ def _getIA2RelationFirstTarget(
15941600
pass
15951601
return None
15961602

1603+
def _getIA2RelationTargetsOfType(
1604+
self,
1605+
relationType: Union[str, IAccessibleHandler.RelationType]
1606+
) -> typing.Iterable["IAccessible"]:
1607+
""" Get the targets for the relation of type.
1608+
Higher level function than _getIA2TargetsForRelationsOfType
1609+
@param relationType: The type of relation to fetch.
1610+
"""
1611+
if not isinstance(relationType, IAccessibleHandler.RelationType):
1612+
if isinstance(relationType, str):
1613+
relationType = IAccessibleHandler.RelationType(relationType)
1614+
else:
1615+
raise TypeError(f"Bad type for 'relationType' arg, got: {type(relationType)}")
1616+
1617+
relationType = typing.cast(IAccessibleHandler.RelationType, relationType)
1618+
1619+
try:
1620+
# rather than fetch all the relations and querying the type, do that in process for performance reasons
1621+
# Bug in Chrome, Chrome does not respect maxRelations param.
1622+
# https://crbug.com/1399184. In future, uncomment the next line.
1623+
# maxRelsToFetch = self.IAccessibleObject.nRelations # they may or may not all match 'relationType'
1624+
maxRelsToFetch = 10
1625+
targetsGen = self._getIA2TargetsForRelationsOfType(relationType, maxRelations=maxRelsToFetch)
1626+
for target in targetsGen:
1627+
ia2Object = IAccessibleHandler.normalizeIAccessible(target)
1628+
ia = IAccessible(
1629+
IAccessibleObject=ia2Object,
1630+
IAccessibleChildID=0
1631+
)
1632+
yield ia
1633+
# NotImplementedError is expected to occur for all targets or none.
1634+
return # Iterated all targets without error.
1635+
except (NotImplementedError, COMError):
1636+
log.debugWarning("Unable to use _getIA2TargetsForRelationsOfType, fallback to _IA2Relations.")
1637+
1638+
# IA2_2 is not available, fall back to old approach
1639+
log.debugWarning("IA2_2 is not available, fall back to old approach")
1640+
try:
1641+
for relation in self._IA2Relations:
1642+
if relation.relationType == relationType:
1643+
# Take the first of 'relation.nTargets' see IAccessibleRelation._methods_
1644+
for i in range(0, relation.nTargets):
1645+
target = relation.target(i)
1646+
ia2Object = IAccessibleHandler.normalizeIAccessible(target)
1647+
yield IAccessible(
1648+
IAccessibleObject=ia2Object,
1649+
IAccessibleChildID=0
1650+
)
1651+
return
1652+
except (NotImplementedError, COMError):
1653+
log.debug("Unable to fetch _IA2Relations", exc_info=True)
1654+
pass
1655+
return None
1656+
15971657
#: Type definition for auto prop '_get_detailsRelations'
1598-
detailsRelations: typing.Iterable["IAccessible"]
1658+
detailsRelations: Tuple["IAccessible"]
1659+
1660+
def _get_detailsRelations(self) -> Tuple["IAccessible"]:
1661+
detailsRelsGen = self._getIA2RelationTargetsOfType(IAccessibleHandler.RelationType.DETAILS)
1662+
# due to caching of baseObject.AutoPropertyObject, do not attempt to return a generator.
1663+
return tuple(detailsRelsGen)
15991664

16001665
def _get_controllerFor(self) -> List[NVDAObject]:
16011666
control = self._getIA2RelationFirstTarget(IAccessibleHandler.RelationType.CONTROLLER_FOR)
16021667
if control:
16031668
return [control]
16041669
return []
16051670

1606-
def _get_detailsRelations(self) -> typing.Iterable["IAccessible"]:
1607-
relationTarget = self._getIA2RelationFirstTarget(IAccessibleHandler.RelationType.DETAILS)
1608-
if not relationTarget:
1609-
return ()
1610-
return (relationTarget, )
1611-
16121671
#: Type definition for auto prop '_get_flowsTo'
16131672
flowsTo: typing.Optional["IAccessible"]
16141673

@@ -1763,7 +1822,7 @@ def _get_devInfo(self):
17631822
ret = "exception: %s" % e
17641823
info.append("IAccessible2 attributes: %s" % ret)
17651824
try:
1766-
ret = ", ".join(r.RelationType for r in self._IA2Relations)
1825+
ret = ", ".join(f"{r.RelationType} * {r.nTargets}" for r in self._IA2Relations)
17671826
except Exception as e:
17681827
ret = f"exception: {e}"
17691828
info.append(f"IAccessible2 relations: {ret}")

source/NVDAObjects/IAccessible/ia2Web.py

Lines changed: 90 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,17 @@
66
"""Base classes with common support for browsers exposing IAccessible2.
77
"""
88
import typing
9+
from typing import (
10+
Iterable,
11+
)
912
from ctypes import c_short
1013
from comtypes import COMError, BSTR
1114

1215
import oleacc
16+
from annotation import (
17+
AnnotationTarget,
18+
AnnotationOrigin,
19+
)
1320
from comInterfaces import IAccessible2Lib as IA2
1421
import controlTypes
1522
from logHandler import log
@@ -24,6 +31,76 @@
2431
import NVDAObjects
2532

2633

34+
class IA2WebAnnotationTarget(AnnotationTarget):
35+
def __init__(self, target: IAccessible):
36+
self._target: IAccessible = target
37+
38+
@property
39+
def summary(self) -> str:
40+
return self._target.summarizeInProcess()
41+
42+
@property
43+
def role(self) -> controlTypes.Role:
44+
return self._target.role
45+
46+
@property
47+
def targetObject(self) -> IAccessible:
48+
return self._target
49+
50+
51+
class IA2WebAnnotation(AnnotationOrigin):
52+
_originObj: "Ia2Web"
53+
54+
def __bool__(self) -> bool:
55+
return bool(
56+
self._originObj.IA2Attributes.get("details-roles")
57+
)
58+
59+
@property
60+
def targets(self) -> Iterable[AnnotationTarget]:
61+
if not bool(self):
62+
# optimisation that avoids having to fetch details relations which may be a more costly procedure.
63+
if config.conf["debugLog"]["annotations"]:
64+
log.debug("no annotations available")
65+
return
66+
67+
ia2WebAnnotationTargetsGen = (
68+
IA2WebAnnotationTarget(rel)
69+
for rel in self._originObj.detailsRelations
70+
)
71+
yield from ia2WebAnnotationTargetsGen
72+
73+
@property
74+
def roles(self) -> Iterable[controlTypes.Role]:
75+
"""
76+
Since Chromium exposes the roles via the "details-roles" IA2Attributes, an optimisation can be used
77+
to return them.
78+
@remarks: The order of "details-roles" IA2Attributes is expected to match the order of detailsRelations
79+
objects.
80+
"""
81+
from .chromium import supportedAriaDetailsRoles
82+
# Currently only defined in Chrome as of May 2022
83+
# Refer to ComputeDetailsRoles
84+
# https://chromium.googlesource.com/chromium/src/+/main/ui/accessibility/platform/ax_platform_node_base.cc#2419
85+
detailsRoles = self._originObj.IA2Attributes.get("details-roles")
86+
if not detailsRoles:
87+
if config.conf["debugLog"]["annotations"]:
88+
log.debug("details-roles not found")
89+
return None
90+
91+
for roleStr in detailsRoles.split(" "):
92+
# Created supported details role
93+
detailsRole = supportedAriaDetailsRoles.get(roleStr)
94+
if config.conf["debugLog"]["annotations"]:
95+
log.debug(f"detailsRole: {repr(detailsRole)}")
96+
yield detailsRole
97+
98+
@property
99+
def summaries(self) -> Iterable[str]:
100+
for target in self.targets:
101+
yield target.summary
102+
103+
27104
class Ia2Web(IAccessible):
28105
IAccessibleTableUsesTableCellIndexAttrib=True
29106
caretMovementDetectionUsesEvents = False
@@ -60,42 +137,26 @@ def _get_descriptionFrom(self) -> controlTypes.DescriptionFrom:
60137
log.debugWarning(f"Unknown 'description-from' IA2Attribute value: {ia2attrDescriptionFrom}")
61138
return controlTypes.DescriptionFrom.UNKNOWN
62139

140+
annotations: "IA2WebAnnotation"
141+
"""Typing information for auto property _get_annotations
142+
"""
143+
def _get_annotations(self) -> "AnnotationOrigin":
144+
annotationOrigin = IA2WebAnnotation(self)
145+
return annotationOrigin
146+
63147
def _get_detailsSummary(self) -> typing.Optional[str]:
64-
if not self.hasDetails:
65-
# optimisation that avoids having to fetch details relations which may be a more costly procedure.
66-
if config.conf["debugLog"]["annotations"]:
67-
log.debug("no details-roles")
68-
return None
69-
detailsRelations = self.detailsRelations
70-
if not detailsRelations:
71-
log.error("should be able to fetch detailsRelations")
72-
return None
73-
for target in detailsRelations:
148+
for summary in self.annotations.summaries:
74149
# just take the first for now.
75-
return target.summarizeInProcess()
150+
return summary
76151

77152
@property
78153
def hasDetails(self) -> bool:
79-
return bool(self.IA2Attributes.get("details-roles"))
154+
return bool(self.annotations)
80155

81156
def _get_detailsRole(self) -> typing.Optional[controlTypes.Role]:
82-
from .chromium import supportedAriaDetailsRoles
83-
# Currently only defined in Chrome as of May 2022
84-
# Refer to ComputeDetailsRoles
85-
# https://chromium.googlesource.com/chromium/src/+/main/ui/accessibility/platform/ax_platform_node_base.cc#2419
86-
detailsRoles = self.IA2Attributes.get("details-roles")
87-
if not detailsRoles:
88-
if config.conf["debugLog"]["annotations"]:
89-
log.debug("details-roles not found")
90-
return None
91-
92-
firstDetailsRole = detailsRoles.split(" ")[0]
93-
# return a supported details role
94-
detailsRole = supportedAriaDetailsRoles.get(firstDetailsRole)
95-
if config.conf["debugLog"]["annotations"]:
96-
log.debug(f"detailsRole: {repr(detailsRole)}")
97-
return detailsRole
98-
157+
for role in self.annotations.roles:
158+
# just take the first for now.
159+
return role
99160

100161
def _get_isCurrent(self) -> controlTypes.IsCurrent:
101162
ia2attrCurrent: str = self.IA2Attributes.get("current", "false")

0 commit comments

Comments
 (0)