Skip to content

Commit 0d477a3

Browse files
Merge eefd0ec into 8929dd0
2 parents 8929dd0 + eefd0ec commit 0d477a3

6 files changed

Lines changed: 130 additions & 46 deletions

File tree

source/IAccessibleHandler/types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,4 @@ class RelationType(str, enum.Enum):
2525
CONTAINING_DOCUMENT = "containingDocument"
2626
DETAILS = "details"
2727
DETAILS_FOR = "detailsFor"
28+
CONTROLLER_FOR = "controllerFor"

source/NVDAObjects/IAccessible/__init__.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1596,6 +1596,12 @@ def _getIA2RelationFirstTarget(
15961596
#: Type definition for auto prop '_get_detailsRelations'
15971597
detailsRelations: typing.Iterable["IAccessible"]
15981598

1599+
def _get_controllerFor(self):
1600+
control = self._getIA2RelationFirstTarget(IAccessibleHandler.RelationType.CONTROLLER_FOR)
1601+
if control:
1602+
return [control]
1603+
return []
1604+
15991605
def _get_detailsRelations(self) -> typing.Iterable["IAccessible"]:
16001606
relationTarget = self._getIA2RelationFirstTarget(IAccessibleHandler.RelationType.DETAILS)
16011607
if not relationTarget:
@@ -1655,9 +1661,6 @@ def _get_groupName(self):
16551661
else:
16561662
return super(IAccessible,self)._get_groupName()
16571663

1658-
def event_selection(self):
1659-
return self.event_stateChange()
1660-
16611664
def event_selectionAdd(self):
16621665
return self.event_stateChange()
16631666

source/NVDAObjects/IAccessible/ia2Web.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,20 @@ class Ia2Web(IAccessible):
2626
IAccessibleTableUsesTableCellIndexAttrib=True
2727
caretMovementDetectionUsesEvents = False
2828

29+
def isDescendantOf(self, obj: "NVDAObjects.NVDAObject") -> bool:
30+
if obj.windowHandle != self.windowHandle:
31+
# Only supported on the same window.
32+
raise NotImplementedError
33+
if not isinstance(obj, Ia2Web):
34+
# #4080: Input composition NVDAObjects are the same window but not IAccessible2!
35+
raise NotImplementedError
36+
accId = obj.IA2UniqueID
37+
try:
38+
res = obj.IAccessibleObject.accChild(accId)
39+
except COMError:
40+
return False
41+
return bool(res)
42+
2943
def _get_positionInfo(self):
3044
info=super(Ia2Web,self).positionInfo
3145
level=info.get('level',None)

source/NVDAObjects/UIA/__init__.py

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
Dict,
1414
Tuple,
1515
)
16+
import array
1617
from ctypes.wintypes import POINT
1718
from comtypes import COMError
1819
import time
@@ -2006,6 +2007,27 @@ def _get_positionInfo(self):
20062007
def scrollIntoView(self):
20072008
pass
20082009

2010+
def isDescendantOf(self, obj: "NVDAObjects.NVDAObject") -> bool:
2011+
if isinstance(obj, UIA):
2012+
# As both objects are UIA,
2013+
# We can search this object's ancestors for obj with a UIA treeWalker
2014+
# which is much more efficient than fetching each parent.
2015+
objID = obj.UIAElement.GetRuntimeId()
2016+
objIDArray = array.array("l", objID)
2017+
UIACondition = UIAHandler.handler.clientObject.createPropertyCondition(
2018+
UIAHandler.UIA_RuntimeIdPropertyId,
2019+
objIDArray
2020+
)
2021+
UIAWalker = UIAHandler.handler.clientObject.createTreeWalker(UIACondition)
2022+
try:
2023+
objUIAElement = UIAWalker.normalizeElement(self.UIAElement)
2024+
except COMError:
2025+
log.debugWarning("Error walking ancestors", exc_info=True)
2026+
objUIAElement = None
2027+
return bool(objUIAElement)
2028+
else: # not UIA
2029+
raise NotImplementedError
2030+
20092031
def _get_controllerFor(self):
20102032
e=self._getUIACacheablePropertyValue(UIAHandler.UIA_ControllerForPropertyId)
20112033
if UIAHandler.handler.clientObject.checkNotSupported(e):
@@ -2020,8 +2042,11 @@ def _get_controllerFor(self):
20202042
objList.append(obj)
20212043
return objList
20222044

2045+
def event_UIA_controllerFor(self):
2046+
return self.event_controllerForChange()
2047+
20232048
def event_UIA_elementSelected(self):
2024-
self.event_stateChange()
2049+
self.event_selection()
20252050

20262051
def event_valueChange(self):
20272052
if issubclass(self.TextInfo, UIATextInfo):
@@ -2265,15 +2290,10 @@ def event_stateChange(self):
22652290

