Fix web content editable label, line boundary, and punctuation bugs
Bug 1: Content editable entry label not announced on caret-nav focus - When navigating into a content editable via caret navigation (e.g. down arrow from message list to message entry in Discord), the label and role were suppressed because the code treated entering the field the same as navigating within it. - Fixed locus_of_focus_changed in web/script.py to detect when focus enters a content editable from outside and generate full object speech. - Fixed _generateLabelOrName in web/speech_generator.py to preserve the label when the prior object was outside the content editable. Bug 2: Adjacent button included in line contents on first line - When reading line contents inside a content editable, the layout-mode expansion could walk outside the content editable boundary and include adjacent UI elements (e.g. a 'More options' button) that happened to share the same visual line. - Fixed _getLineContentsAtOffset in web/script_utilities.py to stop expanding at content editable boundaries in both directions. Bug 3: Punctuation stripped from live regions and AT-SPI announcements - presentMessage uses resetStyles=True by default, which sets punctuation to NONE for system voice messages. This is correct for generated text but wrong for user content in live regions and AT-SPI announcements. - Fixed liveregions.py pumpMessages and default.py onAnnouncement to pass resetStyles=False, preserving the user's punctuation settings.
This commit is contained in:
@@ -276,7 +276,9 @@ class LiveRegionManager:
|
||||
utts = message['labels'] + message['content']
|
||||
|
||||
if self.monitoring:
|
||||
self._script.presentMessage(utts)
|
||||
# Live region content is user-generated text, not system messages.
|
||||
# Use resetStyles=False to preserve the user's punctuation settings.
|
||||
self._script.presentMessage(utts, resetStyles=False)
|
||||
else:
|
||||
msg = "INFO: Not presenting message because monitoring is off"
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
|
||||
@@ -1473,7 +1473,9 @@ class Script(script.Script):
|
||||
"""Callback for object:announcement events."""
|
||||
|
||||
if isinstance(event.any_data, str):
|
||||
self.presentMessage(event.any_data)
|
||||
# AT-SPI announcements contain application content, not system messages.
|
||||
# Use resetStyles=False to preserve the user's punctuation settings.
|
||||
self.presentMessage(event.any_data, resetStyles=False)
|
||||
|
||||
def onNameChanged(self, event):
|
||||
"""Callback for object:property-change:accessible-name events."""
|
||||
|
||||
@@ -1648,9 +1648,21 @@ class Script(default.Script):
|
||||
elif self.utilities.isContentEditableWithEmbeddedObjects(newFocus) \
|
||||
and (self._lastCommandWasCaretNav or self._lastCommandWasStructNav) \
|
||||
and not (AXUtilities.is_table_cell(newFocus) and AXObject.get_name(newFocus)):
|
||||
tokens = ["WEB: New focus", newFocus, "content editable. Generating line."]
|
||||
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
||||
contents = self.utilities.getLineContentsAtOffset(newFocus, caretOffset)
|
||||
# Check if we're entering the content editable from outside (e.g. down arrow
|
||||
# from a message list into a message entry). In that case, generate full object
|
||||
# speech (with label and role) rather than just line contents.
|
||||
enteredFromOutside = oldFocus is not None \
|
||||
and oldFocus != newFocus \
|
||||
and not AXObject.find_ancestor(oldFocus, lambda x: x == newFocus)
|
||||
if enteredFromOutside:
|
||||
tokens = ["WEB: New focus", newFocus,
|
||||
"content editable entered from outside. Generating speech."]
|
||||
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
||||
args['priorObj'] = oldFocus
|
||||
else:
|
||||
tokens = ["WEB: New focus", newFocus, "content editable. Generating line."]
|
||||
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
||||
contents = self.utilities.getLineContentsAtOffset(newFocus, caretOffset)
|
||||
elif self.utilities.isAnchor(newFocus):
|
||||
tokens = ["WEB: New focus", newFocus, "is anchor. Generating line."]
|
||||
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
||||
|
||||
@@ -1834,6 +1834,16 @@ class Utilities(script_utilities.Utilities):
|
||||
prevObj, pOffset = self.findPreviousCaretInOrder(firstObj, firstStart)
|
||||
nextObj, nOffset = self.findNextCaretInOrder(lastObj, lastEnd - 1)
|
||||
|
||||
# If we're inside a content editable, don't expand line contents beyond
|
||||
# its boundaries (e.g. don't include a "More options" button adjacent to
|
||||
# a message entry just because it's on the same visual line).
|
||||
contentEditableBoundary = None
|
||||
if self.isContentEditableWithEmbeddedObjects(obj):
|
||||
contentEditableBoundary = obj
|
||||
else:
|
||||
contentEditableBoundary = AXObject.find_ancestor(
|
||||
obj, self.isContentEditableWithEmbeddedObjects)
|
||||
|
||||
# Check for things on the same line to the left of this object.
|
||||
prevStartTime = time.time()
|
||||
while prevObj and self.getDocumentForObject(prevObj) == document:
|
||||
@@ -1848,6 +1858,10 @@ class Utilities(script_utilities.Utilities):
|
||||
if objRow != AXObject.find_ancestor(prevObj, AXUtilities.is_table_row):
|
||||
break
|
||||
|
||||
if contentEditableBoundary and prevObj != contentEditableBoundary \
|
||||
and not AXObject.find_ancestor(prevObj, lambda x: x == contentEditableBoundary):
|
||||
break
|
||||
|
||||
onLeft = self._getContentsForObj(prevObj, pOffset, boundary)
|
||||
onLeft = list(filter(_include, onLeft))
|
||||
if not onLeft:
|
||||
@@ -1878,6 +1892,10 @@ class Utilities(script_utilities.Utilities):
|
||||
if objRow != AXObject.find_ancestor(nextObj, AXUtilities.is_table_row):
|
||||
break
|
||||
|
||||
if contentEditableBoundary and nextObj != contentEditableBoundary \
|
||||
and not AXObject.find_ancestor(nextObj, lambda x: x == contentEditableBoundary):
|
||||
break
|
||||
|
||||
onRight = self._getContentsForObj(nextObj, nOffset, boundary)
|
||||
if onRight and self._contentIsSubsetOf(objects[0], onRight[-1]):
|
||||
onRight = onRight[0:-1]
|
||||
|
||||
@@ -361,7 +361,13 @@ class SpeechGenerator(speech_generator.SpeechGenerator):
|
||||
if self._script.utilities.isContentEditableWithEmbeddedObjects(obj) \
|
||||
or self._script.utilities.isDocument(obj):
|
||||
if input_event_manager.get_manager().last_event_was_caret_navigation():
|
||||
return []
|
||||
# Still generate the label if we just entered this object from outside
|
||||
# (e.g. down arrow from message list into message entry in Discord).
|
||||
enteredFromOutside = priorObj is not None \
|
||||
and priorObj != obj \
|
||||
and not AXObject.find_ancestor(priorObj, lambda x: x == obj)
|
||||
if not enteredFromOutside:
|
||||
return []
|
||||
|
||||
if AXUtilities.is_page_tab(priorObj) and AXObject.get_name(priorObj) == objName:
|
||||
return []
|
||||
|
||||
Reference in New Issue
Block a user