%PDF- %PDF-
| Direktori : /lib/python3/dist-packages/orca/ |
| Current File : //lib/python3/dist-packages/orca/ax_object.py |
# Utilities for obtaining information about accessible objects.
#
# Copyright 2023 Igalia, S.L.
# Author: Joanmarie Diggs <jdiggs@igalia.com>
#
# 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.
"""
Utilities for obtaining information about accessible objects.
These utilities are app-type- and toolkit-agnostic. Utilities that might have
different implementations or results depending on the type of app (e.g. terminal,
chat, web) or toolkit (e.g. Qt, Gtk) should be in script_utilities.py file(s).
N.B. There are currently utilities that should never have custom implementations
that live in script_utilities.py files. These will be moved over time.
"""
__id__ = "$Id$"
__version__ = "$Revision$"
__date__ = "$Date$"
__copyright__ = "Copyright (c) 2023 Igalia, S.L."
__license__ = "LGPL"
import re
import threading
import time
import gi
gi.require_version("Atspi", "2.0")
from gi.repository import Atspi
from . import debug
class AXObject:
"""Utilities for obtaining information about accessible objects."""
KNOWN_DEAD = {}
REAL_APP_FOR_MUTTER_FRAME = {}
REAL_FRAME_FOR_MUTTER_FRAME = {}
OBJECT_ATTRIBUTES = {}
_lock = threading.Lock()
@staticmethod
def _clear_stored_data():
"""Clears any data we have cached for objects"""
while True:
time.sleep(60)
AXObject._clear_all_dictionaries()
@staticmethod
def _clear_all_dictionaries(reason=""):
msg = "AXObject: Clearing local cache."
if reason:
msg += f" Reason: {reason}"
debug.printMessage(debug.LEVEL_INFO, msg, True)
with AXObject._lock:
tokens = ["AXObject: Clearing known dead-or-alive state for",
len(AXObject.KNOWN_DEAD), "objects"]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
AXObject.KNOWN_DEAD.clear()
tokens = ["AXObject: Clearing", len(AXObject.REAL_APP_FOR_MUTTER_FRAME),
"real apps for mutter frames"]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
AXObject.REAL_APP_FOR_MUTTER_FRAME.clear()
tokens = ["AXObject: Clearing", len(AXObject.REAL_FRAME_FOR_MUTTER_FRAME),
"real frames for mutter frames"]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
AXObject.REAL_FRAME_FOR_MUTTER_FRAME.clear()
tokens = ["AXObject: Clearing cached object attributes for",
len(AXObject.OBJECT_ATTRIBUTES), "objects"]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
AXObject.OBJECT_ATTRIBUTES.clear()
@staticmethod
def clear_cache_now(reason=""):
"""Clears all cached information immediately."""
AXObject._clear_all_dictionaries(reason)
@staticmethod
def start_cache_clearing_thread():
"""Starts thread to periodically clear cached details."""
thread = threading.Thread(target=AXObject._clear_stored_data)
thread.daemon = True
thread.start()
@staticmethod
def is_bogus(obj):
"""Hack to ignore certain objects. All entries must have a bug."""
# TODO - JD: Periodically check for fixes and remove hacks which are no
# longer needed.
# https://bugzilla.mozilla.org/show_bug.cgi?id=1879750
if AXObject.get_role(obj) == Atspi.Role.SECTION \
and AXObject.get_role(AXObject.get_parent(obj)) == Atspi.Role.FRAME \
and Atspi.Accessible.get_toolkit_name(obj).lower() == "gecko":
tokens = ["AXObject:", obj, "is bogus. See mozilla bug 1879750."]
debug.printTokens(debug.LEVEL_INFO, tokens, True, True)
return True
return False
@staticmethod
def is_valid(obj):
"""Returns False if we know for certain this object is invalid"""
return not (obj is None or AXObject.object_is_known_dead(obj))
@staticmethod
def object_is_known_dead(obj):
"""Returns True if we know for certain this object no longer exists"""
return obj and AXObject.KNOWN_DEAD.get(hash(obj)) is True
@staticmethod
def _set_known_dead_status(obj, is_dead):
"""Updates the known-dead status of obj"""
if obj is None:
return
current_status = AXObject.KNOWN_DEAD.get(hash(obj))
if current_status == is_dead:
return
AXObject.KNOWN_DEAD[hash(obj)] = is_dead
if is_dead:
msg = "AXObject: Adding to known dead objects"
debug.printMessage(debug.LEVEL_INFO, msg, True, True)
return
if current_status:
tokens = ["AXObject: Removing", obj, "from known-dead objects"]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
@staticmethod
def handle_error(obj, error, msg):
"""Parses the exception and potentially updates our status for obj"""
error = str(error)
if re.search(r"accessible/\d+ does not exist", error):
msg = msg.replace(error, "object no longer exists")
debug.printMessage(debug.LEVEL_INFO, msg, True)
elif re.search(r"The application no longer exists", error):
msg = msg.replace(error, "app no longer exists")
debug.printMessage(debug.LEVEL_INFO, msg, True)
else:
debug.printMessage(debug.LEVEL_INFO, msg, True)
return
if AXObject.KNOWN_DEAD.get(hash(obj)) is False:
AXObject._set_known_dead_status(obj, True)
@staticmethod
def supports_action(obj):
"""Returns True if the action interface is supported on obj"""
if not AXObject.is_valid(obj):
return False
try:
iface = Atspi.Accessible.get_action_iface(obj)
except Exception as error:
msg = f"AXObject: Exception calling get_action_iface on {obj}: {error}"
AXObject.handle_error(obj, error, msg)
return False
return iface is not None
@staticmethod
def supports_collection(obj):
"""Returns True if the collection interface is supported on obj"""
if not AXObject.is_valid(obj):
return False
app_name = AXObject.get_name(AXObject.get_application(obj))
if app_name in ["soffice"]:
tokens = ["AXObject: Treating", app_name, "as not supporting collection."]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return False
try:
iface = Atspi.Accessible.get_collection_iface(obj)
except Exception as error:
msg = f"AXObject: Exception calling get_collection_iface on {obj}: {error}"
AXObject.handle_error(obj, error, msg)
return False
return iface is not None
@staticmethod
def supports_component(obj):
"""Returns True if the component interface is supported on obj"""
if not AXObject.is_valid(obj):
return False
try:
iface = Atspi.Accessible.get_component_iface(obj)
except Exception as error:
msg = f"AXObject: Exception calling get_component_iface on {obj}: {error}"
AXObject.handle_error(obj, error, msg)
return False
return iface is not None
@staticmethod
def supports_document(obj):
"""Returns True if the document interface is supported on obj"""
if not AXObject.is_valid(obj):
return False
try:
iface = Atspi.Accessible.get_document_iface(obj)
except Exception as error:
msg = f"AXObject: Exception calling get_document_iface on {obj}: {error}"
AXObject.handle_error(obj, error, msg)
return False
return iface is not None
@staticmethod
def supports_editable_text(obj):
"""Returns True if the editable-text interface is supported on obj"""
if not AXObject.is_valid(obj):
return False
try:
iface = Atspi.Accessible.get_editable_text_iface(obj)
except Exception as error:
msg = f"AXObject: Exception calling get_editable_text_iface on {obj}: {error}"
AXObject.handle_error(obj, error, msg)
return False
return iface is not None
@staticmethod
def supports_hyperlink(obj):
"""Returns True if the hyperlink interface is supported on obj"""
if not AXObject.is_valid(obj):
return False
try:
iface = Atspi.Accessible.get_hyperlink(obj)
except Exception as error:
msg = f"AXObject: Exception calling get_hyperlink on {obj}: {error}"
AXObject.handle_error(obj, error, msg)
return False
return iface is not None
@staticmethod
def supports_hypertext(obj):
"""Returns True if the hypertext interface is supported on obj"""
if not AXObject.is_valid(obj):
return False
try:
iface = Atspi.Accessible.get_hypertext_iface(obj)
except Exception as error:
msg = f"AXObject: Exception calling get_hypertext_iface on {obj}: {error}"
AXObject.handle_error(obj, error, msg)
return False
return iface is not None
@staticmethod
def supports_image(obj):
"""Returns True if the image interface is supported on obj"""
if not AXObject.is_valid(obj):
return False
try:
iface = Atspi.Accessible.get_image_iface(obj)
except Exception as error:
msg = f"AXObject: Exception calling get_image_iface on {obj}: {error}"
AXObject.handle_error(obj, error, msg)
return False
return iface is not None
@staticmethod
def supports_selection(obj):
"""Returns True if the selection interface is supported on obj"""
if not AXObject.is_valid(obj):
return False
try:
iface = Atspi.Accessible.get_selection_iface(obj)
except Exception as error:
msg = f"AXObject: Exception calling get_selection_iface on {obj}: {error}"
AXObject.handle_error(obj, error, msg)
return False
return iface is not None
@staticmethod
def supports_table(obj):
"""Returns True if the table interface is supported on obj"""
if not AXObject.is_valid(obj):
return False
try:
iface = Atspi.Accessible.get_table_iface(obj)
except Exception as error:
msg = f"AXObject: Exception calling get_table_iface on {obj}: {error}"
AXObject.handle_error(obj, error, msg)
return False
return iface is not None
@staticmethod
def supports_table_cell(obj):
"""Returns True if the table cell interface is supported on obj"""
if not AXObject.is_valid(obj):
return False
try:
iface = Atspi.Accessible.get_table_cell(obj)
except Exception as error:
msg = f"AXObject: Exception calling get_table_cell on {obj}: {error}"
AXObject.handle_error(obj, error, msg)
return False
return iface is not None
@staticmethod
def supports_text(obj):
"""Returns True if the text interface is supported on obj"""
if not AXObject.is_valid(obj):
return False
try:
iface = Atspi.Accessible.get_text_iface(obj)
except Exception as error:
msg = f"AXObject: Exception calling get_text_iface on {obj}: {error}"
AXObject.handle_error(obj, error, msg)
return False
return iface is not None
@staticmethod
def supports_value(obj):
"""Returns True if the value interface is supported on obj"""
if not AXObject.is_valid(obj):
return False
try:
iface = Atspi.Accessible.get_value_iface(obj)
except Exception as error:
msg = f"AXObject: Exception calling get_value_iface on {obj}: {error}"
AXObject.handle_error(obj, error, msg)
return False
return iface is not None
@staticmethod
def supported_interfaces_as_string(obj):
"""Returns the supported interfaces of obj as a string"""
if not AXObject.is_valid(obj):
return ""
iface_checks = [
(AXObject.supports_action, "Action"),
(AXObject.supports_collection, "Collection"),
(AXObject.supports_component, "Component"),
(AXObject.supports_document, "Document"),
(AXObject.supports_editable_text, "EditableText"),
(AXObject.supports_hyperlink, "Hyperlink"),
(AXObject.supports_hypertext, "Hypertext"),
(AXObject.supports_image, "Image"),
(AXObject.supports_selection, "Selection"),
(AXObject.supports_table, "Table"),
(AXObject.supports_table_cell, "TableCell"),
(AXObject.supports_text, "Text"),
(AXObject.supports_value, "Value"),
]
ifaces = [iface for check, iface in iface_checks if check(obj)]
return ", ".join(ifaces)
@staticmethod
def get_path(obj):
"""Returns the path from application to obj as list of child indices"""
if not AXObject.is_valid(obj):
return []
path = []
acc = obj
while acc:
try:
path.append(Atspi.Accessible.get_index_in_parent(acc))
except Exception as error:
msg = f"AXObject: Exception getting index in parent for {acc}: {error}"
AXObject.handle_error(acc, error, msg)
return []
acc = AXObject.get_parent_checked(acc)
path.reverse()
return path
@staticmethod
def get_index_in_parent(obj):
"""Returns the child index of obj within its parent"""
if not AXObject.is_valid(obj):
return -1
try:
index = Atspi.Accessible.get_index_in_parent(obj)
except Exception as error:
msg = f"AXObject: Exception in get_index_in_parent: {error}"
AXObject.handle_error(obj, error, msg)
return -1
return index
@staticmethod
def get_parent(obj):
"""Returns the accessible parent of obj. See also get_parent_checked."""
if not AXObject.is_valid(obj):
return None
try:
parent = Atspi.Accessible.get_parent(obj)
except Exception as error:
msg = f"AXObject: Exception in get_parent: {error}"
AXObject.handle_error(obj, error, msg)
return None
if parent == obj:
tokens = ["AXObject:", obj, "claims to be its own parent"]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return None
if parent is None \
and AXObject.get_role(obj) not in [Atspi.Role.INVALID, Atspi.Role.DESKTOP_FRAME]:
tokens = ["AXObject:", obj, "claims to have no parent"]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return parent
@staticmethod
def get_parent_checked(obj):
"""Returns the parent of obj, doing checks for tree validity"""
if not AXObject.is_valid(obj):
return None
role = AXObject.get_role(obj)
if role in [Atspi.Role.INVALID, Atspi.Role.APPLICATION]:
return None
parent = AXObject.get_parent(obj)
if parent is None:
return None
if debug.LEVEL_INFO < debug.debugLevel:
return parent
if AXObject.is_dead(obj):
return parent
index = AXObject.get_index_in_parent(obj)
n_children = AXObject.get_child_count(parent)
if index < 0 or index >= n_children:
tokens = ["AXObject:", obj, "has index", index,
"; parent", parent, "has", n_children, "children"]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return parent
# This performs our check and includes any errors. We don't need the return value here.
AXObject.get_active_descendant_checked(parent, obj)
return parent
@staticmethod
def find_ancestor(obj, pred):
"""Returns the ancestor of obj if the function pred is true"""
if not AXObject.is_valid(obj):
return None
# Keep track of objects we've encountered in order to handle broken trees.
objects = [obj]
parent = AXObject.get_parent_checked(obj)
while parent:
if parent in objects:
tokens = ["AXObject: Circular tree suspected in find_ancestor. ",
parent, "already in: ", objects]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return None
if pred(parent):
return parent
objects.append(parent)
parent = AXObject.get_parent_checked(parent)
return None
@staticmethod
def is_ancestor(obj, ancestor):
"""Returns true if ancestor is an ancestor of obj"""
if not AXObject.is_valid(obj):
return False
if not AXObject.is_valid(ancestor):
return False
return AXObject.find_ancestor(obj, lambda x: x == ancestor) is not None
@staticmethod
def get_child(obj, index):
"""Returns the nth child of obj. See also get_child_checked."""
if not AXObject.is_valid(obj):
return None
n_children = AXObject.get_child_count(obj)
if n_children <= 0:
return None
if index == -1:
index = n_children - 1
if not 0 <= index < n_children:
return None
try:
child = Atspi.Accessible.get_child_at_index(obj, index)
except Exception as error:
msg = f"AXObject: Exception in get_child: {error}"
AXObject.handle_error(obj, error, msg)
return None
if child == obj:
tokens = ["AXObject:", obj, "claims to be its own child"]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return None
return child
@staticmethod
def get_child_checked(obj, index):
"""Returns the nth child of obj, doing checks for tree validity"""
if not AXObject.is_valid(obj):
return None
child = AXObject.get_child(obj, index)
if debug.LEVEL_INFO < debug.debugLevel:
return child
parent = AXObject.get_parent(child)
if obj != parent:
tokens = ["AXObject:", obj, "claims", child, "as child; child's parent is", parent]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return child
@staticmethod
def get_active_descendant_checked(container, reported_child):
"""Checks the reported active descendant and return the real/valid one."""
if not AXObject.has_state(container, Atspi.StateType.MANAGES_DESCENDANTS):
return reported_child
index = AXObject.get_index_in_parent(reported_child)
try:
real_child = Atspi.Accessible.get_child_at_index(container, index)
except Exception as error:
msg = f"AXObject: Exception in get_active_descendant_checked: {error}"
AXObject.handle_error(container, error, msg)
return reported_child
if real_child != reported_child:
tokens = ["AXObject: ", container, f"'s child at {index} is ", real_child,
"; not reported child", reported_child]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return real_child
@staticmethod
def _find_descendant(obj, pred):
"""Returns the descendant of obj if the function pred is true"""
if not AXObject.is_valid(obj):
return None
for i in range(AXObject.get_child_count(obj)):
child = AXObject.get_child_checked(obj, i)
if child and pred(child):
return child
child = AXObject._find_descendant(child, pred)
if child and pred(child):
return child
return None
@staticmethod
def find_descendant(obj, pred):
"""Returns the descendant of obj if the function pred is true"""
start = time.time()
result = AXObject._find_descendant(obj, pred)
tokens = ["AXObject: find_descendant: found", result, f"in {time.time() - start:.4f}s"]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return result
@staticmethod
def find_deepest_descendant(obj):
"""Returns the deepest descendant of obj"""
if not AXObject.is_valid(obj):
return None
last_child = AXObject.get_child(obj, AXObject.get_child_count(obj) - 1)
if last_child is None:
return obj
return AXObject.find_deepest_descendant(last_child)
@staticmethod
def _find_all_descendants(obj, include_if, exclude_if, matches):
"""Returns all descendants which match the specified inclusion and exclusion"""
if not AXObject.is_valid(obj):
return
child_count = AXObject.get_child_count(obj)
for i in range(child_count):
child = AXObject.get_child(obj, i)
if exclude_if and exclude_if(child):
continue
if include_if and include_if(child):
matches.append(child)
AXObject._find_all_descendants(child, include_if, exclude_if, matches)
@staticmethod
def find_all_descendants(root, include_if=None, exclude_if=None):
"""Returns all descendants which match the specified inclusion and exclusion"""
start = time.time()
matches = []
AXObject._find_all_descendants(root, include_if, exclude_if, matches)
msg = (
f"AXObject: find_all_descendants: {len(matches)} "
f"matches found in {time.time() - start:.4f}s"
)
debug.printMessage(debug.LEVEL_INFO, msg, True)
return matches
@staticmethod
def get_role(obj):
"""Returns the accessible role of obj"""
if not AXObject.is_valid(obj):
return Atspi.Role.INVALID
try:
role = Atspi.Accessible.get_role(obj)
except Exception as error:
msg = f"AXObject: Exception in get_role: {error}"
AXObject.handle_error(obj, error, msg)
return Atspi.Role.INVALID
AXObject._set_known_dead_status(obj, False)
return role
@staticmethod
def get_role_name(obj):
"""Returns the accessible role name of obj"""
if not AXObject.is_valid(obj):
return ""
try:
role_name = Atspi.Accessible.get_role_name(obj)
except Exception as error:
msg = f"AXObject: Exception in get_role_name: {error}"
AXObject.handle_error(obj, error, msg)
return ""
return role_name
@staticmethod
def get_name(obj):
"""Returns the accessible name of obj"""
if not AXObject.is_valid(obj):
return ""
try:
name = Atspi.Accessible.get_name(obj)
except Exception as error:
msg = f"AXObject: Exception in get_name: {error}"
AXObject.handle_error(obj, error, msg)
return ""
AXObject._set_known_dead_status(obj, False)
return name
@staticmethod
def has_same_non_empty_name(obj1, obj2):
"""Returns true if obj1 and obj2 share the same non-empty name"""
name1 = AXObject.get_name(obj1)
if not name1:
return False
return name1 == AXObject.get_name(obj2)
@staticmethod
def get_description(obj):
"""Returns the accessible description of obj"""
if not AXObject.is_valid(obj):
return ""
try:
description = Atspi.Accessible.get_description(obj)
except Exception as error:
msg = f"AXObject: Exception in get_description: {error}"
AXObject.handle_error(obj, error, msg)
return ""
return description
@staticmethod
def get_image_description(obj):
"""Returns the accessible image description of obj"""
if not AXObject.supports_image(obj):
return ""
try:
description = Atspi.Image.get_image_description(obj)
except Exception as error:
msg = f"AXObject: Exception in get_image_description: {error}"
AXObject.handle_error(obj, error, msg)
return ""
return description
@staticmethod
def get_image_size(obj):
"""Returns a (width, height) tuple of the image in obj"""
if not AXObject.supports_image(obj):
return 0, 0
try:
result = Atspi.Image.get_image_size(obj)
except Exception as error:
msg = f"AXObject: Exception in get_image_size: {error}"
AXObject.handle_error(obj, error, msg)
return 0, 0
# The return value is an AtspiPoint, hence x and y.
return result.x, result.y
@staticmethod
def get_help_text(obj):
"""Returns the accessible help text of obj"""
if not AXObject.is_valid(obj):
return ""
try:
# Added in Atspi 2.52.
text = Atspi.Accessible.get_help_text(obj)
except Exception:
# This is for prototyping in the meantime.
text = AXObject.get_attribute(obj, "helptext") or ""
return text
@staticmethod
def get_child_count(obj):
"""Returns the child count of obj"""
if not AXObject.is_valid(obj):
return 0
try:
count = Atspi.Accessible.get_child_count(obj)
except Exception as error:
msg = f"AXObject: Exception in get_child_count: {error}"
AXObject.handle_error(obj, error, msg)
return 0
return count
@staticmethod
def iter_children(obj, pred=None):
"""Generator to iterate through obj's children. If the function pred is
specified, children for which pred is False will be skipped."""
if not AXObject.is_valid(obj):
return
child_count = AXObject.get_child_count(obj)
for index in range(child_count):
child = AXObject.get_child(obj, index)
if child is not None and (pred is None or pred(child)):
yield child
@staticmethod
def get_previous_sibling(obj):
"""Returns the previous sibling of obj, based on child indices"""
if not AXObject.is_valid(obj):
return None
parent = AXObject.get_parent(obj)
if parent is None:
return None
index = AXObject.get_index_in_parent(obj)
if index <= 0:
return None
sibling = AXObject.get_child(parent, index - 1)
if sibling == obj:
tokens = ["AXObject:", obj, "claims to be its own sibling"]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return None
return sibling
@staticmethod
def get_next_sibling(obj):
"""Returns the next sibling of obj, based on child indices"""
if not AXObject.is_valid(obj):
return None
parent = AXObject.get_parent(obj)
if parent is None:
return None
index = AXObject.get_index_in_parent(obj)
if index < 0:
return None
sibling = AXObject.get_child(parent, index + 1)
if sibling == obj:
tokens = ["AXObject:", obj, "claims to be its own sibling"]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return None
return sibling
@staticmethod
def get_next_object(obj):
"""Returns the next object (depth first) in the accessibility tree"""
if not AXObject.is_valid(obj):
return None
index = AXObject.get_index_in_parent(obj) + 1
parent = AXObject.get_parent(obj)
while parent and not 0 < index < AXObject.get_child_count(parent):
obj = parent
index = AXObject.get_index_in_parent(obj) + 1
parent = AXObject.get_parent(obj)
if parent is None:
return None
next_object = AXObject.get_child(parent, index)
if next_object == obj:
tokens = ["AXObject:", obj, "claims to be its own next object"]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return None
return next_object
@staticmethod
def get_previous_object(obj):
"""Returns the previous object (depth first) in the accessibility tree"""
if not AXObject.is_valid(obj):
return None
index = AXObject.get_index_in_parent(obj) - 1
parent = AXObject.get_parent(obj)
while parent and not 0 <= index < AXObject.get_child_count(parent) - 1:
obj = parent
index = AXObject.get_index_in_parent(obj) - 1
parent = AXObject.get_parent(obj)
if parent is None:
return None
previous_object = AXObject.get_child(parent, index)
if previous_object == obj:
tokens = ["AXObject:", obj, "claims to be its own previous object"]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return None
return previous_object
@staticmethod
def get_state_set(obj):
"""Returns the state set associated with obj"""
if not AXObject.is_valid(obj):
return Atspi.StateSet()
try:
state_set = Atspi.Accessible.get_state_set(obj)
except Exception as error:
msg = f"AXObject: Exception in get_state_set: {error}"
AXObject.handle_error(obj, error, msg)
return Atspi.StateSet()
AXObject._set_known_dead_status(obj, False)
return state_set
@staticmethod
def has_state(obj, state):
"""Returns true if obj has the specified state"""
if not AXObject.is_valid(obj):
return False
return AXObject.get_state_set(obj).contains(state)
@staticmethod
def state_set_as_string(obj):
"""Returns the state set associated with obj as a string"""
if not AXObject.is_valid(obj):
return ""
def as_string(state):
return state.value_name[12:].replace("_", "-").lower()
return ", ".join(map(as_string, AXObject.get_state_set(obj).get_states()))
@staticmethod
def get_relations(obj):
"""Returns the list of Atspi.Relation objects associated with obj"""
if not AXObject.is_valid(obj):
return []
try:
relations = Atspi.Accessible.get_relation_set(obj)
except Exception as error:
msg = f"AXObject: Exception in get_relations: {error}"
AXObject.handle_error(obj, error, msg)
return []
return relations
@staticmethod
def get_relation(obj, relation_type):
"""Returns the specified Atspi.Relation for obj"""
if not AXObject.is_valid(obj):
return None
for relation in AXObject.get_relations(obj):
if relation and relation.get_relation_type() == relation_type:
return relation
return None
@staticmethod
def has_relation(obj, relation_type):
"""Returns true if obj has the specified relation type"""
if not AXObject.is_valid(obj):
return False
return AXObject.get_relation(obj, relation_type) is not None
@staticmethod
def get_relation_targets(obj, relation_type, pred=None):
"""Returns the list of targets with the specified relation type to obj.
If pred is provided, a target will only be included if pred is true."""
if not AXObject.is_valid(obj):
return []
relation = AXObject.get_relation(obj, relation_type)
if relation is None:
return []
targets = set()
for i in range(relation.get_n_targets()):
target = relation.get_target(i)
if pred is None or pred(target):
targets.add(target)
# We want to avoid self-referential relationships.
type_includes_object = [Atspi.RelationType.MEMBER_OF]
if relation_type not in type_includes_object and obj in targets:
tokens = ["AXObject: ", obj, "is in its own", relation_type, "target list"]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
targets.remove(obj)
return list(targets)
@staticmethod
def relations_as_string(obj):
"""Returns the relations associated with obj as a string"""
if not AXObject.is_valid(obj):
return ""
def as_string(relations):
return relations.value_name[15:].replace("_", "-").lower()
def obj_as_string(acc):
result = AXObject.get_role_name(obj)
name = AXObject.get_name(obj)
if name:
result += f": '{name}'"
if not result:
result = "DEAD"
return f"[{result}]"
results = []
for rel in AXObject.get_relations(obj):
type_string = as_string(rel.get_relation_type())
targets = AXObject.get_relation_targets(obj, rel.get_relation_type())
target_string = ",".join(map(obj_as_string, targets))
results.append(f"{type_string}: {target_string}")
return "; ".join(results)
@staticmethod
def find_real_app_and_window_for(obj, app=None):
"""Work around for window events coming from mutter-x11-frames."""
if app is None:
try:
app = Atspi.Accessible.get_application(obj)
except Exception as error:
msg = f"AXObject: Exception getting application of {obj}: {error}"
AXObject.handle_error(obj, error, msg)
return None, None
if AXObject.get_name(app) != "mutter-x11-frames":
return app, obj
real_app = AXObject.REAL_APP_FOR_MUTTER_FRAME.get(hash(obj))
real_frame = AXObject.REAL_FRAME_FOR_MUTTER_FRAME.get(hash(obj))
if real_app is not None and real_frame is not None:
return real_app, real_frame
tokens = ["AXObject:", app, "is not valid app for", obj]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
try:
desktop = Atspi.get_desktop(0)
except Exception as error:
tokens = ["AXObject: Exception getting desktop from Atspi:", error]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return None, None
name = AXObject.get_name(obj)
for desktop_app in AXObject.iter_children(desktop):
if AXObject.get_name(desktop_app) == "mutter-x11-frames":
continue
for frame in AXObject.iter_children(desktop_app):
if name == AXObject.get_name(frame):
real_app = desktop_app
real_frame = frame
tokens = ["AXObject:", real_app, "is real app for", obj]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
if real_frame != obj:
msg = "AXObject: Updated frame to frame from real app"
debug.printMessage(debug.LEVEL_INFO, msg, True)
AXObject.REAL_APP_FOR_MUTTER_FRAME[hash(obj)] = real_app
AXObject.REAL_FRAME_FOR_MUTTER_FRAME[hash(obj)] = real_frame
return real_app, real_frame
@staticmethod
def get_application(obj):
"""Returns the accessible application associated with obj"""
if not AXObject.is_valid(obj):
return None
app = AXObject.REAL_APP_FOR_MUTTER_FRAME.get(hash(obj))
if app is not None:
return app
try:
app = Atspi.Accessible.get_application(obj)
except Exception as error:
msg = f"AXObject: Exception in get_application: {error}"
AXObject.handle_error(obj, error, msg)
return None
if AXObject.get_name(app) != "mutter-x11-frames":
return app
real_app = AXObject.find_real_app_and_window_for(obj, app)[0]
if real_app is not None:
app = real_app
return app
@staticmethod
def get_application_toolkit_name(obj):
"""Returns the toolkit name reported for obj's application."""
if not AXObject.is_valid(obj):
return ""
app = AXObject.get_application(obj)
if app is None:
return ""
try:
name = Atspi.Accessible.get_toolkit_name(app)
except Exception as error:
tokens = ["AXObject: Exception in get_application_toolkit_name:", error]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return ""
return name
@staticmethod
def get_application_toolkit_version(obj):
"""Returns the toolkit version reported for obj's application."""
if not AXObject.is_valid(obj):
return ""
app = AXObject.get_application(obj)
if app is None:
return ""
try:
version = Atspi.Accessible.get_toolkit_version(app)
except Exception as error:
tokens = ["AXObject: Exception in get_application_toolkit_version:", error]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return ""
return version
@staticmethod
def application_as_string(obj):
"""Returns the application details of obj as a string."""
if not AXObject.is_valid(obj):
return ""
app = AXObject.get_application(obj)
if app is None:
return ""
string = (
f"{AXObject.get_name(app)} "
f"({AXObject.get_application_toolkit_name(obj)} "
f"{AXObject.get_application_toolkit_version(obj)})"
)
return string
@staticmethod
def clear_cache(obj, recursive=False, reason=""):
"""Clears the Atspi cached information associated with obj"""
if obj is None:
return
tokens = ["AXObject: Clearing AT-SPI cache on", obj, f"Recursive: {recursive}."]
if reason:
tokens.append(f" Reason: {reason}")
debug.printTokens(debug.LEVEL_INFO, tokens, True)
if not recursive:
try:
Atspi.Accessible.clear_cache_single(obj)
except Exception as error:
msg = f"AXObject: Exception in clear_cache_single: {error}"
debug.printMessage(debug.LEVEL_INFO, msg, True)
return
try:
Atspi.Accessible.clear_cache(obj)
except Exception as error:
msg = f"AXObject: Exception in clear_cache: {error}"
AXObject.handle_error(obj, error, msg)
@staticmethod
def get_process_id(obj):
"""Returns the process id associated with obj"""
if not AXObject.is_valid(obj):
return -1
try:
pid = Atspi.Accessible.get_process_id(obj)
except Exception as error:
msg = f"AXObject: Exception in get_process_id: {error}"
AXObject.handle_error(obj, error, msg)
return -1
return pid
@staticmethod
def is_dead(obj):
"""Returns true of obj exists but is believed to be dead."""
if obj is None:
return False
if not AXObject.is_valid(obj):
return True
try:
# We use the Atspi function rather than the AXObject function because the
# latter intentionally handles exceptions.
Atspi.Accessible.get_name(obj)
except Exception as error:
msg = f"AXObject: Accessible is dead: {error}"
AXObject.handle_error(obj, error, msg)
return True
AXObject._set_known_dead_status(obj, False)
return False
@staticmethod
def get_attributes_dict(obj, use_cache=True):
"""Returns the object attributes of obj as a dictionary."""
if not AXObject.is_valid(obj):
return {}
if use_cache:
attributes = AXObject.OBJECT_ATTRIBUTES.get(hash(obj))
if attributes:
return attributes
try:
attributes = Atspi.Accessible.get_attributes(obj)
except Exception as error:
msg = f"AXObject: Exception in get_attributes_dict: {error}"
AXObject.handle_error(obj, error, msg)
return {}
if attributes is None:
return {}
AXObject.OBJECT_ATTRIBUTES[hash(obj)] = attributes
return attributes
@staticmethod
def get_attribute(obj, attribute_name, use_cache=True):
"""Returns the value of the specified attribute as a string."""
if not AXObject.is_valid(obj):
return ""
attributes = AXObject.get_attributes_dict(obj, use_cache)
return attributes.get(attribute_name, "")
@staticmethod
def attributes_as_string(obj):
"""Returns the object attributes of obj as a string."""
if not AXObject.is_valid(obj):
return ""
def as_string(attribute):
return f"{attribute[0]}:{attribute[1]}"
return ", ".join(map(as_string, AXObject.get_attributes_dict(obj).items()))
@staticmethod
def get_n_actions(obj):
"""Returns the number of actions supported on obj."""
if not AXObject.supports_action(obj):
return 0
try:
count = Atspi.Action.get_n_actions(obj)
except Exception as error:
msg = f"AXObject: Exception in get_n_actions: {error}"
AXObject.handle_error(obj, error, msg)
return 0
return count
@staticmethod
def _normalize_action_name(action_name):
"""Adjusts the name to account for differences in implementations."""
if not action_name:
return ""
name = re.sub(r'(?<=[a-z])([A-Z])', r'-\1', action_name).lower()
name = re.sub('[!\"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~]', '-', name)
return name
@staticmethod
def get_action_name(obj, i):
"""Returns the name of obj's action at index i."""
if not 0 <= i < AXObject.get_n_actions(obj):
return ""
try:
name = Atspi.Action.get_action_name(obj, i)
except Exception as error:
msg = f"AXObject: Exception in get_action_name: {error}"
AXObject.handle_error(obj, error, msg)
return ""
return AXObject._normalize_action_name(name)
@staticmethod
def get_action_names(obj):
"""Returns the list of actions supported on obj."""
results = []
for i in range(AXObject.get_n_actions(obj)):
name = AXObject.get_action_name(obj, i)
if name:
results.append(name)
return results
@staticmethod
def get_action_description(obj, i):
"""Returns the description of obj's action at index i."""
if not 0 <= i < AXObject.get_n_actions(obj):
return ""
try:
description = Atspi.Action.get_action_description(obj, i)
except Exception as error:
msg = f"AXObject: Exception in get_action_description: {error}"
AXObject.handle_error(obj, error, msg)
return ""
return description
@staticmethod
def get_action_key_binding(obj, i):
"""Returns the key binding string of obj's action at index i."""
if not 0 <= i < AXObject.get_n_actions(obj):
return ""
try:
keybinding = Atspi.Action.get_key_binding(obj, i)
except Exception as error:
msg = f"AXObject: Exception in get_action_key_binding: {error}"
AXObject.handle_error(obj, error, msg)
return ""
return keybinding
@staticmethod
def has_action(obj, action_name):
"""Returns true if the named action is supported on obj."""
return AXObject.get_action_index(obj, action_name) >= 0
@staticmethod
def get_action_index(obj, action_name):
"""Returns the index of the named action or -1 if unsupported."""
action_name = AXObject._normalize_action_name(action_name)
for i in range(AXObject.get_n_actions(obj)):
if action_name == AXObject.get_action_name(obj, i):
return i
return -1
@staticmethod
def do_action(obj, i):
"""Invokes obj's action at index i. The return value, if true, may be
meaningless because most implementors return true without knowing if
the action was successfully performed."""
if not 0 <= i < AXObject.get_n_actions(obj):
return False
try:
result = Atspi.Action.do_action(obj, i)
except Exception as error:
msg = f"AXObject: Exception in do_action: {error}"
AXObject.handle_error(obj, error, msg)
return False
return result
@staticmethod
def do_named_action(obj, action_name):
"""Invokes the named action on obj. The return value, if true, may be
meaningless because most implementors return true without knowing if
the action was successfully performed."""
index = AXObject.get_action_index(obj, action_name)
if index == -1:
tokens = ["INFO:", action_name, "not an available action for", obj]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return False
return AXObject.do_action(obj, index)
@staticmethod
def actions_as_string(obj):
"""Returns information about the actions as a string."""
results = []
for i in range(AXObject.get_n_actions(obj)):
result = AXObject.get_action_name(obj, i)
keybinding = AXObject.get_action_key_binding(obj, i)
if keybinding:
result += f" ({keybinding})"
results.append(result)
return "; ".join(results)
@staticmethod
def grab_focus(obj):
"""Attempts to grab focus on obj. Returns true if successful."""
if not AXObject.supports_component(obj):
return False
try:
result = Atspi.Component.grab_focus(obj)
except Exception as error:
msg = f"AXObject: Exception in grab_focus: {error}"
AXObject.handle_error(obj, error, msg)
return False
if debug.LEVEL_INFO < debug.debugLevel:
return result
if result and not AXObject.has_state(obj, Atspi.StateType.FOCUSED):
tokens = ["AXObject:", obj, "lacks focused state after focus grab"]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return result
AXObject.start_cache_clearing_thread()