22662291
class SearchField(EditableTextWithSuggestions, UIA):
22672292
"""An edit field that presents suggestions based on a search term.
2293+
This is now an empty class as functionality has been moved to the base EditableText behaviour.
2294+
@warning Deprecated, may be removed in future versions.
22682295
"""
22692296

2270-
def event_UIA_controllerFor(self):
2271-
# Only useful if suggestions appear and disappear.
2272-
if self == api.getFocusObject() and len(self.controllerFor)>0:
2273-
self.event_suggestionsOpened()
2274-
else:
2275-
self.event_suggestionsClosed()
2276-
22772297

22782298
class SuggestionsList(UIA):
22792299
"""A list of suggestions in response to search terms being entered.
@@ -2302,20 +2322,11 @@ def event_UIA_layoutInvalidated(self):
23022322
class SuggestionListItem(UIA):
23032323
"""Recent Windows releases use suggestions lists for various things, including Start menu suggestions, Store, Settings app and so on.
23042324
Unlike suggestions list class, top suggestion is automatically selected.
2325+
Note that support for reporting the selection is now handled generically on the base NVDAObject.
23052326
"""
23062327

23072328
role = controlTypes.Role.LISTITEM
23082329

2309-
def event_UIA_elementSelected(self):
2310-
focusControllerFor = api.getFocusObject().controllerFor
2311-
if len(focusControllerFor) > 0 and focusControllerFor[0].appModule is self.appModule and self.name:
2312-
speech.cancelSpeech()
2313-
if api.setNavigatorObject(self, isFocus=True):
2314-
self.reportFocus()
2315-
# Display results as flash messages.
2316-
braille.handler.message(braille.getPropertiesBraille(
2317-
name=self.name, role=self.role, positionInfo=self.positionInfo
2318-
))
23192330

23202331
# NetUIDropdownAnchor comboBoxes (such as in the MS Office Options dialog)
23212332
class NetUIDropdownAnchor(UIA):

source/NVDAObjects/__init__.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1093,6 +1093,10 @@ def reportFocus(self):
10931093
"""
10941094
speech.speakObject(self, reason=controlTypes.OutputReason.FOCUS)
10951095

1096+
def isDescendantOf(self, obj: "NVDAObject") -> bool:
1097+
""" is this object a descendant of obj? """
1098+
raise NotImplementedError
1099+
10961100
def _get_placeholder(self):
10971101
"""If it exists for this object get the value of the placeholder text.
10981102
For example this might be the aria-placeholder text for a field in a web page.
@@ -1177,6 +1181,29 @@ def event_mouseMove(self,x,y):
11771181
speech.cancelSpeech()
11781182
speech.speakText(text)
11791183

1184+
def event_selection(self):
1185+
# This object has been selected.
1186+
# If this object's container / parent is being controlled by the focus,
1187+
# then report this selection.
1188+
focus = api.getFocusObject()
1189+
controls = focus.controllerFor
1190+
for control in controls:
1191+
# The focus is controling one or more objects.
1192+
# If possible, check if this object is a descendant of the object being controlled.
1193+
try:
1194+
isDescendant = self.isDescendantOf(control)
1195+
except NotImplementedError:
1196+
isDescendant = False
1197+
if isDescendant:
1198+
speech.cancelSpeech()
1199+
if api.setNavigatorObject(self, isFocus=True):
1200+
self.reportFocus()
1201+
# Display results as flash messages.
1202+
braille.handler.message(braille.getPropertiesBraille(
1203+
name=self.name, role=self.role, positionInfo=self.positionInfo
1204+
))
1205+
self.event_stateChange()
1206+
11801207
def event_stateChange(self):
11811208
if self is api.getFocusObject():
11821209
speech.speakObjectProperties(self, states=True, reason=controlTypes.OutputReason.CHANGE)
@@ -1246,6 +1273,9 @@ def event_descriptionChange(self):
12461273
braille.handler.handleUpdate(self)
12471274
vision.handler.handleUpdate(self, property="description")
12481275

1276+
def event_controllerForChange(self):
1277+
pass
1278+
12491279
def event_caret(self):
12501280
if self is api.getFocusObject() and not eventHandler.isPendingEvents("gainFocus"):
12511281
braille.handler.handleCaretMove(self)

source/NVDAObjects/behaviors.py

Lines changed: 50 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,42 @@ def _get_isPresentableFocusAncestor(self):
163163
self.isPresentableFocusAncestor = res = super(Dialog, self).isPresentableFocusAncestor
164164
return res
165165

166-
class EditableText(editableText.EditableText, NVDAObject):
166+
167+
class InputFieldWithSuggestions(NVDAObject):
168+
"""Allows NVDA to announce appearance/disappearance of suggestions as content is entered.
169+
This is used in various places, including Windows 10 search edit fields and others.
170+
Subclasses should provide L{event_suggestionsOpened} and can optionally override L{event_suggestionsClosed}.
171+
These events are fired when suggestions appear and disappear, respectively.
172+
"""
173+
174+
def event_suggestionsOpened(self):
175+
"""Called when suggestions appear when text is entered e.g. search suggestions.
176+
Subclasses should provide custom implementations if possible.
177+
By default NVDA will announce appearance of suggestions using speech, braille or a sound will be played.
178+
"""
179+
# Translators: Announced in braille when suggestions appear when search term is entered
180+
# in various search fields such as Start search box in Windows 10.
181+
braille.handler.message(_("Suggestions"))
182+
if config.conf["presentation"]["reportAutoSuggestionsWithSound"]:
183+
nvwave.playWaveFile(os.path.join(globalVars.appDir, "waves", "suggestionsOpened.wav"))
184+
185+
def event_suggestionsClosed(self):
186+
"""Called when suggestions list or container is closed.
187+
Subclasses should provide custom implementations if possible.
188+
By default NVDA will announce this via speech, braille or via a sound.
189+
"""
190+
if config.conf["presentation"]["reportAutoSuggestionsWithSound"]:
191+
nvwave.playWaveFile(os.path.join(globalVars.appDir, "waves", "suggestionsClosed.wav"))
192+
193+
def event_controllerForChange(self):
194+
# Report when suggestions appear and disappear.
195+
if self is api.getFocusObject() and len(self.controllerFor) > 0:
196+
self.event_suggestionsOpened()
197+
else:
198+
self.event_suggestionsClosed()
199+
200+
201+
class EditableTextBase(editableText.EditableText, NVDAObject):
167202
"""Provides scripts to report appropriately when moving the caret in editable text fields.
168203
This does not handle selection changes.
169204
To handle selection changes, use either L{EditableTextWithAutoSelectDetection} or L{EditableTextWithoutAutoSelectDetection}.
@@ -236,6 +271,20 @@ def event_typedCharacter(self, ch: str):
236271
super().event_typedCharacter(ch)
237272

