Backport browser fixes for chromium and new d-bus functionality from Orca.

This commit is contained in:
Storm Dragon
2025-08-10 12:45:50 -04:00
parent 89df8991f7
commit eae9a5896e
3 changed files with 77 additions and 15 deletions
+37 -4
View File
@@ -2490,6 +2490,39 @@ class Utilities:
return child 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): def characterOffsetInParent(self, obj):
"""Returns the character offset of the embedded object """Returns the character offset of the embedded object
character for this object in its parent's accessible text. character for this object in its parent's accessible text.
@@ -2582,7 +2615,7 @@ class Utilities:
toBuild = list(string) toBuild = list(string)
for i, char in enumerate(toBuild): for i, char in enumerate(toBuild):
if char == self.EMBEDDED_OBJECT_CHARACTER: if char == self.EMBEDDED_OBJECT_CHARACTER:
child = self.getChildAtOffset(obj, i + startOffset) child = self.findChildAtOffset(obj, i + startOffset)
result = self.expandEOCs(child) result = self.expandEOCs(child)
if child and AXObject.get_role(child) in blockRoles: if child and AXObject.get_role(child) in blockRoles:
result += " " result += " "
@@ -5440,14 +5473,14 @@ class Utilities:
if oldString.endswith(self.EMBEDDED_OBJECT_CHARACTER) and oldEnd == changeStart: if oldString.endswith(self.EMBEDDED_OBJECT_CHARACTER) and oldEnd == changeStart:
# There's a possibility that we have a link spanning multiple lines. If so, # There's a possibility that we have a link spanning multiple lines. If so,
# we want to present the continuation that just became selected. # 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) self.handleTextSelectionChange(child, False)
else: else:
changes.append([changeStart, changeEnd, messages.TEXT_UNSELECTED]) changes.append([changeStart, changeEnd, messages.TEXT_UNSELECTED])
if newString.endswith(self.EMBEDDED_OBJECT_CHARACTER): if newString.endswith(self.EMBEDDED_OBJECT_CHARACTER):
# There's a possibility that we have a link spanning multiple lines. If so, # There's a possibility that we have a link spanning multiple lines. If so,
# we want to present the continuation that just became unselected. # 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) self.handleTextSelectionChange(child, False)
speakMessage = speakMessage and not _settingsManager.getSetting('onlySpeakDisplayedText') speakMessage = speakMessage and not _settingsManager.getSetting('onlySpeakDisplayedText')
@@ -5463,7 +5496,7 @@ class Utilities:
self._script.speakMessage(message, interrupt=False) self._script.speakMessage(message, interrupt=False)
if endsWithChild: if endsWithChild:
child = self.getChildAtOffset(obj, end) child = self.findChildAtOffset(obj, end)
self.handleTextSelectionChange(child, speakMessage) self.handleTextSelectionChange(child, speakMessage)
return True return True
+26 -1
View File
@@ -44,6 +44,7 @@ import time
import cthulhu.braille as braille import cthulhu.braille as braille
import cthulhu.cmdnames as cmdnames import cthulhu.cmdnames as cmdnames
import cthulhu.dbus_service as dbus_service
import cthulhu.debug as debug import cthulhu.debug as debug
import cthulhu.find as find import cthulhu.find as find
import cthulhu.flat_review as flat_review import cthulhu.flat_review as flat_review
@@ -128,6 +129,17 @@ class Script(script.Script):
Atspi.Accessible.set_cache_mask( Atspi.Accessible.set_cache_mask(
app, Atspi.Cache.DEFAULT ^ Atspi.Cache.NAME ^ Atspi.Cache.DESCRIPTION) 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): def setupInputEventHandlers(self):
"""Defines InputEventHandler fields for this script that can be """Defines InputEventHandler fields for this script that can be
called by the key and braille bindings.""" called by the key and braille bindings."""
@@ -1082,7 +1094,9 @@ class Script(script.Script):
for character in itemString: for character in itemString:
self.speakCharacter(character) 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 obj = obj or cthulhu_state.locusOfFocus
if not obj or AXObject.is_dead(obj): if not obj or AXObject.is_dead(obj):
self.presentMessage(messages.LOCATION_NOT_FOUND_FULL) self.presentMessage(messages.LOCATION_NOT_FOUND_FULL)
@@ -2798,6 +2812,17 @@ class Script(script.Script):
try: try:
[lineString, startOffset, endOffset] = text.getTextAtOffset( [lineString, startOffset, endOffset] = text.getTextAtOffset(
fixedTargetOffset, Atspi.TextBoundaryType.LINE_START) 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: except Exception:
return ["", 0, 0] return ["", 0, 0]
+14 -10
View File
@@ -870,7 +870,7 @@ class Utilities(script_utilities.Utilities):
if not self.isTextBlockElement(obj): if not self.isTextBlockElement(obj):
return -1 return -1
child = self.getChildAtOffset(obj, offset) child = self.findChildAtOffset(obj, offset)
if child and not self.isTextBlockElement(child): if child and not self.isTextBlockElement(child):
matches = [x for x in contents if x[0] == child] matches = [x for x in contents if x[0] == child]
if len(matches) == 1: if len(matches) == 1:
@@ -1344,14 +1344,14 @@ class Utilities(script_utilities.Utilities):
if not string: if not string:
return [[obj, start, end, string]] return [[obj, start, end, string]]
stringOffset = offset - start stringOffset = max(0, offset - start)
try: try:
char = string[stringOffset] char = string[stringOffset]
except Exception: except Exception:
pass pass
else: else:
if char == self.EMBEDDED_OBJECT_CHARACTER: if char == self.EMBEDDED_OBJECT_CHARACTER:
child = self.getChildAtOffset(obj, offset) child = self.findChildAtOffset(obj, offset)
if child: if child:
return self._getContentsForObj(child, 0, boundary) return self._getContentsForObj(child, 0, boundary)
@@ -1699,7 +1699,7 @@ class Utilities(script_utilities.Utilities):
offset = max(0, offset) offset = max(0, offset)
if (AXUtilities.is_tool_bar(obj) or AXUtilities.is_menu_bar(obj)) \ if (AXUtilities.is_tool_bar(obj) or AXUtilities.is_menu_bar(obj)) \
and not self._treatObjectAsWhole(obj): and not self._treatObjectAsWhole(obj):
child = self.getChildAtOffset(obj, offset) child = self.findChildAtOffset(obj, offset)
if child: if child:
obj = child obj = child
offset = 0 offset = 0
@@ -2362,7 +2362,7 @@ class Utilities(script_utilities.Utilities):
string, start, end = super().textAtPoint(obj, x, y, coordType, boundary) string, start, end = super().textAtPoint(obj, x, y, coordType, boundary)
if string == self.EMBEDDED_OBJECT_CHARACTER: if string == self.EMBEDDED_OBJECT_CHARACTER:
child = self.getChildAtOffset(obj, start) child = self.findChildAtOffset(obj, start)
if child: if child:
return self.textAtPoint(child, x, y, coordType, boundary) return self.textAtPoint(child, x, y, coordType, boundary)
@@ -4804,7 +4804,7 @@ class Utilities(script_utilities.Utilities):
obj = None obj = None
else: else:
contextObj, contextOffset = obj, offset contextObj, contextOffset = obj, offset
child = self.getChildAtOffset(obj, offset) child = self.findChildAtOffset(obj, offset)
if child: if child:
obj = child obj = child
else: else:
@@ -5177,7 +5177,7 @@ class Utilities(script_utilities.Utilities):
debug.printMessage(debug.LEVEL_INFO, msg, True) debug.printMessage(debug.LEVEL_INFO, msg, True)
return obj, offset return obj, offset
child = self.getChildAtOffset(obj, offset) child = self.findChildAtOffset(obj, offset)
if not child: if not child:
msg = "WEB: Child at offset is null. Returning context unchanged." msg = "WEB: Child at offset is null. Returning context unchanged."
debug.printMessage(debug.LEVEL_INFO, msg, True) 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."] tokens = ["WEB: Child", child, "of", obj, "at offset", offset, "cannot be context."]
debug.printTokens(debug.LEVEL_INFO, tokens, True) debug.printTokens(debug.LEVEL_INFO, tokens, True)
offset += 1 offset += 1
child = self.getChildAtOffset(obj, offset) child = self.findChildAtOffset(obj, offset)
if self.isListItemMarker(child): if self.isListItemMarker(child):
tokens = ["WEB: First caret context is next offset in", obj, ":", tokens = ["WEB: First caret context is next offset in", obj, ":",
@@ -5233,11 +5233,15 @@ class Utilities(script_utilities.Utilities):
if text: if text:
allText = text.getText(0, -1) allText = text.getText(0, -1)
for i in range(offset + 1, len(allText)): 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: if child and allText[i] != self.EMBEDDED_OBJECT_CHARACTER:
tokens = ["ERROR: Child", child, "found at offset with char '", tokens = ["ERROR: Child", child, "found at offset with char '",
allText[i].replace("\n", "\\n"), "'"] allText[i].replace("\n", "\\n"), "'"]
debug.printTokens(debug.LEVEL_INFO, tokens, True) 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._canHaveCaretContext(child):
if self._treatObjectAsWhole(child, -1): if self._treatObjectAsWhole(child, -1):
return child, 0 return child, 0
@@ -5305,7 +5309,7 @@ class Utilities(script_utilities.Utilities):
if offset == -1 or offset > len(allText): if offset == -1 or offset > len(allText):
offset = len(allText) offset = len(allText)
for i in range(offset - 1, -1, -1): 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: if child and allText[i] != self.EMBEDDED_OBJECT_CHARACTER:
tokens = ["ERROR: Child", child, "found at offset with char '", tokens = ["ERROR: Child", child, "found at offset with char '",
allText[i].replace("\n", "\\n"), "'"] allText[i].replace("\n", "\\n"), "'"]