Wow, there was a lot of stuff left to do, that and bug fixes. I think we're pretty much back to a working state now. Will test and merge if I don't find anything.

This commit is contained in:
Storm Dragon
2025-12-26 21:00:54 -05:00
parent e134bf97d5
commit 0b599f9509
36 changed files with 795 additions and 996 deletions
+5 -1
View File
@@ -1,5 +1,6 @@
# Orca #!/usr/bin/env python3
# #
# Copyright (c) 2024 Stormux
# Copyright 2024 Igalia, S.L. # Copyright 2024 Igalia, S.L.
# Copyright 2024 GNOME Foundation Inc. # Copyright 2024 GNOME Foundation Inc.
# Author: Joanmarie Diggs <jdiggs@igalia.com> # Author: Joanmarie Diggs <jdiggs@igalia.com>
@@ -18,6 +19,9 @@
# License along with this library; if not, write to the # License along with this library; if not, write to the
# Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Free Software Foundation, Inc., Franklin Street, Fifth Floor,
# Boston MA 02110-1301 USA. # Boston MA 02110-1301 USA.
#
# Forked from Orca screen reader.
# Cthulhu project: https://git.stormux.org/storm/cthulhu
# pylint: disable=wrong-import-position # pylint: disable=wrong-import-position
+5 -1
View File
@@ -1,5 +1,6 @@
# Orca #!/usr/bin/env python3
# #
# Copyright (c) 2024 Stormux
# Copyright 2024 Igalia, S.L. # Copyright 2024 Igalia, S.L.
# Copyright 2024 GNOME Foundation Inc. # Copyright 2024 GNOME Foundation Inc.
# Author: Joanmarie Diggs <jdiggs@igalia.com> # Author: Joanmarie Diggs <jdiggs@igalia.com>
@@ -18,6 +19,9 @@
# License along with this library; if not, write to the # License along with this library; if not, write to the
# Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Free Software Foundation, Inc., Franklin Street, Fifth Floor,
# Boston MA 02110-1301 USA. # Boston MA 02110-1301 USA.
#
# Forked from Orca screen reader.
# Cthulhu project: https://git.stormux.org/storm/cthulhu
# pylint: disable=wrong-import-position # pylint: disable=wrong-import-position
+5 -1
View File
@@ -1,5 +1,6 @@
# Orca #!/usr/bin/env python3
# #
# Copyright (c) 2024 Stormux
# Copyright 2005-2008 Sun Microsystems Inc. # Copyright 2005-2008 Sun Microsystems Inc.
# Copyright 2018-2023 Igalia, S.L. # Copyright 2018-2023 Igalia, S.L.
# #
@@ -17,6 +18,9 @@
# License along with this library; if not, write to the # License along with this library; if not, write to the
# Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Free Software Foundation, Inc., Franklin Street, Fifth Floor,
# Boston MA 02110-1301 USA. # Boston MA 02110-1301 USA.
#
# Forked from Orca screen reader.
# Cthulhu project: https://git.stormux.org/storm/cthulhu
# pylint: disable=wrong-import-position # pylint: disable=wrong-import-position
# pylint: disable=too-many-public-methods # pylint: disable=too-many-public-methods
+5 -1
View File
@@ -1,5 +1,6 @@
# Orca #!/usr/bin/env python3
# #
# Copyright (c) 2024 Stormux
# Copyright 2024 Igalia, S.L. # Copyright 2024 Igalia, S.L.
# Author: Joanmarie Diggs <jdiggs@igalia.com> # Author: Joanmarie Diggs <jdiggs@igalia.com>
# #
@@ -17,6 +18,9 @@
# License along with this library; if not, write to the # License along with this library; if not, write to the
# Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Free Software Foundation, Inc., Franklin Street, Fifth Floor,
# Boston MA 02110-1301 USA. # Boston MA 02110-1301 USA.
#
# Forked from Orca screen reader.
# Cthulhu project: https://git.stormux.org/storm/cthulhu
# pylint: disable=wrong-import-position # pylint: disable=wrong-import-position
+5 -1
View File
@@ -1,5 +1,6 @@
# Orca #!/usr/bin/env python3
# #
# Copyright (c) 2024 Stormux
# Copyright 2024 Igalia, S.L. # Copyright 2024 Igalia, S.L.
# Copyright 2024 GNOME Foundation Inc. # Copyright 2024 GNOME Foundation Inc.
# Author: Joanmarie Diggs <jdiggs@igalia.com> # Author: Joanmarie Diggs <jdiggs@igalia.com>
@@ -18,6 +19,9 @@
# License along with this library; if not, write to the # License along with this library; if not, write to the
# Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Free Software Foundation, Inc., Franklin Street, Fifth Floor,
# Boston MA 02110-1301 USA. # Boston MA 02110-1301 USA.
#
# Forked from Orca screen reader.
# Cthulhu project: https://git.stormux.org/storm/cthulhu
# pylint: disable=wrong-import-position # pylint: disable=wrong-import-position
# pylint: disable=too-many-locals # pylint: disable=too-many-locals
+5 -1
View File
@@ -1,5 +1,6 @@
# Orca #!/usr/bin/env python3
# #
# Copyright (c) 2024 Stormux
# Copyright 2024 Igalia, S.L. # Copyright 2024 Igalia, S.L.
# Copyright 2024 GNOME Foundation Inc. # Copyright 2024 GNOME Foundation Inc.
# Author: Joanmarie Diggs <jdiggs@igalia.com> # Author: Joanmarie Diggs <jdiggs@igalia.com>
@@ -18,6 +19,9 @@
# License along with this library; if not, write to the # License along with this library; if not, write to the
# Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Free Software Foundation, Inc., Franklin Street, Fifth Floor,
# Boston MA 02110-1301 USA. # Boston MA 02110-1301 USA.
#
# Forked from Orca screen reader.
# Cthulhu project: https://git.stormux.org/storm/cthulhu
# pylint: disable=wrong-import-position # pylint: disable=wrong-import-position
# pylint: disable=too-many-branches # pylint: disable=too-many-branches
+5 -1
View File
@@ -1,5 +1,6 @@
# Orca #!/usr/bin/env python3
# #
# Copyright (c) 2024 Stormux
# Copyright 2024 Igalia, S.L. # Copyright 2024 Igalia, S.L.
# Copyright 2024 GNOME Foundation Inc. # Copyright 2024 GNOME Foundation Inc.
# Author: Joanmarie Diggs <jdiggs@igalia.com> # Author: Joanmarie Diggs <jdiggs@igalia.com>
@@ -18,6 +19,9 @@
# License along with this library; if not, write to the # License along with this library; if not, write to the
# Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Free Software Foundation, Inc., Franklin Street, Fifth Floor,
# Boston MA 02110-1301 USA. # Boston MA 02110-1301 USA.
#
# Forked from Orca screen reader.
# Cthulhu project: https://git.stormux.org/storm/cthulhu
# pylint: disable=wrong-import-position # pylint: disable=wrong-import-position
# pylint: disable=too-many-public-methods # pylint: disable=too-many-public-methods
+5 -1
View File
@@ -1,5 +1,6 @@
# Orca #!/usr/bin/env python3
# #
# Copyright (c) 2024 Stormux
# Copyright 2024 Igalia, S.L. # Copyright 2024 Igalia, S.L.
# Copyright 2024 GNOME Foundation Inc. # Copyright 2024 GNOME Foundation Inc.
# Author: Joanmarie Diggs <jdiggs@igalia.com> # Author: Joanmarie Diggs <jdiggs@igalia.com>
@@ -18,6 +19,9 @@
# License along with this library; if not, write to the # License along with this library; if not, write to the
# Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Free Software Foundation, Inc., Franklin Street, Fifth Floor,
# Boston MA 02110-1301 USA. # Boston MA 02110-1301 USA.
#
# Forked from Orca screen reader.
# Cthulhu project: https://git.stormux.org/storm/cthulhu
# pylint: disable=wrong-import-position # pylint: disable=wrong-import-position
+21 -20
View File
@@ -42,6 +42,9 @@ import signal
import os import os
import re import re
import gi
gi.require_version("Atspi", "2.0")
from gi.repository import Atspi
from gi.repository import GLib from gi.repository import GLib
from . import brltablenames from . import brltablenames
@@ -54,6 +57,7 @@ from . import settings_manager
from .ax_event_synthesizer import AXEventSynthesizer from .ax_event_synthesizer import AXEventSynthesizer
from .ax_object import AXObject from .ax_object import AXObject
from .ax_hypertext import AXHypertext
from .cthulhu_platform import tablesdir from .cthulhu_platform import tablesdir
_logger = logger.getLogger() _logger = logger.getLogger()
@@ -537,10 +541,11 @@ class Component(Region):
if cthulhu_state.activeScript and cthulhu_state.activeScript.utilities.\ if cthulhu_state.activeScript and cthulhu_state.activeScript.utilities.\
grabFocusBeforeRouting(self.accessible, offset): grabFocusBeforeRouting(self.accessible, offset):
try: if AXObject.supports_component(self.accessible):
self.accessible.queryComponent().grabFocus() try:
except Exception: Atspi.Component.grab_focus(self.accessible)
pass except Exception:
pass
if AXObject.do_action(self.accessible, 0): if AXObject.do_action(self.accessible, 0):
return return
@@ -747,22 +752,18 @@ class Text(Region):
return "" return ""
if getLinkMask and linkIndicator != settings.BRAILLE_UNDERLINE_NONE: if getLinkMask and linkIndicator != settings.BRAILLE_UNDERLINE_NONE:
try: if AXObject.supports_hypertext(self.accessible):
hyperText = self.accessible.queryHypertext() for link in AXHypertext.get_all_links(self.accessible):
nLinks = hyperText.getNLinks() start = AXHypertext.get_link_start_offset(link)
except Exception: end = AXHypertext.get_link_end_offset(link)
nLinks = 0 if start < 0 or end < 0:
continue
n = 0 if self.lineOffset <= start:
while n < nLinks: for i in range(start, end):
link = hyperText.getLink(n) try:
if self.lineOffset <= link.startIndex: regionMask[i] |= linkIndicator
for i in range(link.startIndex, link.endIndex): except Exception:
try: pass
regionMask[i] |= linkIndicator
except Exception:
pass
n += 1
if attrIndicator: if attrIndicator:
keys, enabledAttributes = script.utilities.stringToKeysAndDict( keys, enabledAttributes = script.utilities.stringToKeysAndDict(
+25 -4
View File
@@ -500,7 +500,7 @@ class Context:
self.container = None self.container = None
self.focusObj = cthulhu.getActiveModeAndObjectOfInterest()[1] or cthulhu_state.locusOfFocus self.focusObj = cthulhu.getActiveModeAndObjectOfInterest()[1] or cthulhu_state.locusOfFocus
self.topLevel = None self.topLevel = None
self.bounds = 0, 0, 0, 0 self.bounds = Atspi.Rect()
frame, dialog = script.utilities.frameAndDialog(self.focusObj) frame, dialog = script.utilities.frameAndDialog(self.focusObj)
if root is not None: if root is not None:
@@ -513,9 +513,7 @@ class Context:
debug.printTokens(debug.LEVEL_INFO, tokens, True) debug.printTokens(debug.LEVEL_INFO, tokens, True)
try: try:
if AXObject.supports_component(self.topLevel): self.bounds = AXComponent.get_rect(self.topLevel)
rect = Atspi.Component.get_extents(self.topLevel, Atspi.CoordType.SCREEN)
self.bounds = rect.x, rect.y, rect.width, rect.height
except Exception: except Exception:
tokens = ["ERROR: Exception getting extents of", self.topLevel] tokens = ["ERROR: Exception getting extents of", self.topLevel]
debug.printTokens(debug.LEVEL_INFO, tokens, True) debug.printTokens(debug.LEVEL_INFO, tokens, True)
@@ -567,6 +565,7 @@ class Context:
Returns a list of Zones for the visible text. Returns a list of Zones for the visible text.
""" """
cliprect = self._ensureRect(cliprect)
zones = [] zones = []
substrings = [(*m.span(), m.group(0)) for m in re.finditer(r"[^\ufffc]+", string)] substrings = [(*m.span(), m.group(0)) for m in re.finditer(r"[^\ufffc]+", string)]
substrings = list(map(lambda x: (x[0] + startOffset, x[1] + startOffset, x[2]), substrings)) substrings = list(map(lambda x: (x[0] + startOffset, x[1] + startOffset, x[2]), substrings))
@@ -610,6 +609,7 @@ class Context:
Returns a list of Zones. Returns a list of Zones.
""" """
cliprect = self._ensureRect(cliprect)
if not self.script.utilities.hasPresentableText(accessible): if not self.script.utilities.hasPresentableText(accessible):
return [] return []
@@ -716,6 +716,7 @@ class Context:
def getZonesFromAccessible(self, accessible, cliprect): def getZonesFromAccessible(self, accessible, cliprect):
"""Returns a list of Zones for the given accessible.""" """Returns a list of Zones for the given accessible."""
cliprect = self._ensureRect(cliprect)
try: try:
if AXObject.supports_component(accessible): if AXObject.supports_component(accessible):
rect = Atspi.Component.get_extents(accessible, Atspi.CoordType.SCREEN) rect = Atspi.Component.get_extents(accessible, Atspi.CoordType.SCREEN)
@@ -760,6 +761,25 @@ class Context:
return AXObject.find_ancestor(child, lambda x: x == parent) return AXObject.find_ancestor(child, lambda x: x == parent)
@staticmethod
def _ensureRect(rect):
if rect is None:
return Atspi.Rect()
if hasattr(rect, "x") and hasattr(rect, "y") \
and hasattr(rect, "width") and hasattr(rect, "height"):
return rect
try:
x, y, width, height = rect
except Exception:
return Atspi.Rect()
newRect = Atspi.Rect()
newRect.x = x
newRect.y = y
newRect.width = width
newRect.height = height
return newRect
def setCurrentToZoneWithObject(self, obj): def setCurrentToZoneWithObject(self, obj):
"""Attempts to set the current zone to obj, if obj is in the current context.""" """Attempts to set the current zone to obj, if obj is in the current context."""
@@ -822,6 +842,7 @@ class Context:
if boundingbox is None: if boundingbox is None:
boundingbox = self.bounds boundingbox = self.bounds
boundingbox = self._ensureRect(boundingbox)
objs = self.script.utilities.getOnScreenObjects(root, boundingbox) objs = self.script.utilities.getOnScreenObjects(root, boundingbox)
tokens = ["FLAT REVIEW:", len(objs), "on-screen objects found for", root] tokens = ["FLAT REVIEW:", len(objs), "on-screen objects found for", root]
+5 -1
View File
@@ -1,5 +1,6 @@
# Orca #!/usr/bin/env python3
# #
# Copyright (c) 2024 Stormux
# Copyright 2005-2008 Sun Microsystems Inc. # Copyright 2005-2008 Sun Microsystems Inc.
# Copyright 2016-2023 Igalia, S.L. # Copyright 2016-2023 Igalia, S.L.
# #
@@ -19,6 +20,9 @@
# License along with this library; if not, write to the # License along with this library; if not, write to the
# Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Free Software Foundation, Inc., Franklin Street, Fifth Floor,
# Boston MA 02110-1301 USA. # Boston MA 02110-1301 USA.
#
# Forked from Orca screen reader.
# Cthulhu project: https://git.stormux.org/storm/cthulhu
# pylint: disable=wrong-import-position # pylint: disable=wrong-import-position
# pylint: disable=too-many-instance-attributes # pylint: disable=too-many-instance-attributes
+8 -9
View File
@@ -51,6 +51,7 @@ from . import object_properties
from . import settings from . import settings
from . import settings_manager from . import settings_manager
from .ax_object import AXObject from .ax_object import AXObject
from .ax_text import AXText
from .ax_utilities import AXUtilities from .ax_utilities import AXUtilities
from .ax_utilities_relation import AXUtilitiesRelation from .ax_utilities_relation import AXUtilitiesRelation
@@ -526,14 +527,9 @@ class Generator:
exists. Otherwise, an empty array is returned. exists. Otherwise, an empty array is returned.
""" """
result = [] result = []
try: description = AXObject.get_image_description(obj)
image = obj.queryImage() if description and len(description):
except NotImplementedError: result.append(description)
pass
else:
description = image.imageDescription
if description and len(description):
result.append(description)
return result return result
##################################################################### #####################################################################
@@ -1194,7 +1190,10 @@ class Generator:
if not (AXUtilities.is_table_cell(rad) and AXObject.get_child_count(rad)): if not (AXUtilities.is_table_cell(rad) and AXObject.get_child_count(rad)):
return self._generateDisplayedText(rad, **args) return self._generateDisplayedText(rad, **args)
content = set([self._script.utilities.displayedText(x).strip() for x in rad]) content = {
(AXObject.get_name(x) or AXText.get_all_text(x)).strip()
for x in AXObject.iter_children(rad)
}
rv = " ".join(filter(lambda x: x, content)) rv = " ".join(filter(lambda x: x, content))
if not rv: if not rv:
return self._generateDisplayedText(rad, **args) return self._generateDisplayedText(rad, **args)
+5 -1
View File
@@ -1,5 +1,6 @@
# Orca #!/usr/bin/env python3
# #
# Copyright (c) 2024 Stormux
# Copyright 2024 Igalia, S.L. # Copyright 2024 Igalia, S.L.
# Copyright 2024 GNOME Foundation Inc. # Copyright 2024 GNOME Foundation Inc.
# Author: Joanmarie Diggs <jdiggs@igalia.com> # Author: Joanmarie Diggs <jdiggs@igalia.com>
@@ -18,6 +19,9 @@
# License along with this library; if not, write to the # License along with this library; if not, write to the
# Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Free Software Foundation, Inc., Franklin Street, Fifth Floor,
# Boston MA 02110-1301 USA. # Boston MA 02110-1301 USA.
#
# Forked from Orca screen reader.
# Cthulhu project: https://git.stormux.org/storm/cthulhu
# pylint: disable=wrong-import-position # pylint: disable=wrong-import-position
# pylint: disable=too-many-public-methods # pylint: disable=too-many-public-methods
+18 -16
View File
@@ -37,6 +37,7 @@ from gi.repository import Atspi
from . import debug from . import debug
from .ax_object import AXObject from .ax_object import AXObject
from .ax_table import AXTable
from .ax_text import AXText from .ax_text import AXText
from .ax_utilities import AXUtilities from .ax_utilities import AXUtilities
@@ -236,16 +237,16 @@ class LabelInference:
return extents return extents
if not (extents[2] and extents[3]): if not (extents[2] and extents[3]):
try: if not AXObject.supports_component(obj):
ext = obj.queryComponent().getExtents(0) tokens = ["LABEL INFERENCE:", obj, "does not support the component interface"]
except NotImplementedError:
tokens = ["LABEL INFERENCE:", obj, "does not implement the component interface"]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
except Exception:
tokens = ["LABEL INFERENCE: Exception getting extents for", obj]
debug.printTokens(debug.LEVEL_INFO, tokens, True) debug.printTokens(debug.LEVEL_INFO, tokens, True)
else: else:
extents = ext.x, ext.y, ext.width, ext.height try:
ext = Atspi.Component.get_extents(obj, Atspi.CoordType.SCREEN)
extents = ext.x, ext.y, ext.width, ext.height
except Exception:
tokens = ["LABEL INFERENCE: Exception getting extents for", obj]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
self._extentsCache[(hash(obj), startOffset, endOffset)] = extents self._extentsCache[(hash(obj), startOffset, endOffset)] = extents
return extents return extents
@@ -484,11 +485,12 @@ class LabelInference:
if rowindex < 0 or colindex < 0: if rowindex < 0 or colindex < 0:
return None return None
iface = table.queryTable() rows = AXTable.get_row_count(table, prefer_attribute=False)
if rowindex >= iface.nRows or colindex >= iface.nColumns: cols = AXTable.get_column_count(table, prefer_attribute=False)
if rowindex >= rows or colindex >= cols:
return None return None
return table.queryTable().getAccessibleAt(rowindex, colindex) return AXTable.get_cell_at(table, rowindex, colindex)
def _getCellFromRow(self, row, colindex): def _getCellFromRow(self, row, colindex):
if 0 <= colindex < AXObject.get_child_count(row): if 0 <= colindex < AXObject.get_child_count(row):
@@ -587,12 +589,12 @@ class LabelInference:
# as a functional label. Therefore, see if this table looks like a grid # as a functional label. Therefore, see if this table looks like a grid
# of widgets with the functional labels in the first row. # of widgets with the functional labels in the first row.
try: rows = AXTable.get_row_count(grid, prefer_attribute=False)
table = grid.queryTable() cols = AXTable.get_column_count(grid, prefer_attribute=False)
except NotImplementedError: if rows <= 0 or cols <= 0:
return None, [] return None, []
firstRow = [table.getAccessibleAt(0, i) for i in range(table.nColumns)] firstRow = [AXTable.get_cell_at(grid, 0, i) for i in range(cols)]
if not firstRow or list(filter(self._isWidget, firstRow)): if not firstRow or list(filter(self._isWidget, firstRow)):
return None, [] return None, []
@@ -604,7 +606,7 @@ class LabelInference:
return False return False
return not AXUtilities.have_same_role(AXObject.get_child(x, 0), obj) return not AXUtilities.have_same_role(AXObject.get_child(x, 0), obj)
cells = [table.getAccessibleAt(i, colindex) for i in range(1, table.nRows)] cells = [AXTable.get_cell_at(grid, i, colindex) for i in range(1, rows)]
if list(filter(isMatch, cells)): if list(filter(isMatch, cells)):
return None, [] return None, []
+9 -3
View File
@@ -546,8 +546,11 @@ class MouseReviewer:
if coordType is None: if coordType is None:
coordType = Atspi.CoordType.SCREEN coordType = Atspi.CoordType.SCREEN
if not AXObject.supports_component(obj):
return False
try: try:
return obj.queryComponent().contains(x, y, coordType) return Atspi.Component.contains(obj, x, y, coordType)
except Exception: except Exception:
return False return False
@@ -557,12 +560,15 @@ class MouseReviewer:
if coordType is None: if coordType is None:
coordType = Atspi.CoordType.SCREEN coordType = Atspi.CoordType.SCREEN
if not AXObject.supports_component(obj):
return False
try: try:
extents = obj.queryComponent().getExtents(coordType) extents = Atspi.Component.get_extents(obj, coordType)
except Exception: except Exception:
return False return False
return list(extents) == list(bounds) return [extents.x, extents.y, extents.width, extents.height] == list(bounds)
def _accessible_window_at_point(self, pX, pY): def _accessible_window_at_point(self, pX, pY):
"""Returns the accessible window at the specified coordinates.""" """Returns the accessible window at the specified coordinates."""
+31 -32
View File
@@ -28,6 +28,7 @@ from cthulhu import settings_manager
from cthulhu import cthulhu_state from cthulhu import cthulhu_state
from cthulhu import ax_object from cthulhu import ax_object
from cthulhu.ax_text import AXText from cthulhu.ax_text import AXText
from cthulhu.ax_value import AXValue
from cthulhu import ax_utilities from cthulhu import ax_utilities
from cthulhu.ax_utilities_state import AXUtilitiesState from cthulhu.ax_utilities_state import AXUtilitiesState
from cthulhu.plugins.AIAssistant.ai_providers import create_provider from cthulhu.plugins.AIAssistant.ai_providers import create_provider
@@ -758,12 +759,8 @@ class AIAssistant(Plugin):
"""Get value from an accessibility object.""" """Get value from an accessibility object."""
try: try:
if ax_object.AXObject.supports_value(obj): if ax_object.AXObject.supports_value(obj):
try: value = AXValue.get_current_value(obj)
value_iface = obj.queryValue() return str(value) if value is not None else ""
if value_iface:
return str(value_iface.currentValue) or ""
except:
pass
return "" return ""
except Exception as e: except Exception as e:
@@ -818,18 +815,16 @@ class AIAssistant(Plugin):
def _get_object_position(self, obj): def _get_object_position(self, obj):
"""Get position and size information from an accessibility object.""" """Get position and size information from an accessibility object."""
try: try:
if hasattr(obj, 'queryComponent'): if ax_object.AXObject.supports_component(obj):
component = obj.queryComponent() extents = Atspi.Component.get_extents(obj, Atspi.CoordType.SCREEN)
if component: return {
extents = component.getExtents(Atspi.CoordType.SCREEN) 'x': extents.x,
return { 'y': extents.y,
'x': extents.x, 'width': extents.width,
'y': extents.y, 'height': extents.height
'width': extents.width, }
'height': extents.height
}
return None return None
except Exception as e: except Exception as e:
logger.error(f"Error getting object position: {e}") logger.error(f"Error getting object position: {e}")
return None return None
@@ -1166,21 +1161,25 @@ class AIAssistant(Plugin):
actions = [] actions = []
# Check for AT-SPI action interface # Check for AT-SPI action interface
try: if ax_object.AXObject.supports_action(obj):
if hasattr(obj, 'queryAction'): try:
action_iface = obj.queryAction() action_count = Atspi.Action.get_n_actions(obj)
if action_iface: except Exception:
action_count = action_iface.get_nActions() action_count = 0
for i in range(action_count): for i in range(action_count):
action_name = action_iface.getName(i) try:
action_desc = action_iface.getDescription(i) action_name = Atspi.Action.get_name(obj, i)
actions.append({ except Exception:
'name': action_name or '', action_name = ""
'description': action_desc or '', try:
'index': i action_desc = Atspi.Action.get_description(obj, i)
}) except Exception:
except: action_desc = ""
pass actions.append({
'name': action_name or '',
'description': action_desc or '',
'index': i
})
return actions return actions
+22 -15
View File
@@ -114,8 +114,10 @@ class SpeechHistory(Plugin):
self._create_window() self._create_window()
self._window.show_all() self._window.show_all()
if self._treeView and len(self._filterModel) > 0:
if self._filterEntry: self._treeView.grab_focus()
self._treeView.set_cursor(Gtk.TreePath.new_first())
elif self._filterEntry:
self._filterEntry.grab_focus() self._filterEntry.grab_focus()
debug.printMessage(debug.LEVEL_INFO, "SpeechHistory: Window shown", True) debug.printMessage(debug.LEVEL_INFO, "SpeechHistory: Window shown", True)
@@ -137,6 +139,8 @@ class SpeechHistory(Plugin):
mainBox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10) mainBox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
self._filterText = ""
# Filter row # Filter row
filterRow = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10) filterRow = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
filterLabel = Gtk.Label(label="_Filter:") filterLabel = Gtk.Label(label="_Filter:")
@@ -153,7 +157,7 @@ class SpeechHistory(Plugin):
mainBox.pack_start(filterRow, False, False, 0) mainBox.pack_start(filterRow, False, False, 0)
# List # List
self._listStore = Gtk.ListStore(int, str) self._listStore = Gtk.ListStore(str)
self._filterModel = self._listStore.filter_new() self._filterModel = self._listStore.filter_new()
self._filterModel.set_visible_func(self._filter_visible_func) self._filterModel.set_visible_func(self._filter_visible_func)
@@ -163,16 +167,10 @@ class SpeechHistory(Plugin):
selection = self._treeView.get_selection() selection = self._treeView.get_selection()
selection.set_mode(Gtk.SelectionMode.SINGLE) selection.set_mode(Gtk.SelectionMode.SINGLE)
idxRenderer = Gtk.CellRendererText()
idxColumn = Gtk.TreeViewColumn("Item", idxRenderer, text=0)
idxColumn.set_resizable(False)
idxColumn.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
self._treeView.append_column(idxColumn)
textRenderer = Gtk.CellRendererText() textRenderer = Gtk.CellRendererText()
textRenderer.set_property("wrap-width", 640) textRenderer.set_property("wrap-width", 640)
textRenderer.set_property("wrap-mode", 2) # Pango.WrapMode.WORD_CHAR textRenderer.set_property("wrap-mode", 2) # Pango.WrapMode.WORD_CHAR
textColumn = Gtk.TreeViewColumn("Spoken Text", textRenderer, text=1) textColumn = Gtk.TreeViewColumn("Spoken Text", textRenderer, text=0)
textColumn.set_resizable(True) textColumn.set_resizable(True)
textColumn.set_expand(True) textColumn.set_expand(True)
self._treeView.append_column(textColumn) self._treeView.append_column(textColumn)
@@ -221,7 +219,7 @@ class SpeechHistory(Plugin):
if not filterText: if not filterText:
return True return True
spokenText = model[treeIter][1] or "" spokenText = model[treeIter][0] or ""
return spokenText.lower().startswith(filterText) return spokenText.lower().startswith(filterText)
except Exception as e: except Exception as e:
debug.printMessage(debug.LEVEL_INFO, f"SpeechHistory: ERROR in filter func: {e}", True) debug.printMessage(debug.LEVEL_INFO, f"SpeechHistory: ERROR in filter func: {e}", True)
@@ -236,11 +234,21 @@ class SpeechHistory(Plugin):
self._listStore.clear() self._listStore.clear()
items = speech_history.get_items() items = speech_history.get_items()
for idx, item in enumerate(items, start=1): debug.printMessage(
self._listStore.append([idx, item]) debug.LEVEL_INFO,
f"SpeechHistory: Retrieved {len(items)} items (paused={speech_history.is_capture_paused()})",
True,
)
for item in items:
self._listStore.append([item])
if self._filterModel: if self._filterModel:
self._filterModel.refilter() self._filterModel.refilter()
debug.printMessage(
debug.LEVEL_INFO,
f"SpeechHistory: Filtered items visible={len(self._filterModel)} filter='{self._filterText}'",
True,
)
if selectFirst and self._treeView and len(self._filterModel) > 0: if selectFirst and self._treeView and len(self._filterModel) > 0:
selection = self._treeView.get_selection() selection = self._treeView.get_selection()
@@ -259,7 +267,7 @@ class SpeechHistory(Plugin):
if not treeIter: if not treeIter:
return None return None
return model[treeIter][1] return model[treeIter][0]
except Exception as e: except Exception as e:
debug.printMessage(debug.LEVEL_INFO, f"SpeechHistory: ERROR getting selection: {e}", True) debug.printMessage(debug.LEVEL_INFO, f"SpeechHistory: ERROR getting selection: {e}", True)
logger.exception("Error getting selected speech history item") logger.exception("Error getting selected speech history item")
@@ -403,4 +411,3 @@ class SpeechHistory(Plugin):
except Exception as e: except Exception as e:
debug.printMessage(debug.LEVEL_INFO, f"SpeechHistory: ERROR presenting message: {e}", True) debug.printMessage(debug.LEVEL_INFO, f"SpeechHistory: ERROR presenting message: {e}", True)
logger.exception("Error presenting message from SpeechHistory") logger.exception("Error presenting message from SpeechHistory")
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -25,6 +25,7 @@
import cthulhu.scripts.default as default import cthulhu.scripts.default as default
import cthulhu.cthulhu_state as cthulhu_state import cthulhu.cthulhu_state as cthulhu_state
from cthulhu.ax_value import AXValue
from .script_utilities import Utilities from .script_utilities import Utilities
@@ -47,8 +48,7 @@ class Script(default.Script):
def onValueChanged(self, event): def onValueChanged(self, event):
obj = event.source obj = event.source
if self.utilities.isSeekSlider(obj): if self.utilities.isSeekSlider(obj):
value = obj.queryValue() current_value = int(AXValue.get_current_value(obj)) / 1000
current_value = int(value.currentValue)/1000
if current_value in range(self._last_seek_value, self._last_seek_value + 4): if current_value in range(self._last_seek_value, self._last_seek_value + 4):
if self.utilities.isSameObject(obj, cthulhu_state.locusOfFocus): if self.utilities.isSameObject(obj, cthulhu_state.locusOfFocus):
self.updateBraille(obj) self.updateBraille(obj)
@@ -25,6 +25,7 @@
import cthulhu.script_utilities as script_utilities import cthulhu.script_utilities as script_utilities
from cthulhu.ax_object import AXObject from cthulhu.ax_object import AXObject
from cthulhu.ax_value import AXValue
from cthulhu.ax_utilities import AXUtilities from cthulhu.ax_utilities import AXUtilities
@@ -56,9 +57,7 @@ class Utilities(script_utilities.Utilities):
if not self.isSeekSlider(obj): if not self.isSeekSlider(obj):
return script_utilities.Utilities.textForValue(self, obj) return script_utilities.Utilities.textForValue(self, obj)
try: if not AXObject.supports_value(obj):
value = obj.queryValue()
except NotImplementedError:
return script_utilities.Utilities.textForValue(self, obj) return script_utilities.Utilities.textForValue(self, obj)
else:
return self._formatDuration(int(value.currentValue)/1000) return self._formatDuration(int(AXValue.get_current_value(obj)) / 1000)
@@ -36,6 +36,7 @@ import cthulhu.scripts.default as default
import cthulhu.settings as settings import cthulhu.settings as settings
import cthulhu.settings_manager as settings_manager import cthulhu.settings_manager as settings_manager
from cthulhu.ax_object import AXObject from cthulhu.ax_object import AXObject
from cthulhu.ax_value import AXValue
_settingsManager = settings_manager.getManager() _settingsManager = settings_manager.getManager()
@@ -47,10 +48,9 @@ _settingsManager = settings_manager.getManager()
class Script(default.Script): class Script(default.Script):
def onValueChanged(self, event): def onValueChanged(self, event):
try: if AXObject.supports_value(event.source):
ivalue = event.source.queryValue() value = int(AXValue.get_current_value(event.source))
value = int(ivalue.currentValue) else:
except NotImplementedError:
value = -1 value = -1
if value >= 0: if value >= 0:
@@ -63,10 +63,9 @@ class Script(default.Script):
def onNameChanged(self, event): def onNameChanged(self, event):
"""Callback for object:property-change:accessible-name events.""" """Callback for object:property-change:accessible-name events."""
try: if AXObject.supports_value(event.source):
ivalue = event.source.queryValue() value = AXValue.get_current_value(event.source)
value = ivalue.currentValue else:
except NotImplementedError:
value = -1 value = -1
message = "" message = ""
@@ -41,6 +41,7 @@ from gi.repository import Atspi
import cthulhu.debug as debug import cthulhu.debug as debug
import cthulhu.script_utilities as script_utilities import cthulhu.script_utilities as script_utilities
from cthulhu.ax_object import AXObject from cthulhu.ax_object import AXObject
from cthulhu.ax_table import AXTable
from cthulhu.ax_utilities import AXUtilities from cthulhu.ax_utilities import AXUtilities
from cthulhu.ax_utilities_relation import AXUtilitiesRelation from cthulhu.ax_utilities_relation import AXUtilitiesRelation
@@ -83,18 +84,18 @@ class Utilities(script_utilities.Utilities):
return script_utilities.Utilities.childNodes(self, obj) return script_utilities.Utilities.childNodes(self, obj)
parent = AXObject.get_parent(obj) parent = AXObject.get_parent(obj)
try: table = AXTable.get_table(parent)
table = parent.queryTable() if table is None:
except Exception: return []
if not AXUtilities.is_expanded(obj):
return [] return []
else:
if not AXUtilities.is_expanded(obj):
return []
nodes = [] nodes = []
index = self.cellIndex(obj) row, col = AXTable.get_cell_coordinates(obj, prefer_attribute=False)
row = table.getRowAtIndex(index) if row < 0 or col < 0:
col = table.getColumnAtIndex(index + 1) return []
col += 1
nodeLevel = self.nodeLevel(obj) nodeLevel = self.nodeLevel(obj)
# Candidates will be in the rows beneath the current row. # Candidates will be in the rows beneath the current row.
@@ -102,8 +103,10 @@ class Utilities(script_utilities.Utilities):
# soon as the node level of a candidate is equal or less # soon as the node level of a candidate is equal or less
# than our current level. # than our current level.
# #
for i in range(row+1, table.nRows): for i in range(row + 1, AXTable.get_row_count(table, prefer_attribute=False)):
cell = table.getAccessibleAt(i, col) cell = AXTable.get_cell_at(table, i, col)
if not cell:
continue
nodeCell = AXObject.get_previous_sibling(cell) nodeCell = AXObject.get_previous_sibling(cell)
nodeOf = AXUtilitiesRelation.get_is_node_child_of(nodeCell) nodeOf = AXUtilitiesRelation.get_is_node_child_of(nodeCell)
if not nodeOf: if not nodeOf:
@@ -135,9 +138,7 @@ class Utilities(script_utilities.Utilities):
obj = AXObject.get_previous_sibling(obj) obj = AXObject.get_previous_sibling(obj)
parent = AXObject.get_parent(obj) parent = AXObject.get_parent(obj)
try: if AXTable.get_table(parent) is None:
parent.queryTable()
except Exception:
return -1 return -1
nodes = [] nodes = []
+4 -1
View File
@@ -856,7 +856,10 @@ class Script(default.Script):
if isinstance(cthulhu_state.lastInputEvent, input_event.MouseButtonEvent): if isinstance(cthulhu_state.lastInputEvent, input_event.MouseButtonEvent):
x = cthulhu_state.lastInputEvent.x x = cthulhu_state.lastInputEvent.x
y = cthulhu_state.lastInputEvent.y y = cthulhu_state.lastInputEvent.y
weToggledIt = obj.queryComponent().contains(x, y, 0) if AXObject.supports_component(obj):
weToggledIt = Atspi.Component.contains(obj, x, y, Atspi.CoordType.SCREEN)
else:
weToggledIt = False
elif AXUtilities.is_focused(obj): elif AXUtilities.is_focused(obj):
weToggledIt = True weToggledIt = True
else: else:
@@ -44,6 +44,7 @@ import cthulhu.messages as messages
import cthulhu.cthulhu_state as cthulhu_state import cthulhu.cthulhu_state as cthulhu_state
import cthulhu.script_utilities as script_utilities import cthulhu.script_utilities as script_utilities
from cthulhu.ax_object import AXObject from cthulhu.ax_object import AXObject
from cthulhu.ax_table import AXTable
from cthulhu.ax_selection import AXSelection from cthulhu.ax_selection import AXSelection
from cthulhu.ax_text import AXText from cthulhu.ax_text import AXText
from cthulhu.ax_utilities import AXUtilities from cthulhu.ax_utilities import AXUtilities
@@ -149,15 +150,11 @@ class Utilities(script_utilities.Utilities):
if table is not None and not AXUtilities.is_table(table): if table is not None and not AXUtilities.is_table(table):
table = AXObject.get_parent(table) table = AXObject.get_parent(table)
try: table = AXTable.get_table(table)
iTable = table.queryTable() if table is None:
except Exception:
return -1, -1, None return -1, -1, None
index = self.cellIndex(cell) row, column = AXTable.get_cell_coordinates(cell, prefer_attribute=False)
row = iTable.getRowAtIndex(index)
column = iTable.getColumnAtIndex(index)
return row, column, table return row, column, table
def rowHeadersForCell(self, obj): def rowHeadersForCell(self, obj):
@@ -195,13 +192,12 @@ class Utilities(script_utilities.Utilities):
getColHeader = \ getColHeader = \
getColHeader and objCol!= self._script.pointOfReference.get("lastColumn") getColHeader and objCol!= self._script.pointOfReference.get("lastColumn")
parentTable = table.queryTable()
rowHeader, colHeader = None, None rowHeader, colHeader = None, None
if getColHeader: if getColHeader:
colHeader = parentTable.getAccessibleAt(headersRow, objCol) colHeader = AXTable.get_cell_at(table, headersRow, objCol)
if getRowHeader: if getRowHeader:
rowHeader = parentTable.getAccessibleAt(objRow, headersCol) rowHeader = AXTable.get_cell_at(table, objRow, headersCol)
return rowHeader, colHeader return rowHeader, colHeader
@@ -631,16 +627,13 @@ class Utilities(script_utilities.Utilities):
return res return res
def _getCellNameForCoordinates(self, obj, row, col, includeContents=False): def _getCellNameForCoordinates(self, obj, row, col, includeContents=False):
try: if not AXObject.supports_table(obj):
table = obj.queryTable()
except Exception:
tokens = ["SOFFICE: Exception querying Table interface of", obj] tokens = ["SOFFICE: Exception querying Table interface of", obj]
debug.printTokens(debug.LEVEL_INFO, tokens, True) debug.printTokens(debug.LEVEL_INFO, tokens, True)
return return
try: cell = AXTable.get_cell_at(obj, row, col)
cell = table.getAccessibleAt(row, col) if not cell:
except Exception:
tokens = [f"SOFFICE: Exception getting cell ({row},{col}) of", obj] tokens = [f"SOFFICE: Exception getting cell ({row},{col}) of", obj]
debug.printTokens(debug.LEVEL_INFO, tokens, True) debug.printTokens(debug.LEVEL_INFO, tokens, True)
return return
@@ -742,9 +735,9 @@ class Utilities(script_utilities.Utilities):
if not (AXObject.supports_table(obj) and AXObject.supports_selection(obj)): if not (AXObject.supports_table(obj) and AXObject.supports_selection(obj)):
return True return True
table = obj.queryTable() cols = set(AXTable.get_selected_columns(obj))
cols = set(table.getSelectedColumns()) rows = set(AXTable.get_selected_rows(obj))
rows = set(table.getSelectedRows()) total_cols = AXTable.get_column_count(obj, prefer_attribute=False)
selectedCols = sorted(cols.difference(set(self._calcSelectedColumns))) selectedCols = sorted(cols.difference(set(self._calcSelectedColumns)))
unselectedCols = sorted(set(self._calcSelectedColumns).difference(cols)) unselectedCols = sorted(set(self._calcSelectedColumns).difference(cols))
@@ -767,11 +760,11 @@ class Utilities(script_utilities.Utilities):
self._calcSelectedColumns = list(cols) self._calcSelectedColumns = list(cols)
self._calcSelectedRows = list(rows) self._calcSelectedRows = list(rows)
if len(cols) == table.nColumns: if len(cols) == total_cols:
self._script.speakMessage(messages.DOCUMENT_SELECTED_ALL) self._script.speakMessage(messages.DOCUMENT_SELECTED_ALL)
return True return True
if not len(cols) and len(unselectedCols) == table.nColumns: if not len(cols) and len(unselectedCols) == total_cols:
self._script.speakMessage(messages.DOCUMENT_UNSELECTED_ALL) self._script.speakMessage(messages.DOCUMENT_UNSELECTED_ALL)
return True return True
@@ -345,10 +345,12 @@ class SpeechGenerator(speech_generator.SpeechGenerator):
result = [] result = []
if AXObject.supports_text(obj): if AXObject.supports_text(obj):
objectText = self._script.utilities.substring(obj, 0, -1) objectText = self._script.utilities.substring(obj, 0, -1)
try: extents = None
extents = obj.queryComponent().getExtents(Atspi.CoordType.SCREEN) if AXObject.supports_component(obj):
except Exception: try:
extents = None extents = Atspi.Component.get_extents(obj, Atspi.CoordType.SCREEN)
except Exception:
extents = None
if extents is not None: if extents is not None:
tooLongCount = 0 tooLongCount = 0
+38 -8
View File
@@ -37,7 +37,9 @@ __license__ = "LGPL"
import gi import gi
gi.require_version('Atspi', '2.0') gi.require_version('Atspi', '2.0')
gi.require_version('Gdk', '3.0')
from gi.repository import Atspi from gi.repository import Atspi
from gi.repository import Gdk
import re import re
import time import time
@@ -49,6 +51,7 @@ 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
import cthulhu.input_event as input_event import cthulhu.input_event as input_event
import cthulhu.input_event_manager as input_event_manager
import cthulhu.keybindings as keybindings import cthulhu.keybindings as keybindings
import cthulhu.messages as messages import cthulhu.messages as messages
import cthulhu.cthulhu as cthulhu import cthulhu.cthulhu as cthulhu
@@ -62,6 +65,7 @@ import cthulhu.sound as sound
import cthulhu.speech as speech import cthulhu.speech as speech
import cthulhu.speechserver as speechserver import cthulhu.speechserver as speechserver
from cthulhu.ax_object import AXObject from cthulhu.ax_object import AXObject
from cthulhu.ax_value import AXValue
from cthulhu.ax_text import AXText from cthulhu.ax_text import AXText
from cthulhu.ax_utilities import AXUtilities from cthulhu.ax_utilities import AXUtilities
from cthulhu.ax_utilities_relation import AXUtilitiesRelation from cthulhu.ax_utilities_relation import AXUtilitiesRelation
@@ -126,6 +130,7 @@ class Script(script.Script):
self._sayAllIsInterrupted = False self._sayAllIsInterrupted = False
self._sayAllContexts = [] self._sayAllContexts = []
self.grab_ids = [] self.grab_ids = []
self._modifierGrabIds = []
if app: if app:
Atspi.Accessible.set_cache_mask( Atspi.Accessible.set_cache_mask(
@@ -585,6 +590,7 @@ class Script(script.Script):
for b in bound: for b in bound:
for id in cthulhu.addKeyGrab(b): for id in cthulhu.addKeyGrab(b):
self.grab_ids.append(id) self.grab_ids.append(id)
self._addModifierGrabs()
def removeKeyGrabs(self): def removeKeyGrabs(self):
""" Removes this script's AT-SPI key grabs. """ """ Removes this script's AT-SPI key grabs. """
@@ -593,6 +599,35 @@ class Script(script.Script):
for id in self.grab_ids: for id in self.grab_ids:
cthulhu.removeKeyGrab(id) cthulhu.removeKeyGrab(id)
self.grab_ids = [] self.grab_ids = []
self._removeModifierGrabs()
def _addModifierGrabs(self):
if cthulhu_state.device is None:
return
if self._modifierGrabIds:
return
manager = input_event_manager.get_manager()
for modifier in settings.cthulhuModifierKeys:
if modifier not in ["Insert", "KP_Insert"]:
continue
keyval = Gdk.keyval_from_name(modifier)
keycode = keybindings.getKeycode(modifier)
if not keyval or not keycode:
continue
grabId = manager.add_grab_for_modifier(modifier, keyval, keycode)
if grabId != -1:
self._modifierGrabIds.append((modifier, grabId))
def _removeModifierGrabs(self):
if not self._modifierGrabIds:
return
manager = input_event_manager.get_manager()
for modifier, grabId in self._modifierGrabIds:
manager.remove_grab_for_modifier(modifier, grabId)
self._modifierGrabIds = []
def refreshKeyGrabs(self): def refreshKeyGrabs(self):
""" Refreshes the enabled key grabs for this script. """ """ Refreshes the enabled key grabs for this script. """
@@ -1837,17 +1872,12 @@ class Script(script.Script):
obj = event.source obj = event.source
role = AXObject.get_role(obj) role = AXObject.get_role(obj)
try: if not AXObject.supports_value(obj):
value = obj.queryValue()
currentValue = value.currentValue
except NotImplementedError:
tokens = ["DEFAULT:", obj, "doesn't implement AtspiValue"] tokens = ["DEFAULT:", obj, "doesn't implement AtspiValue"]
debug.printTokens(debug.LEVEL_INFO, tokens, True) debug.printTokens(debug.LEVEL_INFO, tokens, True)
return return
except Exception:
tokens = ["DEFAULT: Exception getting current value for", obj] currentValue = AXValue.get_current_value(obj)
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return
if "oldValue" in self.pointOfReference \ if "oldValue" in self.pointOfReference \
and (currentValue == self.pointOfReference["oldValue"]): and (currentValue == self.pointOfReference["oldValue"]):
@@ -34,6 +34,10 @@ __date__ = "$Date$"
__copyright__ = "Copyright (c) 2010 Joanmarie Diggs." __copyright__ = "Copyright (c) 2010 Joanmarie Diggs."
__license__ = "LGPL" __license__ = "LGPL"
import gi
gi.require_version("Atspi", "2.0")
from gi.repository import Atspi
import cthulhu.script_utilities as script_utilities import cthulhu.script_utilities as script_utilities
from cthulhu.ax_object import AXObject from cthulhu.ax_object import AXObject
from cthulhu.ax_utilities import AXUtilities from cthulhu.ax_utilities import AXUtilities
@@ -81,15 +85,15 @@ class Utilities(script_utilities.Utilities):
# negatives. # negatives.
# #
if AXUtilities.is_label(obj1) and AXUtilities.is_label(obj2): if AXUtilities.is_label(obj1) and AXUtilities.is_label(obj2):
try: if AXObject.supports_component(obj1) and AXObject.supports_component(obj2):
ext1 = obj1.queryComponent().getExtents(0) try:
ext2 = obj2.queryComponent().getExtents(0) ext1 = Atspi.Component.get_extents(obj1, Atspi.CoordType.SCREEN)
except Exception: ext2 = Atspi.Component.get_extents(obj2, Atspi.CoordType.SCREEN)
pass if ext1.x == ext2.x and ext1.y == ext2.y \
else: and ext1.width == ext2.width and ext1.height == ext2.height:
if ext1.x == ext2.x and ext1.y == ext2.y \ return True
and ext1.width == ext2.width and ext1.height == ext2.height: except Exception:
return True pass
# In java applications, TRANSIENT state is missing for tree items # In java applications, TRANSIENT state is missing for tree items
# (fix for bug #352250) # (fix for bug #352250)
@@ -48,6 +48,7 @@ import cthulhu.cthulhu_state as cthulhu_state
import cthulhu.speech as speech import cthulhu.speech as speech
import cthulhu.structural_navigation as structural_navigation import cthulhu.structural_navigation as structural_navigation
from cthulhu.ax_object import AXObject from cthulhu.ax_object import AXObject
from cthulhu.ax_hypertext import AXHypertext
from cthulhu.ax_text import AXText from cthulhu.ax_text import AXText
from cthulhu.ax_utilities import AXUtilities from cthulhu.ax_utilities import AXUtilities
@@ -613,14 +614,11 @@ class Script(default.Script):
# just done with the current object. If we're still in SayAll, we do # just done with the current object. If we're still in SayAll, we do
# not want to set the caret (and hence set focus) in a link we just # not want to set the caret (and hence set focus) in a link we just
# passed by. # passed by.
try: if AXObject.supports_hypertext(obj):
hypertext = obj.queryHypertext() links = AXHypertext.get_all_links(obj)
except NotImplementedError: if [link for link in links
pass if AXHypertext.get_link_start_offset(link)
else: <= offset <= AXHypertext.get_link_end_offset(link)]:
linkCount = hypertext.getNLinks()
links = [hypertext.getLink(x) for x in range(linkCount)]
if [link for link in links if link.startIndex <= offset <= link.endIndex]:
return return
cthulhu.emitRegionChanged(obj, offset, mode=cthulhu.SAY_ALL) cthulhu.emitRegionChanged(obj, offset, mode=cthulhu.SAY_ALL)
@@ -40,6 +40,7 @@ import cthulhu.keybindings as keybindings
import cthulhu.cthulhu as cthulhu import cthulhu.cthulhu as cthulhu
import cthulhu.cthulhu_state as cthulhu_state import cthulhu.cthulhu_state as cthulhu_state
from cthulhu.ax_object import AXObject from cthulhu.ax_object import AXObject
from cthulhu.ax_hypertext import AXHypertext
from cthulhu.ax_text import AXText from cthulhu.ax_text import AXText
from cthulhu.ax_utilities import AXUtilities from cthulhu.ax_utilities import AXUtilities
@@ -142,9 +143,7 @@ class Utilities(script_utilities.Utilities):
if not AXObject.supports_text(obj): if not AXObject.supports_text(obj):
return [(obj, 0, 1, '')] return [(obj, 0, 1, '')]
try: if not AXObject.supports_hypertext(obj):
htext = obj.queryHypertext()
except (AttributeError, NotImplementedError):
return [(obj, 0, 1, '')] return [(obj, 0, 1, '')]
string = AXText.get_all_text(obj) string = AXText.get_all_text(obj)
@@ -179,10 +178,11 @@ class Utilities(script_utilities.Utilities):
offsets = [x for x in offsets if start <= x < end] offsets = [x for x in offsets if start <= x < end]
objects = [] objects = []
try: objs = []
objs = [obj[htext.getLinkIndex(offset)] for offset in offsets] for offset in offsets:
except Exception: if child := AXHypertext.get_child_at_offset(obj, offset):
objs = [] objs.append(child)
ranges = [self.getHyperlinkRange(x) for x in objs] ranges = [self.getHyperlinkRange(x) for x in objs]
for i, (first, last) in enumerate(ranges): for i, (first, last) in enumerate(ranges):
objects.append((obj, start, first, string[start:first])) objects.append((obj, start, first, string[start:first]))
@@ -212,7 +212,10 @@ class Utilities(script_utilities.Utilities):
if not text: if not text:
return x, y return x, y
objBox = obj.queryComponent().getExtents(coordType) if not AXObject.supports_component(obj):
return x, y
objBox = Atspi.Component.get_extents(obj, coordType)
stringBox = Atspi.Text.get_range_extents( stringBox = Atspi.Text.get_range_extents(
obj, 0, AXText.get_character_count(obj), coordType) obj, 0, AXText.get_character_count(obj), coordType)
if self.intersection(objBox, stringBox) != (0, 0, 0, 0): if self.intersection(objBox, stringBox) != (0, 0, 0, 0):
+12 -4
View File
@@ -32,6 +32,10 @@ __copyright__ = "Copyright (c) 2005-2009 Sun Microsystems Inc." \
__license__ = "LGPL" __license__ = "LGPL"
import time import time
import gi
gi.require_version("Atspi", "2.0")
from gi.repository import Atspi
from gi.repository import Gtk from gi.repository import Gtk
from cthulhu import caret_navigation from cthulhu import caret_navigation
@@ -40,6 +44,7 @@ from cthulhu import keybindings
from cthulhu import debug from cthulhu import debug
from cthulhu import guilabels from cthulhu import guilabels
from cthulhu import input_event from cthulhu import input_event
from cthulhu import input_event_manager
from cthulhu import liveregions from cthulhu import liveregions
from cthulhu import messages from cthulhu import messages
from cthulhu import cthulhu from cthulhu import cthulhu
@@ -1002,8 +1007,7 @@ class Script(default.Script):
document = self.utilities.getTopLevelDocumentForObject(obj) document = self.utilities.getTopLevelDocumentForObject(obj)
obj, offset = self.utilities.getCaretContext(documentFrame=document) obj, offset = self.utilities.getCaretContext(documentFrame=document)
keyString, mods = self.utilities.lastKeyAndModifiers() if input_event_manager.get_manager().last_event_was_right():
if keyString == "Right":
offset -= 1 offset -= 1
wordContents = self.utilities.getWordContentsAtOffset(obj, offset, useCache=True) wordContents = self.utilities.getWordContentsAtOffset(obj, offset, useCache=True)
@@ -1317,8 +1321,8 @@ class Script(default.Script):
if not obj: if not obj:
return return
if AXUtilities.is_focusable(obj): if AXUtilities.is_focusable(obj) and AXObject.supports_component(obj):
obj.queryComponent().grabFocus() Atspi.Component.grab_focus(obj)
contents = self.utilities.get_objectContentsAtOffset(obj, offset) contents = self.utilities.get_objectContentsAtOffset(obj, offset)
self.utilities.setCaretPosition(obj, offset) self.utilities.setCaretPosition(obj, offset)
@@ -2449,6 +2453,10 @@ class Script(default.Script):
msg = "WEB: Event believed to be browser UI page switch" msg = "WEB: Event believed to be browser UI page switch"
debug.printMessage(debug.LEVEL_INFO, msg, True) debug.printMessage(debug.LEVEL_INFO, msg, True)
if event.detail1: if event.detail1:
# Work around stale cache when switching tabs.
AXObject.clear_cache(event.source, False, "Work around Chromium page switch.")
AXUtilities.clear_all_cache_now(reason=msg)
self.utilities.clearCaretContext()
self.presentObject(event.source, priorObj=cthulhu_state.locusOfFocus, interrupt=True) self.presentObject(event.source, priorObj=cthulhu_state.locusOfFocus, interrupt=True)
return True return True
+57 -84
View File
@@ -41,6 +41,7 @@ import urllib
from cthulhu import debug from cthulhu import debug
from cthulhu import input_event from cthulhu import input_event
from cthulhu import input_event_manager
from cthulhu import messages from cthulhu import messages
from cthulhu import cthulhu from cthulhu import cthulhu
from cthulhu import cthulhu_state from cthulhu import cthulhu_state
@@ -48,11 +49,14 @@ from cthulhu import script_utilities
from cthulhu import script_manager from cthulhu import script_manager
from cthulhu import settings_manager from cthulhu import settings_manager
from cthulhu.ax_collection import AXCollection from cthulhu.ax_collection import AXCollection
from cthulhu.ax_component import AXComponent
from cthulhu.ax_document import AXDocument from cthulhu.ax_document import AXDocument
from cthulhu.ax_hypertext import AXHypertext
from cthulhu.ax_object import AXObject from cthulhu.ax_object import AXObject
from cthulhu.ax_text import AXText from cthulhu.ax_text import AXText
from cthulhu.ax_utilities import AXUtilities from cthulhu.ax_utilities import AXUtilities
from cthulhu.ax_utilities_relation import AXUtilitiesRelation from cthulhu.ax_utilities_relation import AXUtilitiesRelation
from cthulhu import speech_and_verbosity_manager
_scriptManager = script_manager.get_manager() _scriptManager = script_manager.get_manager()
_settingsManager = settings_manager.getManager() _settingsManager = settings_manager.getManager()
@@ -307,6 +311,16 @@ class Utilities(script_utilities.Utilities):
documents = list(filter(AXUtilities.is_showing, documents)) documents = list(filter(AXUtilities.is_showing, documents))
if len(documents) == 1: if len(documents) == 1:
return documents[0] return documents[0]
# If multiple documents are showing (e.g., multi-tab browser), use the
# locus of focus to determine which document is currently active.
if documents:
focusDoc = self.getTopLevelDocumentForObject(cthulhu_state.locusOfFocus)
if focusDoc in documents:
tokens = ["WEB: Multiple showing documents, using focus-based document:", focusDoc]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return focusDoc
return None return None
def documentFrame(self, obj=None): def documentFrame(self, obj=None):
@@ -356,11 +370,13 @@ class Utilities(script_utilities.Utilities):
return AXUtilities.is_focusable(obj) return AXUtilities.is_focusable(obj)
def grabFocus(self, obj): def grabFocus(self, obj):
try: if not AXObject.supports_component(obj):
obj.queryComponent().grabFocus() tokens = ["WEB:", obj, "does not support the component interface"]
except NotImplementedError:
tokens = ["WEB:", obj, "does not implement the component interface"]
debug.printTokens(debug.LEVEL_INFO, tokens, True) debug.printTokens(debug.LEVEL_INFO, tokens, True)
return
try:
Atspi.Component.grab_focus(obj)
except Exception: except Exception:
tokens = ["WEB: Exception grabbing focus on", obj] tokens = ["WEB: Exception grabbing focus on", obj]
debug.printTokens(debug.LEVEL_INFO, tokens, True) debug.printTokens(debug.LEVEL_INFO, tokens, True)
@@ -611,10 +627,8 @@ class Utilities(script_utilities.Utilities):
nextobj, nextoffset = self.findNextCaretInOrder(obj, offset) nextobj, nextoffset = self.findNextCaretInOrder(obj, offset)
if skipSpace: if skipSpace:
text = self.queryNonEmptyText(nextobj) while nextobj and AXText.get_character_at_offset(nextobj, nextoffset)[0].isspace():
while text and AXText.get_substring(nextobj, nextoffset, nextoffset + 1) in [" ", "\xa0"]:
nextobj, nextoffset = self.findNextCaretInOrder(nextobj, nextoffset) nextobj, nextoffset = self.findNextCaretInOrder(nextobj, nextoffset)
text = self.queryNonEmptyText(nextobj)
return nextobj, nextoffset return nextobj, nextoffset
@@ -624,10 +638,8 @@ class Utilities(script_utilities.Utilities):
prevobj, prevoffset = self.findPreviousCaretInOrder(obj, offset) prevobj, prevoffset = self.findPreviousCaretInOrder(obj, offset)
if skipSpace: if skipSpace:
text = self.queryNonEmptyText(prevobj) while prevobj and AXText.get_character_at_offset(prevobj, prevoffset)[0].isspace():
while text and AXText.get_substring(prevobj, prevoffset, prevoffset + 1) in [" ", "\xa0"]:
prevobj, prevoffset = self.findPreviousCaretInOrder(prevobj, prevoffset) prevobj, prevoffset = self.findPreviousCaretInOrder(prevobj, prevoffset)
text = self.queryNonEmptyText(prevobj)
return prevobj, prevoffset return prevobj, prevoffset
@@ -689,51 +701,25 @@ class Utilities(script_utilities.Utilities):
if not obj: if not obj:
return [0, 0, 0, 0] return [0, 0, 0, 0]
result = [0, 0, 0, 0] if AXObject.supports_text(obj) and 0 <= startOffset < endOffset:
if AXObject.supports_text(obj): rect = AXText.get_range_rect(obj, startOffset, endOffset)
try: result = [rect.x, rect.y, rect.width, rect.height]
char_count = AXText.get_character_count(obj) if not (result[0] and result[1] and result[2] == 0 and result[3] == 0
if char_count and 0 <= startOffset < endOffset: and AXText.get_substring(obj, startOffset, endOffset).strip()):
result = list(Atspi.Text.get_range_extents( return result
obj, startOffset, endOffset, Atspi.CoordType.SCREEN))
except Exception as error: tokens = ["WEB: Suspected bogus range extents for",
tokens = ["WEB: Exception getting range extents for", obj, ":", error] obj, "(chars:", startOffset, ",", endOffset, "):", result]
debug.printTokens(debug.LEVEL_INFO, tokens, True) debug.printTokens(debug.LEVEL_INFO, tokens, True)
else:
if result[0] and result[1] and result[2] == 0 and result[3] == 0 \
and AXText.get_substring(obj, startOffset, endOffset).strip():
tokens = ["WEB: Suspected bogus range extents for",
obj, "(chars:", startOffset, ",", endOffset, "):", result]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
elif char_count:
return result
parent = AXObject.get_parent(obj) parent = AXObject.get_parent(obj)
if (AXUtilities.is_menu(obj) or AXUtilities.is_list_item(obj)) \ if (AXUtilities.is_menu(obj) or AXUtilities.is_list_item(obj)) \
and (AXUtilities.is_combo_box(parent) or AXUtilities.is_list_box(parent)): and (AXUtilities.is_combo_box(parent) or AXUtilities.is_list_box(parent)):
try: extents = AXComponent.get_rect(parent)
ext = parent.queryComponent().getExtents(0)
except NotImplementedError:
tokens = ["WEB:", parent, "does not implement the component interface"]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return [0, 0, 0, 0]
except Exception:
tokens = ["WEB: Exception getting extents for", parent]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return [0, 0, 0, 0]
else: else:
try: extents = AXComponent.get_rect(obj)
ext = obj.queryComponent().getExtents(0)
except NotImplementedError:
tokens = ["WEB:", obj, "does not implement the component interface"]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return [0, 0, 0, 0]
except Exception:
tokens = ["WEB: Exception getting extents for", obj]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return [0, 0, 0, 0]
return [ext.x, ext.y, ext.width, ext.height] return [extents.x, extents.y, extents.width, extents.height]
def descendantAtPoint(self, root, x, y, coordType=None): def descendantAtPoint(self, root, x, y, coordType=None):
if coordType is None: if coordType is None:
@@ -853,7 +839,7 @@ class Utilities(script_utilities.Utilities):
if not self.isTextBlockElement(obj): if not self.isTextBlockElement(obj):
return -1 return -1
child = self.findChildAtOffset(obj, offset) child = AXHypertext.find_child_at_offset(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:
@@ -1351,7 +1337,7 @@ class Utilities(script_utilities.Utilities):
pass pass
else: else:
if char == self.EMBEDDED_OBJECT_CHARACTER: if char == self.EMBEDDED_OBJECT_CHARACTER:
child = self.findChildAtOffset(obj, offset) child = AXHypertext.find_child_at_offset(obj, offset)
if child: if child:
return self._getContentsForObj(child, 0, boundary) return self._getContentsForObj(child, 0, boundary)
@@ -1698,7 +1684,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.findChildAtOffset(obj, offset) child = AXHypertext.find_child_at_offset(obj, offset)
if child: if child:
obj = child obj = child
offset = 0 offset = 0
@@ -1883,7 +1869,7 @@ class Utilities(script_utilities.Utilities):
tokens = ["WEB: First context on line is: ", firstObj, ", ", firstOffset] tokens = ["WEB: First context on line is: ", firstObj, ", ", firstOffset]
debug.printTokens(debug.LEVEL_INFO, tokens, True) debug.printTokens(debug.LEVEL_INFO, tokens, True)
skipSpace = not self.elementIsPreformattedText(firstObj) skipSpace = not speech_and_verbosity_manager.getManager().get_speak_blank_lines()
obj, offset = self.previousContext(firstObj, firstOffset, skipSpace) obj, offset = self.previousContext(firstObj, firstOffset, skipSpace)
if not obj and firstObj: if not obj and firstObj:
tokens = ["WEB: Previous context is: ", obj, ", ", offset, ". Trying again."] tokens = ["WEB: Previous context is: ", obj, ", ", offset, ". Trying again."]
@@ -1948,7 +1934,7 @@ class Utilities(script_utilities.Utilities):
tokens = ["WEB: Last context on line is: ", lastObj, ", ", lastOffset] tokens = ["WEB: Last context on line is: ", lastObj, ", ", lastOffset]
debug.printTokens(debug.LEVEL_INFO, tokens, True) debug.printTokens(debug.LEVEL_INFO, tokens, True)
skipSpace = not self.elementIsPreformattedText(lastObj) skipSpace = not speech_and_verbosity_manager.getManager().get_speak_blank_lines()
obj, offset = self.nextContext(lastObj, lastOffset, skipSpace) obj, offset = self.nextContext(lastObj, lastOffset, skipSpace)
if not obj and lastObj: if not obj and lastObj:
tokens = ["WEB: Next context is: ", obj, ", ", offset, ". Trying again."] tokens = ["WEB: Next context is: ", obj, ", ", offset, ". Trying again."]
@@ -2355,7 +2341,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.findChildAtOffset(obj, start) child = AXHypertext.find_child_at_offset(obj, start)
if child: if child:
return self.textAtPoint(child, x, y, coordType, boundary) return self.textAtPoint(child, x, y, coordType, boundary)
@@ -4048,11 +4034,10 @@ class Utilities(script_utilities.Utilities):
if uri and not uri.startswith('javascript'): if uri and not uri.startswith('javascript'):
rv = False rv = False
if rv and AXObject.supports_image(obj): if rv and AXObject.supports_image(obj):
image = obj.queryImage() if AXObject.get_image_description(obj):
if image.imageDescription:
rv = False rv = False
elif not self.hasExplicitName(obj) and not self.isRedundantSVG(obj): elif not self.hasExplicitName(obj) and not self.isRedundantSVG(obj):
width, height = image.getImageSize() width, height = AXObject.get_image_size(obj)
if width > 25 and height > 25: if width > 25 and height > 25:
rv = False rv = False
if rv and AXObject.supports_text(obj): if rv and AXObject.supports_text(obj):
@@ -4326,16 +4311,14 @@ class Utilities(script_utilities.Utilities):
if event.type.startswith("object:text-changed") \ if event.type.startswith("object:text-changed") \
or event.type.startswith("object:text-selection-changed"): or event.type.startswith("object:text-selection-changed"):
lastKey, mods = self.lastKeyAndModifiers() if input_event_manager.get_manager().last_event_was_up_or_down():
if lastKey in ["Down", "Up"]:
return True return True
return False return False
def treatEventAsSpinnerValueChange(self, event): def treatEventAsSpinnerValueChange(self, event):
if event.type.startswith("object:text-caret-moved") and self.isSpinnerEntry(event.source): if event.type.startswith("object:text-caret-moved") and self.isSpinnerEntry(event.source):
lastKey, mods = self.lastKeyAndModifiers() if input_event_manager.get_manager().last_event_was_up_or_down():
if lastKey in ["Down", "Up"]:
obj, offset = self.getCaretContext() obj, offset = self.getCaretContext()
return event.source == obj return event.source == obj
@@ -4347,8 +4330,7 @@ class Utilities(script_utilities.Utilities):
if event.type.startswith("object:text-") \ if event.type.startswith("object:text-") \
and self.isSingleLineAutocompleteEntry(event.source): and self.isSingleLineAutocompleteEntry(event.source):
lastKey, mods = self.lastKeyAndModifiers() return input_event_manager.get_manager().last_event_was_return()
return lastKey == "Return"
if event.type.startswith("object:text-") or event.type.endswith("accessible-name"): if event.type.startswith("object:text-") or event.type.endswith("accessible-name"):
return AXUtilities.is_status_bar(event.source) or AXUtilities.is_label(event.source) return AXUtilities.is_status_bar(event.source) or AXUtilities.is_label(event.source)
if event.type.startswith("object:children-changed"): if event.type.startswith("object:children-changed"):
@@ -4377,8 +4359,7 @@ class Utilities(script_utilities.Utilities):
return True return True
if obj == event.source and isComboBoxItem(obj): if obj == event.source and isComboBoxItem(obj):
lastKey, mods = self.lastKeyAndModifiers() if input_event_manager.get_manager().last_event_was_up_or_down():
if lastKey in ["Down", "Up"]:
return True return True
return False return False
@@ -4400,8 +4381,7 @@ class Utilities(script_utilities.Utilities):
if AXUtilities.is_menu_related(event.source) \ if AXUtilities.is_menu_related(event.source) \
and AXUtilities.is_entry(cthulhu_state.locusOfFocus) \ and AXUtilities.is_entry(cthulhu_state.locusOfFocus) \
and AXUtilities.is_focused(cthulhu_state.locusOfFocus): and AXUtilities.is_focused(cthulhu_state.locusOfFocus):
lastKey, mods = self.lastKeyAndModifiers() if not input_event_manager.get_manager().last_event_was_up_or_down():
if lastKey not in ["Down", "Up"]:
return True return True
return False return False
@@ -4417,8 +4397,7 @@ class Utilities(script_utilities.Utilities):
if AXUtilities.is_menu_item_of_any_kind(cthulhu_state.locusOfFocus) \ if AXUtilities.is_menu_item_of_any_kind(cthulhu_state.locusOfFocus) \
or AXUtilities.is_list_item(cthulhu_state.locusOfFocus): or AXUtilities.is_list_item(cthulhu_state.locusOfFocus):
lastKey, mods = self.lastKeyAndModifiers() return input_event_manager.get_manager().last_event_was_up_or_down()
return lastKey in ["Down", "Up"]
return False return False
@@ -4785,12 +4764,6 @@ class Utilities(script_utilities.Utilities):
container = obj container = obj
contextObj, contextOffset = None, -1 contextObj, contextOffset = None, -1
while obj: while obj:
if not AXObject.supports_text(obj):
tokens = ["WEB: Exception getting caret offset of", obj]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
obj = None
continue
offset = AXText.get_caret_offset(obj) offset = AXText.get_caret_offset(obj)
if offset < 0: if offset < 0:
tokens = ["WEB: Exception getting caret offset of", obj] tokens = ["WEB: Exception getting caret offset of", obj]
@@ -4799,7 +4772,7 @@ class Utilities(script_utilities.Utilities):
continue continue
contextObj, contextOffset = obj, offset contextObj, contextOffset = obj, offset
child = self.findChildAtOffset(obj, offset) child = AXHypertext.find_child_at_offset(obj, offset)
if child: if child:
obj = child obj = child
else: else:
@@ -4996,9 +4969,9 @@ class Utilities(script_utilities.Utilities):
obj, offset = None, -1 obj, offset = None, -1
notify = True notify = True
keyString, mods = self.lastKeyAndModifiers() lastWasUp = input_event_manager.get_manager().last_event_was_up()
childCount = AXObject.get_child_count(event.source) childCount = AXObject.get_child_count(event.source)
if keyString == "Up": if lastWasUp:
if event.detail1 >= childCount: if event.detail1 >= childCount:
msg = "WEB: Last child removed. Getting new location from end of parent." msg = "WEB: Last child removed. Getting new location from end of parent."
debug.printMessage(debug.LEVEL_INFO, msg, True) debug.printMessage(debug.LEVEL_INFO, msg, True)
@@ -5178,7 +5151,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.findChildAtOffset(obj, offset) child = AXHypertext.find_child_at_offset(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)
@@ -5189,7 +5162,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.findChildAtOffset(obj, offset) child = AXHypertext.find_child_at_offset(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, ":",
@@ -5234,7 +5207,7 @@ class Utilities(script_utilities.Utilities):
if text: if text:
allText = AXText.get_all_text(obj) allText = AXText.get_all_text(obj)
for i in range(offset + 1, len(allText)): for i in range(offset + 1, len(allText)):
child = self.findChildAtOffset(obj, i) child = AXHypertext.find_child_at_offset(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"), "'"]
@@ -5310,7 +5283,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.findChildAtOffset(obj, i) child = AXHypertext.find_child_at_offset(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"), "'"]
+13 -12
View File
@@ -37,6 +37,7 @@ from gi.repository import Atspi
import urllib import urllib
from cthulhu import debug from cthulhu import debug
from cthulhu import input_event_manager
from cthulhu import messages from cthulhu import messages
from cthulhu import object_properties from cthulhu import object_properties
from cthulhu import cthulhu_state from cthulhu import cthulhu_state
@@ -302,12 +303,13 @@ class SpeechGenerator(speech_generator.SpeechGenerator):
if args.get('leaving'): if args.get('leaving'):
return [] return []
lastKey, mods = self._script.utilities.lastKeyAndModifiers() manager = input_event_manager.get_manager()
if (lastKey in ['Down', 'Right'] or self._script.inSayAll()) and args.get('startOffset'): if (manager.last_event_was_forward_caret_navigation() or self._script.inSayAll()) \
and args.get('startOffset'):
return [] return []
if lastKey in ['Up', 'Left']: if manager.last_event_was_backward_caret_navigation():
text = self._script.utilities.queryNonEmptyText(obj) if self._script.utilities.treatAsTextObject(obj) \
if text and args.get('endOffset') not in [None, AXText.get_character_count(obj)]: and args.get('endOffset') not in [None, AXText.get_character_count(obj)]:
return [] return []
result = [] result = []
@@ -358,8 +360,7 @@ class SpeechGenerator(speech_generator.SpeechGenerator):
if self._script.utilities.isContentEditableWithEmbeddedObjects(obj) \ if self._script.utilities.isContentEditableWithEmbeddedObjects(obj) \
or self._script.utilities.isDocument(obj): or self._script.utilities.isDocument(obj):
lastKey, mods = self._script.utilities.lastKeyAndModifiers() if input_event_manager.get_manager().last_event_was_caret_navigation():
if lastKey in ["Home", "End", "Up", "Down", "Left", "Right", "Page_Up", "Page_Down"]:
return [] return []
if AXUtilities.is_page_tab(priorObj) and AXObject.get_name(priorObj) == objName: if AXUtilities.is_page_tab(priorObj) and AXObject.get_name(priorObj) == objName:
@@ -594,13 +595,14 @@ class SpeechGenerator(speech_generator.SpeechGenerator):
if self._script.utilities.isMenuInCollapsedSelectElement(obj): if self._script.utilities.isMenuInCollapsedSelectElement(obj):
doNotSpeak.append(Atspi.Role.MENU) doNotSpeak.append(Atspi.Role.MENU)
lastKey, mods = self._script.utilities.lastKeyAndModifiers()
isEditable = AXUtilities.is_editable(obj) isEditable = AXUtilities.is_editable(obj)
if isEditable and not self._script.utilities.isContentEditableWithEmbeddedObjects(obj): if isEditable and not self._script.utilities.isContentEditableWithEmbeddedObjects(obj):
if ((lastKey in ["Down", "Right"] and not mods) or self._script.inSayAll()) and start: manager = input_event_manager.get_manager()
if (manager.last_event_was_forward_caret_navigation() or self._script.inSayAll()) \
and start:
return [] return []
if lastKey in ["Up", "Left"] and not mods: if manager.last_event_was_backward_caret_navigation():
text = self._script.utilities.queryNonEmptyText(obj) text = self._script.utilities.queryNonEmptyText(obj)
if text and end not in [None, AXText.get_character_count(obj)]: if text and end not in [None, AXText.get_character_count(obj)]:
return [] return []
@@ -611,8 +613,7 @@ class SpeechGenerator(speech_generator.SpeechGenerator):
elif isEditable and self._script.utilities.isDocument(obj): elif isEditable and self._script.utilities.isDocument(obj):
parent = AXObject.get_parent(obj) parent = AXObject.get_parent(obj)
if parent and not AXUtilities.is_editable(parent) \ if parent and not AXUtilities.is_editable(parent) \
and lastKey not in \ and not input_event_manager.get_manager().last_event_was_caret_navigation():
["Home", "End", "Up", "Down", "Left", "Right", "Page_Up", "Page_Down"]:
result.append(object_properties.ROLE_EDITABLE_CONTENT) result.append(object_properties.ROLE_EDITABLE_CONTENT)
result.extend(self.voice(speech_generator.SYSTEM, obj=obj, **args)) result.extend(self.voice(speech_generator.SYSTEM, obj=obj, **args))
+25 -45
View File
@@ -52,6 +52,7 @@ from . import settings_manager
from . import speech from . import speech
from . import text_attribute_names from . import text_attribute_names
from .ax_object import AXObject from .ax_object import AXObject
from .ax_table import AXTable
from .ax_text import AXText from .ax_text import AXText
from .ax_utilities import AXUtilities from .ax_utilities import AXUtilities
from .ax_utilities_relation import AXUtilitiesRelation from .ax_utilities_relation import AXUtilitiesRelation
@@ -931,11 +932,7 @@ class SpeechGenerator(generator.Generator):
it exists. Otherwise, an empty array is returned. it exists. Otherwise, an empty array is returned.
""" """
result = [] result = []
try: if AXObject.supports_image(obj):
obj.queryImage()
except Exception:
pass
else:
args['role'] = Atspi.Role.IMAGE args['role'] = Atspi.Role.IMAGE
result.extend(self.generate(obj, **args)) result.extend(self.generate(obj, **args))
result.extend(self.voice(DEFAULT, obj=obj, **args)) result.extend(self.voice(DEFAULT, obj=obj, **args))
@@ -1143,15 +1140,9 @@ class SpeechGenerator(generator.Generator):
parent = AXObject.get_parent(obj) parent = AXObject.get_parent(obj)
if AXUtilities.is_table_cell(parent): if AXUtilities.is_table_cell(parent):
obj = parent obj = parent
parent = self._script.utilities.getTable(obj) row, col = self._script.utilities.coordinatesForCell(obj, False)
try: if col < 0 and args.get('guessCoordinates', False):
table = parent.queryTable() col = self._script.pointOfReference.get('lastColumn', -1)
except Exception:
if args.get('guessCoordinates', False):
col = self._script.pointOfReference.get('lastColumn', -1)
else:
index = self._script.utilities.cellIndex(obj)
col = table.getColumnAtIndex(index)
if col >= 0: if col >= 0:
result.append(messages.TABLE_COLUMN % (col + 1)) result.append(messages.TABLE_COLUMN % (col + 1))
if result: if result:
@@ -1182,15 +1173,9 @@ class SpeechGenerator(generator.Generator):
parent = AXObject.get_parent(obj) parent = AXObject.get_parent(obj)
if AXUtilities.is_table_cell(parent): if AXUtilities.is_table_cell(parent):
obj = parent obj = parent
parent = self._script.utilities.getTable(obj) row, col = self._script.utilities.coordinatesForCell(obj, False)
try: if row < 0 and args.get('guessCoordinates', False):
table = parent.queryTable() row = self._script.pointOfReference.get('lastRow', -1)
except Exception:
if args.get('guessCoordinates', False):
row = self._script.pointOfReference.get('lastRow', -1)
else:
index = self._script.utilities.cellIndex(obj)
row = table.getRowAtIndex(index)
if row >= 0: if row >= 0:
result.append(messages.TABLE_ROW % (row + 1)) result.append(messages.TABLE_ROW % (row + 1))
if result: if result:
@@ -1210,21 +1195,17 @@ class SpeechGenerator(generator.Generator):
parent = AXObject.get_parent(obj) parent = AXObject.get_parent(obj)
if AXUtilities.is_table_cell(parent): if AXUtilities.is_table_cell(parent):
obj = parent obj = parent
parent = self._script.utilities.getTable(obj) table = self._script.utilities.getTable(obj)
try: if table:
table = parent.queryTable() row, col = self._script.utilities.coordinatesForCell(obj, False)
except Exception: rows, cols = self._script.utilities.rowAndColumnCount(table, False)
table = None if row >= 0 and col >= 0 and rows > 0 and cols > 0:
else: result.append(messages.TABLE_COLUMN_DETAILED \
index = self._script.utilities.cellIndex(obj) % {"index" : (col + 1),
col = table.getColumnAtIndex(index) "total" : cols})
row = table.getRowAtIndex(index) result.append(messages.TABLE_ROW_DETAILED \
result.append(messages.TABLE_COLUMN_DETAILED \ % {"index" : (row + 1),
% {"index" : (col + 1), "total" : rows})
"total" : table.nColumns})
result.append(messages.TABLE_ROW_DETAILED \
% {"index" : (row + 1),
"total" : table.nRows})
if result: if result:
result.extend(self.voice(SYSTEM, obj=obj, **args)) result.extend(self.voice(SYSTEM, obj=obj, **args))
return result return result
@@ -2817,16 +2798,15 @@ class SpeechGenerator(generator.Generator):
return result return result
def _generateMathTableStart(self, obj, **args): def _generateMathTableStart(self, obj, **args):
try:
table = obj.queryTable()
except Exception:
return []
nestingLevel = self._script.utilities.getMathNestingLevel(obj) nestingLevel = self._script.utilities.getMathNestingLevel(obj)
rows = AXTable.get_row_count(obj, prefer_attribute=False)
cols = AXTable.get_column_count(obj, prefer_attribute=False)
if rows < 0 or cols < 0:
return []
if nestingLevel > 0: if nestingLevel > 0:
result = [messages.mathNestedTableSize(table.nRows, table.nColumns)] result = [messages.mathNestedTableSize(rows, cols)]
else: else:
result = [messages.mathTableSize(table.nRows, table.nColumns)] result = [messages.mathTableSize(rows, cols)]
result.extend(self.voice(SYSTEM, obj=obj, **args)) result.extend(self.voice(SYSTEM, obj=obj, **args))
return result return result
+9 -13
View File
@@ -51,6 +51,7 @@ from . import settings_manager
from .ax_collection import AXCollection from .ax_collection import AXCollection
from .ax_event_synthesizer import AXEventSynthesizer from .ax_event_synthesizer import AXEventSynthesizer
from .ax_object import AXObject from .ax_object import AXObject
from .ax_table import AXTable
from .ax_text import AXText from .ax_text import AXText
from .ax_selection import AXSelection from .ax_selection import AXSelection
from .ax_utilities import AXUtilities from .ax_utilities import AXUtilities
@@ -975,11 +976,11 @@ class StructuralNavigation:
- obj: the accessible table whose caption we want. - obj: the accessible table whose caption we want.
""" """
caption = obj.queryTable().caption caption = AXTable.get_caption(obj)
if not AXObject.supports_text(caption): if not caption:
return None return None
return self._script.utilities.displayedText(caption) return AXText.get_all_text(caption)
def _getTableDescription(self, obj): def _getTableDescription(self, obj):
"""Returns a string which describes the table.""" """Returns a string which describes the table."""
@@ -1246,12 +1247,7 @@ class StructuralNavigation:
if item: if item:
text = AXObject.get_name(item) text = AXObject.get_name(item)
if not text and AXUtilities.is_image(obj): if not text and AXUtilities.is_image(obj):
try: text = AXObject.get_image_description(obj) or AXObject.get_description(obj)
image = obj.queryImage()
except Exception:
text = AXObject.get_description(obj)
else:
text = image.imageDescription or AXObject.get_description(obj)
if not text: if not text:
parent = AXObject.get_parent(obj) parent = AXObject.get_parent(obj)
if AXUtilities.is_link(parent): if AXUtilities.is_link(parent):
@@ -2115,11 +2111,11 @@ class StructuralNavigation:
if attrs.get('layout-guess') == 'true': if attrs.get('layout-guess') == 'true':
return False return False
try: if not AXObject.supports_table(obj):
return obj.queryTable().nRows > 0
except Exception:
return False return False
return AXTable.get_row_count(obj, prefer_attribute=False) > 0
return AXUtilities.find_all_tables(document, is_not_layout_or_empty) return AXUtilities.find_all_tables(document, is_not_layout_or_empty)
def _tablePresentation(self, obj, arg=None): def _tablePresentation(self, obj, arg=None):
@@ -2128,7 +2124,7 @@ class StructuralNavigation:
if caption: if caption:
self._script.presentMessage(caption) self._script.presentMessage(caption)
self._script.presentMessage(self._getTableDescription(obj)) self._script.presentMessage(self._getTableDescription(obj))
cell = obj.queryTable().getAccessibleAt(0, 0) cell = AXTable.get_cell_at(obj, 0, 0)
if not cell: if not cell:
tokens = ["STRUCTURAL NAVIGATION: Broken table interface for", obj] tokens = ["STRUCTURAL NAVIGATION: Broken table interface for", obj]
debug.printTokens(debug.LEVEL_INFO, tokens, True) debug.printTokens(debug.LEVEL_INFO, tokens, True)
+2 -4
View File
@@ -44,6 +44,7 @@ from . import cthulhu_state
from . import settings from . import settings
from .ax_object import AXObject from .ax_object import AXObject
from .ax_table import AXTable
from .ax_utilities import AXUtilities from .ax_utilities import AXUtilities
from .cthulhu_i18n import _ # for gettext support from .cthulhu_i18n import _ # for gettext support
@@ -613,10 +614,7 @@ class TutorialGenerator:
if (not alreadyFocused): if (not alreadyFocused):
parent = AXObject.get_parent(obj) parent = AXObject.get_parent(obj)
try: parent_table = AXTable.get_table(parent)
parent_table = parent.queryTable()
except Exception:
parent_table = None
readFullRow = self._script.utilities.shouldReadFullRow(obj) readFullRow = self._script.utilities.shouldReadFullRow(obj)
if readFullRow and parent_table and not self._script.utilities.isLayoutOnly(parent): if readFullRow and parent_table and not self._script.utilities.isLayoutOnly(parent):
utterances.extend(self._getTutorialForTableCell(obj, utterances.extend(self._getTutorialForTableCell(obj,