238273

274+
class EditableTextWithSuggestions(InputFieldWithSuggestions, EditableTextBase):
275+
""" Represents an editable text field that shows suggestions as you type.
276+
This is an empty class as functionality has been moved to the base InputFieldWithSuggestions class.
277+
"""
278+
279+
280+
class EditableText(EditableTextWithSuggestions, EditableTextBase):
281+
""" Represents an editable text field.
282+
This is an empty class as functionality has been moved to the base EditableTextBase class.
283+
This class also supports reporting of the appearance and disappearence of suggestions
284+
by inheriting from the EditableTextWithSuggestions class.
285+
"""
286+
287+
239288
class EditableTextWithAutoSelectDetection(EditableText):
240289
"""In addition to L{EditableText}, handles reporting of selection changes for objects which notify of them.
241290
To have selection changes reported, the object must notify of selection changes via the caret event.
@@ -873,30 +922,6 @@ def event_alert(self):
873922

874923
event_show = event_alert
875924

876-
class EditableTextWithSuggestions(NVDAObject):
877-
"""Allows NvDA to announce appearance/disappearance of suggestions as text is entered.
878-
This is used in various places, including Windows 10 search edit fields and others.
879-
Subclasses should provide L{event_suggestionsOpened} and can optionally override L{event_suggestionsClosed}.
880-
These events are fired when suggestions appear and disappear, respectively.
881-
"""
882-
883-
def event_suggestionsOpened(self):
884-
"""Called when suggestions appear when text is entered e.g. search suggestions.
885-
Subclasses should provide custom implementations if possible.
886-
By default NVDA will announce appearance of suggestions using speech, braille or a sound will be played.
887-
"""
888-
# Translators: Announced in braille when suggestions appear when search term is entered in various search fields such as Start search box in Windows 10.
889-
braille.handler.message(_("Suggestions"))
890-
if config.conf["presentation"]["reportAutoSuggestionsWithSound"]:
891-
nvwave.playWaveFile(os.path.join(globalVars.appDir, "waves", "suggestionsOpened.wav"))
892-
893-
def event_suggestionsClosed(self):
894-
"""Called when suggestions list or container is closed.
895-
Subclasses should provide custom implementations if possible.
896-
By default NVDA will announce this via speech, braille or via a sound.
897-
"""
898-
if config.conf["presentation"]["reportAutoSuggestionsWithSound"]:
899-
nvwave.playWaveFile(os.path.join(globalVars.appDir, "waves", "suggestionsClosed.wav"))
900925

901926
class WebDialog(NVDAObject):
902927
"""

0 commit comments

Comments
 (0)