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
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
+26 -1
View File
@@ -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]
+14 -10
View File
@@ -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"), "'"]