diff --git a/src/cthulhu/ax_component.py b/src/cthulhu/ax_component.py index 6f6065e..ae08da1 100644 --- a/src/cthulhu/ax_component.py +++ b/src/cthulhu/ax_component.py @@ -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 @@ -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 diff --git a/src/cthulhu/ax_document.py b/src/cthulhu/ax_document.py index e8fc4bd..f261c18 100644 --- a/src/cthulhu/ax_document.py +++ b/src/cthulhu/ax_document.py @@ -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 @@ -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 diff --git a/src/cthulhu/ax_event_synthesizer.py b/src/cthulhu/ax_event_synthesizer.py index b008527..9b55505 100644 --- a/src/cthulhu/ax_event_synthesizer.py +++ b/src/cthulhu/ax_event_synthesizer.py @@ -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 diff --git a/src/cthulhu/ax_hypertext.py b/src/cthulhu/ax_hypertext.py index 47d9c00..4da6dc6 100644 --- a/src/cthulhu/ax_hypertext.py +++ b/src/cthulhu/ax_hypertext.py @@ -1,5 +1,6 @@ -# Orca +#!/usr/bin/env python3 # +# Copyright (c) 2024 Stormux # Copyright 2024 Igalia, S.L. # Author: Joanmarie Diggs # @@ -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 diff --git a/src/cthulhu/ax_text.py b/src/cthulhu/ax_text.py index 66347f7..610e39b 100644 --- a/src/cthulhu/ax_text.py +++ b/src/cthulhu/ax_text.py @@ -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 @@ -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 diff --git a/src/cthulhu/ax_utilities_debugging.py b/src/cthulhu/ax_utilities_debugging.py index 15d7904..3bb1dde 100644 --- a/src/cthulhu/ax_utilities_debugging.py +++ b/src/cthulhu/ax_utilities_debugging.py @@ -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 @@ -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 diff --git a/src/cthulhu/ax_utilities_relation.py b/src/cthulhu/ax_utilities_relation.py index 95ddc6d..0947f74 100644 --- a/src/cthulhu/ax_utilities_relation.py +++ b/src/cthulhu/ax_utilities_relation.py @@ -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 @@ -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 diff --git a/src/cthulhu/ax_value.py b/src/cthulhu/ax_value.py index effeccc..fdce81d 100644 --- a/src/cthulhu/ax_value.py +++ b/src/cthulhu/ax_value.py @@ -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 @@ -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 diff --git a/src/cthulhu/braille.py b/src/cthulhu/braille.py index c9a32cf..cbaf5d3 100644 --- a/src/cthulhu/braille.py +++ b/src/cthulhu/braille.py @@ -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( diff --git a/src/cthulhu/flat_review.py b/src/cthulhu/flat_review.py index 2540787..4fddc25 100644 --- a/src/cthulhu/flat_review.py +++ b/src/cthulhu/flat_review.py @@ -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] diff --git a/src/cthulhu/focus_manager.py b/src/cthulhu/focus_manager.py index ce02725..600eaa1 100644 --- a/src/cthulhu/focus_manager.py +++ b/src/cthulhu/focus_manager.py @@ -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 diff --git a/src/cthulhu/generator.py b/src/cthulhu/generator.py index cbf16ba..f774c1b 100644 --- a/src/cthulhu/generator.py +++ b/src/cthulhu/generator.py @@ -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) diff --git a/src/cthulhu/input_event_manager.py b/src/cthulhu/input_event_manager.py index 96bc37b..6d02cce 100644 --- a/src/cthulhu/input_event_manager.py +++ b/src/cthulhu/input_event_manager.py @@ -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 @@ -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 diff --git a/src/cthulhu/label_inference.py b/src/cthulhu/label_inference.py index 097c776..4d8a0ed 100644 --- a/src/cthulhu/label_inference.py +++ b/src/cthulhu/label_inference.py @@ -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, [] diff --git a/src/cthulhu/mouse_review.py b/src/cthulhu/mouse_review.py index d761309..b2e2031 100644 --- a/src/cthulhu/mouse_review.py +++ b/src/cthulhu/mouse_review.py @@ -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.""" diff --git a/src/cthulhu/plugins/AIAssistant/plugin.py b/src/cthulhu/plugins/AIAssistant/plugin.py index d08318b..c2ea69a 100644 --- a/src/cthulhu/plugins/AIAssistant/plugin.py +++ b/src/cthulhu/plugins/AIAssistant/plugin.py @@ -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 diff --git a/src/cthulhu/plugins/SpeechHistory/plugin.py b/src/cthulhu/plugins/SpeechHistory/plugin.py index 89fc1a7..138f5a1 100644 --- a/src/cthulhu/plugins/SpeechHistory/plugin.py +++ b/src/cthulhu/plugins/SpeechHistory/plugin.py @@ -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") - diff --git a/src/cthulhu/script_utilities.py b/src/cthulhu/script_utilities.py index 3453a64..f8b1c03 100644 --- a/src/cthulhu/script_utilities.py +++ b/src/cthulhu/script_utilities.py @@ -54,6 +54,7 @@ from . import debug from . import keynames from . import keybindings from . import input_event +from . import input_event_manager from . import mathsymbols from . import messages from . import cthulhu @@ -65,7 +66,10 @@ from . import settings_manager from . import text_attribute_names from .ax_object import AXObject from .ax_selection import AXSelection +from .ax_table import AXTable from .ax_text import AXText +from .ax_hypertext import AXHypertext +from .ax_value import AXValue from .ax_utilities import AXUtilities from .ax_utilities_relation import AXUtilitiesRelation @@ -276,13 +280,12 @@ class Utilities: """ 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 [] # First see if this accessible implements RELATION_NODE_PARENT_OF. # If it does, the full target list are the nodes. If it doesn't @@ -303,8 +306,10 @@ class Utilities: # row, col = self.coordinatesForCell(obj) nodeLevel = self.nodeLevel(obj) - 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 nodeOf = AXUtilitiesRelation.get_is_node_child_of(cell) if not nodeOf: continue @@ -989,26 +994,17 @@ class Utilities: if not AXUtilities.is_progress_bar(obj): return False - try: - value = obj.queryValue() - except NotImplementedError: + if not AXObject.supports_value(obj): tokens = ["SCRIPT UTILITIES:", obj, "doesn't implement AtspiValue"] debug.printTokens(debug.LEVEL_INFO, tokens, True) return False - except Exception: - tokens = ["SCRIPT UTILITIES: Exception getting value for", obj] + + min_value = AXValue.get_minimum_value(obj) + max_value = AXValue.get_maximum_value(obj) + if max_value == min_value: + tokens = ["SCRIPT UTILITIES:", obj, "is busy indicator"] debug.printTokens(debug.LEVEL_INFO, tokens, True) return False - else: - try: - if value.maximumValue == value.minimumValue: - tokens = ["SCRIPT UTILITIES:", obj, "is busy indicator"] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - return False - except Exception: - tokens = ["SCRIPT UTILITIES:", obj, "is either busy indicator or broken"] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - return False return True @@ -1047,17 +1043,14 @@ class Utilities: return True, "Not handled by any other case" def getValueAsPercent(self, obj): - try: - value = obj.queryValue() - minval, val, maxval = value.minimumValue, value.currentValue, value.maximumValue - except NotImplementedError: + if not AXObject.supports_value(obj): tokens = ["SCRIPT UTILITIES:", obj, "doesn't implement AtspiValue"] debug.printTokens(debug.LEVEL_INFO, tokens, True) return None - except Exception: - tokens = ["SCRIPT UTILITIES: Exception getting value for", obj] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - return None + + val = AXValue.get_current_value(obj) + minval = AXValue.get_minimum_value(obj) + maxval = AXValue.get_maximum_value(obj) if AXUtilities.is_indeterminate(obj): tokens = ["SCRIPT UTILITIES:", obj, "has state indeterminate and value of", val] @@ -1179,7 +1172,7 @@ class Utilities: if AXUtilities.is_document_spreadsheet(doc): return True - return obj.queryTable().nRows > 65536 + return AXTable.get_row_count(obj, prefer_attribute=False) > 65536 def isTextDocumentCell(self, obj): if not AXUtilities.is_table_cell_or_header(obj): @@ -1315,23 +1308,19 @@ class Utilities: Atspi.Role.TREE_ITEM] if role == Atspi.Role.TABLE and attrs.get('layout-guess') != 'true': - try: - table = obj.queryTable() - except NotImplementedError: + if not AXObject.supports_table(obj): tokens = ["SCRIPT UTILITIES: Table", obj, "does not implement table interface"] debug.printTokens(debug.LEVEL_INFO, tokens, True) layoutOnly = True - except Exception as error: - tokens = ["SCRIPT UTILITIES: Error querying table interface of", obj, ":", error] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - layoutOnly = True else: - if not (table.nRows and table.nColumns): + rows = AXTable.get_row_count(obj, prefer_attribute=False) + cols = AXTable.get_column_count(obj, prefer_attribute=False) + if not (rows and cols): layoutOnly = not AXUtilities.is_focused(obj) elif attrs.get('xml-roles') == 'table' or attrs.get('tag') == 'table': layoutOnly = False elif not (AXObject.get_name(obj) or self.displayedLabel(obj)): - layoutOnly = not (table.getColumnHeader(0) or table.getRowHeader(0)) + layoutOnly = not (AXTable.has_column_headers(obj) or AXTable.has_row_headers(obj)) elif role == Atspi.Role.TABLE_CELL and AXObject.get_child_count(obj): if parentRole == Atspi.Role.TREE_TABLE: layoutOnly = not AXObject.get_name(obj) @@ -1502,10 +1491,10 @@ class Utilities: # Comparing the extents of objects which claim to be different # addresses both managed descendants and implementations which # recreate accessibles for the same widget. - extents1 = \ - obj1.queryComponent().getExtents(Atspi.CoordType.WINDOW) - extents2 = \ - obj2.queryComponent().getExtents(Atspi.CoordType.WINDOW) + if not AXObject.supports_component(obj1) or not AXObject.supports_component(obj2): + return False + extents1 = Atspi.Component.get_extents(obj1, Atspi.CoordType.WINDOW) + extents2 = Atspi.Component.get_extents(obj2, Atspi.CoordType.WINDOW) # Objects which claim to be different and which are in different # locations are almost certainly not recreated objects. @@ -1570,56 +1559,50 @@ class Utilities: basename command in a shell.""" basename = None + uri = AXHypertext.get_link_uri(obj) + if uri and len(uri): + # Sometimes the URI is an expression that includes a URL. + # Currently that can be found at the bottom of safeway.com. + # It can also be seen in the backwards.html test file. + # + expression = uri.split(',') + if len(expression) > 1: + for item in expression: + if item.find('://') >=0: + if not item[0].isalnum(): + item = item[1:-1] + if not item[-1].isalnum(): + item = item[0:-2] + uri = item + break - try: - hyperlink = obj.queryHyperlink() - except Exception: - pass - else: - uri = hyperlink.getURI(0) - if uri and len(uri): - # Sometimes the URI is an expression that includes a URL. - # Currently that can be found at the bottom of safeway.com. - # It can also be seen in the backwards.html test file. + # We're assuming that there IS a base name to be had. + # What if there's not? See backwards.html. + # + uri = uri.split('://')[-1] + if not uri: + return basename + + # Get the last thing after all the /'s, unless it ends + # in a /. If it ends in a /, we'll look to the stuff + # before the ending /. + # + if uri[-1] == "/": + basename = uri[0:-1] + basename = basename.split('/')[-1] + elif not uri.count("/"): + basename = uri + else: + basename = uri.split('/')[-1] + if basename.startswith("index"): + basename = uri.split('/')[-2] + + # Now, try to strip off the suffixes. # - expression = uri.split(',') - if len(expression) > 1: - for item in expression: - if item.find('://') >=0: - if not item[0].isalnum(): - item = item[1:-1] - if not item[-1].isalnum(): - item = item[0:-2] - uri = item - break - - # We're assuming that there IS a base name to be had. - # What if there's not? See backwards.html. - # - uri = uri.split('://')[-1] - if not uri: - return basename - - # Get the last thing after all the /'s, unless it ends - # in a /. If it ends in a /, we'll look to the stuff - # before the ending /. - # - if uri[-1] == "/": - basename = uri[0:-1] - basename = basename.split('/')[-1] - elif not uri.count("/"): - basename = uri - else: - basename = uri.split('/')[-1] - if basename.startswith("index"): - basename = uri.split('/')[-2] - - # Now, try to strip off the suffixes. - # - basename = basename.split('.')[0] - basename = basename.split('?')[0] - basename = basename.split('#')[0] - basename = basename.split('%')[0] + basename = basename.split('.')[0] + basename = basename.split('?')[0] + basename = basename.split('#')[0] + basename = basename.split('%')[0] return basename @@ -1641,15 +1624,13 @@ class Utilities: if not AXObject.supports_text(obj): return -1 - try: - hypertext = obj.queryHypertext() - except NotImplementedError: + if not AXObject.supports_hypertext(obj): return -1 - for i in range(hypertext.getNLinks()): - link = hypertext.getLink(i) - if (characterIndex >= link.startIndex) \ - and (characterIndex <= link.endIndex): + for i, link in enumerate(AXHypertext.get_all_links(obj)): + start = AXHypertext.get_link_start_offset(link) + end = AXHypertext.get_link_end_offset(link) + if (characterIndex >= start) and (characterIndex <= end): return i return -1 @@ -1743,8 +1724,13 @@ class Utilities: debug.printTokens(debug.LEVEL_INFO, tokens, True) return False + if not AXObject.supports_component(obj): + tokens = ["SCRIPT UTILITIES:", obj, "does not support component interface"] + debug.printTokens(debug.LEVEL_INFO, tokens, True) + return False + try: - box = obj.queryComponent().getExtents(Atspi.CoordType.SCREEN) + box = Atspi.Component.get_extents(obj, Atspi.CoordType.SCREEN) except Exception: tokens = ["SCRIPT UTILITIES: Exception getting extents for", obj] debug.printTokens(debug.LEVEL_INFO, tokens, True) @@ -1753,17 +1739,20 @@ class Utilities: tokens = ["SCRIPT UTILITIES: Extents for", obj, "are:", box] debug.printTokens(debug.LEVEL_INFO, tokens, True) - if box.x > 10000 or box.y > 10000: + boxTuple = self._extentsToTuple(box) + x, y, width, height = boxTuple + + if x > 10000 or y > 10000: tokens = ["SCRIPT UTILITIES:", obj, "seems to have bogus coordinates"] debug.printTokens(debug.LEVEL_INFO, tokens, True) return False - if box.x < 0 and box.y < 0 and tuple(box) != (-1, -1, -1, -1): + if x < 0 and y < 0 and boxTuple != (-1, -1, -1, -1): tokens = ["SCRIPT UTILITIES:", obj, "has negative coordinates"] debug.printTokens(debug.LEVEL_INFO, tokens, True) return False - if not (box.width or box.height): + if not (width or height): if not AXObject.get_child_count(obj): tokens = ["SCRIPT UTILITIES:", obj, "has no size and no children"] debug.printTokens(debug.LEVEL_INFO, tokens, True) @@ -1778,7 +1767,8 @@ class Utilities: if boundingbox is None or not self._boundsIncludeChildren(AXObject.get_parent(obj)): return True - if not self.containsRegion(box, boundingbox) and tuple(box) != (-1, -1, -1, -1): + boundingbox = self._extentsToTuple(boundingbox) + if not self.containsRegion(boxTuple, boundingbox) and boxTuple != (-1, -1, -1, -1): tokens = ["SCRIPT UTILITIES:", obj, box, "not in", boundingbox] debug.printTokens(debug.LEVEL_INFO, tokens, True) return False @@ -1866,13 +1856,15 @@ class Utilities: debug.printTokens(debug.LEVEL_INFO, tokens, True) if extents is None: - try: - component = root.queryComponent() - extents = component.getExtents(Atspi.CoordType.SCREEN) - except Exception: - tokens = ["SCRIPT UTILITIES: Exception getting extents of", root] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - extents = 0, 0, 0, 0 + if AXObject.supports_component(root): + try: + extents = Atspi.Component.get_extents(root, Atspi.CoordType.SCREEN) + except Exception: + tokens = ["SCRIPT UTILITIES: Exception getting extents of", root] + debug.printTokens(debug.LEVEL_INFO, tokens, True) + extents = Atspi.Rect() + else: + extents = Atspi.Rect() if AXObject.supports_table(root) and AXObject.supports_selection(root): visibleCells = self.getVisibleTableCells(root) @@ -1975,7 +1967,7 @@ class Utilities: return obj def pred(x): - return x and not self.isStaticTextLeaf(x) and self.displayedText(x).strip() + return AXObject.get_name(x) or AXText.get_all_text(x) child = AXObject.find_descendant(obj, pred) if child is not None: @@ -2098,9 +2090,12 @@ class Utilities: def onSameLine(obj1, obj2, delta=0): """Determines if obj1 and obj2 are on the same line.""" + if not AXObject.supports_component(obj1) or not AXObject.supports_component(obj2): + return False + try: - bbox1 = obj1.queryComponent().getExtents(Atspi.CoordType.SCREEN) - bbox2 = obj2.queryComponent().getExtents(Atspi.CoordType.SCREEN) + bbox1 = Atspi.Component.get_extents(obj1, Atspi.CoordType.SCREEN) + bbox2 = Atspi.Component.get_extents(obj2, Atspi.CoordType.SCREEN) except Exception: return False @@ -2131,17 +2126,22 @@ class Utilities: @staticmethod def sizeComparison(obj1, obj2): - try: - bbox = obj1.queryComponent().getExtents(Atspi.CoordType.SCREEN) - width1, height1 = bbox.width, bbox.height - except Exception: - width1, height1 = 0, 0 + width1, height1 = 0, 0 + width2, height2 = 0, 0 - try: - bbox = obj2.queryComponent().getExtents(Atspi.CoordType.SCREEN) - width2, height2 = bbox.width, bbox.height - except Exception: - width2, height2 = 0, 0 + if AXObject.supports_component(obj1): + try: + bbox = Atspi.Component.get_extents(obj1, Atspi.CoordType.SCREEN) + width1, height1 = bbox.width, bbox.height + except Exception: + pass + + if AXObject.supports_component(obj2): + try: + bbox = Atspi.Component.get_extents(obj2, Atspi.CoordType.SCREEN) + width2, height2 = bbox.width, bbox.height + except Exception: + pass return (width1 * height1) - (width2 * height2) @@ -2151,17 +2151,22 @@ class Utilities: 0, or 1 to indicate if obj1 physically is before, is in the same place as, or is after obj2.""" - try: - bbox = obj1.queryComponent().getExtents(Atspi.CoordType.SCREEN) - x1, y1 = bbox.x, bbox.y - except Exception: - x1, y1 = 0, 0 + x1, y1 = 0, 0 + x2, y2 = 0, 0 - try: - bbox = obj2.queryComponent().getExtents(Atspi.CoordType.SCREEN) - x2, y2 = bbox.x, bbox.y - except Exception: - x2, y2 = 0, 0 + if AXObject.supports_component(obj1): + try: + bbox = Atspi.Component.get_extents(obj1, Atspi.CoordType.SCREEN) + x1, y1 = bbox.x, bbox.y + except Exception: + pass + + if AXObject.supports_component(obj2): + try: + bbox = Atspi.Component.get_extents(obj2, Atspi.CoordType.SCREEN) + x2, y2 = bbox.x, bbox.y + except Exception: + pass rv = y1 - y2 or x1 - x2 @@ -2191,8 +2196,11 @@ class Utilities: return extents def getBoundingBox(self, obj): + if not AXObject.supports_component(obj): + return -1, -1, 0, 0 + try: - extents = obj.queryComponent().getExtents(Atspi.CoordType.SCREEN) + extents = Atspi.Component.get_extents(obj, Atspi.CoordType.SCREEN) except Exception: tokens = ["SCRIPT UTILITIES: Exception getting extents of", obj] debug.printTokens(debug.LEVEL_INFO, tokens, True) @@ -2207,8 +2215,11 @@ class Utilities: if AXUtilities.is_application(obj): return False + if not AXObject.supports_component(obj): + return True + try: - extents = obj.queryComponent().getExtents(Atspi.CoordType.SCREEN) + extents = Atspi.Component.get_extents(obj, Atspi.CoordType.SCREEN) except Exception: tokens = ["SCRIPT UTILITIES: Exception getting extents for", obj] debug.printTokens(debug.LEVEL_INFO, tokens, True) @@ -2324,10 +2335,8 @@ class Utilities: - obj: the Accessible object. """ - try: - return obj.queryHyperlink().getURI(0) - except Exception: - return None + uri = AXHypertext.get_link_uri(obj) + return uri or None ######################################################################### # # @@ -2441,37 +2450,12 @@ class Utilities: return AXText.get_selected_ranges(obj) def getChildAtOffset(self, obj, offset): - try: - hypertext = obj.queryHypertext() - except NotImplementedError: - tokens = ["SCRIPT UTILITIES:", obj, "does not implement the hypertext interface"] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - return None - except Exception: - tokens = ["SCRIPT UTILITIES: Exception querying hypertext interface for", obj] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + child = AXHypertext.get_child_at_offset(obj, offset) + if not child: return None - index = hypertext.getLinkIndex(offset) - if index == -1: - return None - - hyperlink = hypertext.getLink(index) - if not hyperlink: - tokens = ["SCRIPT UTILITIES: No hyperlink object at index", index, "for", obj] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - return None - - child = hyperlink.get_object(0) - tokens = ["SCRIPT UTILITIES: Hyperlink object at index", index, "for", obj, "is", child] + tokens = ["SCRIPT UTILITIES: Hyperlink object at offset", offset, "for", obj, "is", child] debug.printTokens(debug.LEVEL_INFO, tokens, True) - if offset != hyperlink.startIndex: - msg = ( - f"SCRIPT UTILITIES: Hyperlink start index {hyperlink.startIndex} " - f"should match the offset {offset}" - ) - debug.printMessage(debug.LEVEL_INFO, msg, True) - return child def findChildAtOffset(self, obj, offset): @@ -2521,25 +2505,19 @@ class Utilities: """ offset = -1 - try: - hyperlink = obj.queryHyperlink() - except NotImplementedError: - tokens = ["SCRIPT UTILITIES:", obj, "does not implement the hyperlink interface"] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + # We need to make sure that this is an embedded object in + # some accessible text (as opposed to an imagemap link). + # + parent = AXObject.get_parent(obj) + if AXObject.supports_text(parent): + offset = AXHypertext.get_link_start_offset(obj) else: - # We need to make sure that this is an embedded object in - # some accessible text (as opposed to an imagemap link). - # - parent = AXObject.get_parent(obj) - if AXObject.supports_text(parent): - offset = hyperlink.startIndex - else: - tokens = ["SCRIPT UTILITIES: Exception getting startIndex for", - obj, "in parent", parent] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - tokens = ["SCRIPT UTILITIES: startIndex of", obj, f"is {offset}"] + tokens = ["SCRIPT UTILITIES: Exception getting startIndex for", + obj, "in parent", parent] debug.printTokens(debug.LEVEL_INFO, tokens, True) + tokens = ["SCRIPT UTILITIES: startIndex of", obj, f"is {offset}"] + debug.printTokens(debug.LEVEL_INFO, tokens, True) return offset def clearTextSelection(self, obj): @@ -3001,25 +2979,22 @@ class Utilities: from . import punctuation_settings endOffset = startOffset + len(line) - try: - hyperText = obj.queryHypertext() - nLinks = hyperText.getNLinks() - except Exception: - nLinks = 0 + links = AXHypertext.get_all_links(obj) if AXObject.supports_hypertext(obj) else [] adjustedLine = list(line) - for n in range(nLinks, 0, -1): - link = hyperText.getLink(n - 1) - if not link: + for link in reversed(links): + start_index = AXHypertext.get_link_start_offset(link) + end_index = AXHypertext.get_link_end_offset(link) + if start_index < 0 or end_index < 0: continue # We only care about links in the string, line: # - if startOffset < link.endIndex <= endOffset: - index = link.endIndex - startOffset - elif startOffset <= link.startIndex < endOffset: + if startOffset < end_index <= endOffset: + index = end_index - startOffset + elif startOffset <= start_index < endOffset: index = len(line) - if link.endIndex < endOffset: + if end_index < endOffset: index -= 1 else: continue @@ -3290,15 +3265,28 @@ class Utilities: if coordType is None: coordType = Atspi.CoordType.SCREEN + if not AXObject.supports_component(obj1) or not AXObject.supports_component(obj2): + return 0, 0, 0, 0 + try: - extents1 = obj1.queryComponent().getExtents(coordType) - extents2 = obj2.queryComponent().getExtents(coordType) + extents1 = Atspi.Component.get_extents(obj1, coordType) + extents2 = Atspi.Component.get_extents(obj2, coordType) except Exception: return 0, 0, 0, 0 return self.intersection(extents1, extents2) def intersection(self, extents1, extents2): + def _toTuple(extents): + if extents is None: + return 0, 0, 0, 0 + if hasattr(extents, "x") and hasattr(extents, "y") \ + and hasattr(extents, "width") and hasattr(extents, "height"): + return extents.x, extents.y, extents.width, extents.height + return extents + + extents1 = _toTuple(extents1) + extents2 = _toTuple(extents2) x1, y1, width1, height1 = extents1 x2, y2, width2, height2 = extents2 @@ -3323,6 +3311,15 @@ class Utilities: def containsRegion(self, extents1, extents2): return self.intersection(extents1, extents2) != (0, 0, 0, 0) + @staticmethod + def _extentsToTuple(extents): + if extents is None: + return 0, 0, 0, 0 + if hasattr(extents, "x") and hasattr(extents, "y") \ + and hasattr(extents, "width") and hasattr(extents, "height"): + return extents.x, extents.y, extents.width, extents.height + return tuple(extents) + @staticmethod def _allNamesForKeyCode(keycode): keymap = Gdk.Keymap.get_default() @@ -3477,28 +3474,16 @@ class Utilities: if valuetext: return valuetext - try: - value = obj.queryValue() - except NotImplementedError: + if not AXObject.supports_value(obj): return "" - else: - currentValue = value.currentValue + + currentValue = AXValue.get_current_value(obj) # "The reports of my implementation are greatly exaggerated." + maxValue = AXValue.get_maximum_value(obj) + minValue = AXValue.get_minimum_value(obj) try: - maxValue = value.maximumValue - except Exception as error: - maxValue = 0.0 - tokens = ["SCRIPT UTILITIES: Could not get maximum value for", obj, ":", error] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - try: - minValue = value.minimumValue - except Exception as error: - minValue = 0.0 - tokens = ["SCRIPT UTILITIES: Could not get minimum value for", obj, ":", error] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - try: - minIncrement = value.minimumIncrement + minIncrement = Atspi.Value.get_minimum_increment(obj) except Exception as error: minIncrement = (maxValue - minValue) / 100.0 tokens = ["SCRIPT UTILITIES: Could not get minimum increment for", obj, ":", error] @@ -3564,14 +3549,9 @@ class Utilities: def getHyperlinkRange(self, obj): """Returns the text range in parent associated with obj.""" - try: - hyperlink = obj.queryHyperlink() - start, end = hyperlink.startIndex, hyperlink.endIndex - except NotImplementedError: - tokens = ["SCRIPT UTILITIES:", obj, "does not implement the hyperlink interface"] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - return -1, -1 - except Exception: + start = AXHypertext.get_link_start_offset(obj) + end = AXHypertext.get_link_end_offset(obj) + if start < 0 or end < 0: tokens = ["SCRIPT UTILITIES: Exception getting hyperlink indices for", obj] debug.printTokens(debug.LEVEL_INFO, tokens, True) return -1, -1 @@ -3653,9 +3633,9 @@ class Utilities: def selectedChildCount(self, obj): if AXObject.supports_table(obj): - table = obj.queryTable() - if table.nSelectedRows: - return table.nSelectedRows + count = AXTable.get_selected_row_count(obj) + if count: + return count return AXSelection.get_selected_child_count(obj) @@ -3853,30 +3833,7 @@ class Utilities: debug.printMessage(debug.LEVEL_INFO, msg, True) return [] - if AXObject.supports_table_cell(obj): - tableCell = obj.queryTableCell() - try: - headers = tableCell.columnHeaderCells - except Exception: - tokens = ["SCRIPT UTILITIES: Exception getting column headers for", obj] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - else: - return headers - - parent = AXObject.find_ancestor(obj, AXObject.supports_table) - try: - table = parent.queryTable() - except Exception: - return [] - - row, col = self.coordinatesForCell(obj) - rowspan, colspan = self.rowAndColumnSpan(obj) - - headers = [] - for c in range(col, col+colspan): - headers.append(table.getColumnHeader(c)) - - return headers + return AXTable.get_column_headers(obj) def rowHeadersForCell(self, obj): result = self._rowHeadersForCell(obj) @@ -3897,30 +3854,7 @@ class Utilities: debug.printMessage(debug.LEVEL_INFO, msg, True) return [] - if AXObject.supports_table_cell(obj): - tableCell = obj.queryTableCell() - try: - headers = tableCell.rowHeaderCells - except Exception: - tokens = ["SCRIPT UTILITIES: Exception getting row headers for", obj] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - else: - return headers - - parent = AXObject.find_ancestor(obj, AXObject.supports_table) - try: - table = parent.queryTable() - except Exception: - return [] - - row, col = self.coordinatesForCell(obj) - rowspan, colspan = self.rowAndColumnSpan(obj) - - headers = [] - for r in range(row, row+rowspan): - headers.append(table.getRowHeader(r)) - - return headers + return AXTable.get_row_headers(obj) def columnHeaderForCell(self, obj): headers = self.columnHeadersForCell(obj) @@ -3940,79 +3874,13 @@ class Utilities: return True def coordinatesForCell(self, obj, preferAttribute=True, findCellAncestor=False): - if not AXUtilities.is_table_cell_or_header(obj): - if not findCellAncestor: - return -1, -1 - - cell = AXObject.find_ancestor(obj, AXUtilities.is_table_cell_or_header) - return self.coordinatesForCell(cell, preferAttribute, False) - - if AXObject.supports_table_cell(obj) \ - and self._shouldUseTableCellInterfaceForCoordinates(): - tableCell = obj.queryTableCell() - try: - successful, row, col = tableCell.position - except Exception: - tokens = ["SCRIPT UTILITIES: Exception getting table cell position of", obj] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - else: - if successful: - tokens = ["SCRIPT UTILITIES: position of", obj, f"is row: {row}, col: {col}"] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - return row, col - tokens = ["SCRIPT UTILITIES: Failed to get position of", obj, "via table cell"] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - - parent = AXObject.find_ancestor(obj, AXObject.supports_table) - if not parent: - tokens = ["SCRIPT UTILITIES: Couldn't find table-implementing ancestor for", obj] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - return -1, -1 - - try: - table = parent.queryTable() - except Exception: - tokens = ["SCRIPT UTILITIES: Exception querying table interface", parent] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - return -1, -1 - - index = self.cellIndex(obj) - try: - row = table.getRowAtIndex(index) - col = table.getColumnAtIndex(index) - except Exception: - tokens = ["SCRIPT UTILITIES: Exception getting row and column at index from", parent] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - return -1, -1 - - return row, col + return AXTable.get_cell_coordinates( + obj, prefer_attribute=preferAttribute, find_cell=findCellAncestor) def rowAndColumnSpan(self, obj): if not AXUtilities.is_table_cell_or_header(obj): return -1, -1 - - if AXObject.supports_table_cell(obj): - tableCell = obj.queryTableCell() - try: - rowSpan, colSpan = tableCell.rowSpan, tableCell.columnSpan - except Exception: - tokens = ["SCRIPT UTILITIES: Exception getting table row and col span of", - obj, "via table cell"] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - else: - return rowSpan, colSpan - - parent = AXObject.find_ancestor(obj, AXObject.supports_table) - try: - table = parent.queryTable() - except Exception: - return -1, -1 - - row, col = self.coordinatesForCell(obj) - if (row < 0 or col < 0): - return -1, -1 - - return table.getRowExtentAt(row, col), table.getColumnExtentAt(row, col) + return AXTable.get_cell_spans(obj, prefer_attribute=True) def setSizeUnknown(self, obj): return AXUtilities.is_indeterminate(obj) @@ -4021,12 +3889,12 @@ class Utilities: return AXUtilities.is_indeterminate(obj) def rowAndColumnCount(self, obj, preferAttribute=True): - try: - table = obj.queryTable() - except Exception: + if not AXObject.supports_table(obj): return -1, -1 - return table.nRows, table.nColumns + rows = AXTable.get_row_count(obj, prefer_attribute=preferAttribute) + cols = AXTable.get_column_count(obj, prefer_attribute=preferAttribute) + return rows, cols def _objectBoundsMightBeBogus(self, obj): return False @@ -4042,22 +3910,23 @@ class Utilities: if self._objectMightBeBogus(obj): return False - try: - component = obj.queryComponent() - except Exception: + if not AXObject.supports_component(obj): return False if coordType is None: coordType = Atspi.CoordType.SCREEN - if component.contains(x, y, coordType): - return True + try: + if Atspi.Component.contains(obj, x, y, coordType): + return True - x1, y1 = x + margin, y + margin - if component.contains(x1, y1, coordType): - tokens = ["SCRIPT UTILITIES: ", obj, f"contains ({x1},{y1}); not ({x},{y}"] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - return True + x1, y1 = x + margin, y + margin + if Atspi.Component.contains(obj, x1, y1, coordType): + tokens = ["SCRIPT UTILITIES: ", obj, f"contains ({x1},{y1}); not ({x},{y}"] + debug.printTokens(debug.LEVEL_INFO, tokens, True) + return True + except Exception: + return False return False @@ -4101,17 +3970,21 @@ class Utilities: if self.isHidden(root): return None - try: - component = root.queryComponent() - except Exception: - tokens = ["SCRIPT UTILITIES: Exception querying component of", root] + if not AXObject.supports_component(root): + tokens = ["SCRIPT UTILITIES:", root, "does not support component interface"] debug.printTokens(debug.LEVEL_INFO, tokens, True) return None if coordType is None: coordType = Atspi.CoordType.SCREEN - result = component.getAccessibleAtPoint(x, y, coordType) + try: + result = Atspi.Component.get_accessible_at_point(root, x, y, coordType) + except Exception: + tokens = ["SCRIPT UTILITIES: Exception getting accessible at point for", root] + debug.printTokens(debug.LEVEL_INFO, tokens, True) + return None + tokens = ["SCRIPT UTILITIES: ", result, "is descendant of", root, f"at ({x}, {y})"] debug.printTokens(debug.LEVEL_INFO, tokens, True) return result @@ -4343,10 +4216,11 @@ class Utilities: return string, start, end def visibleRows(self, obj, boundingbox): - try: - table = obj.queryTable() - nRows = table.nRows - except Exception: + if not AXObject.supports_table(obj): + return [] + + nRows = AXTable.get_row_count(obj, prefer_attribute=False) + if nRows < 0: return [] tokens = ["SCRIPT UTILITIES: ", obj, f"has {nRows} rows"] @@ -4360,16 +4234,18 @@ class Utilities: debug.printTokens(debug.LEVEL_INFO, tokens, True) # Just in case the row above is a static header row in a scrollable table. - try: - extents = cell.queryComponent().getExtents(Atspi.CoordType.SCREEN) - except Exception: - nextIndex = startIndex + if cell and AXObject.supports_component(cell): + try: + extents = Atspi.Component.get_extents(cell, Atspi.CoordType.SCREEN) + cell = self.descendantAtPoint(obj, x, y + extents.height + 1) + row, col = self.coordinatesForCell(cell) + nextIndex = max(startIndex, row) + tokens = ["SCRIPT UTILITIES: Next cell:", cell, f"(row: {row}"] + debug.printTokens(debug.LEVEL_INFO, tokens, True) + except Exception: + nextIndex = startIndex else: - cell = self.descendantAtPoint(obj, x, y + extents.height + 1) - row, col = self.coordinatesForCell(cell) - nextIndex = max(startIndex, row) - tokens = ["SCRIPT UTILITIES: Next cell:", cell, f"(row: {row}"] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + nextIndex = startIndex cell = self.descendantAtPoint(obj, x, y + height - 1) row, col = self.coordinatesForCell(cell) @@ -4387,14 +4263,14 @@ class Utilities: return rows def getVisibleTableCells(self, obj): - try: - table = obj.queryTable() - except Exception: + if not AXObject.supports_table(obj): + return [] + + if not AXObject.supports_component(obj): return [] try: - component = obj.queryComponent() - extents = component.getExtents(Atspi.CoordType.SCREEN) + extents = Atspi.Component.get_extents(obj, Atspi.CoordType.SCREEN) except Exception: tokens = ["SCRIPT UTILITIES: Exception getting extents of", obj] debug.printTokens(debug.LEVEL_INFO, tokens, True) @@ -4410,12 +4286,15 @@ class Utilities: cells = [] for col in range(colStartIndex, colEndIndex): - colHeader = table.getColumnHeader(col) - if colHeader: - cells.append(colHeader) + try: + colHeader = Atspi.Table.get_column_header(obj, col) + if colHeader: + cells.append(colHeader) + except Exception: + pass for row in rows: try: - cell = table.getAccessibleAt(row, col) + cell = Atspi.Table.get_accessible_at(obj, row, col) except Exception: continue if cell and self.isOnScreen(cell): @@ -4430,31 +4309,40 @@ class Utilities: return startIndex, endIndex parent = self.getTable(obj) - try: - component = parent.queryComponent() - except Exception: - tokens = ["SCRIPT UTILITIES: Exception querying component interface of", parent] + if not parent or not AXObject.supports_component(parent): + tokens = ["SCRIPT UTILITIES:", parent, "does not support component interface"] debug.printTokens(debug.LEVEL_INFO, tokens, True) return startIndex, endIndex - x, y, width, height = component.getExtents(Atspi.CoordType.SCREEN) - cell = component.getAccessibleAtPoint(x+1, y, Atspi.CoordType.SCREEN) - if cell: - row, column = self.coordinatesForCell(cell) - startIndex = column + try: + extents = Atspi.Component.get_extents(parent, Atspi.CoordType.SCREEN) + x, y, width, height = extents.x, extents.y, extents.width, extents.height + except Exception: + tokens = ["SCRIPT UTILITIES: Exception getting extents of", parent] + debug.printTokens(debug.LEVEL_INFO, tokens, True) + return startIndex, endIndex - cell = component.getAccessibleAtPoint(x+width-1, y, Atspi.CoordType.SCREEN) - if cell: - row, column = self.coordinatesForCell(cell) - endIndex = column + 1 + try: + cell = Atspi.Component.get_accessible_at_point(parent, x+1, y, Atspi.CoordType.SCREEN) + if cell: + row, column = self.coordinatesForCell(cell) + startIndex = column + except Exception: + pass + + try: + cell = Atspi.Component.get_accessible_at_point(parent, x+width-1, y, Atspi.CoordType.SCREEN) + if cell: + row, column = self.coordinatesForCell(cell) + endIndex = column + 1 + except Exception: + pass return startIndex, endIndex def getShowingCellsInSameRow(self, obj, forceFullRow=False): parent = self.getTable(obj) - try: - table = parent.queryTable() - except Exception: + if not parent or not AXObject.supports_table(parent): tokens = ["SCRIPT UTILITIES: Exception querying table interface of", parent] debug.printTokens(debug.LEVEL_INFO, tokens, True) return [] @@ -4464,7 +4352,7 @@ class Utilities: return [] if forceFullRow: - startIndex, endIndex = 0, table.nColumns + startIndex, endIndex = 0, AXTable.get_column_count(parent, prefer_attribute=False) else: startIndex, endIndex = self._getTableRowRange(obj) if startIndex == endIndex: @@ -4472,19 +4360,17 @@ class Utilities: cells = [] for i in range(startIndex, endIndex): - cell = table.getAccessibleAt(row, i) - if AXUtilities.is_showing(cell): + cell = AXTable.get_cell_at(parent, row, i) + if cell and AXUtilities.is_showing(cell): cells.append(cell) return cells def cellForCoordinates(self, obj, row, column, showingOnly=False): - try: - table = obj.queryTable() - except Exception: + if not AXObject.supports_table(obj): return None - cell = table.getAccessibleAt(row, column) + cell = AXTable.get_cell_at(obj, row, column) if not showingOnly: return cell @@ -4497,28 +4383,20 @@ class Utilities: if not AXUtilities.is_table_cell(obj): return False - parent = AXObject.find_ancestor(obj, AXObject.supports_table) - try: - table = parent.queryTable() - except Exception: + table = AXTable.get_table(obj) + if table is None: return False row, col = self.coordinatesForCell(obj) - return row + 1 == table.nRows and col + 1 == table.nColumns + rows = AXTable.get_row_count(table, prefer_attribute=False) + cols = AXTable.get_column_count(table, prefer_attribute=False) + return row + 1 == rows and col + 1 == cols def isNonUniformTable(self, obj, maxRows=25, maxCols=25): - try: - table = obj.queryTable() - except Exception: + if not AXObject.supports_table(obj): return False - for r in range(min(maxRows, table.nRows)): - for c in range(min(maxCols, table.nColumns)): - if table.getRowExtentAt(r, c) > 1 \ - or table.getColumnExtentAt(r, c) > 1: - return True - - return False + return AXTable.is_non_uniform_table(obj, maxRows, maxCols) def isShowingAndVisible(self, obj): if AXUtilities.is_showing(obj) and AXUtilities.is_visible(obj): @@ -4786,254 +4664,108 @@ class Utilities: return event and event.isFromApplication(self._script.app) def lastInputEventWasPrintableKey(self): - event = cthulhu_state.lastInputEvent - if not isinstance(event, input_event.KeyboardEvent): - return False - - return event.is_printable_key() + return input_event_manager.get_manager().last_event_was_printable_key() def lastInputEventWasCommand(self): - keyString, mods = self.lastKeyAndModifiers() - return mods & keybindings.CTRL_MODIFIER_MASK + return input_event_manager.get_manager().last_event_was_command() def lastInputEventWasPageSwitch(self): - keyString, mods = self.lastKeyAndModifiers() - if keyString.isnumeric(): - return mods & keybindings.ALT_MODIFIER_MASK - - if keyString in ["Page_Up", "Page_Down"]: - return mods & keybindings.CTRL_MODIFIER_MASK - - return False + return input_event_manager.get_manager().last_event_was_page_switch() def lastInputEventWasUnmodifiedArrow(self): - keyString, mods = self.lastKeyAndModifiers() - if keyString not in ["Left", "Right", "Up", "Down"]: - return False - - if mods & keybindings.CTRL_MODIFIER_MASK \ - or mods & keybindings.SHIFT_MODIFIER_MASK \ - or mods & keybindings.ALT_MODIFIER_MASK \ - or mods & keybindings.CTHULHU_MODIFIER_MASK: - return False - - return True + return input_event_manager.get_manager().last_event_was_unmodified_arrow() def lastInputEventWasCaretNav(self): - return self.lastInputEventWasCharNav() \ - or self.lastInputEventWasWordNav() \ - or self.lastInputEventWasLineNav() \ - or self.lastInputEventWasLineBoundaryNav() + return input_event_manager.get_manager().last_event_was_caret_navigation() def lastInputEventWasCharNav(self): - keyString, mods = self.lastKeyAndModifiers() - if keyString not in ["Left", "Right"]: - return False - - if mods & keybindings.CTRL_MODIFIER_MASK \ - or mods & keybindings.ALT_MODIFIER_MASK: - return False - - return True + return input_event_manager.get_manager().last_event_was_character_navigation() def lastInputEventWasWordNav(self): - keyString, mods = self.lastKeyAndModifiers() - if keyString not in ["Left", "Right"]: - return False - - return mods & keybindings.CTRL_MODIFIER_MASK + return input_event_manager.get_manager().last_event_was_word_navigation() def lastInputEventWasPrevWordNav(self): - keyString, mods = self.lastKeyAndModifiers() - if not keyString == "Left": - return False - - return mods & keybindings.CTRL_MODIFIER_MASK + return input_event_manager.get_manager().last_event_was_previous_word_navigation() def lastInputEventWasNextWordNav(self): - keyString, mods = self.lastKeyAndModifiers() - if not keyString == "Right": - return False - - return mods & keybindings.CTRL_MODIFIER_MASK + return input_event_manager.get_manager().last_event_was_next_word_navigation() def lastInputEventWasLineNav(self): - keyString, mods = self.lastKeyAndModifiers() - if keyString not in ["Up", "Down"]: + if not input_event_manager.get_manager().last_event_was_line_navigation(): return False if self.isEditableDescendantOfComboBox(cthulhu_state.locusOfFocus): return False - return not (mods & keybindings.CTRL_MODIFIER_MASK) - - def lastInputEventWasLineBoundaryNav(self): - keyString, mods = self.lastKeyAndModifiers() - if keyString not in ["Home", "End"]: - return False - - return not (mods & keybindings.CTRL_MODIFIER_MASK) - - def lastInputEventWasPageNav(self): - keyString, mods = self.lastKeyAndModifiers() - if keyString not in ["Page_Up", "Page_Down"]: - return False - - if self.isEditableDescendantOfComboBox(cthulhu_state.locusOfFocus): - return False - - return not (mods & keybindings.CTRL_MODIFIER_MASK) - - def lastInputEventWasFileBoundaryNav(self): - keyString, mods = self.lastKeyAndModifiers() - if keyString not in ["Home", "End"]: - return False - - return mods & keybindings.CTRL_MODIFIER_MASK - - def lastInputEventWasCaretNavWithSelection(self): - keyString, mods = self.lastKeyAndModifiers() - if mods & keybindings.SHIFT_MODIFIER_MASK: - return keyString in ["Home", "End", "Up", "Down", "Left", "Right"] - - return False - - def lastInputEventWasUndo(self): - keycode, mods = self._lastKeyCodeAndModifiers() - keynames = self._allNamesForKeyCode(keycode) - if 'z' not in keynames: - return False - - if mods & keybindings.CTRL_MODIFIER_MASK: - return not (mods & keybindings.SHIFT_MODIFIER_MASK) - - return False - - def lastInputEventWasRedo(self): - keycode, mods = self._lastKeyCodeAndModifiers() - keynames = self._allNamesForKeyCode(keycode) - if 'z' not in keynames: - return False - - if mods & keybindings.CTRL_MODIFIER_MASK: - return mods & keybindings.SHIFT_MODIFIER_MASK - - return False - - def lastInputEventWasCut(self): - keycode, mods = self._lastKeyCodeAndModifiers() - keynames = self._allNamesForKeyCode(keycode) - if 'x' not in keynames: - return False - - if mods & keybindings.CTRL_MODIFIER_MASK: - return not (mods & keybindings.SHIFT_MODIFIER_MASK) - - return False - - def lastInputEventWasCopy(self): - keycode, mods = self._lastKeyCodeAndModifiers() - keynames = self._allNamesForKeyCode(keycode) - if 'c' not in keynames: - return False - - if mods & keybindings.CTRL_MODIFIER_MASK: - return not (mods & keybindings.SHIFT_MODIFIER_MASK) - - return False - - def lastInputEventWasPaste(self): - keycode, mods = self._lastKeyCodeAndModifiers() - keynames = self._allNamesForKeyCode(keycode) - if 'v' not in keynames: - return False - - if mods & keybindings.CTRL_MODIFIER_MASK: - return not (mods & keybindings.SHIFT_MODIFIER_MASK) - - return False - - def lastInputEventWasSelectAll(self): - keycode, mods = self._lastKeyCodeAndModifiers() - keynames = self._allNamesForKeyCode(keycode) - if 'a' not in keynames: - return False - - if mods & keybindings.CTRL_MODIFIER_MASK: - return not (mods & keybindings.SHIFT_MODIFIER_MASK) - - return False - - def lastInputEventWasDelete(self): - keyString, mods = self.lastKeyAndModifiers() - if keyString in ["Delete", "KP_Delete"]: - return True - - keycode, mods = self._lastKeyCodeAndModifiers() - keynames = self._allNamesForKeyCode(keycode) - if 'd' not in keynames: - return False - - return mods & keybindings.CTRL_MODIFIER_MASK - - def lastInputEventWasTab(self): - keyString, mods = self.lastKeyAndModifiers() - if keyString not in ["Tab", "ISO_Left_Tab"]: - return False - - if mods & keybindings.CTRL_MODIFIER_MASK \ - or mods & keybindings.ALT_MODIFIER_MASK \ - or mods & keybindings.CTHULHU_MODIFIER_MASK: - return False - return True + def lastInputEventWasLineBoundaryNav(self): + return input_event_manager.get_manager().last_event_was_line_boundary_navigation() + + def lastInputEventWasPageNav(self): + if not input_event_manager.get_manager().last_event_was_page_navigation(): + return False + + if self.isEditableDescendantOfComboBox(cthulhu_state.locusOfFocus): + return False + + return True + + def lastInputEventWasFileBoundaryNav(self): + return input_event_manager.get_manager().last_event_was_file_boundary_navigation() + + def lastInputEventWasCaretNavWithSelection(self): + return input_event_manager.get_manager().last_event_was_caret_selection() + + def lastInputEventWasUndo(self): + return input_event_manager.get_manager().last_event_was_undo() + + def lastInputEventWasRedo(self): + return input_event_manager.get_manager().last_event_was_redo() + + def lastInputEventWasCut(self): + return input_event_manager.get_manager().last_event_was_cut() + + def lastInputEventWasCopy(self): + return input_event_manager.get_manager().last_event_was_copy() + + def lastInputEventWasPaste(self): + return input_event_manager.get_manager().last_event_was_paste() + + def lastInputEventWasSelectAll(self): + return input_event_manager.get_manager().last_event_was_select_all() + + def lastInputEventWasDelete(self): + return input_event_manager.get_manager().last_event_was_delete() + + def lastInputEventWasTab(self): + return input_event_manager.get_manager().last_event_was_tab() + def lastInputEventWasMouseButton(self): - return isinstance(cthulhu_state.lastInputEvent, input_event.MouseButtonEvent) + return input_event_manager.get_manager().last_event_was_mouse_button() def lastInputEventWasPrimaryMouseClick(self): - event = cthulhu_state.lastInputEvent - if isinstance(event, input_event.MouseButtonEvent): - return event.button == "1" and event.pressed - - return False + return input_event_manager.get_manager().last_event_was_primary_click() def lastInputEventWasMiddleMouseClick(self): - event = cthulhu_state.lastInputEvent - if isinstance(event, input_event.MouseButtonEvent): - return event.button == "2" and event.pressed - - return False + return input_event_manager.get_manager().last_event_was_middle_click() def lastInputEventWasSecondaryMouseClick(self): - event = cthulhu_state.lastInputEvent - if isinstance(event, input_event.MouseButtonEvent): - return event.button == "3" and event.pressed - - return False + return input_event_manager.get_manager().last_event_was_secondary_click() def lastInputEventWasPrimaryMouseRelease(self): - event = cthulhu_state.lastInputEvent - if isinstance(event, input_event.MouseButtonEvent): - return event.button == "1" and not event.pressed - - return False + return input_event_manager.get_manager().last_event_was_primary_release() def lastInputEventWasMiddleMouseRelease(self): - event = cthulhu_state.lastInputEvent - if isinstance(event, input_event.MouseButtonEvent): - return event.button == "2" and not event.pressed - - return False + return input_event_manager.get_manager().last_event_was_middle_release() def lastInputEventWasSecondaryMouseRelease(self): - event = cthulhu_state.lastInputEvent - if isinstance(event, input_event.MouseButtonEvent): - return event.button == "3" and not event.pressed - - return False + return input_event_manager.get_manager().last_event_was_secondary_release() def lastInputEventWasTableSort(self, delta=0.5): + if not input_event_manager.get_manager().last_event_was_table_sort(): + return False + event = cthulhu_state.lastInputEvent if not event: return False @@ -5351,14 +5083,18 @@ class Utilities: if not AXObject.supports_table(obj): return False - table = obj.queryTable() - if table.nSelectedRows == table.nRows: - msg = f"SCRIPT UTILITIES: All {table.nRows} rows believed to be selected" + rows = AXTable.get_row_count(obj, prefer_attribute=False) + cols = AXTable.get_column_count(obj, prefer_attribute=False) + selected_rows = AXTable.get_selected_row_count(obj) + selected_cols = AXTable.get_selected_column_count(obj) + + if selected_rows == rows: + msg = f"SCRIPT UTILITIES: All {rows} rows believed to be selected" debug.printMessage(debug.LEVEL_INFO, msg, True) return True - if table.nSelectedColumns == table.nColumns: - msg = f"SCRIPT UTILITIES: All {table.nColumns} columns believed to be selected" + if selected_cols == cols: + msg = f"SCRIPT UTILITIES: All {cols} columns believed to be selected" debug.printMessage(debug.LEVEL_INFO, msg, True) return True diff --git a/src/cthulhu/scripts/apps/Banshee/script.py b/src/cthulhu/scripts/apps/Banshee/script.py index cb10841..21f71f2 100644 --- a/src/cthulhu/scripts/apps/Banshee/script.py +++ b/src/cthulhu/scripts/apps/Banshee/script.py @@ -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) diff --git a/src/cthulhu/scripts/apps/Banshee/script_utilities.py b/src/cthulhu/scripts/apps/Banshee/script_utilities.py index b70f41c..f113449 100644 --- a/src/cthulhu/scripts/apps/Banshee/script_utilities.py +++ b/src/cthulhu/scripts/apps/Banshee/script_utilities.py @@ -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) diff --git a/src/cthulhu/scripts/apps/notify-osd/script.py b/src/cthulhu/scripts/apps/notify-osd/script.py index 3ee1b0c..767a3fa 100644 --- a/src/cthulhu/scripts/apps/notify-osd/script.py +++ b/src/cthulhu/scripts/apps/notify-osd/script.py @@ -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 = "" diff --git a/src/cthulhu/scripts/apps/pidgin/script_utilities.py b/src/cthulhu/scripts/apps/pidgin/script_utilities.py index a16e090..fab236e 100644 --- a/src/cthulhu/scripts/apps/pidgin/script_utilities.py +++ b/src/cthulhu/scripts/apps/pidgin/script_utilities.py @@ -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 = [] diff --git a/src/cthulhu/scripts/apps/soffice/script.py b/src/cthulhu/scripts/apps/soffice/script.py index dd3e9bd..b6dab74 100644 --- a/src/cthulhu/scripts/apps/soffice/script.py +++ b/src/cthulhu/scripts/apps/soffice/script.py @@ -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: diff --git a/src/cthulhu/scripts/apps/soffice/script_utilities.py b/src/cthulhu/scripts/apps/soffice/script_utilities.py index 1e667be..bbf266b 100644 --- a/src/cthulhu/scripts/apps/soffice/script_utilities.py +++ b/src/cthulhu/scripts/apps/soffice/script_utilities.py @@ -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 diff --git a/src/cthulhu/scripts/apps/soffice/speech_generator.py b/src/cthulhu/scripts/apps/soffice/speech_generator.py index 2970d42..db8645c 100644 --- a/src/cthulhu/scripts/apps/soffice/speech_generator.py +++ b/src/cthulhu/scripts/apps/soffice/speech_generator.py @@ -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 diff --git a/src/cthulhu/scripts/default.py b/src/cthulhu/scripts/default.py index b01e290..5199f32 100644 --- a/src/cthulhu/scripts/default.py +++ b/src/cthulhu/scripts/default.py @@ -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"]): diff --git a/src/cthulhu/scripts/toolkits/J2SE-access-bridge/script_utilities.py b/src/cthulhu/scripts/toolkits/J2SE-access-bridge/script_utilities.py index b64f15e..1773dad 100644 --- a/src/cthulhu/scripts/toolkits/J2SE-access-bridge/script_utilities.py +++ b/src/cthulhu/scripts/toolkits/J2SE-access-bridge/script_utilities.py @@ -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) diff --git a/src/cthulhu/scripts/toolkits/WebKitGtk/script.py b/src/cthulhu/scripts/toolkits/WebKitGtk/script.py index 883bc6f..748d8b6 100644 --- a/src/cthulhu/scripts/toolkits/WebKitGtk/script.py +++ b/src/cthulhu/scripts/toolkits/WebKitGtk/script.py @@ -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) diff --git a/src/cthulhu/scripts/toolkits/WebKitGtk/script_utilities.py b/src/cthulhu/scripts/toolkits/WebKitGtk/script_utilities.py index 094b711..7efa861 100644 --- a/src/cthulhu/scripts/toolkits/WebKitGtk/script_utilities.py +++ b/src/cthulhu/scripts/toolkits/WebKitGtk/script_utilities.py @@ -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])) diff --git a/src/cthulhu/scripts/toolkits/gtk/script_utilities.py b/src/cthulhu/scripts/toolkits/gtk/script_utilities.py index 62c31e7..449c22f 100644 --- a/src/cthulhu/scripts/toolkits/gtk/script_utilities.py +++ b/src/cthulhu/scripts/toolkits/gtk/script_utilities.py @@ -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): diff --git a/src/cthulhu/scripts/web/script.py b/src/cthulhu/scripts/web/script.py index 5a309e6..80dadbd 100644 --- a/src/cthulhu/scripts/web/script.py +++ b/src/cthulhu/scripts/web/script.py @@ -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 diff --git a/src/cthulhu/scripts/web/script_utilities.py b/src/cthulhu/scripts/web/script_utilities.py index d1d3c31..7655198 100644 --- a/src/cthulhu/scripts/web/script_utilities.py +++ b/src/cthulhu/scripts/web/script_utilities.py @@ -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"), "'"] diff --git a/src/cthulhu/scripts/web/speech_generator.py b/src/cthulhu/scripts/web/speech_generator.py index 2cf417e..d21c176 100644 --- a/src/cthulhu/scripts/web/speech_generator.py +++ b/src/cthulhu/scripts/web/speech_generator.py @@ -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)) diff --git a/src/cthulhu/speech_generator.py b/src/cthulhu/speech_generator.py index 74c62cc..50495a1 100644 --- a/src/cthulhu/speech_generator.py +++ b/src/cthulhu/speech_generator.py @@ -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 diff --git a/src/cthulhu/structural_navigation.py b/src/cthulhu/structural_navigation.py index a385b4f..3abe229 100644 --- a/src/cthulhu/structural_navigation.py +++ b/src/cthulhu/structural_navigation.py @@ -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) diff --git a/src/cthulhu/tutorialgenerator.py b/src/cthulhu/tutorialgenerator.py index 658e11d..670ee71 100644 --- a/src/cthulhu/tutorialgenerator.py +++ b/src/cthulhu/tutorialgenerator.py @@ -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,