%PDF- %PDF-
| Direktori : /lib/python3/dist-packages/orca/ |
| Current File : //lib/python3/dist-packages/orca/script_utilities.py |
# Orca
#
# Copyright 2010 Joanmarie Diggs.
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
# Boston MA 02110-1301 USA.
"""Commonly-required utility methods needed by -- and potentially
customized by -- application and toolkit scripts. They have
been pulled out from the scripts because certain scripts had
gotten way too large as a result of including these methods."""
__id__ = "$Id$"
__version__ = "$Revision$"
__date__ = "$Date$"
__copyright__ = "Copyright (c) 2010 Joanmarie Diggs."
__license__ = "LGPL"
import gi
import locale
import re
import time
from difflib import SequenceMatcher
gi.require_version("Atspi", "2.0")
from gi.repository import Atspi
from gi.repository import Gdk
from gi.repository import Gtk
from . import colornames
from . import debug
from . import focus_manager
from . import keynames
from . import keybindings
from . import input_event
from . import mathsymbols
from . import messages
from . import orca_state
from . import object_properties
from . import pronunciation_dict
from . import script_manager
from . import settings
from . import settings_manager
from . import text_attribute_names
from .ax_component import AXComponent
from .ax_hypertext import AXHypertext
from .ax_object import AXObject
from .ax_selection import AXSelection
from .ax_table import AXTable
from .ax_text import AXText
from .ax_utilities import AXUtilities
from .ax_value import AXValue
#############################################################################
# #
# Utilities #
# #
#############################################################################
class Utilities:
_last_clipboard_update = time.time()
EMBEDDED_OBJECT_CHARACTER = '\ufffc'
ZERO_WIDTH_NO_BREAK_SPACE = '\ufeff'
SUPERSCRIPT_DIGITS = \
['\u2070', '\u00b9', '\u00b2', '\u00b3', '\u2074',
'\u2075', '\u2076', '\u2077', '\u2078', '\u2079']
SUBSCRIPT_DIGITS = \
['\u2080', '\u2081', '\u2082', '\u2083', '\u2084',
'\u2085', '\u2086', '\u2087', '\u2088', '\u2089']
flags = re.UNICODE
WORDS_RE = re.compile(r"(\W+)", flags)
SUPERSCRIPTS_RE = re.compile(f"[{''.join(SUPERSCRIPT_DIGITS)}]+", flags)
SUBSCRIPTS_RE = re.compile(f"[{''.join(SUBSCRIPT_DIGITS)}]+", flags)
PUNCTUATION = re.compile(r"[^\w\s]", flags)
# generatorCache
#
DISPLAYED_DESCRIPTION = 'displayedDescription'
DISPLAYED_LABEL = 'displayedLabel'
DISPLAYED_TEXT = 'displayedText'
KEY_BINDING = 'keyBinding'
NESTING_LEVEL = 'nestingLevel'
NODE_LEVEL = 'nodeLevel'
def __init__(self, script):
"""Creates an instance of the Utilities class.
Arguments:
- script: the script with which this instance is associated.
"""
self._script = script
self._clipboardHandlerId = None
self._selectedMenuBarMenu = {}
#########################################################################
# #
# Utilities for finding, identifying, and comparing accessibles #
# #
#########################################################################
def childNodes(self, obj):
"""Gets all of the children that have RELATION_NODE_CHILD_OF pointing
to this expanded table cell.
Arguments:
-obj: the Accessible Object
Returns: a list of all the child nodes
"""
if not AXUtilities.is_expanded(obj):
return []
parent = AXTable.get_table(obj)
if parent is None:
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
# we'll do an old-school, row-by-row search for child nodes.
def pred(x):
return AXObject.get_index_in_parent(x) >= 0
nodes = AXObject.get_relation_targets(obj, Atspi.RelationType.NODE_PARENT_OF, pred)
tokens = ["SCRIPT UTILITIES:", len(nodes), "child nodes for", obj, "via node-parent-of"]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
if nodes:
return nodes
# Candidates will be in the rows beneath the current row.
# Only check in the current column and stop checking as
# soon as the node level of a candidate is equal or less
# than our current level.
#
row, col = AXTable.get_cell_coordinates(obj, prefer_attribute=False)
nodeLevel = self.nodeLevel(obj)
for i in range(row + 1, AXTable.get_row_count(parent, prefer_attribute=False)):
cell = AXTable.get_cell_at(parent, i, col)
relation = AXObject.get_relation(cell, Atspi.RelationType.NODE_CHILD_OF)
if not relation:
continue
nodeOf = relation.get_target(0)
if self.isSameObject(obj, nodeOf):
nodes.append(cell)
elif self.nodeLevel(nodeOf) <= nodeLevel:
break
tokens = ["SCRIPT UTILITIES:", len(nodes), "child nodes for", obj, "via node-child-of"]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return nodes
def commonAncestor(self, a, b):
"""Finds the common ancestor between Accessible a and Accessible b.
Arguments:
- a: Accessible
- b: Accessible
"""
tokens = ["SCRIPT UTILITIES: Looking for common ancestor of", a, "and", b]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
if not (a and b):
return None
if a == b:
return a
aParents = [a]
parent = AXObject.get_parent_checked(a)
while parent:
aParents.append(parent)
parent = AXObject.get_parent_checked(parent)
aParents.reverse()
bParents = [b]
parent = AXObject.get_parent_checked(b)
while parent:
bParents.append(parent)
parent = AXObject.get_parent_checked(parent)
bParents.reverse()
commonAncestor = None
maxSearch = min(len(aParents), len(bParents))
i = 0
while i < maxSearch:
if self.isSameObject(aParents[i], bParents[i]):
commonAncestor = aParents[i]
i += 1
else:
break
tokens = ["SCRIPT UTILITIES: Common ancestor of", a, "and", b, "is", commonAncestor]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return commonAncestor
def displayedLabel(self, obj):
"""If there is an object labelling the given object, return the
text being displayed for the object labelling this object.
Otherwise, return None.
Argument:
- obj: the object in question
Returns the string of the object labelling this object, or None
if there is nothing of interest here.
"""
try:
return self._script.generatorCache[self.DISPLAYED_LABEL][obj]
except Exception:
if self.DISPLAYED_LABEL not in self._script.generatorCache:
self._script.generatorCache[self.DISPLAYED_LABEL] = {}
labelString = None
labels = self.labelsForObject(obj)
for label in labels:
labelString = \
self.appendString(labelString, self.displayedText(label))
self._script.generatorCache[self.DISPLAYED_LABEL][obj] = labelString
return self._script.generatorCache[self.DISPLAYED_LABEL][obj]
def preferDescriptionOverName(self, obj):
return False
def descriptionsForObject(self, obj):
"""Return a list of objects describing obj."""
descriptions = AXObject.get_relation_targets(obj, Atspi.RelationType.DESCRIBED_BY)
if not descriptions:
return []
labels = AXObject.get_relation_targets(obj, Atspi.RelationType.LABELLED_BY)
if descriptions == labels:
tokens = ["SCRIPT UTILITIES:", obj,
"'s described-by targets are the same as labelled-by targets"]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return []
return descriptions
def detailsContentForObject(self, obj):
details = self.detailsForObject(obj)
return list(map(self.displayedText, details))
def detailsForObject(self, obj, textOnly=True):
"""Return a list of objects containing details for obj."""
details = AXObject.get_relation_targets(obj, Atspi.RelationType.DETAILS)
if not details and AXUtilities.is_toggle_button(obj) \
and AXUtilities.is_expanded(obj):
details = [child for child in AXObject.iter_children(obj)]
if not textOnly:
return details
textObjects = []
for detail in details:
textObjects.extend(self.findAllDescendants(
detail, lambda x: not AXText.is_whitespace_or_empty(x)))
return textObjects
def displayedDescription(self, obj):
"""Returns the text being displayed for the object describing obj."""
try:
return self._script.generatorCache[self.DISPLAYED_DESCRIPTION][obj]
except Exception:
if self.DISPLAYED_DESCRIPTION not in self._script.generatorCache:
self._script.generatorCache[self.DISPLAYED_DESCRIPTION] = {}
string = " ".join(map(self.displayedText, self.descriptionsForObject(obj)))
self._script.generatorCache[self.DISPLAYED_DESCRIPTION][obj] = string
return self._script.generatorCache[self.DISPLAYED_DESCRIPTION][obj]
def displayedText(self, obj):
"""Returns the text being displayed for an object.
Arguments:
- obj: the object
Returns the text being displayed for an object or None if there isn't
any text being shown.
"""
# TODO - JD: It's finally time to consider killing this for real.
try:
return self._script.generatorCache[self.DISPLAYED_TEXT][obj]
except Exception:
displayedText = None
name = AXObject.get_name(obj)
role = AXObject.get_role(obj)
if role in [Atspi.Role.PUSH_BUTTON, Atspi.Role.LABEL] and name:
return name
displayedText = AXText.get_all_text(obj)
if self.EMBEDDED_OBJECT_CHARACTER in displayedText:
displayedText = None
if not displayedText and role not in [Atspi.Role.COMBO_BOX, Atspi.Role.SPIN_BUTTON]:
# TODO - JD: This should probably get nuked. But all sorts of
# existing code might be relying upon this bogus hack. So it
# will need thorough testing when removed.
displayedText = name
if not displayedText and role in [Atspi.Role.PUSH_BUTTON, Atspi.Role.LIST_ITEM]:
labels = self.unrelatedLabels(obj, minimumWords=1)
if not labels:
labels = self.unrelatedLabels(obj, onlyShowing=False, minimumWords=1)
displayedText = " ".join(map(self.displayedText, labels))
if self.DISPLAYED_TEXT not in self._script.generatorCache:
self._script.generatorCache[self.DISPLAYED_TEXT] = {}
self._script.generatorCache[self.DISPLAYED_TEXT][obj] = displayedText
return self._script.generatorCache[self.DISPLAYED_TEXT][obj]
def documentFrame(self, obj=None):
"""Returns the document frame which is displaying the content.
Note that this is intended primarily for web content."""
if not obj:
obj, offset = self.getCaretContext()
document = AXObject.find_ancestor(obj, AXUtilities.is_document)
if document:
return document
focus = focus_manager.getManager().get_locus_of_focus()
if AXUtilities.is_document(focus):
return focus
return None
def frameAndDialog(self, obj):
"""Returns the frame and (possibly) the dialog containing obj."""
results = [None, None]
obj = obj or focus_manager.getManager().get_locus_of_focus()
if not obj:
msg = "SCRIPT UTILITIES: frameAndDialog() called without valid object"
debug.printMessage(debug.LEVEL_INFO, msg, True)
return results
topLevel = self.topLevelObject(obj)
if topLevel is None:
tokens = ["SCRIPT UTILITIES: could not find top-level object for", obj]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return results
dialog_roles = [Atspi.Role.DIALOG, Atspi.Role.FILE_CHOOSER]
if self._treatAlertsAsDialogs():
dialog_roles.append(Atspi.Role.ALERT)
role = AXObject.get_role(topLevel)
if role in dialog_roles:
results[1] = topLevel
else:
if role in [Atspi.Role.FRAME, Atspi.Role.WINDOW]:
results[0] = topLevel
def isDialog(x):
return AXObject.get_role(x) in dialog_roles
if isDialog(obj):
results[1] = obj
else:
results[1] = AXObject.find_ancestor(obj, isDialog)
tokens = ["SCRIPT UTILITIES:", obj, "is in frame", results[0], "and dialog", results[1]]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return results
def presentEventFromNonShowingObject(self, event):
if event.source == focus_manager.getManager().get_locus_of_focus():
return True
return False
def grabFocusWhenSettingCaret(self, obj):
return AXUtilities.is_focusable(obj)
def grabFocusBeforeRouting(self, obj, offset):
"""Whether or not we should perform a grabFocus before routing
the cursor via the braille cursor routing keys.
Arguments:
- obj: the accessible object where the cursor should be routed
- offset: the offset to which it should be routed
Returns True if we should do an explicit grabFocus on obj prior
to routing the cursor.
"""
return AXUtilities.is_combo_box(obj) \
and not self.isSameObject(obj, focus_manager.getManager().get_locus_of_focus())
def hasMatchingHierarchy(self, obj, rolesList):
"""Called to determine if the given object and it's hierarchy of
parent objects, each have the desired roles. Please note: You
should strongly consider an alternative means for determining
that a given object is the desired item. Failing that, you should
include only enough of the hierarchy to make the determination.
If the developer of the application you are providing access to
does so much as add an Adjustment to reposition a widget, this
method can fail. You have been warned.
Arguments:
- obj: the accessible object to check.
- rolesList: the list of desired roles for the components and the
hierarchy of its parents.
Returns True if all roles match.
"""
current = obj
for role in rolesList:
if current is None:
return False
if not isinstance(role, list):
role = [role]
if isinstance(role[0], str):
current_role = AXObject.get_role_name(current)
else:
current_role = AXObject.get_role(current)
if current_role not in role:
return False
current = AXObject.get_parent_checked(current)
return True
def inFindContainer(self, obj=None):
if obj is None:
obj = focus_manager.getManager().get_locus_of_focus()
if not AXUtilities.is_entry(obj):
return False
return AXObject.find_ancestor(obj, AXUtilities.is_tool_bar) is not None
def getFindResultsCount(self, root=None):
return ""
def isAnchor(self, obj):
return False
def isCode(self, obj):
return False
def isCodeDescendant(self, obj):
return False
def isDockedFrame(self, obj):
if not AXUtilities.is_frame(obj):
return False
attrs = AXObject.get_attributes_dict(obj)
return attrs.get('window-type') == 'dock'
def isDesktop(self, obj):
if not AXUtilities.is_frame(obj):
return False
attrs = AXObject.get_attributes_dict(obj)
return attrs.get('is-desktop') == 'true'
def isComboBoxWithToggleDescendant(self, obj):
return False
def isToggleDescendantOfComboBox(self, obj):
return False
def isTypeahead(self, obj):
return False
def isOrDescendsFrom(self, obj, ancestor):
if obj == ancestor:
return True
return AXObject.find_ancestor(obj, lambda x: x and x == ancestor)
def isFunctionalDialog(self, obj):
"""Returns True if the window is a functioning as a dialog.
This method should be subclassed by application scripts as
needed.
"""
return False
def isComment(self, obj):
return False
def isContentDeletion(self, obj):
return False
def isContentError(self, obj):
return False
def isContentInsertion(self, obj):
return False
def isContentMarked(self, obj):
return False
def isContentSuggestion(self, obj):
return False
def isInlineSuggestion(self, obj):
return False
def isFirstItemInInlineContentSuggestion(self, obj):
return False
def isLastItemInInlineContentSuggestion(self, obj):
return False
def isEmpty(self, obj):
return False
def isHidden(self, obj):
return False
def isDPub(self, obj):
return False
def isDPubAbstract(self, obj):
return False
def isDPubAcknowledgments(self, obj):
return False
def isDPubAfterword(self, obj):
return False
def isDPubAppendix(self, obj):
return False
def isDPubBibliography(self, obj):
return False
def isDPubBacklink(self, obj):
return False
def isDPubBiblioref(self, obj):
return False
def isDPubChapter(self, obj):
return False
def isDPubColophon(self, obj):
return False
def isDPubConclusion(self, obj):
return False
def isDPubCover(self, obj):
return False
def isDPubCredit(self, obj):
return False
def isDPubCredits(self, obj):
return False
def isDPubDedication(self, obj):
return False
def isDPubEndnote(self, obj):
return False
def isDPubEndnotes(self, obj):
return False
def isDPubEpigraph(self, obj):
return False
def isDPubEpilogue(self, obj):
return False
def isDPubErrata(self, obj):
return False
def isDPubExample(self, obj):
return False
def isDPubFootnote(self, obj):
return False
def isDPubForeword(self, obj):
return False
def isDPubGlossary(self, obj):
return False
def isDPubGlossref(self, obj):
return False
def isDPubIndex(self, obj):
return False
def isDPubIntroduction(self, obj):
return False
def isDPubPagelist(self, obj):
return False
def isDPubPagebreak(self, obj):
return False
def isDPubPart(self, obj):
return False
def isDPubPreface(self, obj):
return False
def isDPubPrologue(self, obj):
return False
def isDPubPullquote(self, obj):
return False
def isDPubQna(self, obj):
return False
def isDPubSubtitle(self, obj):
return False
def isDPubToc(self, obj):
return False
def isFeed(self, obj):
return False
def isFeedArticle(self, obj):
return False
def isFigure(self, obj):
return False
def isGrid(self, obj):
return False
def isGridCell(self, obj):
return False
def isLandmark(self, obj):
return False
def isLandmarkWithoutType(self, obj):
return False
def isLandmarkBanner(self, obj):
return False
def isLandmarkComplementary(self, obj):
return False
def isLandmarkContentInfo(self, obj):
return False
def isLandmarkForm(self, obj):
return False
def isLandmarkMain(self, obj):
return False
def isLandmarkNavigation(self, obj):
return False
def isDPubNoteref(self, obj):
return False
def isLandmarkRegion(self, obj):
return False
def isLandmarkSearch(self, obj):
return False
def isSVG(self, obj):
return False
def speakMathSymbolNames(self, obj=None):
return False
def isInMath(self):
return False
def isMath(self, obj):
return False
def isMathLayoutOnly(self, obj):
return False
def isMathMultiline(self, obj):
return False
def isMathEnclosed(self, obj):
return False
def isMathFenced(self, obj):
return False
def isMathFractionWithoutBar(self, obj):
return False
def isMathPhantom(self, obj):
return False
def isMathMultiScript(self, obj):
return False
def isMathSubOrSuperScript(self, obj):
return False
def isMathUnderOrOverScript(self, obj):
return False
def isMathSquareRoot(self, obj):
return False
def isMathTable(self, obj):
return False
def isMathTableRow(self, obj):
return False
def isMathTableCell(self, obj):
return False
def isMathToken(self, obj):
return False
def isMathTopLevel(self, obj):
return False
def getMathDenominator(self, obj):
return None
def getMathNumerator(self, obj):
return None
def getMathRootBase(self, obj):
return None
def getMathRootIndex(self, obj):
return None
def getMathScriptBase(self, obj):
return None
def getMathScriptSubscript(self, obj):
return None
def getMathScriptSuperscript(self, obj):
return None
def getMathScriptUnderscript(self, obj):
return None
def getMathScriptOverscript(self, obj):
return None
def getMathPrescripts(self, obj):
return []
def getMathPostscripts(self, obj):
return []
def getMathEnclosures(self, obj):
return []
def getMathFencedSeparators(self, obj):
return ['']
def getMathFences(self, obj):
return ['', '']
def getMathNestingLevel(self, obj, test=None):
return 0
def getLandmarkTypes(self):
return ["banner",
"complementary",
"contentinfo",
"doc-acknowledgments",
"doc-afterword",
"doc-appendix",
"doc-bibliography",
"doc-chapter",
"doc-conclusion",
"doc-credits",
"doc-endnotes",
"doc-epilogue",
"doc-errata",
"doc-foreword",
"doc-glossary",
"doc-index",
"doc-introduction",
"doc-pagelist",
"doc-part",
"doc-preface",
"doc-prologue",
"doc-toc",
"form",
"main",
"navigation",
"region",
"search"]
def isProgressBar(self, obj):
if not AXUtilities.is_progress_bar(obj):
return False
return AXValue.get_value_as_percent(obj) is not None
def topLevelObjectIsActiveWindow(self, obj):
return self.isSameObject(
self.topLevelObject(obj), focus_manager.getManager().get_active_window())
def isProgressBarUpdate(self, obj):
if not settings_manager.getManager().getSetting('speakProgressBarUpdates') \
and not settings_manager.getManager().getSetting('brailleProgressBarUpdates') \
and not settings_manager.getManager().getSetting('beepProgressBarUpdates'):
return False, "Updates not enabled"
if not self.isProgressBar(obj):
return False, "Is not progress bar"
if AXComponent.has_no_size(obj):
return False, "Has no size"
if settings_manager.getManager().getSetting('ignoreStatusBarProgressBars'):
if AXObject.find_ancestor(obj, AXUtilities.is_status_bar):
return False, "Is status bar descendant"
verbosity = settings_manager.getManager().getSetting('progressBarVerbosity')
if verbosity == settings.PROGRESS_BAR_ALL:
return True, "Verbosity is all"
if verbosity == settings.PROGRESS_BAR_WINDOW:
if self.topLevelObjectIsActiveWindow(obj):
return True, "Verbosity is window"
return False, "Top-level object is not active window"
if verbosity == settings.PROGRESS_BAR_APPLICATION:
app = AXObject.get_application(obj)
activeApp = script_manager.getManager().getActiveScriptApp()
if app == activeApp:
return True, "Verbosity is app"
return False, "App is not active app"
return True, "Not handled by any other case"
def isBlockquote(self, obj):
return AXUtilities.is_block_quote(obj)
def isDescriptionList(self, obj):
return AXUtilities.is_description_list(obj)
def isDescriptionListTerm(self, obj):
return AXUtilities.is_description_term(obj)
def isDescriptionListDescription(self, obj):
return AXUtilities.is_description_value(obj)
def descriptionListTerms(self, obj):
if not self.isDescriptionList(obj):
return []
_include = self.isDescriptionListTerm
_exclude = self.isDescriptionList
return self.findAllDescendants(obj, _include, _exclude)
def isDocumentList(self, obj):
if AXObject.get_role(obj) not in [Atspi.Role.LIST, Atspi.Role.DESCRIPTION_LIST]:
return False
return AXObject.find_ancestor(obj, AXUtilities.is_document) is not None
def isDocumentPanel(self, obj):
if not AXUtilities.is_panel(obj):
return False
return AXObject.find_ancestor(obj, AXUtilities.is_document) is not None
def isDocument(self, obj):
return AXUtilities.is_document(obj)
def inDocumentContent(self, obj=None):
obj = obj or focus_manager.getManager().get_locus_of_focus()
return self.getDocumentForObject(obj) is not None
def activeDocument(self, window=None):
return self.getTopLevelDocumentForObject(focus_manager.getManager().get_locus_of_focus())
def isTopLevelDocument(self, obj):
return self.isDocument(obj) and not AXObject.find_ancestor(obj, self.isDocument)
def getTopLevelDocumentForObject(self, obj):
if self.isTopLevelDocument(obj):
return obj
return AXObject.find_ancestor(obj, self.isTopLevelDocument)
def getDocumentForObject(self, obj):
if not obj:
return None
if self.isDocument(obj):
return obj
return AXObject.find_ancestor(obj, self.isDocument)
def getModalDialog(self, obj):
if not obj:
return False
if AXUtilities.is_modal_dialog(obj):
return obj
return AXObject.find_ancestor(obj, AXUtilities.is_modal_dialog)
def isModalDialogDescendant(self, obj):
if not obj:
return False
return self.getModalDialog(obj) is not None
def columnConvert(self, column):
return column
def isTextDocumentTable(self, obj):
if not AXUtilities.is_table(obj):
return False
doc = self.getDocumentForObject(obj)
return doc is not None and not AXUtilities.is_document_spreadsheet(doc)
def isGUITable(self, obj):
return AXUtilities.is_table(obj) and self.getDocumentForObject(obj) is None
def isSpreadSheetTable(self, obj):
if not (AXUtilities.is_table(obj) and AXObject.supports_table(obj)):
return False
doc = self.getDocumentForObject(obj)
if doc is None:
return False
if AXUtilities.is_document_spreadsheet(doc):
return True
return AXTable.get_row_count(obj) > 65536
def isTextDocumentCell(self, obj):
if not AXUtilities.is_table_cell_or_header(obj):
return False
return AXObject.find_ancestor(obj, self.isTextDocumentTable)
def isGUICell(self, obj):
if not AXUtilities.is_table_cell_or_header(obj):
return False
return AXObject.find_ancestor(obj, self.isGUITable)
def isSpreadSheetCell(self, obj):
if not AXUtilities.is_table_cell_or_header(obj):
return False
return AXObject.find_ancestor(obj, self.isSpreadSheetTable)
def cellColumnChanged(self, cell, prevCell=None):
column = AXTable.get_cell_coordinates(cell)[1]
if column == -1:
return False
if prevCell is None:
lastColumn = self._script.pointOfReference.get("lastColumn")
else:
lastColumn = AXTable.get_cell_coordinates(prevCell)[1]
return column != lastColumn
def cellRowChanged(self, cell, prevCell=None):
row = AXTable.get_cell_coordinates(cell)[0]
if row == -1:
return False
if prevCell is None:
lastRow = self._script.pointOfReference.get("lastRow")
else:
lastRow = AXTable.get_cell_coordinates(prevCell)[0]
return row != lastRow
def shouldReadFullRow(self, obj, prevObj=None):
if self._script.inSayAll():
return False
if self._script.getTableNavigator().last_input_event_was_navigation_command():
return False
if not self.cellRowChanged(obj, prevObj):
return False
table = AXTable.get_table(obj)
if table is None:
return False
if not self.getDocumentForObject(table):
return settings_manager.getManager().getSetting('readFullRowInGUITable')
if self.isSpreadSheetTable(table):
return settings_manager.getManager().getSetting('readFullRowInSpreadSheet')
return settings_manager.getManager().getSetting('readFullRowInDocumentTable')
def isSorted(self, obj):
return False
def isAscending(self, obj):
return False
def isDescending(self, obj):
return False
def getSortOrderDescription(self, obj, includeName=False):
if not (obj and self.isSorted(obj)):
return ""
if self.isAscending(obj):
result = object_properties.SORT_ORDER_ASCENDING
elif self.isDescending(obj):
result = object_properties.SORT_ORDER_DESCENDING
else:
result = object_properties.SORT_ORDER_OTHER
if includeName and AXObject.get_name(obj):
result = f"{AXObject.get_name(obj)}. {result}"
return result
def isFocusableLabel(self, obj):
return AXUtilities.is_label(obj) and AXUtilities.is_focusable(obj)
def isNonFocusableList(self, obj):
return AXUtilities.is_list(obj) and not AXUtilities.is_focusable(obj)
def isStatusBarNotification(self, obj):
if not AXUtilities.is_notification(obj):
return False
return AXObject.find_ancestor(obj, AXUtilities.is_status_bar) is not None
def getNotificationContent(self, obj):
if not AXUtilities.is_notification(obj):
return ""
tokens = []
name = AXObject.get_name(obj)
if name:
tokens.append(name)
text = self.expandEOCs(obj)
if text and text not in tokens:
tokens.append(text)
else:
labels = " ".join(map(self.displayedText, self.unrelatedLabels(obj, False, 1)))
if labels and labels not in tokens:
tokens.append(labels)
description = AXObject.get_description(obj)
if description and description not in tokens:
tokens.append(description)
return " ".join(tokens)
def isTreeDescendant(self, obj):
if obj is None:
return False
if AXUtilities.is_tree_item(obj):
return True
return AXObject.find_ancestor(obj, AXUtilities.is_tree_or_tree_table) is not None
def isLayoutOnly(self, obj):
"""Returns True if the given object is a container which has
no presentable information (label, name, displayed text, etc.)."""
layoutOnly = False
if not AXObject.is_valid(obj):
return True
role = AXObject.get_role(obj)
parentRole = AXObject.get_role(AXObject.get_parent(obj))
firstChild = AXObject.get_child(obj, 0)
topLevelRoles = self._topLevelRoles()
ignorePanelParent = [Atspi.Role.MENU,
Atspi.Role.MENU_ITEM,
Atspi.Role.LIST_ITEM,
Atspi.Role.TREE_ITEM]
if role == Atspi.Role.TABLE:
layoutOnly = AXTable.is_layout_table(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)
elif AXUtilities.is_table_cell(firstChild):
layoutOnly = True
elif parentRole == Atspi.Role.TABLE:
layoutOnly = self.isLayoutOnly(AXObject.get_parent(obj))
elif role == Atspi.Role.SECTION:
layoutOnly = not self.isBlockquote(obj)
elif role == Atspi.Role.BLOCK_QUOTE:
layoutOnly = False
elif role == Atspi.Role.FILLER:
layoutOnly = True
elif role == Atspi.Role.SCROLL_PANE:
layoutOnly = True
elif role == Atspi.Role.LAYERED_PANE:
layoutOnly = self.isDesktop(self.topLevelObject(obj))
elif role == Atspi.Role.AUTOCOMPLETE:
layoutOnly = True
elif role in [Atspi.Role.TEAROFF_MENU_ITEM, Atspi.Role.SEPARATOR]:
layoutOnly = True
elif role in [Atspi.Role.LIST_BOX, Atspi.Role.TREE_TABLE]:
layoutOnly = False
elif role in topLevelRoles:
layoutOnly = False
elif role == Atspi.Role.MENU:
layoutOnly = parentRole == Atspi.Role.COMBO_BOX
elif role == Atspi.Role.COMBO_BOX:
layoutOnly = False
elif role == Atspi.Role.LIST:
layoutOnly = False
elif role == Atspi.Role.FORM:
layoutOnly = False
elif role in [Atspi.Role.PUSH_BUTTON, Atspi.Role.TOGGLE_BUTTON]:
layoutOnly = False
elif role in [Atspi.Role.TEXT, Atspi.Role.PASSWORD_TEXT, Atspi.Role.ENTRY]:
layoutOnly = False
elif role == Atspi.Role.LIST_ITEM and parentRole == Atspi.Role.LIST_BOX:
layoutOnly = False
elif role in [Atspi.Role.REDUNDANT_OBJECT, Atspi.Role.UNKNOWN]:
layoutOnly = True
elif self.isTableRow(obj):
layoutOnly = not (AXUtilities.is_focusable(obj) or AXUtilities.is_selectable(obj))
elif role == Atspi.Role.PANEL and AXObject.get_role(firstChild) in ignorePanelParent:
layoutOnly = True
elif role == Atspi.Role.PANEL \
and AXObject.has_same_non_empty_name(obj, AXObject.get_application(obj)):
layoutOnly = True
elif AXObject.get_child_count(obj) == 1 \
and AXObject.has_same_non_empty_name(obj, firstChild):
layoutOnly = True
elif self.isHidden(obj):
layoutOnly = True
else:
if not (self.displayedText(obj) or self.displayedLabel(obj)):
layoutOnly = True
if layoutOnly:
tokens = ["SCRIPT UTILITIES:", obj, "is deemed to be layout only"]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return layoutOnly
@staticmethod
def isInActiveApp(obj):
"""Returns True if the given object is from the same application that
currently has keyboard focus.
Arguments:
- obj: an Accessible object
"""
focus = focus_manager.getManager().get_locus_of_focus()
if not (obj and focus):
return False
return AXObject.get_application(focus) == AXObject.get_application(obj)
def isLink(self, obj):
"""Returns True if obj is a link."""
return AXUtilities.is_link(obj)
def isReadOnlyTextArea(self, obj):
"""Returns True if obj is a text entry area that is read only."""
if not self.isTextArea(obj):
return False
if AXUtilities.is_read_only(obj):
return True
return AXUtilities.is_focusable(obj) and not AXUtilities.is_editable(obj)
def isSwitch(self, obj):
return False
def getObjectFromPath(self, path):
start = self._script.app
rv = None
for p in path:
if p == -1:
continue
try:
start = start[p]
except Exception:
break
else:
rv = start
return rv
def _hasSamePath(self, obj1, obj2):
path1 = AXObject.get_path(obj1)
path2 = AXObject.get_path(obj2)
if len(path1) != len(path2):
return False
if not (path1 and path2):
return False
# The first item in all paths, even valid ones, is -1.
path1 = path1[1:]
path2 = path2[1:]
# If the object is being destroyed and the replacement is too, which
# sadly can happen in at least Firefox, both will have an index of -1.
# If the rest of the paths are valid and match, it's probably ok.
if path1[-1] == -1 and path2[-1] == -1:
path1 = path1[:-1]
path2 = path2[:-1]
# If both have invalid child indices, all bets are off.
if path1.count(-1) and path2.count(-1):
return False
try:
index = path1.index(-1)
except ValueError:
try:
index = path2.index(-1)
except ValueError:
index = len(path2)
return path1[0:index] == path2[0:index]
def isSameObject(self, obj1, obj2, comparePaths=False, ignoreNames=False,
ignoreDescriptions=True):
if obj1 == obj2:
return True
if obj1 is None or obj2 is None:
return False
if not AXUtilities.have_same_role(obj1, obj2):
return False
if not ignoreNames and AXObject.get_name(obj1) != AXObject.get_name(obj2):
return False
if not ignoreDescriptions \
and AXObject.get_description(obj1) != AXObject.get_description(obj2):
return False
if comparePaths and self._hasSamePath(obj1, obj2):
return True
# Objects which claim to be different and which are in different
# locations are almost certainly not recreated objects.
if not AXComponent.objects_have_same_rect(obj1, obj2):
return False
if not AXComponent.has_no_size(obj1):
return True
return False
def isTextArea(self, obj):
"""Returns True if obj is a GUI component that is for entering text.
Arguments:
- obj: an accessible
"""
if self.isLink(obj):
return False
# TODO - JD: This might have been enough way back when, but additional
# checks are needed now.
return AXUtilities.is_text_input(obj) \
or AXUtilities.is_text(obj) \
or AXUtilities.is_paragraph(obj)
def labelsForObject(self, obj):
"""Return a list of the labels for this object."""
def isNotAncestor(acc):
return not AXObject.find_ancestor(obj, lambda x: x == acc)
result = AXObject.get_relation_targets(obj, Atspi.RelationType.LABELLED_BY)
return list(filter(isNotAncestor, result))
def nestingLevel(self, obj):
"""Determines the nesting level of this object.
Arguments:
-obj: the Accessible object
"""
if obj is None:
return 0
try:
return self._script.generatorCache[self.NESTING_LEVEL][obj]
except Exception:
if self.NESTING_LEVEL not in self._script.generatorCache:
self._script.generatorCache[self.NESTING_LEVEL] = {}
def pred(x):
if self.isBlockquote(obj):
return self.isBlockquote(x)
if AXUtilities.is_list_item(obj):
return AXUtilities.is_list(AXObject.get_parent(x))
return AXUtilities.have_same_role(obj, x)
ancestors = []
ancestor = AXObject.find_ancestor(obj, pred)
while ancestor:
ancestors.append(ancestor)
ancestor = AXObject.find_ancestor(ancestor, pred)
nestingLevel = len(ancestors)
self._script.generatorCache[self.NESTING_LEVEL][obj] = nestingLevel
return self._script.generatorCache[self.NESTING_LEVEL][obj]
def nodeLevel(self, obj):
"""Determines the node level of this object if it is in a tree
relation, with 0 being the top level node. If this object is
not in a tree relation, then -1 will be returned.
Arguments:
-obj: the Accessible object
"""
if not self.isTreeDescendant(obj):
return -1
try:
return self._script.generatorCache[self.NODE_LEVEL][obj]
except Exception:
if self.NODE_LEVEL not in self._script.generatorCache:
self._script.generatorCache[self.NODE_LEVEL] = {}
nodes = []
node = obj
done = False
while not done:
relation = AXObject.get_relation(node, Atspi.RelationType.NODE_CHILD_OF)
node = None
if relation:
node = relation.get_target(0)
# We want to avoid situations where something gives us an
# infinite cycle of nodes. Bon Echo has been seen to do
# this (see bug 351847).
if nodes.count(node):
tokens = ["SCRIPT UTILITIES:", node, "is already in the list of nodes for", obj]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
done = True
if len(nodes) > 100:
tokens = ["SCRIPT UTILITIES: More than 100 nodes found for", obj]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
done = True
elif node:
nodes.append(node)
else:
done = True
self._script.generatorCache[self.NODE_LEVEL][obj] = len(nodes) - 1
return self._script.generatorCache[self.NODE_LEVEL][obj]
def isOnScreen(self, obj, boundingbox=None):
if AXObject.is_dead(obj):
return False
if self.isHidden(obj):
return False
if not (AXUtilities.is_showing(obj) and AXUtilities.is_visible(obj)):
tokens = ["SCRIPT UTILITIES:", obj, "is not showing and visible"]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
if AXUtilities.is_filler(obj):
AXObject.clear_cache(obj, False, "Suspecting filler might have wrong state")
if AXUtilities.is_showing(obj) and AXUtilities.is_visible(obj):
tokens = ["WARNING: Now", obj, "is showing and visible"]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return True
return False
if AXComponent.has_no_size_or_invalid_rect(obj):
tokens = ["SCRIPT UTILITIES: Rect of", obj, "is unhelpful. Treating as onscreen"]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return True
if AXComponent.object_is_off_screen(obj):
return False
if boundingbox is None:
return True
if not AXComponent.object_intersects_rect(obj, boundingbox):
tokens = ["SCRIPT UTILITIES:", obj, "not in", boundingbox]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return False
return True
def selectedMenuBarMenu(self, menubar):
if not AXUtilities.is_menu_bar(menubar):
return None
if AXObject.supports_selection(menubar):
selected = self.selectedChildren(menubar)
if selected:
return selected[0]
return None
for menu in AXObject.iter_children(menubar):
# TODO - JD: Can we remove this?
AXObject.clear_cache(menu, False, "Ensuring we have the correct state.")
if AXUtilities.is_expanded(menu) or AXUtilities.is_selected(menu):
return menu
return None
def isInOpenMenuBarMenu(self, obj):
if obj is None:
return False
menubar = AXObject.find_ancestor(obj, AXUtilities.is_menu_bar)
if menubar is None:
return False
selectedMenu = self._selectedMenuBarMenu.get(hash(menubar))
if selectedMenu is None:
selectedMenu = self.selectedMenuBarMenu(menubar)
if selectedMenu is None:
return False
def inSelectedMenu(x):
return x == selectedMenu
if inSelectedMenu(obj):
return True
return AXObject.find_ancestor(obj, inSelectedMenu) is not None
def isStaticTextLeaf(self, obj):
return False
def isListItemMarker(self, obj):
return False
def hasPresentableText(self, obj):
if self.isStaticTextLeaf(obj):
return False
return bool(re.search(r"\w+", AXText.get_all_text(obj)))
def getOnScreenObjects(self, root, extents=None):
if not self.isOnScreen(root, extents):
return []
if AXObject.get_role(root) == Atspi.Role.INVALID:
return []
if AXUtilities.is_button(root) or AXUtilities.is_combo_box(root):
return [root]
if AXUtilities.is_menu_bar(root):
self._selectedMenuBarMenu[hash(root)] = self.selectedMenuBarMenu(root)
if AXUtilities.is_menu_bar(AXObject.get_parent(root)) \
and not self.isInOpenMenuBarMenu(root):
return [root]
if AXUtilities.is_filler(root) and not AXObject.get_child_count(root):
AXObject.clear_cache(root, True, "Root is empty filler.")
count = AXObject.get_child_count(root)
tokens = ["SCRIPT UTILITIES:", root, f"now reports {count} children"]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
if not count:
tokens = ["WARNING: unexpectedly empty filler", root]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
if extents is None:
extents = AXComponent.get_rect(root)
if AXObject.supports_table(root) and AXObject.supports_selection(root):
visibleCells = self.getVisibleTableCells(root)
if visibleCells:
return visibleCells
objects = []
hasNameOrDesc = AXObject.get_name(root) or AXObject.get_description(root)
if hasNameOrDesc and (AXUtilities.is_page_tab(root) or AXUtilities.is_image(root)):
objects.append(root)
elif self.hasPresentableText(root):
objects.append(root)
def pred(x):
return x is not None and not self.isStaticTextLeaf(x)
for child in AXObject.iter_children(root, pred):
objects.extend(self.getOnScreenObjects(child, extents))
if AXUtilities.is_menu_bar(root):
self._selectedMenuBarMenu[hash(root)] = None
if objects:
return objects
if AXUtilities.is_label(root) and not hasNameOrDesc and AXText.is_whitespace_or_empty(root):
return []
containers = [Atspi.Role.CANVAS,
Atspi.Role.FILLER,
Atspi.Role.IMAGE,
Atspi.Role.LINK,
Atspi.Role.LIST_BOX,
Atspi.Role.PANEL,
Atspi.Role.SECTION,
Atspi.Role.SCROLL_PANE,
Atspi.Role.VIEWPORT]
if AXObject.get_role(root) in containers and not hasNameOrDesc:
return []
return [root]
@staticmethod
def isTableRow(obj):
"""Determines if obj is a table row -- real or functionally."""
childCount = AXObject.get_child_count(obj)
if not childCount:
return False
if AXObject.get_parent(obj) is None:
return False
if AXUtilities.is_table_row(obj):
return True
if AXUtilities.is_table_cell_or_header(obj):
return False
if not AXUtilities.is_table(AXObject.get_parent(obj)):
return False
cells = [x for x in AXObject.iter_children(obj, AXUtilities.is_table_cell_or_header)]
if len(cells) == childCount:
return True
return False
def realActiveAncestor(self, obj):
if AXUtilities.is_focused(obj):
return obj
def pred(x):
return AXUtilities.is_table_cell_or_header(x) or AXUtilities.is_list_item(x)
ancestor = AXObject.find_ancestor(obj, pred)
if ancestor is not None \
and not self._script.utilities.isLayoutOnly(AXObject.get_parent(ancestor)):
obj = ancestor
return obj
def realActiveDescendant(self, obj):
"""Given an object that should be a child of an object that
manages its descendants, return the child that is the real
active descendant carrying useful information.
Arguments:
- obj: an object that should be a child of an object that
manages its descendants.
"""
if AXObject.is_dead(obj):
return None
if not AXUtilities.is_table_cell(obj):
return obj
if AXObject.get_name(obj):
return obj
def pred(x):
return x and not self.isStaticTextLeaf(x) and self.displayedText(x).strip()
child = AXObject.find_descendant(obj, pred)
if child is not None:
return child
return obj
def isStatusBarDescendant(self, obj):
if obj is None:
return False
return AXObject.find_ancestor(obj, AXUtilities.is_status_bar) is not None
def statusBarItems(self, obj):
if not AXUtilities.is_status_bar(obj):
return []
start = time.time()
items = self._script.pointOfReference.get('statusBarItems')
if not items:
def include(x):
return not AXUtilities.is_status_bar(x)
items = list(filter(include, self.getOnScreenObjects(obj)))
self._script.pointOfReference['statusBarItems'] = items
end = time.time()
msg = f"SCRIPT UTILITIES: Time getting status bar items: {end - start:.4f}"
debug.printMessage(debug.LEVEL_INFO, msg, True)
return items
def infoBar(self, root):
return None
def _topLevelRoles(self):
roles = [Atspi.Role.DIALOG,
Atspi.Role.FILE_CHOOSER,
Atspi.Role.FRAME,
Atspi.Role.WINDOW]
if self._treatAlertsAsDialogs():
roles.append(Atspi.Role.ALERT)
return roles
def _locusOfFocusIsTopLevelObject(self):
focus = focus_manager.getManager().get_locus_of_focus()
if not focus:
return False
rv = focus == self.topLevelObject(focus)
tokens = ["SCRIPT UTILITIES:", focus, "is top-level object:", rv]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return rv
def _findWindowWithDescendant(self, child):
"""Searches each frame/window/dialog of an application to find the one
which contains child. This is extremely non-performant and should only
be used to work around broken accessibility trees where topLevelObject
fails."""
app = AXObject.get_application(child)
if app is None:
return None
for i in range(AXObject.get_child_count(app)):
window = AXObject.get_child(app, i)
if AXObject.find_descendant(window, lambda x: x == child) is not None:
tokens = ["SCRIPT UTILITIES:", window, "contains", child]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return window
tokens = ["SCRIPT UTILITIES:", window, "does not contain", child]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return None
def _isTopLevelObject(self, obj):
return AXObject.get_role(obj) in self._topLevelRoles() \
and AXObject.get_role(AXObject.get_parent(obj)) == Atspi.Role.APPLICATION
def topLevelObject(self, obj, useFallbackSearch=False):
"""Returns the top-level object (frame, dialog ...) containing obj,
or None if obj is not inside a top-level object.
Arguments:
- obj: the Accessible object
"""
if self._isTopLevelObject(obj):
rv = obj
else:
rv = AXObject.find_ancestor(obj, self._isTopLevelObject)
tokens = ["SCRIPT UTILITIES:", rv, "is top-level object for:", obj]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
if rv is None and useFallbackSearch:
msg = "SCRIPT UTILITIES: Attempting to find top-level object via fallback search"
debug.printMessage(debug.LEVEL_INFO, msg, True)
rv = self._findWindowWithDescendant(obj)
return rv
def topLevelObjectIsActiveAndCurrent(self, obj=None):
obj = obj or focus_manager.getManager().get_locus_of_focus()
topLevel = self.topLevelObject(obj)
if not topLevel:
return False
AXObject.clear_cache(topLevel, False, "Ensuring we have the correct state.")
if not AXUtilities.is_active(topLevel) or AXUtilities.is_defunct(topLevel):
return False
if not self.isSameObject(topLevel, focus_manager.getManager().get_active_window()):
return False
return True
@staticmethod
def pathComparison(path1, path2):
"""Compares the two paths and returns -1, 0, or 1 to indicate if path1
is before, the same, or after path2."""
if path1 == path2:
return 0
size = max(len(path1), len(path2))
path1 = (path1 + [-1] * size)[:size]
path2 = (path2 + [-1] * size)[:size]
for x in range(min(len(path1), len(path2))):
if path1[x] < path2[x]:
return -1
if path1[x] > path2[x]:
return 1
return 0
def findAllDescendants(self, root, includeIf=None, excludeIf=None):
return AXObject.find_all_descendants(root, includeIf, excludeIf)
def unrelatedLabels(self, root, onlyShowing=True, minimumWords=3):
"""Returns a list containing all the unrelated (i.e., have no
relations to anything and are not a fundamental element of a
more atomic component like a combo box) labels under the given
root. Note that the labels must also be showing on the display.
Arguments:
- root: the Accessible object to traverse
- onlyShowing: if True, only return labels with STATE_SHOWING
Returns a list of unrelated labels under the given root.
"""
if self._script.spellcheck and self._script.spellcheck.isCheckWindow(root):
return []
labelRoles = [Atspi.Role.LABEL, Atspi.Role.STATIC]
skipRoles = [Atspi.Role.COMBO_BOX,
Atspi.Role.LIST_BOX,
Atspi.Role.MENU,
Atspi.Role.MENU_BAR,
Atspi.Role.SCROLL_PANE,
Atspi.Role.SPLIT_PANE,
Atspi.Role.TABLE,
Atspi.Role.TREE,
Atspi.Role.TREE_TABLE]
def _include(x):
if not (x and AXObject.get_role(x) in labelRoles):
return False
if AXObject.get_relations(x):
return False
if onlyShowing and not AXUtilities.is_showing(x):
return False
return True
def _exclude(x):
if not x or AXObject.get_role(x) in skipRoles:
return True
if onlyShowing and not AXUtilities.is_showing(x):
return True
return False
labels = self.findAllDescendants(root, _include, _exclude)
rootName = AXObject.get_name(root)
# Eliminate things suspected to be labels for widgets
labels_filtered = []
for label in labels:
name = AXObject.get_name(label) or self.displayedText(label)
if name and name in [rootName, AXObject.get_name(AXObject.get_parent(label))]:
continue
if len(name.split()) < minimumWords:
continue
if rootName.find(name) >= 0:
continue
labels_filtered.append(label)
return AXComponent.sort_objects_by_position(labels_filtered)
def _treatAlertsAsDialogs(self):
return True
def unfocusedAlertAndDialogCount(self, obj):
"""If the current application has one or more alert or dialog
windows and the currently focused window is not an alert or a dialog,
return a count of the number of alert and dialog windows, otherwise
return a count of zero.
Arguments:
- obj: the Accessible object
Returns the alert and dialog count.
"""
roles = [Atspi.Role.DIALOG]
if self._treatAlertsAsDialogs():
roles.append(Atspi.Role.ALERT)
def isDialog(x):
return AXObject.get_role(x) in roles or self.isFunctionalDialog(x)
dialogs = [x for x in AXObject.iter_children(AXObject.get_application(obj), isDialog)]
dialogs.extend([x for x in AXObject.iter_children(self.topLevelObject(obj), isDialog)])
def isPresentable(x):
return AXUtilities.is_showing(x) and AXUtilities.is_visible(x) \
and (AXObject.get_name(x) or AXObject.get_child_count(x))
def cannotBeActiveWindow(x):
return not focus_manager.getManager().can_be_active_window(x)
presentable = list(filter(isPresentable, set(dialogs)))
unfocused = list(filter(cannotBeActiveWindow, presentable))
return len(unfocused)
#########################################################################
# #
# Utilities for working with the accessible text interface #
# #
#########################################################################
def findPreviousObject(self, obj):
"""Finds the object before this one."""
if not AXObject.is_valid(obj):
return None
relation = AXObject.get_relation(obj, Atspi.RelationType.FLOWS_FROM)
if relation:
return relation.get_target(0)
return AXObject.get_previous_object(obj)
def findNextObject(self, obj):
"""Finds the object after this one."""
if not AXObject.is_valid(obj):
return None
relation = AXObject.get_relation(obj, Atspi.RelationType.FLOWS_TO)
if relation:
return relation.get_target(0)
return AXObject.get_next_object(obj)
def allSelectedText(self, obj):
"""Get all the text applicable text selections for the given object.
including any previous or next text objects that also have
selected text and add in their text contents.
Arguments:
- obj: the text object to start extracting the selected text from.
Returns: all the selected text contents plus the start and end
offsets within the text for the given object.
"""
# TODO - JD: Move to AXText if possible
textContents, startOffset, endOffset = AXText.get_selected_text(obj)
if textContents and self._script.pointOfReference.get('entireDocumentSelected'):
return textContents, startOffset, endOffset
if self.isSpreadSheetCell(obj):
return textContents, startOffset, endOffset
prevObj = self.findPreviousObject(obj)
while prevObj:
selection = AXText.get_selected_text(prevObj)[0]
if not selection:
break
textContents = f"{selection} {textContents}"
prevObj = self.findPreviousObject(prevObj)
nextObj = self.findNextObject(obj)
while nextObj:
selection = AXText.get_selected_text(nextObj)[0]
if not selection:
break
textContents = f"{textContents} {selection}"
nextObj = self.findNextObject(nextObj)
return textContents, startOffset, endOffset
def expandEOCs(self, obj, startOffset=0, endOffset=-1):
"""Expands the current object replacing EMBEDDED_OBJECT_CHARACTERS
with their text.
Arguments
- obj: the object whose text should be expanded
- startOffset: the offset of the first character to be included
- endOffset: the offset of the last character to be included
Returns the fully expanded text for the object.
"""
try:
string = self.substring(obj, startOffset, endOffset)
except Exception:
return ""
if self.EMBEDDED_OBJECT_CHARACTER not in string:
return string
blockRoles = [Atspi.Role.HEADING,
Atspi.Role.LIST,
Atspi.Role.LIST_ITEM,
Atspi.Role.PARAGRAPH,
Atspi.Role.SECTION,
Atspi.Role.TABLE,
Atspi.Role.TABLE_CELL,
Atspi.Role.TABLE_ROW]
toBuild = list(string)
for i, char in enumerate(toBuild):
if char == self.EMBEDDED_OBJECT_CHARACTER:
child = AXHypertext.get_child_at_offset(obj, i + startOffset)
result = self.expandEOCs(child)
if child and AXObject.get_role(child) in blockRoles:
result += " "
toBuild[i] = result
return "".join(toBuild)
def getError(self, obj):
return AXUtilities.is_invalid_entry(obj)
def getErrorMessage(self, obj):
return ""
def isErrorMessage(self, obj):
return False
def deletedText(self, event):
return event.any_data
def insertedText(self, event):
if event.any_data:
return event.any_data
msg = "SCRIPT UTILITIES: Broken text insertion event"
debug.printMessage(debug.LEVEL_INFO, msg, True)
if AXUtilities.is_password_text(event.source):
string = AXText.get_all_text(event.source)
if string:
tokens = ["HACK: Returning last char in '", string, "'"]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return string[-1]
msg = "FAIL: Unable to correct broken text insertion event"
debug.printMessage(debug.LEVEL_INFO, msg, True)
return ""
def getCaretContext(self):
obj = focus_manager.getManager().get_locus_of_focus()
offset = AXText.get_caret_offset(obj)
return obj, offset
def getFirstCaretPosition(self, obj):
# TODO - JD: Do we still need this function? We need to audit callers,
# mainly in structural navigation.
return obj, 0
def setCaretPosition(self, obj, offset, documentFrame=None):
focus_manager.getManager().set_locus_of_focus(None, obj, False)
self.setCaretOffset(obj, offset)
def setCaretOffset(self, obj, offset):
# TODO - JD. Remove this function if the web override can be adjusted
AXText.set_caret_offset(obj, offset)
def substring(self, obj, startOffset, endOffset):
# TODO - JD. Remove this function if the web override can be adjusted
return AXText.get_substring(obj, startOffset, endOffset)
def getAppNameForAttribute(self, attribName):
"""Converts the given Atk attribute name into the application's
equivalent. This is necessary because an application or toolkit
(e.g. Gecko) might invent entirely new names for the same text
attributes.
Arguments:
- attribName: The name of the text attribute
Returns the application's equivalent name if found or attribName
otherwise.
"""
for key, value in self._script.attributeNamesDict.items():
if value == attribName:
return key
return attribName
def getAtkNameForAttribute(self, attribName):
"""Converts the given attribute name into the Atk equivalent. This
is necessary because an application or toolkit (e.g. Gecko) might
invent entirely new names for the same attributes.
Arguments:
- attribName: The name of the text attribute
Returns the Atk equivalent name if found or attribName otherwise.
"""
return self._script.attributeNamesDict.get(attribName, attribName)
def textAttributes(self, acc, offset=None, get_defaults=False):
# TODO - JD: Replace all calls to this function with the one below
return AXText.get_text_attributes_at_offset(acc, offset)
def localizeTextAttribute(self, key, value):
if key == "weight" and (value == "bold" or int(value) > 400):
return messages.BOLD
if key.endswith("spelling") or value == "spelling":
return messages.MISSPELLED
localizedKey = text_attribute_names.getTextAttributeName(key, self._script)
if key == "family-name":
localizedValue = value.split(",")[0].strip().strip('"')
elif value and value.endswith("px"):
value = value.split("px")[0]
if locale.localeconv()["decimal_point"] in value:
localizedValue = messages.pixelCount(float(value))
else:
localizedValue = messages.pixelCount(int(value))
elif key.endswith("color"):
r, g, b = self.rgbFromString(value)
if settings.useColorNames:
localizedValue = colornames.rgbToName(r, g, b)
else:
localizedValue = "%i %i %i" % (r, g, b)
else:
localizedValue = text_attribute_names.getTextAttributeName(value, self._script)
return f"{localizedKey}: {localizedValue}"
def splitSubstringByLanguage(self, obj, start, end):
"""Returns a list of (start, end, string, language, dialect) tuples."""
rv = []
allSubstrings = self.getLanguageAndDialectFromTextAttributes(obj, start, end)
for startOffset, endOffset, language, dialect in allSubstrings:
if start >= endOffset:
continue
if end <= startOffset:
break
startOffset = max(start, startOffset)
endOffset = min(end, endOffset)
string = self.substring(obj, startOffset, endOffset)
rv.append([startOffset, endOffset, string, language, dialect])
return rv
def getLanguageAndDialectForSubstring(self, obj, start, end):
"""Returns a (language, dialect) tuple. If multiple languages apply to
the substring, language and dialect will be empty strings. Callers must
do any preprocessing to avoid that condition."""
allSubstrings = self.getLanguageAndDialectFromTextAttributes(obj, start, end)
for startOffset, endOffset, language, dialect in allSubstrings:
if startOffset <= start and endOffset >= end:
return language, dialect
return "", ""
def getLanguageAndDialectFromTextAttributes(self, obj, startOffset=0, endOffset=-1):
"""Returns a list of (start, end, language, dialect) tuples for obj
based on what is exposed via text attributes."""
rv = []
attributeSet = AXText.get_all_text_attributes(obj, startOffset, endOffset)
lastLanguage = lastDialect = ""
for (start, end, attrs) in attributeSet:
language = attrs.get("language", "")
dialect = ""
if "-" in language:
language, dialect = language.split("-", 1)
if rv and lastLanguage == language and lastDialect == dialect:
rv[-1] = rv[-1][0], end, language, dialect
else:
rv.append((start, end, language, dialect))
lastLanguage, lastDialect = language, dialect
return rv
def willEchoCharacter(self, event):
"""Given a keyboard event containing an alphanumeric key,
determine if the script is likely to echo it as a character.
"""
focus = focus_manager.getManager().get_locus_of_focus()
if not focus or not settings.enableEchoByCharacter:
return False
if len(event.event_string) != 1 \
or event.modifiers & keybindings.ORCA_CTRL_MODIFIER_MASK:
return False
if AXUtilities.is_password_text(focus):
return False
if AXUtilities.is_editable(focus):
return True
return False
#########################################################################
# #
# Miscellaneous Utilities #
# #
#########################################################################
def _addRepeatSegment(self, segment, line):
"""Add in the latest line segment, adjusting for repeat characters
and punctuation.
Arguments:
- segment: the segment of repeated characters.
- line: the current built-up line to characters to speak.
Returns: the current built-up line plus the new segment, after
adjusting for repeat character counts and punctuation.
"""
if segment.isalnum():
return line + segment
count = len(segment)
if count >= settings.repeatCharacterLimit and segment[0] not in self._script.whitespace:
repeatChar = segment[0]
repeatSegment = messages.repeatedCharCount(repeatChar, count)
line = f"{line} {repeatSegment} "
else:
line += segment
return line
def shouldVerbalizeAllPunctuation(self, obj):
if not (self.isCode(obj) or self.isCodeDescendant(obj)):
return False
# If the user has set their punctuation level to All, then the synthesizer will
# do the work for us. If the user has set their punctuation level to None, then
# they really don't want punctuation and we mustn't override that.
style = settings_manager.getManager().getSetting("verbalizePunctuationStyle")
if style in [settings.PUNCTUATION_STYLE_ALL, settings.PUNCTUATION_STYLE_NONE]:
return False
return True
def verbalizeAllPunctuation(self, string):
result = string
for symbol in set(re.findall(self.PUNCTUATION, result)):
charName = f" {symbol} "
result = re.sub(r"\%s" % symbol, charName, result)
return result
def adjustForLinks(self, obj, line, startOffset):
"""Adjust line to include the word "link" after any hypertext links.
Arguments:
- obj: the accessible object that this line came from.
- line: the string to adjust for links.
- startOffset: the caret offset at the start of the line.
Returns: a new line adjusted to add the speaking of "link" after
text which is also a link.
"""
endOffset = startOffset + len(line)
links = AXHypertext.get_all_links_in_range(obj, startOffset, endOffset)
offsets = [AXHypertext.get_link_end_offset(link) for link in links]
offsets = sorted([offset - startOffset for offset in offsets], reverse=True)
tokens = list(line)
for o in offsets:
string = f" {messages.LINK}"
if o < len(tokens) and tokens[o].isalnum():
string += " "
tokens[o:o] = string
return "".join(tokens)
@staticmethod
def _processMultiCaseString(string):
return re.sub(r'(?<=[a-z])(?=[A-Z])', ' ', string)
@staticmethod
def _convertWordToDigits(word):
if not word.isnumeric():
return word
return ' '.join(list(word))
def adjustForPronunciation(self, line):
"""Adjust the line to replace words in the pronunciation dictionary,
with what those words actually sound like.
Arguments:
- line: the string to adjust for words in the pronunciation dictionary.
Returns: a new line adjusted for words found in the pronunciation
dictionary.
"""
# TODO - JD: We had been making this change in response to bgo#591734.
# It may or may not still be needed or wanted to replace no-break-space
# characters with plain spaces. Surely modern synthesizers can cope with
# both types of spaces.
line = line.replace("\u00a0", " ")
if settings.speakMultiCaseStringsAsWords:
line = self._processMultiCaseString(line)
if self.speakMathSymbolNames():
line = mathsymbols.adjustForSpeech(line)
if settings.speakNumbersAsDigits:
words = self.WORDS_RE.split(line)
line = ''.join(map(self._convertWordToDigits, words))
line = self.adjustForDigits(line)
if len(line) == 1 and not self._script.inSayAll() and self.isInMath():
charname = mathsymbols.getCharacterName(line)
if charname != line:
return charname
if not settings.usePronunciationDictionary:
return line
newLine = ""
words = self.WORDS_RE.split(line)
newLine = ''.join(map(pronunciation_dict.getPronunciation, words))
if settings.speakMultiCaseStringsAsWords:
newLine = self._processMultiCaseString(newLine)
return newLine
def adjustForRepeats(self, line):
"""Adjust line to include repeat character counts. As some people
will want this and others might not, there is a setting in
settings.py that determines whether this functionality is enabled.
repeatCharacterLimit = <n>
If <n> is 0, then there would be no repeat characters.
Otherwise <n> would be the number of same characters (or more)
in a row that cause the repeat character count output.
If the value is set to 1, 2 or 3 then it's treated as if it was
zero. In other words, no repeat character count is given.
Arguments:
- line: the string to adjust for repeat character counts.
Returns: a new line adjusted for repeat character counts (if enabled).
"""
if (len(line) < 4) or (settings.repeatCharacterLimit < 4):
return line
newLine = ''
segment = lastChar = line[0]
for i in range(1, len(line)):
if line[i] == lastChar:
segment += line[i]
else:
newLine = self._addRepeatSegment(segment, newLine)
segment = line[i]
lastChar = line[i]
return self._addRepeatSegment(segment, newLine)
def adjustForDigits(self, string):
"""Adjusts the string to convert digit-like text, such as subscript
and superscript numbers, into actual digits.
Arguments:
- string: the string to be adjusted
Returns: a new string which contains actual digits.
"""
subscripted = set(re.findall(self.SUBSCRIPTS_RE, string))
superscripted = set(re.findall(self.SUPERSCRIPTS_RE, string))
for number in superscripted:
new = [str(self.SUPERSCRIPT_DIGITS.index(d)) for d in number]
newString = messages.DIGITS_SUPERSCRIPT % "".join(new)
string = re.sub(number, newString, string)
for number in subscripted:
new = [str(self.SUBSCRIPT_DIGITS.index(d)) for d in number]
newString = messages.DIGITS_SUBSCRIPT % "".join(new)
string = re.sub(number, newString, string)
return string
def indentationDescription(self, line):
if settings_manager.getManager().getSetting('onlySpeakDisplayedText') \
or not settings_manager.getManager().getSetting('enableSpeechIndentation'):
return ""
line = line.replace("\u00a0", " ")
end = re.search("[^ \t]", line)
if end:
line = line[:end.start()]
result = ""
spaces = [m.span() for m in re.finditer(" +", line)]
tabs = [m.span() for m in re.finditer("\t+", line)]
spans = sorted(spaces + tabs)
for (start, end) in spans:
if (start, end) in spaces:
result += f"{messages.spacesCount(end - start)} "
else:
result += f"{messages.tabsCount(end - start)} "
return result
@staticmethod
def absoluteMouseCoordinates():
"""Gets the absolute position of the mouse pointer."""
from gi.repository import Gtk
rootWindow = Gtk.Window().get_screen().get_root_window()
window, x, y, modifiers = rootWindow.get_pointer()
return x, y
@staticmethod
def appendString(text, newText, delimiter=" "):
"""Appends the newText to the given text with the delimiter in between
and returns the new string. Edge cases, such as no initial text or
no newText, are handled gracefully."""
if not newText:
return text
if not text:
return newText
return text + delimiter + newText
def treatAsDuplicateEvent(self, event1, event2):
if not (event1 and event2):
return False
# The goal is to find event spam so we can ignore the event.
if event1 == event2:
return False
return event1.source == event2.source \
and event1.type == event2.type \
and event1.detail1 == event2.detail1 \
and event1.detail2 == event2.detail2 \
and event1.any_data == event2.any_data
def isAutoTextEvent(self, event):
"""Returns True if event is associated with text being autocompleted
or autoinserted or autocorrected or autosomethingelsed.
Arguments:
- event: the accessible event being examined
"""
if event.type.startswith("object:text-changed:insert"):
if not event.any_data or not event.source:
return False
if not AXUtilities.is_editable(event.source):
return False
if not AXUtilities.is_showing(event.source):
return False
if AXUtilities.is_focusable(event.source):
AXObject.clear_cache(event.source, False, "Ensuring we have the correct state.")
if not AXUtilities.is_focused(event.source):
return False
lastKey, mods = self.lastKeyAndModifiers()
if lastKey == "Tab" and event.any_data != "\t":
return True
if lastKey == "Return" and event.any_data != "\n":
return True
if lastKey in ["Up", "Down", "Page_Up", "Page_Down"]:
return self.isEditableDescendantOfComboBox(event.source)
if not self.lastInputEventWasPrintableKey():
return False
string = AXText.get_all_text(event.source)
if string.endswith(event.any_data):
selection, start, end = AXText.get_selected_text(event.source)
if selection == event.any_data:
return True
if string == event.any_data and string.endswith(selection):
beginning = string[:string.find(selection)]
return beginning.lower().endswith(lastKey.lower())
return False
def isSentenceDelimiter(self, currentChar, previousChar):
"""Returns True if we are positioned at the end of a sentence.
This is determined by checking if the current character is a
white space character and the previous character is one of the
normal end-of-sentence punctuation characters.
Arguments:
- currentChar: the current character
- previousChar: the previous character
Returns True if the given character is a sentence delimiter.
"""
if currentChar == '\r' or currentChar == '\n':
return True
return currentChar in self._script.whitespace \
and previousChar in '!.?:;'
def isWordDelimiter(self, character):
"""Returns True if the given character is a word delimiter.
Arguments:
- character: the character in question
Returns True if the given character is a word delimiter.
"""
return character in self._script.whitespace \
or character in r'!*+,-./:;<=>?@[\]^_{|}' \
or character == self._script.NO_BREAK_SPACE_CHARACTER
@staticmethod
def _allNamesForKeyCode(keycode):
keymap = Gdk.Keymap.get_default()
entries = keymap.get_entries_for_keycode(keycode)[-1]
return list(map(Gdk.keyval_name, set(entries)))
@staticmethod
def _lastKeyCodeAndModifiers():
if not isinstance(orca_state.lastInputEvent, input_event.KeyboardEvent):
return 0, 0
event = orca_state.lastNonModifierKeyEvent
if event:
return event.hw_code, event.modifiers
return 0, 0
@staticmethod
def lastKeyAndModifiers():
"""Convenience method which returns a tuple containing the event
string and modifiers of the last non-modifier key event or ("", 0)
if there is no such event."""
if isinstance(orca_state.lastInputEvent, input_event.KeyboardEvent) \
and orca_state.lastNonModifierKeyEvent:
event = orca_state.lastNonModifierKeyEvent
if event.keyval_name in ["BackSpace", "Delete"]:
eventStr = event.keyval_name
else:
eventStr = event.event_string
mods = orca_state.lastInputEvent.modifiers
else:
eventStr = ""
mods = 0
return (eventStr, mods)
@staticmethod
def labelFromKeySequence(sequence):
"""Turns a key sequence into a user-presentable label."""
try:
from gi.repository import Gtk
key, mods = Gtk.accelerator_parse(sequence)
newSequence = Gtk.accelerator_get_label(key, mods)
if newSequence and \
(not newSequence.endswith('+') or newSequence.endswith('++')):
sequence = newSequence
except Exception:
sequence = sequence.replace("<", "")
sequence = sequence.replace(">", " ").strip()
return keynames.localizeKeySequence(sequence)
def mnemonicShortcutAccelerator(self, obj):
"""Gets the mnemonic, accelerator string and possibly shortcut
for the given object. These are based upon the first accessible
action for the object.
Arguments:
- obj: the Accessible object
Returns: list containing strings: [mnemonic, shortcut, accelerator]
"""
try:
return self._script.generatorCache[self.KEY_BINDING][obj]
except Exception:
if self.KEY_BINDING not in self._script.generatorCache:
self._script.generatorCache[self.KEY_BINDING] = {}
keybinding = AXObject.get_action_key_binding(obj, 0)
if not keybinding:
self._script.generatorCache[self.KEY_BINDING][obj] = ["", "", ""]
return self._script.generatorCache[self.KEY_BINDING][obj]
# Action is a string in the format, where the mnemonic and/or
# accelerator can be missing.
#
# <mnemonic>;<full-path>;<accelerator>
#
# The keybindings in <full-path> should be separated by ":"
#
bindingStrings = keybinding.split(';')
if len(bindingStrings) == 3:
mnemonic = bindingStrings[0]
fullShortcut = bindingStrings[1]
accelerator = bindingStrings[2]
elif len(bindingStrings) > 0:
mnemonic = ""
fullShortcut = bindingStrings[0]
try:
accelerator = bindingStrings[1]
except Exception:
accelerator = ""
else:
mnemonic = ""
fullShortcut = ""
accelerator = ""
fullShortcut = fullShortcut.replace(":", " ").strip()
fullShortcut = self.labelFromKeySequence(fullShortcut)
mnemonic = self.labelFromKeySequence(mnemonic)
accelerator = self.labelFromKeySequence(accelerator)
if self.KEY_BINDING not in self._script.generatorCache:
self._script.generatorCache[self.KEY_BINDING] = {}
self._script.generatorCache[self.KEY_BINDING][obj] = \
[mnemonic, fullShortcut, accelerator]
return self._script.generatorCache[self.KEY_BINDING][obj]
@staticmethod
def stringToKeysAndDict(string):
"""Converts a string made up of a series of <key>:<value>; pairs
into a dictionary of keys and values. Text before the colon is the
key and text afterwards is the value. The final semi-colon, if
found, is ignored.
Arguments:
- string: the string of tokens containing <key>:<value>; pairs.
Returns a list containing two items:
A list of the keys in the order they were extracted from the
string and a dictionary of key/value items.
"""
try:
items = [s.strip() for s in string.split(";")]
items = [item for item in items if len(item.split(':')) == 2]
keys = [item.split(':')[0].strip() for item in items]
dictionary = dict([item.split(':') for item in items])
except Exception:
return [], {}
return [keys, dictionary]
@staticmethod
def unicodeValueString(character):
""" Returns a four hex digit representation of the given character
Arguments:
- The character to return representation
Returns a string representaition of the given character unicode vlue
"""
try:
return f"{ord(character):04x}"
except Exception:
debug.printException(debug.LEVEL_WARNING)
return ""
def getLineContentsAtOffset(self, obj, offset, layoutMode=True, useCache=True):
return []
def getObjectContentsAtOffset(self, obj, offset=0, useCache=True):
return []
def previousContext(self, obj=None, offset=-1, skipSpace=False):
if not obj:
obj, offset = self.getCaretContext()
return obj, offset - 1
def nextContext(self, obj=None, offset=-1, skipSpace=False):
if not obj:
obj, offset = self.getCaretContext()
return obj, offset + 1
def lastContext(self, root):
offset = max(0, AXText.get_character_count(root) - 1)
return root, offset
def selectedChildren(self, obj):
children = AXSelection.get_selected_children(obj)
if children:
return children
msg = "SCRIPT UTILITIES: Selected children not retrieved via selection interface."
debug.printMessage(debug.LEVEL_INFO, msg, True)
role = AXObject.get_role(obj)
if role == Atspi.Role.MENU and not children:
children = self.findAllDescendants(obj, AXUtilities.is_selected)
if role == Atspi.Role.COMBO_BOX \
and children and AXObject.get_role(children[0]) == Atspi.Role.MENU:
children = self.selectedChildren(children[0])
name = AXObject.get_name(obj)
if not children and name:
def pred(x):
return AXObject.get_name(x) == name
children = self.findAllDescendants(obj, pred)
return children
def speakSelectedCellRange(self, obj):
return False
def getSelectionContainer(self, obj):
if not obj:
return None
if self.isTextArea(obj):
return None
if AXObject.supports_selection(obj):
return obj
rolemap = {
Atspi.Role.CANVAS: [Atspi.Role.LAYERED_PANE],
Atspi.Role.ICON: [Atspi.Role.LAYERED_PANE],
Atspi.Role.LIST_ITEM: [Atspi.Role.LIST_BOX],
Atspi.Role.TREE_ITEM: [Atspi.Role.TREE, Atspi.Role.TREE_TABLE],
Atspi.Role.TABLE_CELL: [Atspi.Role.TABLE, Atspi.Role.TREE_TABLE],
Atspi.Role.TABLE_ROW: [Atspi.Role.TABLE, Atspi.Role.TREE_TABLE],
}
matchingRoles = rolemap.get(AXObject.get_role(obj))
def isMatch(x):
if matchingRoles and AXObject.get_role(x) not in matchingRoles:
return False
return AXObject.supports_selection(x)
return AXObject.find_ancestor(obj, isMatch)
def selectableChildCount(self, obj):
if not AXObject.supports_selection(obj):
return 0
if AXObject.supports_table(obj):
rows = AXTable.get_row_count(obj)
return max(0, rows)
rolemap = {
Atspi.Role.LIST_BOX: [Atspi.Role.LIST_ITEM],
Atspi.Role.TREE: [Atspi.Role.TREE_ITEM],
}
role = AXObject.get_role(obj)
if role not in rolemap:
return AXObject.get_child_count(obj)
def isMatch(x):
return AXObject.get_role(x) in rolemap.get(role)
return len(self.findAllDescendants(obj, isMatch))
def selectedChildCount(self, obj):
if AXObject.supports_table(obj):
return AXTable.get_selected_row_count(obj)
return AXSelection.get_selected_child_count(obj)
def popupMenuFor(self, obj):
if obj is None:
return None
menus = [child for child in AXObject.iter_children(obj, AXUtilities.is_menu)]
for menu in menus:
if AXUtilities.is_enabled(menu):
return menu
return None
def isButtonWithPopup(self, obj):
return AXUtilities.is_button(obj) and AXUtilities.has_popup(obj)
def isPopupMenuForCurrentItem(self, obj):
focus = focus_manager.getManager().get_locus_of_focus()
if obj == focus:
return False
if not AXUtilities.is_menu(obj):
return False
name = AXObject.get_name(obj)
if not name:
return False
return name == AXObject.get_name(focus)
def isMenuWithNoSelectedChild(self, obj):
return AXUtilities.is_menu(obj) and not self.selectedChildCount(obj)
def isMenuButton(self, obj):
return AXUtilities.is_button(obj) and self.popupMenuFor(obj) is not None
def inMenu(self, obj=None):
obj = obj or focus_manager.getManager().get_locus_of_focus()
if obj is None:
return False
if AXUtilities.is_menu_item_of_any_kind(obj) or AXUtilities.is_menu(obj):
return True
if AXUtilities.is_panel(obj) or AXUtilities.is_separator(obj):
return AXObject.find_ancestor(obj, AXUtilities.is_menu) is not None
return False
def inContextMenu(self, obj=None):
obj = obj or focus_manager.getManager().get_locus_of_focus()
if not self.inMenu(obj):
return False
return AXObject.find_ancestor(obj, self.isContextMenu) is not None
def _contextMenuParentRoles(self):
return Atspi.Role.FRAME, Atspi.Role.WINDOW
def isContextMenu(self, obj):
if not AXUtilities.is_menu(obj):
return False
return AXObject.get_role(AXObject.get_parent(obj)) in self._contextMenuParentRoles()
def isTopLevelMenu(self, obj):
if not AXUtilities.is_menu(obj):
return False
return AXObject.get_parent(obj) == self.topLevelObject(obj)
def isSingleLineAutocompleteEntry(self, obj):
if not AXUtilities.is_entry(obj):
return False
if not AXUtilities.supports_autocompletion(obj):
return False
return AXUtilities.is_single_line(obj)
def isEntryCompletionPopupItem(self, obj):
return False
def getEntryForEditableComboBox(self, obj):
if not AXUtilities.is_combo_box(obj):
return None
children = [x for x in AXObject.iter_children(obj, self.isEditableTextArea)]
if len(children) == 1:
return children[0]
return None
def isEditableComboBox(self, obj):
return self.getEntryForEditableComboBox(obj) is not None
def isEditableDescendantOfComboBox(self, obj):
if not AXUtilities.is_editable(obj):
return False
return AXObject.find_ancestor(obj, AXUtilities.is_combo_box) is not None
def getComboBoxValue(self, obj):
if not AXObject.get_child_count(obj):
return self.displayedText(obj)
entry = self.getEntryForEditableComboBox(obj)
if entry:
return self.displayedText(entry)
selected = self._script.utilities.selectedChildren(obj)
selected = selected or self._script.utilities.selectedChildren(AXObject.get_child(obj, 0))
if len(selected) == 1:
return selected[0].name or self.displayedText(selected[0])
return self.displayedText(obj)
def isPopOver(self, obj):
return False
def isNonModalPopOver(self, obj):
if not self.isPopOver(obj):
return False
return not AXUtilities.is_modal(obj)
def isUselessPanel(self, obj):
return False
def rgbFromString(self, attributeValue):
regex = re.compile(r"rgb|[^\w,]", re.IGNORECASE)
string = re.sub(regex, "", attributeValue)
red, green, blue = string.split(",")
return int(red), int(green), int(blue)
def isClickableElement(self, obj):
return False
def hasLongDesc(self, obj):
return False
def hasDetails(self, obj):
return False
def isDetails(self, obj):
return False
def detailsFor(self, obj):
return []
def hasVisibleCaption(self, obj):
return False
def popupType(self, obj):
return ''
def headingLevel(self, obj):
if not AXUtilities.is_heading(obj):
return 0
use_cache = not AXUtilities.is_editable(obj)
attrs = AXObject.get_attributes_dict(obj, use_cache)
try:
value = int(attrs.get('level', '0'))
except ValueError:
tokens = ["SCRIPT UTILITIES: Exception getting value for", obj, "(", attrs, ")"]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return 0
return value
def hasMeaningfulToggleAction(self, obj):
return AXObject.has_action(obj, "toggle") \
or AXObject.has_action(obj, object_properties.ACTION_TOGGLE)
def containingTableHeader(self, obj):
if AXUtilities.is_table_header(obj):
return obj
return AXObject.find_ancestor(obj, AXUtilities.is_table_header)
def setSizeUnknown(self, obj):
return AXUtilities.is_indeterminate(obj)
def rowOrColumnCountUnknown(self, obj):
return AXUtilities.is_indeterminate(obj)
def _boundsIncludeChildren(self, obj):
if obj is None:
return False
if AXComponent.has_no_size(obj):
return False
return not (AXUtilities.is_menu(obj) or AXUtilities.is_page_tab(obj))
def treatAsEntry(self, obj):
return False
def _treatAsLeafNode(self, obj):
if obj is None or AXObject.is_dead(obj):
return False
if not AXObject.get_child_count(obj):
return True
if AXUtilities.is_autocomplete(obj) or AXUtilities.is_table_row(obj):
return False
if AXUtilities.is_combo_box(obj):
return AXObject.find_descendant(obj, AXUtilities.is_entry) is None
if AXUtilities.is_link(obj) and AXObject.get_name(obj):
return True
if AXUtilities.is_expandable(obj):
return not AXUtilities.is_expanded(obj)
if AXUtilities.is_button(obj):
return True
return False
def isMultiParagraphObject(self, obj):
if not obj:
return False
if not AXObject.supports_text(obj):
return False
string = AXText.get_all_text(obj)
chunks = list(filter(lambda x: x.strip(), string.split("\n\n")))
return len(chunks) > 1
def getWordAtOffsetAdjustedForNavigation(self, obj, offset=None):
word, start, end = AXText.get_word_at_offset(obj, offset)
prevObj, prevOffset = self._script.pointOfReference.get(
"penultimateCursorPosition", (None, -1))
if prevObj != obj:
return word, start, end
# If we're in an ongoing series of native navigation-by-word commands, just present the
# newly-traversed string.
prevWord, prevStart, prevEnd = AXText.get_word_at_offset(prevObj, prevOffset)
if self._script.pointOfReference.get("lastTextUnitSpoken") == "word":
if self.lastInputEventWasPrevWordNav():
start = offset
end = prevOffset
elif self.lastInputEventWasNextWordNav():
start = prevOffset
end = offset
word = AXText.get_substring(obj, start, end)
debugString = word.replace("\n", "\\n")
msg = (
f"SCRIPT UTILITIES: Adjusted word at offset {offset} for ongoing word nav is "
f"'{debugString}' ({start}-{end})"
)
debug.printMessage(debug.LEVEL_INFO, msg, True)
return word, start, end
# Otherwise, attempt some smarts so that the user winds up with the same presentation
# they would get were this an ongoing series of native navigation-by-word commands.
if self.lastInputEventWasPrevWordNav():
# If we moved left via native nav, this should be the start of a native-navigation
# word boundary, regardless of what ATK/AT-SPI2 tells us.
start = offset
# The ATK/AT-SPI2 word typically ends in a space; if the ending is neither a space,
# nor an alphanumeric character, then suspect that character is a navigation boundary
# where we would have landed before via the native previous word command.
if not (word[-1].isspace() or word[-1].isalnum()):
end -= 1
elif self.lastInputEventWasNextWordNav():
# If we moved right via native nav, this should be the end of a native-navigation
# word boundary, regardless of what ATK/AT-SPI2 tells us.
end = offset
# This suggests we just moved to the end of the previous word.
if word != prevWord and prevStart < offset <= prevEnd:
start = prevStart
# If the character to the left of our present position is neither a space, nor
# an alphanumeric character, then suspect that character is a navigation boundary
# where we would have landed before via the native next word command.
lastChar = AXText.get_substring(obj, offset - 1, offset)
if not (lastChar.isspace() or lastChar.isalnum()):
start = offset - 1
word = AXText.get_substring(obj, start, end)
# We only want to present the newline character when we cross a boundary moving from one
# word to another. If we're in the same word, strip it out.
if "\n" in word and word == prevWord:
if word.startswith("\n"):
start += 1
elif word.endswith("\n"):
end -= 1
word = AXText.get_substring(obj, start, end)
debugString = word.replace("\n", "\\n")
msg = (
f"SCRIPT UTILITIES: Adjusted word at offset {offset} for new word nav is "
f"'{debugString}' ({start}-{end})"
)
debug.printMessage(debug.LEVEL_INFO, msg, True)
return word, start, end
def textAtPoint(self, obj, x, y, boundary=None):
# TODO - JD: Audit callers so we don't have to use boundaries.
# Also, can the logic be entirely moved to AXText?
if boundary in (None, Atspi.TextBoundaryType.LINE_START):
string, start, end = AXText.get_line_at_point(obj, x, y)
elif boundary == Atspi.TextBoundaryType.SENTENCE_START:
string, start, end = AXText.get_sentence_at_point(obj, x, y)
elif boundary == Atspi.TextBoundaryType.WORD_START:
string, start, end = AXText.get_word_at_point(obj, x, y)
elif boundary == Atspi.TextBoundaryType.CHAR:
string, start, end = AXText.get_character_at_point(obj, x, y)
else:
return "", 0, 0
if not string:
return "", start, end
if boundary == Atspi.TextBoundaryType.WORD_START and not string.strip():
return "", 0, 0
extents = AXText.get_range_rect(obj, start, end)
rect = Atspi.Rect()
rect.x = x
rect.y = y
rect.width = rect.height = 0
if not AXComponent.get_rect_intersection(extents, rect) and string != "\n":
return "", 0, 0
if not string.endswith("\n") or string == "\n":
return string, start, end
if boundary == Atspi.TextBoundaryType.CHAR:
return string, start, end
char = self.textAtPoint(obj, x, y, Atspi.TextBoundaryType.CHAR)
if char[0] == "\n" and char[2] - char[1] == 1:
return char
return string, start, end
def visibleRows(self, obj, table_rect):
nRows = AXTable.get_row_count(obj)
tokens = ["SCRIPT UTILITIES: ", obj, f"has {nRows} rows"]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
cell = AXComponent.get_descendant_at_point(obj, table_rect.x, table_rect.y + 1)
row = AXTable.get_cell_coordinates(cell, prefer_attribute=False)[0]
startIndex = max(0, row)
tokens = ["SCRIPT UTILITIES: First cell:", cell, f"(row: {row}"]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
# Just in case the row above is a static header row in a scrollable table.
cell_rect = AXComponent.get_rect(cell)
cell = AXComponent.get_descendant_at_point(
obj, table_rect.x, table_rect.y + cell_rect.height + 1)
row, AXTable.get_cell_coordinates(cell, prefer_attribute=False)[0]
nextIndex = max(startIndex, row)
tokens = ["SCRIPT UTILITIES: Next cell:", cell, f"(row: {row})"]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
cell = AXComponent.get_descendant_at_point(
obj, table_rect.x, table_rect.y + table_rect.height - 1)
row = AXTable.get_cell_coordinates(cell, prefer_attribute=False)[0]
tokens = ["SCRIPT UTILITIES: Last cell:", cell, f"(row: {row})"]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
if row == -1:
row = nRows
endIndex = row
rows = list(range(nextIndex, endIndex))
if startIndex not in rows:
rows.insert(0, startIndex)
return rows
def getVisibleTableCells(self, obj):
if not AXObject.supports_table(obj):
return []
rows = self.visibleRows(obj, AXComponent.get_rect(obj))
if not rows:
return []
colStartIndex, colEndIndex = self._getTableRowRange(obj)
if colStartIndex == colEndIndex:
return []
cells = []
for col in range(colStartIndex, colEndIndex):
headers = []
for row in rows:
cell = AXTable.get_cell_at(obj, row, col)
if cell is None:
continue
if not headers:
# TODO - JD: This is needed for flat review to include the column headers
# above the message list in Thunderbird v110. It does not appear necessary
# for more recent versions of Thunderbird (e.g. v115). Looks like a potential
# case of broken table support in (at least) Thunderbird 110. Who else might
# have this same bug?
headers = AXTable.get_column_headers(cell)
if headers and self.isOnScreen(headers[0]):
cells.append(headers[0])
if self.isOnScreen(cell):
cells.append(cell)
return cells
def _getTableRowRange(self, obj):
table = AXTable.get_table(obj)
if table is None:
return -1, -1
columnCount = AXTable.get_column_count(table, False)
startIndex, endIndex = 0, columnCount
if not self.isSpreadSheetCell(obj):
return startIndex, endIndex
rect = AXComponent.get_rect(table)
cell = AXComponent.get_descendant_at_point(table, rect.x + 1, rect.y)
if cell:
column = AXTable.get_cell_coordinates(cell, prefer_attribute=False)[1]
startIndex = column
cell = AXComponent.get_descendant_at_point(table, rect.x + rect.width - 1, rect.y)
if cell:
column = AXTable.get_cell_coordinates(cell, prefer_attribute=False)[1]
endIndex = column + 1
return startIndex, endIndex
def getShowingCellsInSameRow(self, obj, forceFullRow=False):
row = AXTable.get_cell_coordinates(obj, prefer_attribute=False)[0]
if row == -1:
return []
table = AXTable.get_table(obj)
if forceFullRow:
startIndex, endIndex = 0, AXTable.get_column_count(table)
else:
startIndex, endIndex = self._getTableRowRange(obj)
if startIndex == endIndex:
return []
cells = []
for i in range(startIndex, endIndex):
cell = AXTable.get_cell_at(table, row, i)
if AXUtilities.is_showing(cell):
cells.append(cell)
return cells
def findReplicant(self, root, obj):
tokens = ["SCRIPT UTILITIES: Searching for replicant for", obj, "in", root]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
if not (root and obj):
return None
if AXUtilities.is_table(root) or AXUtilities.is_embedded(root):
return None
def isSame(x):
return self.isSameObject(x, obj, comparePaths=True, ignoreNames=True)
if isSame(root):
replicant = root
else:
replicant = AXObject.find_descendant(root, isSame)
tokens = ["HACK: Returning", replicant, "as replicant for invalid object", obj]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return replicant
def getFunctionalChildCount(self, obj):
relation = AXObject.get_relation(obj, Atspi.RelationType.NODE_PARENT_OF)
if relation:
return relation.get_n_targets()
return AXObject.get_child_count(obj)
def getFunctionalChildren(self, obj, sibling=None):
result = AXObject.get_relation_targets(obj, Atspi.RelationType.NODE_PARENT_OF)
if result:
return result
if self.isDescriptionListTerm(sibling):
return self.descriptionListTerms(obj)
if self.isDescriptionListDescription(sibling):
return self.valuesForTerm(self.termForValue(sibling))
return [x for x in AXObject.iter_children(obj)]
def getFunctionalParent(self, obj):
relation = AXObject.get_relation(obj, Atspi.RelationType.NODE_CHILD_OF)
if relation:
return relation.get_target(0)
return AXObject.get_parent(obj)
def getPositionAndSetSize(self, obj, **args):
if obj is None:
return -1, -1
if AXUtilities.is_table_cell(obj) and args.get("readingRow"):
row = AXTable.get_cell_coordinates(obj)[0]
rowcount = AXTable.get_row_count(AXTable.get_table(obj))
return row, rowcount
if AXUtilities.is_combo_box(obj):
selected = self.selectedChildren(obj)
if selected:
obj = selected[0]
else:
def isMenu(x):
return AXUtilities.is_menu(x) or AXUtilities.is_list_box(x)
selected = self.selectedChildren(AXObject.find_descendant(obj, isMenu))
if selected:
obj = selected[0]
else:
return -1, -1
parent = self.getFunctionalParent(obj)
childCount = self.getFunctionalChildCount(parent)
if childCount > 100 and parent == AXObject.get_parent(obj):
return AXObject.get_index_in_parent(obj), childCount
siblings = self.getFunctionalChildren(parent, obj)
if len(siblings) < 100 and not AXObject.find_ancestor(obj, AXUtilities.is_combo_box):
layoutRoles = [Atspi.Role.SEPARATOR, Atspi.Role.TEAROFF_MENU_ITEM]
def isNotLayoutOnly(x):
return AXObject.is_valid(x) and AXObject.get_role(x) not in layoutRoles
siblings = list(filter(isNotLayoutOnly, siblings))
if not (siblings and obj in siblings):
return -1, -1
if self.isFocusableLabel(obj):
siblings = list(filter(self.isFocusableLabel, siblings))
if len(siblings) == 1:
return -1, -1
position = siblings.index(obj)
setSize = len(siblings)
return position, setSize
def termForValue(self, obj):
if not self.isDescriptionListDescription(obj):
return None
while obj and not self.isDescriptionListTerm(obj):
obj = AXObject.get_previous_sibling(obj)
return obj
def valuesForTerm(self, obj):
if not self.isDescriptionListTerm(obj):
return []
values = []
obj = AXObject.get_next_sibling(obj)
while obj and self.isDescriptionListDescription(obj):
values.append(obj)
obj = AXObject.get_next_sibling(obj)
return values
def getValueCountForTerm(self, obj):
return len(self.valuesForTerm(obj))
def getRoleDescription(self, obj, isBraille=False):
return ""
def getCachedTextSelection(self, obj):
textSelections = self._script.pointOfReference.get('textSelections', {})
start, end, string = textSelections.get(hash(obj), (0, 0, ''))
tokens = ["SCRIPT UTILITIES: Cached selection for", obj, f"is '{string}' ({start}, {end})"]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return start, end, string
def updateCachedTextSelection(self, obj):
if self._script.pointOfReference.get('entireDocumentSelected'):
selectedText = self.allSelectedText(obj)[0]
if not selectedText:
self._script.pointOfReference['entireDocumentSelected'] = False
self._script.pointOfReference['textSelections'] = {}
textSelections = self._script.pointOfReference.get('textSelections', {})
# Because some apps and toolkits create, destroy, and duplicate objects
# and events.
if hash(obj) in textSelections:
value = textSelections.pop(hash(obj))
for x in [k for k in textSelections.keys() if textSelections.get(k) == value]:
textSelections.pop(x)
string, start, end = AXText.get_selected_text(obj)
tokens = ["SCRIPT UTILITIES: New selection for", obj, f"is '{string}' ({start}, {end})"]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
textSelections[hash(obj)] = start, end, string
self._script.pointOfReference['textSelections'] = textSelections
@staticmethod
def onClipboardContentsChanged(*args):
script = script_manager.getManager().getActiveScript()
if script is None:
return
if time.time() - Utilities._last_clipboard_update < 0.05:
msg = "SCRIPT UTILITIES: Clipboard contents change believed to be duplicate"
debug.printMessage(debug.LEVEL_INFO, msg, True)
return
Utilities._last_clipboard_update = time.time()
script.onClipboardContentsChanged(*args)
def connectToClipboard(self):
if self._clipboardHandlerId is not None:
return
clipboard = Gtk.Clipboard.get(Gdk.Atom.intern("CLIPBOARD", False))
self._clipboardHandlerId = clipboard.connect(
'owner-change', self.onClipboardContentsChanged)
def disconnectFromClipboard(self):
if self._clipboardHandlerId is None:
return
clipboard = Gtk.Clipboard.get(Gdk.Atom.intern("CLIPBOARD", False))
clipboard.disconnect(self._clipboardHandlerId)
def getClipboardContents(self):
clipboard = Gtk.Clipboard.get(Gdk.Atom.intern("CLIPBOARD", False))
return clipboard.wait_for_text()
def setClipboardText(self, text):
clipboard = Gtk.Clipboard.get(Gdk.Atom.intern("CLIPBOARD", False))
clipboard.set_text(text, -1)
def appendTextToClipboard(self, text):
clipboard = Gtk.Clipboard.get(Gdk.Atom.intern("CLIPBOARD", False))
clipboard.request_text(self._appendTextToClipboardCallback, text)
def _appendTextToClipboardCallback(self, clipboard, text, newText, separator="\n"):
text = text.rstrip("\n")
text = f"{text}{separator}{newText}"
clipboard.set_text(text, -1)
def lastInputEventCameFromThisApp(self):
if not isinstance(orca_state.lastInputEvent, input_event.KeyboardEvent):
return False
event = orca_state.lastNonModifierKeyEvent
return event and event.isFromApplication(self._script.app)
def lastInputEventWasPrintableKey(self):
event = orca_state.lastInputEvent
if not isinstance(event, input_event.KeyboardEvent):
return False
return event.isPrintableKey()
def lastInputEventWasCommand(self):
keyString, mods = self.lastKeyAndModifiers()
return mods & keybindings.CTRL_MODIFIER_MASK
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
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.ORCA_MODIFIER_MASK:
return False
return True
def lastInputEventWasCaretNav(self):
return self.lastInputEventWasCharNav() \
or self.lastInputEventWasWordNav() \
or self.lastInputEventWasLineNav() \
or self.lastInputEventWasLineBoundaryNav()
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
def lastInputEventWasWordNav(self):
keyString, mods = self.lastKeyAndModifiers()
if keyString not in ["Left", "Right"]:
return False
return mods & keybindings.CTRL_MODIFIER_MASK
def lastInputEventWasPrevWordNav(self):
keyString, mods = self.lastKeyAndModifiers()
if not keyString == "Left":
return False
return mods & keybindings.CTRL_MODIFIER_MASK
def lastInputEventWasNextWordNav(self):
keyString, mods = self.lastKeyAndModifiers()
if not keyString == "Right":
return False
return mods & keybindings.CTRL_MODIFIER_MASK
def lastInputEventWasLineNav(self):
keyString, mods = self.lastKeyAndModifiers()
if keyString not in ["Up", "Down"]:
return False
if self.isEditableDescendantOfComboBox(focus_manager.getManager().get_locus_of_focus()):
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(focus_manager.getManager().get_locus_of_focus()):
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.ORCA_MODIFIER_MASK:
return False
return True
def lastInputEventWasMouseButton(self):
return isinstance(orca_state.lastInputEvent, input_event.MouseButtonEvent)
def lastInputEventWasPrimaryMouseClick(self):
event = orca_state.lastInputEvent
if isinstance(event, input_event.MouseButtonEvent):
return event.button == "1" and event.pressed
return False
def lastInputEventWasMiddleMouseClick(self):
event = orca_state.lastInputEvent
if isinstance(event, input_event.MouseButtonEvent):
return event.button == "2" and event.pressed
return False
def lastInputEventWasSecondaryMouseClick(self):
event = orca_state.lastInputEvent
if isinstance(event, input_event.MouseButtonEvent):
return event.button == "3" and event.pressed
return False
def lastInputEventWasPrimaryMouseRelease(self):
event = orca_state.lastInputEvent
if isinstance(event, input_event.MouseButtonEvent):
return event.button == "1" and not event.pressed
return False
def lastInputEventWasMiddleMouseRelease(self):
event = orca_state.lastInputEvent
if isinstance(event, input_event.MouseButtonEvent):
return event.button == "2" and not event.pressed
return False
def lastInputEventWasSecondaryMouseRelease(self):
event = orca_state.lastInputEvent
if isinstance(event, input_event.MouseButtonEvent):
return event.button == "3" and not event.pressed
return False
def lastInputEventWasTableSort(self, delta=0.5):
event = orca_state.lastInputEvent
if not event:
return False
now = time.time()
if now - event.time > delta:
return False
lastSortTime = self._script.pointOfReference.get('last-table-sort-time', 0.0)
if now - lastSortTime < delta:
return False
if isinstance(event, input_event.MouseButtonEvent):
if not self.lastInputEventWasPrimaryMouseRelease():
return False
elif isinstance(event, input_event.KeyboardEvent):
if not event.isHandledBy(self._script.leftClickReviewItem):
keyString, mods = self.lastKeyAndModifiers()
if keyString not in ["Return", "space", " "]:
return False
return AXUtilities.is_table_header(focus_manager.getManager().get_locus_of_focus())
def isPresentableExpandedChangedEvent(self, event):
if self.isSameObject(event.source, focus_manager.getManager().get_locus_of_focus()):
return True
if AXUtilities.is_table_row(event.source) or AXUtilities.is_list_box(event.source):
return True
if AXUtilities.is_combo_box(event.source) or AXUtilities.is_button(event.source):
return AXUtilities.is_focused(event.source)
return False
def isPresentableTextChangedEventForLocusOfFocus(self, event):
if not event.type.startswith("object:text-changed:") \
and not event.type.startswith("object:text-attributes-changed"):
return False
if AXUtilities.is_menu_related(event.source) \
or AXUtilities.is_slider(event.source) \
or AXUtilities.is_spin_button(event.source) \
or AXUtilities.is_label(event.source):
msg = "SCRIPT UTILITIES: Event is not being presented due to role"
debug.printMessage(debug.LEVEL_INFO, msg, True)
return False
if AXUtilities.is_focused(event.source):
if self.isTypeahead(event.source):
return True
if AXUtilities.is_password_text(event.source):
return True
if focus_manager.getManager().focus_is_dead():
return True
elif AXUtilities.is_table_cell(event.source) and not AXUtilities.is_selected(event.source):
msg = "SCRIPT UTILITIES: Event is not being presented due to role and states"
debug.printMessage(debug.LEVEL_INFO, msg, True)
return False
if focus_manager.getManager().get_locus_of_focus() in \
[event.source, AXObject.get_parent(event.source)]:
return True
msg = "SCRIPT UTILITIES: Event is not being presented due to lack of cause"
debug.printMessage(debug.LEVEL_INFO, msg, True)
return False
def isBackSpaceCommandTextDeletionEvent(self, event):
if not event.type.startswith("object:text-changed:delete"):
return False
if self.isHidden(event.source):
return False
keyString, mods = self.lastKeyAndModifiers()
if keyString == "BackSpace":
return True
return False
def isDeleteCommandTextDeletionEvent(self, event):
if not event.type.startswith("object:text-changed:delete"):
return False
if event.type.endswith("system"):
return False
return self.lastInputEventWasDelete()
def isUndoCommandTextDeletionEvent(self, event):
if not event.type.startswith("object:text-changed:delete"):
return False
if not self.lastInputEventWasUndo():
return False
start, end, string = self.getCachedTextSelection(event.source)
return not string
def isSelectedTextDeletionEvent(self, event):
if not event.type.startswith("object:text-changed:delete"):
return False
if self.lastInputEventWasPaste():
return False
start, end, string = self.getCachedTextSelection(event.source)
return string and string.strip() == event.any_data.strip()
def isSelectedTextInsertionEvent(self, event):
if not event.type.startswith("object:text-changed:insert"):
return False
self.updateCachedTextSelection(event.source)
start, end, string = self.getCachedTextSelection(event.source)
return string and string == event.any_data and start == event.detail1
def isSelectedTextRestoredEvent(self, event):
if not self.lastInputEventWasUndo():
return False
if self.isSelectedTextInsertionEvent(event):
return True
return False
def isMiddleMouseButtonTextInsertionEvent(self, event):
if not event.type.startswith("object:text-changed:insert"):
return False
return self.lastInputEventWasMiddleMouseClick()
def isEchoableTextInsertionEvent(self, event):
if not event.type.startswith("object:text-changed:insert"):
return False
if AXUtilities.is_focusable(event.source) \
and not AXUtilities.is_focused(event.source) \
and event.source != focus_manager.getManager().get_locus_of_focus():
msg = "SCRIPT UTILITIES: Not echoable text insertion event: " \
"focusable source is not focused"
debug.printMessage(debug.LEVEL_INFO, msg, True)
return False
if AXUtilities.is_password_text(event.source):
return settings_manager.getManager().getSetting("enableKeyEcho")
if len(event.any_data.strip()) == 1:
return settings_manager.getManager().getSetting("enableEchoByCharacter")
return False
def isEditableTextArea(self, obj):
if not self.isTextArea(obj):
return False
return AXUtilities.is_editable(obj)
def isClipboardTextChangedEvent(self, event):
if not event.type.startswith("object:text-changed"):
return False
if not self.lastInputEventWasCommand() or self.lastInputEventWasUndo():
return False
if self.isBackSpaceCommandTextDeletionEvent(event):
return False
if "delete" in event.type and self.lastInputEventWasPaste():
return False
if not self.isEditableTextArea(event.source):
return False
contents = self.getClipboardContents()
if not contents:
return False
if event.any_data == contents:
return True
if bool(re.search(r"\w", event.any_data)) != bool(re.search(r"\w", contents)):
return False
# HACK: If the application treats each paragraph as a separate object,
# we'll get individual events for each paragraph rather than a single
# event whose any_data matches the clipboard contents.
if "\n" in contents and event.any_data.rstrip() in contents:
return True
return False
def objectContentsAreInClipboard(self, obj=None):
obj = obj or focus_manager.getManager().get_locus_of_focus()
if not obj or AXObject.is_dead(obj):
return False
contents = self.getClipboardContents()
if not contents:
return False
string, start, end = AXText.get_selected_text(obj)
if string and string in contents:
return True
obj = self.realActiveDescendant(obj) or obj
if AXObject.is_dead(obj):
return False
return obj and AXObject.get_name(obj) in contents
def clearCachedCommandState(self):
self._script.pointOfReference['undo'] = False
self._script.pointOfReference['redo'] = False
self._script.pointOfReference['paste'] = False
self._script.pointOfReference['last-selection-message'] = ''
def handleUndoTextEvent(self, event):
if self.lastInputEventWasUndo():
if not self._script.pointOfReference.get('undo'):
self._script.presentMessage(messages.UNDO)
self._script.pointOfReference['undo'] = True
self.updateCachedTextSelection(event.source)
return True
if self.lastInputEventWasRedo():
if not self._script.pointOfReference.get('redo'):
self._script.presentMessage(messages.REDO)
self._script.pointOfReference['redo'] = True
self.updateCachedTextSelection(event.source)
return True
return False
def handleUndoLocusOfFocusChange(self):
if self._locusOfFocusIsTopLevelObject():
return False
if self.lastInputEventWasUndo():
if not self._script.pointOfReference.get('undo'):
self._script.presentMessage(messages.UNDO)
self._script.pointOfReference['undo'] = True
return True
if self.lastInputEventWasRedo():
if not self._script.pointOfReference.get('redo'):
self._script.presentMessage(messages.REDO)
self._script.pointOfReference['redo'] = True
return True
return False
def handlePasteLocusOfFocusChange(self):
if self._locusOfFocusIsTopLevelObject():
return False
if self.lastInputEventWasPaste():
if not self._script.pointOfReference.get('paste'):
self._script.presentMessage(
messages.CLIPBOARD_PASTED_FULL, messages.CLIPBOARD_PASTED_BRIEF)
self._script.pointOfReference['paste'] = True
return True
return False
def eventIsCanvasNoise(self, event):
return False
def eventIsSpinnerNoise(self, event):
return False
def eventIsUserTriggered(self, event):
if not orca_state.lastInputEvent:
msg = "SCRIPT UTILITIES: Not user triggered: No last input event."
debug.printMessage(debug.LEVEL_INFO, msg, True)
return False
delta = time.time() - orca_state.lastInputEvent.time
if delta > 1:
msg = f"SCRIPT UTILITIES: Not user triggered: Last input event {delta:.2f}s ago."
debug.printMessage(debug.LEVEL_INFO, msg, True)
return False
return True
def presentFocusChangeReason(self):
if self.handleUndoLocusOfFocusChange():
return True
if self.handlePasteLocusOfFocusChange():
return True
return False
def allItemsSelected(self, obj):
if not AXObject.supports_selection(obj):
return False
if AXUtilities.is_expandable(obj) and not AXUtilities.is_expanded(obj):
return False
if AXUtilities.is_combo_box(obj) or AXUtilities.is_menu(obj):
return False
childCount = AXObject.get_child_count(obj)
if childCount == AXSelection.get_selected_child_count(obj):
# The selection interface gives us access to what is selected, which might
# not actually be a direct child.
child = AXSelection.get_selected_child(obj, 0)
if AXObject.get_parent(child) != obj:
return False
msg = f"SCRIPT UTILITIES: All {childCount} children believed to be selected"
debug.printMessage(debug.LEVEL_INFO, msg, True)
return True
return AXTable.all_cells_are_selected(obj)
def handleContainerSelectionChange(self, obj):
allAlreadySelected = self._script.pointOfReference.get('allItemsSelected')
allCurrentlySelected = self.allItemsSelected(obj)
if allAlreadySelected and allCurrentlySelected:
return True
self._script.pointOfReference['allItemsSelected'] = allCurrentlySelected
if self.lastInputEventWasSelectAll() and allCurrentlySelected:
self._script.presentMessage(messages.CONTAINER_SELECTED_ALL)
focus_manager.getManager().set_locus_of_focus(None, obj, False)
return True
return False
def handleTextSelectionChange(self, obj, speakMessage=True):
# Note: This guesswork to figure out what actually changed with respect
# to text selection will get eliminated once the new text-selection API
# is added to ATK and implemented by the toolkits. (BGO 638378)
if not AXObject.supports_text(obj):
return False
oldStart, oldEnd, oldString = self.getCachedTextSelection(obj)
self.updateCachedTextSelection(obj)
newStart, newEnd, newString = self.getCachedTextSelection(obj)
if self._speakTextSelectionState(len(newString)):
return True
# Even though we present a message, treat it as unhandled so the new location is
# still presented.
if not self.lastInputEventWasCaretNavWithSelection() and oldString and not newString:
self._script.speakMessage(messages.SELECTION_REMOVED)
return False
changes = []
oldChars = set(range(oldStart, oldEnd))
newChars = set(range(newStart, newEnd))
if not oldChars.union(newChars):
return False
if oldChars and newChars and not oldChars.intersection(newChars):
# A simultaneous unselection and selection centered at one offset.
changes.append([oldStart, oldEnd, messages.TEXT_UNSELECTED])
changes.append([newStart, newEnd, messages.TEXT_SELECTED])
else:
change = sorted(oldChars.symmetric_difference(newChars))
if not change:
return False
changeStart, changeEnd = change[0], change[-1] + 1
if oldChars < newChars:
changes.append([changeStart, changeEnd, messages.TEXT_SELECTED])
if oldString.endswith(self.EMBEDDED_OBJECT_CHARACTER) and oldEnd == changeStart:
# There's a possibility that we have a link spanning multiple lines. If so,
# we want to present the continuation that just became selected.
child = AXHypertext.get_child_at_offset(obj, oldEnd - 1)
self.handleTextSelectionChange(child, False)
else:
changes.append([changeStart, changeEnd, messages.TEXT_UNSELECTED])
if newString.endswith(self.EMBEDDED_OBJECT_CHARACTER):
# There's a possibility that we have a link spanning multiple lines. If so,
# we want to present the continuation that just became unselected.
child = AXHypertext.get_child_at_offset(obj, newEnd - 1)
self.handleTextSelectionChange(child, False)
speakMessage = speakMessage \
and not settings_manager.getManager().getSetting('onlySpeakDisplayedText')
for start, end, message in changes:
string = AXText.get_substring(obj, start, end)
endsWithChild = string.endswith(self.EMBEDDED_OBJECT_CHARACTER)
if endsWithChild:
end -= 1
self._script.sayPhrase(obj, start, end)
if speakMessage and not endsWithChild:
self._script.speakMessage(message, interrupt=False)
if endsWithChild:
child = AXHypertext.get_child_at_offset(obj, end)
self.handleTextSelectionChange(child, speakMessage)
return True
def _getCtrlShiftSelectionsStrings(self):
"""Hacky and to-be-obsoleted method."""
return [messages.PARAGRAPH_SELECTED_DOWN,
messages.PARAGRAPH_UNSELECTED_DOWN,
messages.PARAGRAPH_SELECTED_UP,
messages.PARAGRAPH_UNSELECTED_UP]
def _speakTextSelectionState(self, nSelections):
"""Hacky and to-be-obsoleted method."""
if settings_manager.getManager().getSetting('onlySpeakDisplayedText'):
return False
eventStr, mods = self.lastKeyAndModifiers()
isControlKey = mods & keybindings.CTRL_MODIFIER_MASK
isShiftKey = mods & keybindings.SHIFT_MODIFIER_MASK
selectedText = nSelections > 0
line = None
if (eventStr == "Page_Down") and isShiftKey and isControlKey:
line = messages.LINE_SELECTED_RIGHT
elif (eventStr == "Page_Up") and isShiftKey and isControlKey:
line = messages.LINE_SELECTED_LEFT
elif (eventStr == "Page_Down") and isShiftKey and not isControlKey:
if selectedText:
line = messages.PAGE_SELECTED_DOWN
else:
line = messages.PAGE_UNSELECTED_DOWN
elif (eventStr == "Page_Up") and isShiftKey and not isControlKey:
if selectedText:
line = messages.PAGE_SELECTED_UP
else:
line = messages.PAGE_UNSELECTED_UP
elif (eventStr == "Down") and isShiftKey and isControlKey:
strings = self._getCtrlShiftSelectionsStrings()
if selectedText:
line = strings[0]
else:
line = strings[1]
elif (eventStr == "Up") and isShiftKey and isControlKey:
strings = self._getCtrlShiftSelectionsStrings()
if selectedText:
line = strings[2]
else:
line = strings[3]
elif (eventStr == "Home") and isShiftKey and isControlKey:
if selectedText:
line = messages.DOCUMENT_SELECTED_UP
else:
line = messages.DOCUMENT_UNSELECTED_UP
elif (eventStr == "End") and isShiftKey and isControlKey:
if selectedText:
line = messages.DOCUMENT_SELECTED_DOWN
else:
line = messages.DOCUMENT_SELECTED_UP
elif self.lastInputEventWasSelectAll() and selectedText:
if not self._script.pointOfReference.get('entireDocumentSelected'):
self._script.pointOfReference['entireDocumentSelected'] = True
line = messages.DOCUMENT_SELECTED_ALL
else:
return True
if not line:
return False
if line != self._script.pointOfReference.get('last-selection-message'):
self._script.pointOfReference['last-selection-message'] = line
self._script.speakMessage(line)
return True
def shouldInterruptForLocusOfFocusChange(self, oldLocusOfFocus, newLocusOfFocus, event=None):
msg = "SCRIPT UTILITIES: Not interrupting for locusOfFocus change: "
if event is None:
msg += "event is None"
debug.printMessage(debug.LEVEL_INFO, msg, True)
return False
if event is not None and event.type.startswith("object:active-descendant-changed"):
return self._script.stopSpeechOnActiveDescendantChanged(event)
if AXUtilities.is_table_cell(oldLocusOfFocus) and AXUtilities.is_text(newLocusOfFocus) \
and AXUtilities.is_editable(newLocusOfFocus):
msg += "suspected editable cell"
debug.printMessage(debug.LEVEL_INFO, msg, True)
return False
if not AXUtilities.is_menu_related(newLocusOfFocus) \
and (AXUtilities.is_check_menu_item(oldLocusOfFocus) \
or AXUtilities.is_radio_menu_item(oldLocusOfFocus)):
msg += "suspected menuitem state change"
debug.printMessage(debug.LEVEL_INFO, msg, True)
return False
if AXObject.is_ancestor(newLocusOfFocus, oldLocusOfFocus):
if AXObject.get_name(oldLocusOfFocus):
msg += "old locusOfFocus is ancestor with name of new locusOfFocus"
debug.printMessage(debug.LEVEL_INFO, msg, True)
return False
return True
def isOld(target):
return target == oldLocusOfFocus
def isNew(target):
return target == newLocusOfFocus
if AXObject.get_relation_targets(newLocusOfFocus,
Atspi.RelationType.CONTROLLER_FOR, isOld):
msg += "new locusOfFocus controls old locusOfFocus"
debug.printMessage(debug.LEVEL_INFO, msg, True)
return False
if AXObject.get_relation_targets(oldLocusOfFocus,
Atspi.RelationType.CONTROLLER_FOR, isNew):
msg += "old locusOfFocus controls new locusOfFocus"
debug.printMessage(debug.LEVEL_INFO, msg, True)
return False
return True
def stringsAreRedundant(self, str1, str2, threshold=0.5):
if not (str1 and str2):
return False
similarity = round(SequenceMatcher(None, str1.lower(), str2.lower()).ratio(), 2)
msg = (
f"SCRIPT UTILITIES: Similarity between '{str1}', '{str2}': {similarity} "
f"(threshold: {threshold})"
)
debug.printMessage(debug.LEVEL_INFO, msg, True)
return similarity >= threshold