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
|
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
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|
||||||
|
|||||||
@@ -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"), "'"]
|
||||||
|
|||||||
Reference in New Issue
Block a user