Backport browser fixes for chromium and new d-bus functionality from Orca.
This commit is contained in:
@@ -2490,6 +2490,39 @@ class Utilities:
|
||||
|
||||
return child
|
||||
|
||||
def findChildAtOffset(self, obj, offset):
|
||||
"""Attempts to correct for off-by-one brokenness in hypertext implementations.
|
||||
|
||||
We're seeing off-by-one errors in (at least) Chromium where the text
|
||||
at a given offset is an embedded object character, but retrieving the
|
||||
child at that offset returns None.
|
||||
|
||||
This method handles this condition by checking for the child at the
|
||||
previous and next offsets. If there's a child, and its offset in parent
|
||||
matches the desired offset, return that child.
|
||||
"""
|
||||
|
||||
if child := self.getChildAtOffset(obj, offset):
|
||||
return child
|
||||
|
||||
if child_before := self.getChildAtOffset(obj, offset - 1):
|
||||
offset_in_parent = self.characterOffsetInParent(child_before)
|
||||
if offset_in_parent == offset:
|
||||
tokens = [f"SCRIPT UTILITIES: Corrected child at offset {offset} in", obj, "is",
|
||||
child_before, f"at offset {offset - 1}"]
|
||||
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
||||
return child_before
|
||||
|
||||
if child_after := self.getChildAtOffset(obj, offset + 1):
|
||||
offset_in_parent = self.characterOffsetInParent(child_after)
|
||||
if offset_in_parent == offset:
|
||||
tokens = [f"SCRIPT UTILITIES: Corrected child at offset {offset} in", obj, "is",
|
||||
child_after, f"at offset {offset + 1}"]
|
||||
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
||||
return child_after
|
||||
|
||||
return None
|
||||
|
||||
def characterOffsetInParent(self, obj):
|
||||
"""Returns the character offset of the embedded object
|
||||
character for this object in its parent's accessible text.
|
||||
@@ -2582,7 +2615,7 @@ class Utilities:
|
||||
toBuild = list(string)
|
||||
for i, char in enumerate(toBuild):
|
||||
if char == self.EMBEDDED_OBJECT_CHARACTER:
|
||||
child = self.getChildAtOffset(obj, i + startOffset)
|
||||
child = self.findChildAtOffset(obj, i + startOffset)
|
||||
result = self.expandEOCs(child)
|
||||
if child and AXObject.get_role(child) in blockRoles:
|
||||
result += " "
|
||||
@@ -5440,14 +5473,14 @@ class Utilities:
|
||||
if oldString.endswith(self.EMBEDDED_OBJECT_CHARACTER) and oldEnd == changeStart:
|
||||
# There's a possibility that we have a link spanning multiple lines. If so,
|
||||
# we want to present the continuation that just became selected.
|
||||
child = self.getChildAtOffset(obj, oldEnd - 1)
|
||||
child = self.findChildAtOffset(obj, oldEnd - 1)
|
||||
self.handleTextSelectionChange(child, False)
|
||||
else:
|
||||
changes.append([changeStart, changeEnd, messages.TEXT_UNSELECTED])
|
||||
if newString.endswith(self.EMBEDDED_OBJECT_CHARACTER):
|
||||
# There's a possibility that we have a link spanning multiple lines. If so,
|
||||
# we want to present the continuation that just became unselected.
|
||||
child = self.getChildAtOffset(obj, newEnd - 1)
|
||||
child = self.findChildAtOffset(obj, newEnd - 1)
|
||||
self.handleTextSelectionChange(child, False)
|
||||
|
||||
speakMessage = speakMessage and not _settingsManager.getSetting('onlySpeakDisplayedText')
|
||||
@@ -5463,7 +5496,7 @@ class Utilities:
|
||||
self._script.speakMessage(message, interrupt=False)
|
||||
|
||||
if endsWithChild:
|
||||
child = self.getChildAtOffset(obj, end)
|
||||
child = self.findChildAtOffset(obj, end)
|
||||
self.handleTextSelectionChange(child, speakMessage)
|
||||
|
||||
return True
|
||||
|
||||
@@ -44,6 +44,7 @@ import time
|
||||
|
||||
import cthulhu.braille as braille
|
||||
import cthulhu.cmdnames as cmdnames
|
||||
import cthulhu.dbus_service as dbus_service
|
||||
import cthulhu.debug as debug
|
||||
import cthulhu.find as find
|
||||
import cthulhu.flat_review as flat_review
|
||||
@@ -128,6 +129,17 @@ class Script(script.Script):
|
||||
Atspi.Accessible.set_cache_mask(
|
||||
app, Atspi.Cache.DEFAULT ^ Atspi.Cache.NAME ^ Atspi.Cache.DESCRIPTION)
|
||||
|
||||
# Register D-Bus commands if available
|
||||
try:
|
||||
controller = dbus_service.get_remote_controller()
|
||||
if controller:
|
||||
tokens = ["DEFAULT: Registering D-Bus commands for default script"]
|
||||
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
||||
controller.register_decorated_module("DefaultScript", self)
|
||||
except Exception as error:
|
||||
tokens = ["DEFAULT: Exception registering D-Bus commands:", error]
|
||||
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
||||
|
||||
def setupInputEventHandlers(self):
|
||||
"""Defines InputEventHandler fields for this script that can be
|
||||
called by the key and braille bindings."""
|
||||
@@ -1082,7 +1094,9 @@ class Script(script.Script):
|
||||
for character in itemString:
|
||||
self.speakCharacter(character)
|
||||
|
||||
def sayAll(self, inputEvent, obj=None, offset=None):
|
||||
@dbus_service.command
|
||||
def sayAll(self, inputEvent, obj=None, offset=None, notify_user=True):
|
||||
"""Speaks the entire document or text, starting from the current position."""
|
||||
obj = obj or cthulhu_state.locusOfFocus
|
||||
if not obj or AXObject.is_dead(obj):
|
||||
self.presentMessage(messages.LOCATION_NOT_FOUND_FULL)
|
||||
@@ -2798,6 +2812,17 @@ class Script(script.Script):
|
||||
try:
|
||||
[lineString, startOffset, endOffset] = text.getTextAtOffset(
|
||||
fixedTargetOffset, Atspi.TextBoundaryType.LINE_START)
|
||||
|
||||
# Chrome fix: Handle case where get_line_at_offset returns the line
|
||||
# after the offset, which seems to happen when the character at offset
|
||||
# is an embedded object at a line boundary.
|
||||
if 0 <= fixedTargetOffset < startOffset:
|
||||
backup_offset = fixedTargetOffset - 1
|
||||
tokens = [f"DEFAULT: Start offset {startOffset} is greater than target offset {fixedTargetOffset}. Trying with offset {backup_offset}"]
|
||||
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
||||
[lineString, startOffset, endOffset] = text.getTextAtOffset(
|
||||
backup_offset, Atspi.TextBoundaryType.LINE_START)
|
||||
|
||||
except Exception:
|
||||
return ["", 0, 0]
|
||||
|
||||
|
||||
@@ -870,7 +870,7 @@ class Utilities(script_utilities.Utilities):
|
||||
if not self.isTextBlockElement(obj):
|
||||
return -1
|
||||
|
||||
child = self.getChildAtOffset(obj, offset)
|
||||
child = self.findChildAtOffset(obj, offset)
|
||||
if child and not self.isTextBlockElement(child):
|
||||
matches = [x for x in contents if x[0] == child]
|
||||
if len(matches) == 1:
|
||||
@@ -1344,14 +1344,14 @@ class Utilities(script_utilities.Utilities):
|
||||
if not string:
|
||||
return [[obj, start, end, string]]
|
||||
|
||||
stringOffset = offset - start
|
||||
stringOffset = max(0, offset - start)
|
||||
try:
|
||||
char = string[stringOffset]
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
if char == self.EMBEDDED_OBJECT_CHARACTER:
|
||||
child = self.getChildAtOffset(obj, offset)
|
||||
child = self.findChildAtOffset(obj, offset)
|
||||
if child:
|
||||
return self._getContentsForObj(child, 0, boundary)
|
||||
|
||||
@@ -1699,7 +1699,7 @@ class Utilities(script_utilities.Utilities):
|
||||
offset = max(0, offset)
|
||||
if (AXUtilities.is_tool_bar(obj) or AXUtilities.is_menu_bar(obj)) \
|
||||
and not self._treatObjectAsWhole(obj):
|
||||
child = self.getChildAtOffset(obj, offset)
|
||||
child = self.findChildAtOffset(obj, offset)
|
||||
if child:
|
||||
obj = child
|
||||
offset = 0
|
||||
@@ -2362,7 +2362,7 @@ class Utilities(script_utilities.Utilities):
|
||||
|
||||
string, start, end = super().textAtPoint(obj, x, y, coordType, boundary)
|
||||
if string == self.EMBEDDED_OBJECT_CHARACTER:
|
||||
child = self.getChildAtOffset(obj, start)
|
||||
child = self.findChildAtOffset(obj, start)
|
||||
if child:
|
||||
return self.textAtPoint(child, x, y, coordType, boundary)
|
||||
|
||||
@@ -4804,7 +4804,7 @@ class Utilities(script_utilities.Utilities):
|
||||
obj = None
|
||||
else:
|
||||
contextObj, contextOffset = obj, offset
|
||||
child = self.getChildAtOffset(obj, offset)
|
||||
child = self.findChildAtOffset(obj, offset)
|
||||
if child:
|
||||
obj = child
|
||||
else:
|
||||
@@ -5177,7 +5177,7 @@ class Utilities(script_utilities.Utilities):
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
return obj, offset
|
||||
|
||||
child = self.getChildAtOffset(obj, offset)
|
||||
child = self.findChildAtOffset(obj, offset)
|
||||
if not child:
|
||||
msg = "WEB: Child at offset is null. Returning context unchanged."
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
@@ -5188,7 +5188,7 @@ class Utilities(script_utilities.Utilities):
|
||||
tokens = ["WEB: Child", child, "of", obj, "at offset", offset, "cannot be context."]
|
||||
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
||||
offset += 1
|
||||
child = self.getChildAtOffset(obj, offset)
|
||||
child = self.findChildAtOffset(obj, offset)
|
||||
|
||||
if self.isListItemMarker(child):
|
||||
tokens = ["WEB: First caret context is next offset in", obj, ":",
|
||||
@@ -5233,11 +5233,15 @@ class Utilities(script_utilities.Utilities):
|
||||
if text:
|
||||
allText = text.getText(0, -1)
|
||||
for i in range(offset + 1, len(allText)):
|
||||
child = self.getChildAtOffset(obj, i)
|
||||
child = self.findChildAtOffset(obj, i)
|
||||
if child and allText[i] != self.EMBEDDED_OBJECT_CHARACTER:
|
||||
tokens = ["ERROR: Child", child, "found at offset with char '",
|
||||
allText[i].replace("\n", "\\n"), "'"]
|
||||
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
||||
if offset == self.characterOffsetInParent(child):
|
||||
tokens = ["WEB: Handling error by returning", obj, i]
|
||||
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
||||
return obj, i
|
||||
if self._canHaveCaretContext(child):
|
||||
if self._treatObjectAsWhole(child, -1):
|
||||
return child, 0
|
||||
@@ -5305,7 +5309,7 @@ class Utilities(script_utilities.Utilities):
|
||||
if offset == -1 or offset > len(allText):
|
||||
offset = len(allText)
|
||||
for i in range(offset - 1, -1, -1):
|
||||
child = self.getChildAtOffset(obj, i)
|
||||
child = self.findChildAtOffset(obj, i)
|
||||
if child and allText[i] != self.EMBEDDED_OBJECT_CHARACTER:
|
||||
tokens = ["ERROR: Child", child, "found at offset with char '",
|
||||
allText[i].replace("\n", "\\n"), "'"]
|
||||
|
||||
Reference in New Issue
Block a user