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 GNOME Foundation Inc.
# Author: Joanmarie Diggs <jdiggs@igalia.com>
@@ -18,6 +19,9 @@
# License along with this library; if not, write to the
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
# Boston MA 02110-1301 USA.
#
# Forked from Orca screen reader.
# Cthulhu project: https://git.stormux.org/storm/cthulhu
# 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 GNOME Foundation Inc.
# Author: Joanmarie Diggs <jdiggs@igalia.com>
@@ -18,6 +19,9 @@
# License along with this library; if not, write to the
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
# Boston MA 02110-1301 USA.
#
# Forked from Orca screen reader.
# Cthulhu project: https://git.stormux.org/storm/cthulhu
# 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 2018-2023 Igalia, S.L.
#
@@ -17,6 +18,9 @@
# License along with this library; if not, write to the
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
# 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=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.
# Author: Joanmarie Diggs <jdiggs@igalia.com>
#
@@ -17,6 +18,9 @@
# License along with this library; if not, write to the
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
# Boston MA 02110-1301 USA.
#
# Forked from Orca screen reader.
# Cthulhu project: https://git.stormux.org/storm/cthulhu
# 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 GNOME Foundation Inc.
# Author: Joanmarie Diggs <jdiggs@igalia.com>
@@ -18,6 +19,9 @@
# License along with this library; if not, write to the
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
# 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=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 GNOME Foundation Inc.
# Author: Joanmarie Diggs <jdiggs@igalia.com>
@@ -18,6 +19,9 @@
# License along with this library; if not, write to the
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
# 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=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 GNOME Foundation Inc.
# Author: Joanmarie Diggs <jdiggs@igalia.com>
@@ -18,6 +19,9 @@
# License along with this library; if not, write to the
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
# 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=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 GNOME Foundation Inc.
# Author: Joanmarie Diggs <jdiggs@igalia.com>
@@ -18,6 +19,9 @@
# License along with this library; if not, write to the
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
# Boston MA 02110-1301 USA.
#
# Forked from Orca screen reader.
# Cthulhu project: https://git.stormux.org/storm/cthulhu
# pylint: disable=wrong-import-position
+21 -20
View File
@@ -42,6 +42,9 @@ import signal
import os
import re
import gi
gi.require_version("Atspi", "2.0")
from gi.repository import Atspi
from gi.repository import GLib
from . import brltablenames
@@ -54,6 +57,7 @@ from . import settings_manager
from .ax_event_synthesizer import AXEventSynthesizer
from .ax_object import AXObject
from .ax_hypertext import AXHypertext
from .cthulhu_platform import tablesdir
_logger = logger.getLogger()
@@ -537,10 +541,11 @@ class Component(Region):
if cthulhu_state.activeScript and cthulhu_state.activeScript.utilities.\
grabFocusBeforeRouting(self.accessible, offset):
try:
self.accessible.queryComponent().grabFocus()
except Exception:
pass
if AXObject.supports_component(self.accessible):
try:
Atspi.Component.grab_focus(self.accessible)
except Exception:
pass
if AXObject.do_action(self.accessible, 0):
return
@@ -747,22 +752,18 @@ class Text(Region):
return ""
if getLinkMask and linkIndicator != settings.BRAILLE_UNDERLINE_NONE:
try:
hyperText = self.accessible.queryHypertext()
nLinks = hyperText.getNLinks()
except Exception:
nLinks = 0
n = 0
while n < nLinks:
link = hyperText.getLink(n)
if self.lineOffset <= link.startIndex:
for i in range(link.startIndex, link.endIndex):
try:
regionMask[i] |= linkIndicator
except Exception:
pass
n += 1
if AXObject.supports_hypertext(self.accessible):
for link in AXHypertext.get_all_links(self.accessible):
start = AXHypertext.get_link_start_offset(link)
end = AXHypertext.get_link_end_offset(link)
if start < 0 or end < 0:
continue
if self.lineOffset <= start:
for i in range(start, end):
try:
regionMask[i] |= linkIndicator
except Exception:
pass
if attrIndicator:
keys, enabledAttributes = script.utilities.stringToKeysAndDict(
+25 -4
View File
@@ -500,7 +500,7 @@ class Context:
self.container = None
self.focusObj = cthulhu.getActiveModeAndObjectOfInterest()[1] or cthulhu_state.locusOfFocus
self.topLevel = None
self.bounds = 0, 0, 0, 0
self.bounds = Atspi.Rect()
frame, dialog = script.utilities.frameAndDialog(self.focusObj)
if root is not None:
@@ -513,9 +513,7 @@ class Context:
debug.printTokens(debug.LEVEL_INFO, tokens, True)
try:
if AXObject.supports_component(self.topLevel):
rect = Atspi.Component.get_extents(self.topLevel, Atspi.CoordType.SCREEN)
self.bounds = rect.x, rect.y, rect.width, rect.height
self.bounds = AXComponent.get_rect(self.topLevel)
except Exception:
tokens = ["ERROR: Exception getting extents of", self.topLevel]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
@@ -567,6 +565,7 @@ class Context:
Returns a list of Zones for the visible text.
"""
cliprect = self._ensureRect(cliprect)
zones = []
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))
@@ -610,6 +609,7 @@ class Context:
Returns a list of Zones.
"""
cliprect = self._ensureRect(cliprect)
if not self.script.utilities.hasPresentableText(accessible):
return []
@@ -716,6 +716,7 @@ class Context:
def getZonesFromAccessible(self, accessible, cliprect):
"""Returns a list of Zones for the given accessible."""
cliprect = self._ensureRect(cliprect)
try:
if AXObject.supports_component(accessible):
rect = Atspi.Component.get_extents(accessible, Atspi.CoordType.SCREEN)
@@ -760,6 +761,25 @@ class Context:
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):
"""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:
boundingbox = self.bounds
boundingbox = self._ensureRect(boundingbox)
objs = self.script.utilities.getOnScreenObjects(root, boundingbox)
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 2016-2023 Igalia, S.L.
#
@@ -19,6 +20,9 @@
# License along with this library; if not, write to the
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
# 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=too-many-instance-attributes
+8 -9
View File
@@ -51,6 +51,7 @@ from . import object_properties
from . import settings
from . import settings_manager
from .ax_object import AXObject
from .ax_text import AXText
from .ax_utilities import AXUtilities
from .ax_utilities_relation import AXUtilitiesRelation
@@ -526,14 +527,9 @@ class Generator:
exists. Otherwise, an empty array is returned.
"""
result = []
try:
image = obj.queryImage()
except NotImplementedError:
pass
else:
description = image.imageDescription
if description and len(description):
result.append(description)
description = AXObject.get_image_description(obj)
if description and len(description):
result.append(description)
return result
#####################################################################
@@ -1194,7 +1190,10 @@ class Generator:
if not (AXUtilities.is_table_cell(rad) and AXObject.get_child_count(rad)):
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))
if not rv:
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 GNOME Foundation Inc.
# Author: Joanmarie Diggs <jdiggs@igalia.com>
@@ -18,6 +19,9 @@
# License along with this library; if not, write to the
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
# 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=too-many-public-methods
+18 -16
View File
@@ -37,6 +37,7 @@ from gi.repository import Atspi
from . import debug
from .ax_object import AXObject
from .ax_table import AXTable
from .ax_text import AXText
from .ax_utilities import AXUtilities
@@ -236,16 +237,16 @@ class LabelInference:
return extents
if not (extents[2] and extents[3]):
try:
ext = obj.queryComponent().getExtents(0)
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]
if not AXObject.supports_component(obj):
tokens = ["LABEL INFERENCE:", obj, "does not support the component interface"]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
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
return extents
@@ -484,11 +485,12 @@ class LabelInference:
if rowindex < 0 or colindex < 0:
return None
iface = table.queryTable()
if rowindex >= iface.nRows or colindex >= iface.nColumns:
rows = AXTable.get_row_count(table, prefer_attribute=False)
cols = AXTable.get_column_count(table, prefer_attribute=False)
if rowindex >= rows or colindex >= cols:
return None
return table.queryTable().getAccessibleAt(rowindex, colindex)
return AXTable.get_cell_at(table, rowindex, colindex)
def _getCellFromRow(self, row, colindex):
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
# of widgets with the functional labels in the first row.
try:
table = grid.queryTable()
except NotImplementedError:
rows = AXTable.get_row_count(grid, prefer_attribute=False)
cols = AXTable.get_column_count(grid, prefer_attribute=False)
if rows <= 0 or cols <= 0:
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)):
return None, []
@@ -604,7 +606,7 @@ class LabelInference:
return False
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)):
return None, []
+9 -3
View File
@@ -546,8 +546,11 @@ class MouseReviewer:
if coordType is None:
coordType = Atspi.CoordType.SCREEN
if not AXObject.supports_component(obj):
return False
try:
return obj.queryComponent().contains(x, y, coordType)
return Atspi.Component.contains(obj, x, y, coordType)
except Exception:
return False
@@ -557,12 +560,15 @@ class MouseReviewer:
if coordType is None:
coordType = Atspi.CoordType.SCREEN
if not AXObject.supports_component(obj):
return False
try:
extents = obj.queryComponent().getExtents(coordType)
extents = Atspi.Component.get_extents(obj, coordType)
except Exception:
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):
"""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 ax_object
from cthulhu.ax_text import AXText
from cthulhu.ax_value import AXValue
from cthulhu import ax_utilities
from cthulhu.ax_utilities_state import AXUtilitiesState
from cthulhu.plugins.AIAssistant.ai_providers import create_provider
@@ -758,12 +759,8 @@ class AIAssistant(Plugin):
"""Get value from an accessibility object."""
try:
if ax_object.AXObject.supports_value(obj):
try:
value_iface = obj.queryValue()
if value_iface:
return str(value_iface.currentValue) or ""
except:
pass
value = AXValue.get_current_value(obj)
return str(value) if value is not None else ""
return ""
except Exception as e:
@@ -818,18 +815,16 @@ class AIAssistant(Plugin):
def _get_object_position(self, obj):
"""Get position and size information from an accessibility object."""
try:
if hasattr(obj, 'queryComponent'):
component = obj.queryComponent()
if component:
extents = component.getExtents(Atspi.CoordType.SCREEN)
return {
'x': extents.x,
'y': extents.y,
'width': extents.width,
'height': extents.height
}
if ax_object.AXObject.supports_component(obj):
extents = Atspi.Component.get_extents(obj, Atspi.CoordType.SCREEN)
return {
'x': extents.x,
'y': extents.y,
'width': extents.width,
'height': extents.height
}
return None
except Exception as e:
logger.error(f"Error getting object position: {e}")
return None
@@ -1166,21 +1161,25 @@ class AIAssistant(Plugin):
actions = []
# Check for AT-SPI action interface
try:
if hasattr(obj, 'queryAction'):
action_iface = obj.queryAction()
if action_iface:
action_count = action_iface.get_nActions()
for i in range(action_count):
action_name = action_iface.getName(i)
action_desc = action_iface.getDescription(i)
actions.append({
'name': action_name or '',
'description': action_desc or '',
'index': i
})
except:
pass
if ax_object.AXObject.supports_action(obj):
try:
action_count = Atspi.Action.get_n_actions(obj)
except Exception:
action_count = 0
for i in range(action_count):
try:
action_name = Atspi.Action.get_name(obj, i)
except Exception:
action_name = ""
try:
action_desc = Atspi.Action.get_description(obj, i)
except Exception:
action_desc = ""
actions.append({
'name': action_name or '',
'description': action_desc or '',
'index': i
})
return actions
+22 -15
View File
@@ -114,8 +114,10 @@ class SpeechHistory(Plugin):
self._create_window()
self._window.show_all()
if self._filterEntry:
if self._treeView and len(self._filterModel) > 0:
self._treeView.grab_focus()
self._treeView.set_cursor(Gtk.TreePath.new_first())
elif self._filterEntry:
self._filterEntry.grab_focus()
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)
self._filterText = ""
# Filter row
filterRow = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
filterLabel = Gtk.Label(label="_Filter:")
@@ -153,7 +157,7 @@ class SpeechHistory(Plugin):
mainBox.pack_start(filterRow, False, False, 0)
# List
self._listStore = Gtk.ListStore(int, str)
self._listStore = Gtk.ListStore(str)
self._filterModel = self._listStore.filter_new()
self._filterModel.set_visible_func(self._filter_visible_func)
@@ -163,16 +167,10 @@ class SpeechHistory(Plugin):
selection = self._treeView.get_selection()
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.set_property("wrap-width", 640)
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_expand(True)
self._treeView.append_column(textColumn)
@@ -221,7 +219,7 @@ class SpeechHistory(Plugin):
if not filterText:
return True
spokenText = model[treeIter][1] or ""
spokenText = model[treeIter][0] or ""
return spokenText.lower().startswith(filterText)
except Exception as e:
debug.printMessage(debug.LEVEL_INFO, f"SpeechHistory: ERROR in filter func: {e}", True)
@@ -236,11 +234,21 @@ class SpeechHistory(Plugin):
self._listStore.clear()
items = speech_history.get_items()
for idx, item in enumerate(items, start=1):
self._listStore.append([idx, item])
debug.printMessage(
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:
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:
selection = self._treeView.get_selection()
@@ -259,7 +267,7 @@ class SpeechHistory(Plugin):
if not treeIter:
return None
return model[treeIter][1]
return model[treeIter][0]
except Exception as e:
debug.printMessage(debug.LEVEL_INFO, f"SpeechHistory: ERROR getting selection: {e}", True)
logger.exception("Error getting selected speech history item")
@@ -403,4 +411,3 @@ class SpeechHistory(Plugin):
except Exception as e:
debug.printMessage(debug.LEVEL_INFO, f"SpeechHistory: ERROR presenting message: {e}", True)
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.cthulhu_state as cthulhu_state
from cthulhu.ax_value import AXValue
from .script_utilities import Utilities
@@ -47,8 +48,7 @@ class Script(default.Script):
def onValueChanged(self, event):
obj = event.source
if self.utilities.isSeekSlider(obj):
value = obj.queryValue()
current_value = int(value.currentValue)/1000
current_value = int(AXValue.get_current_value(obj)) / 1000
if current_value in range(self._last_seek_value, self._last_seek_value + 4):
if self.utilities.isSameObject(obj, cthulhu_state.locusOfFocus):
self.updateBraille(obj)
@@ -25,6 +25,7 @@
import cthulhu.script_utilities as script_utilities
from cthulhu.ax_object import AXObject
from cthulhu.ax_value import AXValue
from cthulhu.ax_utilities import AXUtilities
@@ -56,9 +57,7 @@ class Utilities(script_utilities.Utilities):
if not self.isSeekSlider(obj):
return script_utilities.Utilities.textForValue(self, obj)
try:
value = obj.queryValue()
except NotImplementedError:
if not AXObject.supports_value(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_manager as settings_manager
from cthulhu.ax_object import AXObject
from cthulhu.ax_value import AXValue
_settingsManager = settings_manager.getManager()
@@ -47,10 +48,9 @@ _settingsManager = settings_manager.getManager()
class Script(default.Script):
def onValueChanged(self, event):
try:
ivalue = event.source.queryValue()
value = int(ivalue.currentValue)
except NotImplementedError:
if AXObject.supports_value(event.source):
value = int(AXValue.get_current_value(event.source))
else:
value = -1
if value >= 0:
@@ -63,10 +63,9 @@ class Script(default.Script):
def onNameChanged(self, event):
"""Callback for object:property-change:accessible-name events."""
try:
ivalue = event.source.queryValue()
value = ivalue.currentValue
except NotImplementedError:
if AXObject.supports_value(event.source):
value = AXValue.get_current_value(event.source)
else:
value = -1
message = ""
@@ -41,6 +41,7 @@ from gi.repository import Atspi
import cthulhu.debug as debug
import cthulhu.script_utilities as script_utilities
from cthulhu.ax_object import AXObject
from cthulhu.ax_table import AXTable
from cthulhu.ax_utilities import AXUtilities
from cthulhu.ax_utilities_relation import AXUtilitiesRelation
@@ -83,18 +84,18 @@ class Utilities(script_utilities.Utilities):
return script_utilities.Utilities.childNodes(self, obj)
parent = AXObject.get_parent(obj)
try:
table = parent.queryTable()
except Exception:
table = AXTable.get_table(parent)
if table is None:
return []
if not AXUtilities.is_expanded(obj):
return []
else:
if not AXUtilities.is_expanded(obj):
return []
nodes = []
index = self.cellIndex(obj)
row = table.getRowAtIndex(index)
col = table.getColumnAtIndex(index + 1)
row, col = AXTable.get_cell_coordinates(obj, prefer_attribute=False)
if row < 0 or col < 0:
return []
col += 1
nodeLevel = self.nodeLevel(obj)
# 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
# than our current level.
#
for i in range(row+1, table.nRows):
cell = table.getAccessibleAt(i, col)
for i in range(row + 1, AXTable.get_row_count(table, prefer_attribute=False)):
cell = AXTable.get_cell_at(table, i, col)
if not cell:
continue
nodeCell = AXObject.get_previous_sibling(cell)
nodeOf = AXUtilitiesRelation.get_is_node_child_of(nodeCell)
if not nodeOf:
@@ -135,9 +138,7 @@ class Utilities(script_utilities.Utilities):
obj = AXObject.get_previous_sibling(obj)
parent = AXObject.get_parent(obj)
try:
parent.queryTable()
except Exception:
if AXTable.get_table(parent) is None:
return -1
nodes = []
+4 -1
View File
@@ -856,7 +856,10 @@ class Script(default.Script):
if isinstance(cthulhu_state.lastInputEvent, input_event.MouseButtonEvent):
x = cthulhu_state.lastInputEvent.x
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):
weToggledIt = True
else:
@@ -44,6 +44,7 @@ import cthulhu.messages as messages
import cthulhu.cthulhu_state as cthulhu_state
import cthulhu.script_utilities as script_utilities
from cthulhu.ax_object import AXObject
from cthulhu.ax_table import AXTable
from cthulhu.ax_selection import AXSelection
from cthulhu.ax_text import AXText
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):
table = AXObject.get_parent(table)
try:
iTable = table.queryTable()
except Exception:
table = AXTable.get_table(table)
if table is None:
return -1, -1, None
index = self.cellIndex(cell)
row = iTable.getRowAtIndex(index)
column = iTable.getColumnAtIndex(index)
row, column = AXTable.get_cell_coordinates(cell, prefer_attribute=False)
return row, column, table
def rowHeadersForCell(self, obj):
@@ -195,13 +192,12 @@ class Utilities(script_utilities.Utilities):
getColHeader = \
getColHeader and objCol!= self._script.pointOfReference.get("lastColumn")
parentTable = table.queryTable()
rowHeader, colHeader = None, None
if getColHeader:
colHeader = parentTable.getAccessibleAt(headersRow, objCol)
colHeader = AXTable.get_cell_at(table, headersRow, objCol)
if getRowHeader:
rowHeader = parentTable.getAccessibleAt(objRow, headersCol)
rowHeader = AXTable.get_cell_at(table, objRow, headersCol)
return rowHeader, colHeader
@@ -631,16 +627,13 @@ class Utilities(script_utilities.Utilities):
return res
def _getCellNameForCoordinates(self, obj, row, col, includeContents=False):
try:
table = obj.queryTable()
except Exception:
if not AXObject.supports_table(obj):
tokens = ["SOFFICE: Exception querying Table interface of", obj]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return
try:
cell = table.getAccessibleAt(row, col)
except Exception:
cell = AXTable.get_cell_at(obj, row, col)
if not cell:
tokens = [f"SOFFICE: Exception getting cell ({row},{col}) of", obj]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return
@@ -742,9 +735,9 @@ class Utilities(script_utilities.Utilities):
if not (AXObject.supports_table(obj) and AXObject.supports_selection(obj)):
return True
table = obj.queryTable()
cols = set(table.getSelectedColumns())
rows = set(table.getSelectedRows())
cols = set(AXTable.get_selected_columns(obj))
rows = set(AXTable.get_selected_rows(obj))
total_cols = AXTable.get_column_count(obj, prefer_attribute=False)
selectedCols = sorted(cols.difference(set(self._calcSelectedColumns)))
unselectedCols = sorted(set(self._calcSelectedColumns).difference(cols))
@@ -767,11 +760,11 @@ class Utilities(script_utilities.Utilities):
self._calcSelectedColumns = list(cols)
self._calcSelectedRows = list(rows)
if len(cols) == table.nColumns:
if len(cols) == total_cols:
self._script.speakMessage(messages.DOCUMENT_SELECTED_ALL)
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)
return True
@@ -345,10 +345,12 @@ class SpeechGenerator(speech_generator.SpeechGenerator):
result = []
if AXObject.supports_text(obj):
objectText = self._script.utilities.substring(obj, 0, -1)
try:
extents = obj.queryComponent().getExtents(Atspi.CoordType.SCREEN)
except Exception:
extents = None
extents = None
if AXObject.supports_component(obj):
try:
extents = Atspi.Component.get_extents(obj, Atspi.CoordType.SCREEN)
except Exception:
extents = None
if extents is not None:
tooLongCount = 0
+38 -8
View File
@@ -37,7 +37,9 @@ __license__ = "LGPL"
import gi
gi.require_version('Atspi', '2.0')
gi.require_version('Gdk', '3.0')
from gi.repository import Atspi
from gi.repository import Gdk
import re
import time
@@ -49,6 +51,7 @@ import cthulhu.debug as debug
import cthulhu.find as find
import cthulhu.flat_review as flat_review
import cthulhu.input_event as input_event
import cthulhu.input_event_manager as input_event_manager
import cthulhu.keybindings as keybindings
import cthulhu.messages as messages
import cthulhu.cthulhu as cthulhu
@@ -62,6 +65,7 @@ import cthulhu.sound as sound
import cthulhu.speech as speech
import cthulhu.speechserver as speechserver
from cthulhu.ax_object import AXObject
from cthulhu.ax_value import AXValue
from cthulhu.ax_text import AXText
from cthulhu.ax_utilities import AXUtilities
from cthulhu.ax_utilities_relation import AXUtilitiesRelation
@@ -126,6 +130,7 @@ class Script(script.Script):
self._sayAllIsInterrupted = False
self._sayAllContexts = []
self.grab_ids = []
self._modifierGrabIds = []
if app:
Atspi.Accessible.set_cache_mask(
@@ -585,6 +590,7 @@ class Script(script.Script):
for b in bound:
for id in cthulhu.addKeyGrab(b):
self.grab_ids.append(id)
self._addModifierGrabs()
def removeKeyGrabs(self):
""" Removes this script's AT-SPI key grabs. """
@@ -593,6 +599,35 @@ class Script(script.Script):
for id in self.grab_ids:
cthulhu.removeKeyGrab(id)
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):
""" Refreshes the enabled key grabs for this script. """
@@ -1837,17 +1872,12 @@ class Script(script.Script):
obj = event.source
role = AXObject.get_role(obj)
try:
value = obj.queryValue()
currentValue = value.currentValue
except NotImplementedError:
if not AXObject.supports_value(obj):
tokens = ["DEFAULT:", obj, "doesn't implement AtspiValue"]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return
except Exception:
tokens = ["DEFAULT: Exception getting current value for", obj]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return
currentValue = AXValue.get_current_value(obj)
if "oldValue" in self.pointOfReference \
and (currentValue == self.pointOfReference["oldValue"]):
@@ -34,6 +34,10 @@ __date__ = "$Date$"
__copyright__ = "Copyright (c) 2010 Joanmarie Diggs."
__license__ = "LGPL"
import gi
gi.require_version("Atspi", "2.0")
from gi.repository import Atspi
import cthulhu.script_utilities as script_utilities
from cthulhu.ax_object import AXObject
from cthulhu.ax_utilities import AXUtilities
@@ -81,15 +85,15 @@ class Utilities(script_utilities.Utilities):
# negatives.
#
if AXUtilities.is_label(obj1) and AXUtilities.is_label(obj2):
try:
ext1 = obj1.queryComponent().getExtents(0)
ext2 = obj2.queryComponent().getExtents(0)
except Exception:
pass
else:
if ext1.x == ext2.x and ext1.y == ext2.y \
and ext1.width == ext2.width and ext1.height == ext2.height:
return True
if AXObject.supports_component(obj1) and AXObject.supports_component(obj2):
try:
ext1 = Atspi.Component.get_extents(obj1, Atspi.CoordType.SCREEN)
ext2 = Atspi.Component.get_extents(obj2, Atspi.CoordType.SCREEN)
if ext1.x == ext2.x and ext1.y == ext2.y \
and ext1.width == ext2.width and ext1.height == ext2.height:
return True
except Exception:
pass
# In java applications, TRANSIENT state is missing for tree items
# (fix for bug #352250)
@@ -48,6 +48,7 @@ import cthulhu.cthulhu_state as cthulhu_state
import cthulhu.speech as speech
import cthulhu.structural_navigation as structural_navigation
from cthulhu.ax_object import AXObject
from cthulhu.ax_hypertext import AXHypertext
from cthulhu.ax_text import AXText
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
# not want to set the caret (and hence set focus) in a link we just
# passed by.
try:
hypertext = obj.queryHypertext()
except NotImplementedError:
pass
else:
linkCount = hypertext.getNLinks()
links = [hypertext.getLink(x) for x in range(linkCount)]
if [link for link in links if link.startIndex <= offset <= link.endIndex]:
if AXObject.supports_hypertext(obj):
links = AXHypertext.get_all_links(obj)
if [link for link in links
if AXHypertext.get_link_start_offset(link)
<= offset <= AXHypertext.get_link_end_offset(link)]:
return
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_state as cthulhu_state
from cthulhu.ax_object import AXObject
from cthulhu.ax_hypertext import AXHypertext
from cthulhu.ax_text import AXText
from cthulhu.ax_utilities import AXUtilities
@@ -142,9 +143,7 @@ class Utilities(script_utilities.Utilities):
if not AXObject.supports_text(obj):
return [(obj, 0, 1, '')]
try:
htext = obj.queryHypertext()
except (AttributeError, NotImplementedError):
if not AXObject.supports_hypertext(obj):
return [(obj, 0, 1, '')]
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]
objects = []
try:
objs = [obj[htext.getLinkIndex(offset)] for offset in offsets]
except Exception:
objs = []
objs = []
for offset in offsets:
if child := AXHypertext.get_child_at_offset(obj, offset):
objs.append(child)
ranges = [self.getHyperlinkRange(x) for x in objs]
for i, (first, last) in enumerate(ranges):
objects.append((obj, start, first, string[start:first]))
@@ -212,7 +212,10 @@ class Utilities(script_utilities.Utilities):
if not text:
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(
obj, 0, AXText.get_character_count(obj), coordType)
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"
import time
import gi
gi.require_version("Atspi", "2.0")
from gi.repository import Atspi
from gi.repository import Gtk
from cthulhu import caret_navigation
@@ -40,6 +44,7 @@ from cthulhu import keybindings
from cthulhu import debug
from cthulhu import guilabels
from cthulhu import input_event
from cthulhu import input_event_manager
from cthulhu import liveregions
from cthulhu import messages
from cthulhu import cthulhu
@@ -1002,8 +1007,7 @@ class Script(default.Script):
document = self.utilities.getTopLevelDocumentForObject(obj)
obj, offset = self.utilities.getCaretContext(documentFrame=document)
keyString, mods = self.utilities.lastKeyAndModifiers()
if keyString == "Right":
if input_event_manager.get_manager().last_event_was_right():
offset -= 1
wordContents = self.utilities.getWordContentsAtOffset(obj, offset, useCache=True)
@@ -1317,8 +1321,8 @@ class Script(default.Script):
if not obj:
return
if AXUtilities.is_focusable(obj):
obj.queryComponent().grabFocus()
if AXUtilities.is_focusable(obj) and AXObject.supports_component(obj):
Atspi.Component.grab_focus(obj)
contents = self.utilities.get_objectContentsAtOffset(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"
debug.printMessage(debug.LEVEL_INFO, msg, True)
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)
return True
+57 -84
View File
@@ -41,6 +41,7 @@ import urllib
from cthulhu import debug
from cthulhu import input_event
from cthulhu import input_event_manager
from cthulhu import messages
from cthulhu import cthulhu
from cthulhu import cthulhu_state
@@ -48,11 +49,14 @@ from cthulhu import script_utilities
from cthulhu import script_manager
from cthulhu import settings_manager
from cthulhu.ax_collection import AXCollection
from cthulhu.ax_component import AXComponent
from cthulhu.ax_document import AXDocument
from cthulhu.ax_hypertext import AXHypertext
from cthulhu.ax_object import AXObject
from cthulhu.ax_text import AXText
from cthulhu.ax_utilities import AXUtilities
from cthulhu.ax_utilities_relation import AXUtilitiesRelation
from cthulhu import speech_and_verbosity_manager
_scriptManager = script_manager.get_manager()
_settingsManager = settings_manager.getManager()
@@ -307,6 +311,16 @@ class Utilities(script_utilities.Utilities):
documents = list(filter(AXUtilities.is_showing, documents))
if len(documents) == 1:
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
def documentFrame(self, obj=None):
@@ -356,11 +370,13 @@ class Utilities(script_utilities.Utilities):
return AXUtilities.is_focusable(obj)
def grabFocus(self, obj):
try:
obj.queryComponent().grabFocus()
except NotImplementedError:
tokens = ["WEB:", obj, "does not implement the component interface"]
if not AXObject.supports_component(obj):
tokens = ["WEB:", obj, "does not support the component interface"]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return
try:
Atspi.Component.grab_focus(obj)
except Exception:
tokens = ["WEB: Exception grabbing focus on", obj]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
@@ -611,10 +627,8 @@ class Utilities(script_utilities.Utilities):
nextobj, nextoffset = self.findNextCaretInOrder(obj, offset)
if skipSpace:
text = self.queryNonEmptyText(nextobj)
while text and AXText.get_substring(nextobj, nextoffset, nextoffset + 1) in [" ", "\xa0"]:
while nextobj and AXText.get_character_at_offset(nextobj, nextoffset)[0].isspace():
nextobj, nextoffset = self.findNextCaretInOrder(nextobj, nextoffset)
text = self.queryNonEmptyText(nextobj)
return nextobj, nextoffset
@@ -624,10 +638,8 @@ class Utilities(script_utilities.Utilities):
prevobj, prevoffset = self.findPreviousCaretInOrder(obj, offset)
if skipSpace:
text = self.queryNonEmptyText(prevobj)
while text and AXText.get_substring(prevobj, prevoffset, prevoffset + 1) in [" ", "\xa0"]:
while prevobj and AXText.get_character_at_offset(prevobj, prevoffset)[0].isspace():
prevobj, prevoffset = self.findPreviousCaretInOrder(prevobj, prevoffset)
text = self.queryNonEmptyText(prevobj)
return prevobj, prevoffset
@@ -689,51 +701,25 @@ class Utilities(script_utilities.Utilities):
if not obj:
return [0, 0, 0, 0]
result = [0, 0, 0, 0]
if AXObject.supports_text(obj):
try:
char_count = AXText.get_character_count(obj)
if char_count and 0 <= startOffset < endOffset:
result = list(Atspi.Text.get_range_extents(
obj, startOffset, endOffset, Atspi.CoordType.SCREEN))
except Exception as error:
tokens = ["WEB: Exception getting range extents for", obj, ":", error]
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
if AXObject.supports_text(obj) and 0 <= startOffset < endOffset:
rect = AXText.get_range_rect(obj, startOffset, endOffset)
result = [rect.x, rect.y, rect.width, rect.height]
if not (result[0] and result[1] and result[2] == 0 and result[3] == 0
and AXText.get_substring(obj, startOffset, endOffset).strip()):
return result
tokens = ["WEB: Suspected bogus range extents for",
obj, "(chars:", startOffset, ",", endOffset, "):", result]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
parent = AXObject.get_parent(obj)
if (AXUtilities.is_menu(obj) or AXUtilities.is_list_item(obj)) \
and (AXUtilities.is_combo_box(parent) or AXUtilities.is_list_box(parent)):
try:
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]
and (AXUtilities.is_combo_box(parent) or AXUtilities.is_list_box(parent)):
extents = AXComponent.get_rect(parent)
else:
try:
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]
extents = AXComponent.get_rect(obj)
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):
if coordType is None:
@@ -853,7 +839,7 @@ class Utilities(script_utilities.Utilities):
if not self.isTextBlockElement(obj):
return -1
child = self.findChildAtOffset(obj, offset)
child = AXHypertext.find_child_at_offset(obj, offset)
if child and not self.isTextBlockElement(child):
matches = [x for x in contents if x[0] == child]
if len(matches) == 1:
@@ -1351,7 +1337,7 @@ class Utilities(script_utilities.Utilities):
pass
else:
if char == self.EMBEDDED_OBJECT_CHARACTER:
child = self.findChildAtOffset(obj, offset)
child = AXHypertext.find_child_at_offset(obj, offset)
if child:
return self._getContentsForObj(child, 0, boundary)
@@ -1698,7 +1684,7 @@ class Utilities(script_utilities.Utilities):
offset = max(0, offset)
if (AXUtilities.is_tool_bar(obj) or AXUtilities.is_menu_bar(obj)) \
and not self._treatObjectAsWhole(obj):
child = self.findChildAtOffset(obj, offset)
child = AXHypertext.find_child_at_offset(obj, offset)
if child:
obj = child
offset = 0
@@ -1883,7 +1869,7 @@ class Utilities(script_utilities.Utilities):
tokens = ["WEB: First context on line is: ", firstObj, ", ", firstOffset]
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)
if not obj and firstObj:
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]
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)
if not obj and lastObj:
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)
if string == self.EMBEDDED_OBJECT_CHARACTER:
child = self.findChildAtOffset(obj, start)
child = AXHypertext.find_child_at_offset(obj, start)
if child:
return self.textAtPoint(child, x, y, coordType, boundary)
@@ -4048,11 +4034,10 @@ class Utilities(script_utilities.Utilities):
if uri and not uri.startswith('javascript'):
rv = False
if rv and AXObject.supports_image(obj):
image = obj.queryImage()
if image.imageDescription:
if AXObject.get_image_description(obj):
rv = False
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:
rv = False
if rv and AXObject.supports_text(obj):
@@ -4326,16 +4311,14 @@ class Utilities(script_utilities.Utilities):
if event.type.startswith("object:text-changed") \
or event.type.startswith("object:text-selection-changed"):
lastKey, mods = self.lastKeyAndModifiers()
if lastKey in ["Down", "Up"]:
if input_event_manager.get_manager().last_event_was_up_or_down():
return True
return False
def treatEventAsSpinnerValueChange(self, event):
if event.type.startswith("object:text-caret-moved") and self.isSpinnerEntry(event.source):
lastKey, mods = self.lastKeyAndModifiers()
if lastKey in ["Down", "Up"]:
if input_event_manager.get_manager().last_event_was_up_or_down():
obj, offset = self.getCaretContext()
return event.source == obj
@@ -4347,8 +4330,7 @@ class Utilities(script_utilities.Utilities):
if event.type.startswith("object:text-") \
and self.isSingleLineAutocompleteEntry(event.source):
lastKey, mods = self.lastKeyAndModifiers()
return lastKey == "Return"
return input_event_manager.get_manager().last_event_was_return()
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)
if event.type.startswith("object:children-changed"):
@@ -4377,8 +4359,7 @@ class Utilities(script_utilities.Utilities):
return True
if obj == event.source and isComboBoxItem(obj):
lastKey, mods = self.lastKeyAndModifiers()
if lastKey in ["Down", "Up"]:
if input_event_manager.get_manager().last_event_was_up_or_down():
return True
return False
@@ -4400,8 +4381,7 @@ class Utilities(script_utilities.Utilities):
if AXUtilities.is_menu_related(event.source) \
and AXUtilities.is_entry(cthulhu_state.locusOfFocus) \
and AXUtilities.is_focused(cthulhu_state.locusOfFocus):
lastKey, mods = self.lastKeyAndModifiers()
if lastKey not in ["Down", "Up"]:
if not input_event_manager.get_manager().last_event_was_up_or_down():
return True
return False
@@ -4417,8 +4397,7 @@ class Utilities(script_utilities.Utilities):
if AXUtilities.is_menu_item_of_any_kind(cthulhu_state.locusOfFocus) \
or AXUtilities.is_list_item(cthulhu_state.locusOfFocus):
lastKey, mods = self.lastKeyAndModifiers()
return lastKey in ["Down", "Up"]
return input_event_manager.get_manager().last_event_was_up_or_down()
return False
@@ -4785,12 +4764,6 @@ class Utilities(script_utilities.Utilities):
container = obj
contextObj, contextOffset = None, -1
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)
if offset < 0:
tokens = ["WEB: Exception getting caret offset of", obj]
@@ -4799,7 +4772,7 @@ class Utilities(script_utilities.Utilities):
continue
contextObj, contextOffset = obj, offset
child = self.findChildAtOffset(obj, offset)
child = AXHypertext.find_child_at_offset(obj, offset)
if child:
obj = child
else:
@@ -4996,9 +4969,9 @@ class Utilities(script_utilities.Utilities):
obj, offset = None, -1
notify = True
keyString, mods = self.lastKeyAndModifiers()
lastWasUp = input_event_manager.get_manager().last_event_was_up()
childCount = AXObject.get_child_count(event.source)
if keyString == "Up":
if lastWasUp:
if event.detail1 >= childCount:
msg = "WEB: Last child removed. Getting new location from end of parent."
debug.printMessage(debug.LEVEL_INFO, msg, True)
@@ -5178,7 +5151,7 @@ class Utilities(script_utilities.Utilities):
debug.printMessage(debug.LEVEL_INFO, msg, True)
return obj, offset
child = self.findChildAtOffset(obj, offset)
child = AXHypertext.find_child_at_offset(obj, offset)
if not child:
msg = "WEB: Child at offset is null. Returning context unchanged."
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."]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
offset += 1
child = self.findChildAtOffset(obj, offset)
child = AXHypertext.find_child_at_offset(obj, offset)
if self.isListItemMarker(child):
tokens = ["WEB: First caret context is next offset in", obj, ":",
@@ -5234,7 +5207,7 @@ class Utilities(script_utilities.Utilities):
if text:
allText = AXText.get_all_text(obj)
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:
tokens = ["ERROR: Child", child, "found at offset with char '",
allText[i].replace("\n", "\\n"), "'"]
@@ -5310,7 +5283,7 @@ class Utilities(script_utilities.Utilities):
if offset == -1 or offset > len(allText):
offset = len(allText)
for i in range(offset - 1, -1, -1):
child = self.findChildAtOffset(obj, i)
child = AXHypertext.find_child_at_offset(obj, i)
if child and allText[i] != self.EMBEDDED_OBJECT_CHARACTER:
tokens = ["ERROR: Child", child, "found at offset with char '",
allText[i].replace("\n", "\\n"), "'"]
+13 -12
View File
@@ -37,6 +37,7 @@ from gi.repository import Atspi
import urllib
from cthulhu import debug
from cthulhu import input_event_manager
from cthulhu import messages
from cthulhu import object_properties
from cthulhu import cthulhu_state
@@ -302,12 +303,13 @@ class SpeechGenerator(speech_generator.SpeechGenerator):
if args.get('leaving'):
return []
lastKey, mods = self._script.utilities.lastKeyAndModifiers()
if (lastKey in ['Down', 'Right'] or self._script.inSayAll()) and args.get('startOffset'):
manager = input_event_manager.get_manager()
if (manager.last_event_was_forward_caret_navigation() or self._script.inSayAll()) \
and args.get('startOffset'):
return []
if lastKey in ['Up', 'Left']:
text = self._script.utilities.queryNonEmptyText(obj)
if text and args.get('endOffset') not in [None, AXText.get_character_count(obj)]:
if manager.last_event_was_backward_caret_navigation():
if self._script.utilities.treatAsTextObject(obj) \
and args.get('endOffset') not in [None, AXText.get_character_count(obj)]:
return []
result = []
@@ -358,8 +360,7 @@ class SpeechGenerator(speech_generator.SpeechGenerator):
if self._script.utilities.isContentEditableWithEmbeddedObjects(obj) \
or self._script.utilities.isDocument(obj):
lastKey, mods = self._script.utilities.lastKeyAndModifiers()
if lastKey in ["Home", "End", "Up", "Down", "Left", "Right", "Page_Up", "Page_Down"]:
if input_event_manager.get_manager().last_event_was_caret_navigation():
return []
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):
doNotSpeak.append(Atspi.Role.MENU)
lastKey, mods = self._script.utilities.lastKeyAndModifiers()
isEditable = AXUtilities.is_editable(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 []
if lastKey in ["Up", "Left"] and not mods:
if manager.last_event_was_backward_caret_navigation():
text = self._script.utilities.queryNonEmptyText(obj)
if text and end not in [None, AXText.get_character_count(obj)]:
return []
@@ -611,8 +613,7 @@ class SpeechGenerator(speech_generator.SpeechGenerator):
elif isEditable and self._script.utilities.isDocument(obj):
parent = AXObject.get_parent(obj)
if parent and not AXUtilities.is_editable(parent) \
and lastKey not in \
["Home", "End", "Up", "Down", "Left", "Right", "Page_Up", "Page_Down"]:
and not input_event_manager.get_manager().last_event_was_caret_navigation():
result.append(object_properties.ROLE_EDITABLE_CONTENT)
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 text_attribute_names
from .ax_object import AXObject
from .ax_table import AXTable
from .ax_text import AXText
from .ax_utilities import AXUtilities
from .ax_utilities_relation import AXUtilitiesRelation
@@ -931,11 +932,7 @@ class SpeechGenerator(generator.Generator):
it exists. Otherwise, an empty array is returned.
"""
result = []
try:
obj.queryImage()
except Exception:
pass
else:
if AXObject.supports_image(obj):
args['role'] = Atspi.Role.IMAGE
result.extend(self.generate(obj, **args))
result.extend(self.voice(DEFAULT, obj=obj, **args))
@@ -1143,15 +1140,9 @@ class SpeechGenerator(generator.Generator):
parent = AXObject.get_parent(obj)
if AXUtilities.is_table_cell(parent):
obj = parent
parent = self._script.utilities.getTable(obj)
try:
table = parent.queryTable()
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)
row, col = self._script.utilities.coordinatesForCell(obj, False)
if col < 0 and args.get('guessCoordinates', False):
col = self._script.pointOfReference.get('lastColumn', -1)
if col >= 0:
result.append(messages.TABLE_COLUMN % (col + 1))
if result:
@@ -1182,15 +1173,9 @@ class SpeechGenerator(generator.Generator):
parent = AXObject.get_parent(obj)
if AXUtilities.is_table_cell(parent):
obj = parent
parent = self._script.utilities.getTable(obj)
try:
table = parent.queryTable()
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)
row, col = self._script.utilities.coordinatesForCell(obj, False)
if row < 0 and args.get('guessCoordinates', False):
row = self._script.pointOfReference.get('lastRow', -1)
if row >= 0:
result.append(messages.TABLE_ROW % (row + 1))
if result:
@@ -1210,21 +1195,17 @@ class SpeechGenerator(generator.Generator):
parent = AXObject.get_parent(obj)
if AXUtilities.is_table_cell(parent):
obj = parent
parent = self._script.utilities.getTable(obj)
try:
table = parent.queryTable()
except Exception:
table = None
else:
index = self._script.utilities.cellIndex(obj)
col = table.getColumnAtIndex(index)
row = table.getRowAtIndex(index)
result.append(messages.TABLE_COLUMN_DETAILED \
% {"index" : (col + 1),
"total" : table.nColumns})
result.append(messages.TABLE_ROW_DETAILED \
% {"index" : (row + 1),
"total" : table.nRows})
table = self._script.utilities.getTable(obj)
if table:
row, col = self._script.utilities.coordinatesForCell(obj, False)
rows, cols = self._script.utilities.rowAndColumnCount(table, False)
if row >= 0 and col >= 0 and rows > 0 and cols > 0:
result.append(messages.TABLE_COLUMN_DETAILED \
% {"index" : (col + 1),
"total" : cols})
result.append(messages.TABLE_ROW_DETAILED \
% {"index" : (row + 1),
"total" : rows})
if result:
result.extend(self.voice(SYSTEM, obj=obj, **args))
return result
@@ -2817,16 +2798,15 @@ class SpeechGenerator(generator.Generator):
return result
def _generateMathTableStart(self, obj, **args):
try:
table = obj.queryTable()
except Exception:
return []
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:
result = [messages.mathNestedTableSize(table.nRows, table.nColumns)]
result = [messages.mathNestedTableSize(rows, cols)]
else:
result = [messages.mathTableSize(table.nRows, table.nColumns)]
result = [messages.mathTableSize(rows, cols)]
result.extend(self.voice(SYSTEM, obj=obj, **args))
return result
+9 -13
View File
@@ -51,6 +51,7 @@ from . import settings_manager
from .ax_collection import AXCollection
from .ax_event_synthesizer import AXEventSynthesizer
from .ax_object import AXObject
from .ax_table import AXTable
from .ax_text import AXText
from .ax_selection import AXSelection
from .ax_utilities import AXUtilities
@@ -975,11 +976,11 @@ class StructuralNavigation:
- obj: the accessible table whose caption we want.
"""
caption = obj.queryTable().caption
if not AXObject.supports_text(caption):
caption = AXTable.get_caption(obj)
if not caption:
return None
return self._script.utilities.displayedText(caption)
return AXText.get_all_text(caption)
def _getTableDescription(self, obj):
"""Returns a string which describes the table."""
@@ -1246,12 +1247,7 @@ class StructuralNavigation:
if item:
text = AXObject.get_name(item)
if not text and AXUtilities.is_image(obj):
try:
image = obj.queryImage()
except Exception:
text = AXObject.get_description(obj)
else:
text = image.imageDescription or AXObject.get_description(obj)
text = AXObject.get_image_description(obj) or AXObject.get_description(obj)
if not text:
parent = AXObject.get_parent(obj)
if AXUtilities.is_link(parent):
@@ -2115,11 +2111,11 @@ class StructuralNavigation:
if attrs.get('layout-guess') == 'true':
return False
try:
return obj.queryTable().nRows > 0
except Exception:
if not AXObject.supports_table(obj):
return False
return AXTable.get_row_count(obj, prefer_attribute=False) > 0
return AXUtilities.find_all_tables(document, is_not_layout_or_empty)
def _tablePresentation(self, obj, arg=None):
@@ -2128,7 +2124,7 @@ class StructuralNavigation:
if caption:
self._script.presentMessage(caption)
self._script.presentMessage(self._getTableDescription(obj))
cell = obj.queryTable().getAccessibleAt(0, 0)
cell = AXTable.get_cell_at(obj, 0, 0)
if not cell:
tokens = ["STRUCTURAL NAVIGATION: Broken table interface for", obj]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
+2 -4
View File
@@ -44,6 +44,7 @@ from . import cthulhu_state
from . import settings
from .ax_object import AXObject
from .ax_table import AXTable
from .ax_utilities import AXUtilities
from .cthulhu_i18n import _ # for gettext support
@@ -613,10 +614,7 @@ class TutorialGenerator:
if (not alreadyFocused):
parent = AXObject.get_parent(obj)
try:
parent_table = parent.queryTable()
except Exception:
parent_table = None
parent_table = AXTable.get_table(parent)
readFullRow = self._script.utilities.shouldReadFullRow(obj)
if readFullRow and parent_table and not self._script.utilities.isLayoutOnly(parent):
utterances.extend(self._getTutorialForTableCell(obj,