aboutsummaryrefslogtreecommitdiffstats
path: root/build/lib/dogtail/tree.py
diff options
context:
space:
mode:
Diffstat (limited to 'build/lib/dogtail/tree.py')
-rw-r--r--build/lib/dogtail/tree.py1318
1 files changed, 1318 insertions, 0 deletions
diff --git a/build/lib/dogtail/tree.py b/build/lib/dogtail/tree.py
new file mode 100644
index 00000000000..e6050bbb98b
--- /dev/null
+++ b/build/lib/dogtail/tree.py
@@ -0,0 +1,1318 @@
+"""Makes some sense of the AT-SPI API
+
+The tree API handles various things for you:
+ - fixes most timing issues
+ - can automatically generate (hopefully) highly-readable logs of what the
+script is doing
+ - traps various UI malfunctions, raising exceptions for them (again,
+hopefully improving the logs)
+
+The most important class is Node. Each Node is an element of the desktop UI.
+There is a tree of nodes, starting at 'root', with applications as its
+children, with the top-level windows and dialogs as their children. The various
+widgets that make up the UI appear as descendents in this tree. All of these
+elements (root, the applications, the windows, and the widgets) are represented
+as instances of Node in a tree (provided that the program of interest is
+correctly exporting its user-interface to the accessibility system). The Node
+class is a mixin for Accessible and the various Accessible interfaces.
+
+The Action class represents an action that the accessibility layer exports as
+performable on a specific node, such as clicking on it. It's a wrapper around
+Accessibility.Action.
+
+We often want to look for a node, based on some criteria, and this is provided
+by the Predicate class.
+
+Dogtail implements a high-level searching system, for finding a node (or
+nodes) satisfying whatever criteria you are interested in. It does this with
+a 'backoff and retry' algorithm. This fixes most timing problems e.g. when a
+dialog is in the process of opening but hasn't yet done so.
+
+If a search fails, it waits 'config.searchBackoffDuration' seconds, and then
+tries again, repeatedly. After several failed attempts (determined by
+config.searchWarningThreshold) it will start sending warnings about the search
+to the debug log. If it still can't succeed after 'config.searchCutoffCount'
+attempts, it raises an exception containing details of the search. You can see
+all of this process in the debug log by setting 'config.debugSearching' to True
+
+We also automatically add a short delay after each action
+('config.defaultDelay' gives the time in seconds). We'd hoped that the search
+backoff and retry code would eliminate the need for this, but unfortunately we
+still run into timing issues. For example, Evolution (and probably most
+other apps) set things up on new dialogs and wizard pages as they appear, and
+we can run into 'setting wars' where the app resets the widgetry to defaults
+after our script has already filled out the desired values, and so we lose our
+values. So we give the app time to set the widgetry up before the rest of the
+script runs.
+
+The classes trap various UI malfunctions and raise exceptions that better
+describe what went wrong. For example, they detects attempts to click on an
+insensitive UI element and raise a specific exception for this.
+
+Unfortunately, some applications do not set up the 'sensitive' state
+correctly on their buttons (e.g. Epiphany on form buttons in a web page). The
+current workaround for this is to set config.ensureSensitivity=False, which
+disables the sensitivity testing.
+
+Authors: Zack Cerza <zcerza@redhat.com>, David Malcolm <dmalcolm@redhat.com>
+"""
+__author__ = """Zack Cerza <zcerza@redhat.com>,
+David Malcolm <dmalcolm@redhat.com>
+"""
+
+from config import config
+if config.checkForA11y:
+ from utils import checkForA11y
+ checkForA11y()
+
+import predicate
+from time import sleep
+from utils import doDelay
+from utils import Blinker
+from utils import Lock
+import rawinput
+import path
+from __builtin__ import xrange
+
+from logging import debugLogger as logger
+
+try:
+ import pyatspi
+ import Accessibility
+except ImportError: # pragma: no cover
+ raise ImportError("Error importing the AT-SPI bindings")
+
+# We optionally import the bindings for libWnck.
+try:
+ from gi.repository import Wnck
+ gotWnck = True # pragma: no cover
+except ImportError:
+ # Skip this warning, since the functionality is almost entirely nonworking anyway.
+ # print "Warning: Dogtail could not import the Python bindings for
+ # libwnck. Window-manager manipulation will not be available."
+ gotWnck = False
+
+from gi.repository import GLib
+
+haveWarnedAboutChildrenLimit = False
+
+
+class SearchError(Exception):
+ pass
+
+
+class NotSensitiveError(Exception):
+
+ """
+ The widget is not sensitive.
+ """
+ message = "Cannot %s %s. It is not sensitive."
+
+ def __init__(self, action):
+ self.action = action
+
+ def __str__(self):
+ return self.message % (self.action.name, self.action.node.getLogString())
+
+
+class ActionNotSupported(Exception):
+
+ """
+ The widget does not support the requested action.
+ """
+ message = "Cannot do '%s' action on %s"
+
+ def __init__(self, actionName, node):
+ self.actionName = actionName
+ self.node = node
+
+ def __str__(self):
+ return self.message % (self.actionName, self.node.getLogString())
+
+
+class Action(object):
+
+ """
+ Class representing an action that can be performed on a specific node
+ """
+ # Valid types of actions we know about. Feel free to add any you see.
+ types = ('click',
+ 'press',
+ 'release',
+ 'activate',
+ 'jump',
+ 'check',
+ 'dock',
+ 'undock',
+ 'open',
+ 'menu')
+
+ def __init__(self, node, action, index):
+ self.node = node
+ self.__action = action
+ self.__index = index
+
+ @property
+ def name(self):
+ return self.__action.getName(self.__index)
+
+ @property
+ def description(self):
+ return self.__action.getDescription(self.__index)
+
+ @property
+ def keyBinding(self):
+ return self.__action.getKeyBinding(self.__index)
+
+ def __str__(self):
+ return "[action | %s | %s ]" % \
+ (self.name, self.keyBinding)
+
+ def do(self):
+ """
+ Performs the given tree.Action, with appropriate delays and logging.
+ """
+ logger.log("%s on %s" % (self.name, self.node.getLogString()))
+ if not self.node.sensitive:
+ if config.ensureSensitivity:
+ raise NotSensitiveError(self)
+ else:
+ nSE = NotSensitiveError(self)
+ logger.log("Warning: " + str(nSE))
+ if config.blinkOnActions:
+ self.node.blink()
+ result = self.__action.doAction(self.__index)
+ doDelay(config.actionDelay)
+ return result
+
+
+class Node(object):
+
+ """
+ A node in the tree of UI elements. This class is mixed in with
+ Accessibility.Accessible to both make it easier to use and to add
+ additional functionality. It also has a debugName which is set up
+ automatically when doing searches.
+ """
+
+ def __setupUserData(self):
+ try:
+ len(self.user_data)
+ except (AttributeError, TypeError):
+ self.user_data = {}
+
+ def debugName():
+ doc = "debug name assigned during search operations"
+
+ def fget(self):
+ self.__setupUserData()
+ return self.user_data.get('debugName', None)
+
+ def fset(self, debugName):
+ self.__setupUserData()
+ self.user_data['debugName'] = debugName
+
+ return property(**locals())
+ debugName = debugName()
+ #
+ # Accessible
+ #
+
+ @property
+ def dead(self):
+ """Is the node dead (defunct) ?"""
+ try:
+ if self.roleName == 'invalid':
+ return True
+ self.role
+ self.name
+ if len(self) > 0:
+ self[0]
+ except:
+ return True
+ return False
+
+ @property
+ def children(self):
+ """a list of this Accessible's children"""
+ if self.parent and self.parent.roleName == 'hyper link':
+ print(self.parent.role)
+ return []
+ children = []
+ childCount = self.childCount
+ if childCount > config.childrenLimit:
+ global haveWarnedAboutChildrenLimit
+ if not haveWarnedAboutChildrenLimit:
+ logger.log("Only returning %s children. You may change "
+ "config.childrenLimit if you wish. This message will only"
+ " be printed once." % str(config.childrenLimit))
+ haveWarnedAboutChildrenLimit = True
+ childCount = config.childrenLimit
+ for i in range(childCount):
+ # Workaround for GNOME bug #465103
+ # also solution for GNOME bug #321273
+ try:
+ child = self[i]
+ except LookupError:
+ child = None
+ if child:
+ children.append(child)
+
+ invalidChildren = childCount - len(children)
+ if invalidChildren and config.debugSearching:
+ logger.log("Skipped %s invalid children of %s" %
+ (invalidChildren, str(self)))
+ try:
+ ht = self.queryHypertext()
+ for li in range(ht.getNLinks()):
+ link = ht.getLink(li)
+ for ai in range(link.nAnchors):
+ child = link.getObject(ai)
+ child.__setupUserData()
+ child.user_data['linkAnchor'] = \
+ LinkAnchor(node=child,
+ hypertext=ht,
+ linkIndex=li,
+ anchorIndex=ai)
+ children.append(child)
+ except (NotImplementedError, AttributeError):
+ pass
+
+ return children
+
+ roleName = property(Accessibility.Accessible.getRoleName)
+
+ role = property(Accessibility.Accessible.getRole)
+
+ indexInParent = property(Accessibility.Accessible.getIndexInParent)
+
+ #
+ # Action
+ #
+
+ # Needed to be renamed from doAction due to conflicts
+ # with 'Accessibility.Accessible.doAction' in gtk3 branch
+ def doActionNamed(self, name):
+ """
+ Perform the action with the specified name. For a list of actions
+ supported by this instance, check the 'actions' property.
+ """
+ actions = self.actions
+ if name in actions:
+ return actions[name].do()
+ raise ActionNotSupported(name, self)
+
+ @property
+ def actions(self):
+ """
+ A dictionary of supported action names as keys, with Action objects as
+ values. Common action names include:
+
+ 'click' 'press' 'release' 'activate' 'jump' 'check' 'dock' 'undock'
+ 'open' 'menu'
+ """
+ actions = {}
+ try:
+ action = self.queryAction()
+ for i in range(action.nActions):
+ a = Action(self, action, i)
+ actions[action.getName(i)] = a
+ finally:
+ return actions
+
+ def combovalue():
+ doc = "The value (as a string) currently selected in the combo box."
+
+ def fget(self):
+ return self.name
+
+ def fset(self, value):
+ logger.log("Setting combobox %s to '%s'" % (self.getLogString(),
+ value))
+ self.childNamed(childName=value).doActionNamed('click')
+ doDelay()
+
+ return property(**locals())
+ combovalue = combovalue()
+ #
+ # Hypertext and Hyperlink
+ #
+
+ @property
+ def URI(self):
+ try:
+ return self.user_data['linkAnchor'].URI
+ except (KeyError, AttributeError):
+ raise NotImplementedError
+
+ #
+ # Text and EditableText
+ #
+ def text():
+ doc = """For instances with an AccessibleText interface, the text as a
+ string. This is read-only, unless the instance also has an
+ AccessibleEditableText interface. In this case, you can write values
+ to the attribute. This will get logged in the debug log, and a delay
+ will be added.
+
+ If this instance corresponds to a password entry, use the passwordText
+ property instead."""
+
+ def fget(self):
+ try:
+ return self.queryText().getText(0, -1)
+ except NotImplementedError:
+ return None
+
+ def fset(self, text):
+ try:
+ if config.debugSearching:
+ msg = "Setting text of %s to %s"
+ # Let's not get too crazy if 'text' is really large...
+ # FIXME: Sometimes the next line screws up Unicode strings.
+ if len(text) > 140:
+ txt = text[:134] + " [...]"
+ else:
+ txt = text
+ logger.log(msg % (self.getLogString(), "'%s'" % txt))
+ self.queryEditableText().setTextContents(text)
+ except NotImplementedError:
+ raise AttributeError("can't set attribute")
+
+ return property(**locals())
+ text = text()
+
+ def caretOffset():
+
+ def fget(self):
+ """For instances with an AccessibleText interface, the caret
+ offset as an integer."""
+ return self.queryText().caretOffset
+
+ def fset(self, offset):
+ return self.queryText().setCaretOffset(offset)
+
+ return property(**locals())
+ caretOffset = caretOffset()
+
+ #
+ # Component
+ #
+
+ @property
+ def position(self):
+ """A tuple containing the position of the Accessible: (x, y)"""
+ return self.queryComponent().getPosition(pyatspi.DESKTOP_COORDS)
+
+ @property
+ def size(self):
+ """A tuple containing the size of the Accessible: (w, h)"""
+ return self.queryComponent().getSize()
+
+ @property
+ def extents(self):
+ """A tuple containing the location and size of the Accessible:
+ (x, y, w, h)"""
+ try:
+ ex = self.queryComponent().getExtents(pyatspi.DESKTOP_COORDS)
+ return (ex.x, ex.y, ex.width, ex.height)
+ except NotImplementedError:
+ return None
+
+ def contains(self, x, y):
+ try:
+ return self.queryComponent().contains(x, y, pyatspi.DESKTOP_COORDS)
+ except NotImplementedError:
+ return False
+
+ def getChildAtPoint(self, x, y):
+ node = self
+ while True:
+ try:
+ child = node.queryComponent().getAccessibleAtPoint(x, y,
+ pyatspi.DESKTOP_COORDS)
+ if child and child.contains(x, y):
+ node = child
+ else:
+ break
+ except NotImplementedError:
+ break
+ if node and node.contains(x, y):
+ return node
+ else:
+ return None
+
+ def grabFocus(self):
+ "Attempts to set the keyboard focus to this Accessible."
+ return self.queryComponent().grabFocus()
+
+ # def blink(self, count=2):
+ #"""
+ # Blink, baby!
+ #"""
+ # if not self.extents: return False
+ # else:
+ #(x, y, w, h) = self.extents
+ #from utils import Blinker
+ #blinkData = Blinker(x, y, w, h, count)
+ # return True
+
+ def click(self, button=1):
+ """
+ Generates a raw mouse click event, using the specified button.
+ - 1 is left,
+ - 2 is middle,
+ - 3 is right.
+ """
+ logger.log("Clicking on %s" % self.getLogString())
+ clickX = self.position[0] + self.size[0] / 2
+ clickY = self.position[1] + self.size[1] / 2
+ if config.debugSearching:
+ logger.log("raw click on %s %s at (%s,%s)" %
+ (self.name, self.getLogString(), str(clickX), str(clickY)))
+ rawinput.click(clickX, clickY, button)
+
+ def doubleClick(self, button=1):
+ """
+ Generates a raw mouse double-click event, using the specified button.
+ """
+ clickX = self.position[0] + self.size[0] / 2
+ clickY = self.position[1] + self.size[1] / 2
+ if config.debugSearching:
+ logger.log("raw click on %s %s at (%s,%s)" %
+ (self.name, self.getLogString(), str(clickX), str(clickY)))
+ rawinput.doubleClick(clickX, clickY, button)
+
+ def point(self, mouseDelay=None):
+ """
+ Move mouse cursor to the center of the widget.
+ """
+ pointX = self.position[0] + self.size[0] / 2
+ pointY = self.position[1] + self.size[1] / 2
+ logger.log("Pointing on %s %s at (%s,%s)" %
+ (self.name, self.getLogString(), str(pointX), str(pointY)))
+ rawinput.registry.generateMouseEvent(pointX, pointY, 'abs')
+ if mouseDelay:
+ doDelay(mouseDelay)
+ else:
+ doDelay()
+
+ #
+ # RelationSet
+ #
+ @property
+ def labeler(self):
+ """'labeller' (read-only list of Node instances):
+ The node(s) that is/are a label for this node. Generated from
+ 'relations'.
+ """
+ relationSet = self.getRelationSet()
+ for relation in relationSet:
+ if relation.getRelationType() == pyatspi.RELATION_LABELLED_BY:
+ if relation.getNTargets() == 1:
+ return relation.getTarget(0)
+ targets = []
+ for i in range(relation.getNTargets()):
+ targets.append(relation.getTarget(i))
+ return targets
+ labeller = labeler
+
+ @property
+ def labelee(self):
+ """'labellee' (read-only list of Node instances):
+ The node(s) that this node is a label for. Generated from 'relations'.
+ """
+ relationSet = self.getRelationSet()
+ for relation in relationSet:
+ if relation.getRelationType() == pyatspi.RELATION_LABEL_FOR:
+ if relation.getNTargets() == 1:
+ return relation.getTarget(0)
+ targets = []
+ for i in range(relation.getNTargets()):
+ targets.append(relation.getTarget(i))
+ return targets
+ labellee = labelee
+
+ #
+ # StateSet
+ #
+ @property
+ def sensitive(self):
+ """Is the Accessible sensitive (i.e. not greyed out)?"""
+ return self.getState().contains(pyatspi.STATE_SENSITIVE)
+
+ @property
+ def showing(self):
+ return self.getState().contains(pyatspi.STATE_SHOWING)
+
+ @property
+ def focusable(self):
+ """Is the Accessible capable of having keyboard focus?"""
+ return self.getState().contains(pyatspi.STATE_FOCUSABLE)
+
+ @property
+ def focused(self):
+ """Does the Accessible have keyboard focus?"""
+ return self.getState().contains(pyatspi.STATE_FOCUSED)
+
+ @property
+ def checked(self):
+ """Is the Accessible a checked checkbox?"""
+ return self.getState().contains(pyatspi.STATE_CHECKED)
+
+ @property
+ def isChecked(self):
+ """Is the Accessible a checked checkbox? Compatibility property, same as Node.checked."""
+ return self.checked
+
+ #
+ # Selection
+ #
+
+ def selectAll(self):
+ """Selects all children."""
+ result = self.querySelection().selectAll()
+ doDelay()
+ return result
+
+ def deselectAll(self):
+ """Deselects all selected children."""
+ result = self.querySelection().clearSelection()
+ doDelay()
+ return result
+
+ def select(self):
+ """Selects the Accessible."""
+ try:
+ parent = self.parent
+ except AttributeError:
+ raise NotImplementedError
+ result = parent.querySelection().selectChild(self.indexInParent)
+ doDelay()
+ return result
+
+ def deselect(self):
+ """Deselects the Accessible."""
+ try:
+ parent = self.parent
+ except AttributeError:
+ raise NotImplementedError
+ result = parent.querySelection().deselectChild(self.indexInParent)
+ doDelay()
+ return result
+
+ @property
+ def isSelected(self):
+ """Is the Accessible selected? Compatibility property, same as Node.selected."""
+ try:
+ parent = self.parent
+ except AttributeError:
+ raise NotImplementedError
+ return parent.querySelection().isChildSelected(self.indexInParent)
+
+ @property
+ def selected(self):
+ """Is the Accessible selected?"""
+ return self.isSelected
+
+ @property
+ def selectedChildren(self):
+ """Returns a list of children that are selected."""
+ # TODO: hideChildren for Hyperlinks?
+ selection = self.querySelection()
+ selectedChildren = []
+ for i in xrange(selection.nSelectedChildren):
+ selectedChildren.append(selection.getSelectedChild(i))
+
+ #
+ # Value
+ #
+
+ def value():
+ doc = "The value contained by the AccessibleValue interface."
+
+ def fget(self):
+ try:
+ return self.queryValue().currentValue
+ except NotImplementedError:
+ pass
+
+ def fset(self, value):
+ self.queryValue().currentValue = value
+
+ return property(**locals())
+ value = value()
+
+ @property
+ def minValue(self):
+ """The minimum value of self.value"""
+ try:
+ return self.queryValue().minimumValue
+ except NotImplementedError:
+ pass
+
+ @property
+ def minValueIncrement(self):
+ """The minimum value increment of self.value"""
+ try:
+ return self.queryValue().minimumIncrement
+ except NotImplementedError:
+ pass
+
+ @property
+ def maxValue(self):
+ """The maximum value of self.value"""
+ try:
+ return self.queryValue().maximumValue
+ except NotImplementedError:
+ pass
+
+ def typeText(self, string):
+ """
+ Type the given text into the node, with appropriate delays and
+ logging.
+ """
+ logger.log("Typing text into %s: '%s'" % (self.getLogString(), string))
+
+ if self.focusable:
+ if not self.focused:
+ try:
+ self.grabFocus()
+ except Exception:
+ logger.log("Node is focusable but I can't grabFocus!")
+ rawinput.typeText(string)
+ else:
+ logger.log("Node is not focusable; falling back to inserting text")
+ et = self.queryEditableText()
+ et.insertText(self.caretOffset, string, len(string))
+ self.caretOffset += len(string)
+ doDelay()
+
+ def keyCombo(self, comboString):
+ if config.debugSearching:
+ logger.log("Pressing keys '%s' into %s" %
+ (comboString, self.getLogString()))
+ if self.focusable:
+ if not self.focused:
+ try:
+ self.grabFocus()
+ except Exception:
+ logger.log("Node is focusable but I can't grabFocus!")
+ else:
+ logger.log("Node is not focusable; trying key combo anyway")
+ rawinput.keyCombo(comboString)
+
+ def getLogString(self):
+ """
+ Get a string describing this node for the logs,
+ respecting the config.absoluteNodePaths boolean.
+ """
+ if config.absoluteNodePaths:
+ return self.getAbsoluteSearchPath()
+ else:
+ return str(self)
+
+ def satisfies(self, pred):
+ """
+ Does this node satisfy the given predicate?
+ """
+ # the logic is handled by the predicate:
+ assert isinstance(pred, predicate.Predicate)
+ return pred.satisfiedByNode(self)
+
+ def dump(self, type='plain', fileName=None):
+ import dump
+ dumper = getattr(dump, type)
+ dumper(self, fileName)
+
+ def getAbsoluteSearchPath(self):
+ """
+ FIXME: this needs rewriting...
+ Generate a SearchPath instance giving the 'best'
+ way to find the Accessible wrapped by this node again, starting
+ at the root and applying each search in turn.
+
+ This is somewhat analagous to an absolute path in a filesystem,
+ except that some of searches may be recursive, rather than just
+ searching direct children.
+
+ Used by the recording framework for identifying nodes in a
+ persistent way, independent of the style of script being
+ written.
+
+ FIXME: try to ensure uniqueness
+ FIXME: need some heuristics to get 'good' searches, whatever
+ that means
+ """
+ if config.debugSearchPaths:
+ logger.log("getAbsoluteSearchPath(%s)" % self)
+
+ if self.roleName == 'application':
+ result = path.SearchPath()
+ result.append(predicate.IsAnApplicationNamed(self.name), False)
+ return result
+ else:
+ if self.parent:
+ (ancestor, pred, isRecursive) = self.getRelativeSearch()
+ if config.debugSearchPaths:
+ logger.log("got ancestor: %s" % ancestor)
+
+ ancestorPath = ancestor.getAbsoluteSearchPath()
+ ancestorPath.append(pred, isRecursive)
+ return ancestorPath
+ else:
+ # This should be the root node:
+ return path.SearchPath()
+
+ def getRelativeSearch(self):
+ """
+ Get a (ancestorNode, predicate, isRecursive) triple that identifies the
+ best way to find this Node uniquely.
+ FIXME: or None if no such search exists?
+ FIXME: may need to make this more robust
+ FIXME: should this be private?
+ """
+ if config.debugSearchPaths:
+ logger.log("getRelativeSearchPath(%s)" % self)
+
+ assert self
+ assert self.parent
+
+ isRecursive = False
+ ancestor = self.parent
+
+ # iterate up ancestors until you reach an identifiable one,
+ # setting the search to be isRecursive if need be:
+ while not self.__nodeIsIdentifiable(ancestor):
+ ancestor = ancestor.parent
+ isRecursive = True
+
+ # Pick the most appropriate predicate for finding this node:
+ if self.labellee:
+ if self.labellee.name:
+ return (ancestor, predicate.IsLabelledAs(self.labellee.name), isRecursive)
+
+ if self.roleName == 'menu':
+ return (ancestor, predicate.IsAMenuNamed(self.name), isRecursive)
+ elif self.roleName == 'menu item' or self.roleName == 'check menu item':
+ return (ancestor, predicate.IsAMenuItemNamed(self.name), isRecursive)
+ elif self.roleName == 'text':
+ return (ancestor, predicate.IsATextEntryNamed(self.name), isRecursive)
+ elif self.roleName == 'push button':
+ return (ancestor, predicate.IsAButtonNamed(self.name), isRecursive)
+ elif self.roleName == 'frame':
+ return (ancestor, predicate.IsAWindowNamed(self.name), isRecursive)
+ elif self.roleName == 'dialog':
+ return (ancestor, predicate.IsADialogNamed(self.name), isRecursive)
+ else:
+ pred = predicate.GenericPredicate(
+ name=self.name, roleName=self.roleName)
+ return (ancestor, pred, isRecursive)
+
+ def __nodeIsIdentifiable(self, ancestor):
+ if ancestor.labellee:
+ return True
+ elif ancestor.name:
+ return True
+ elif not ancestor.parent:
+ return True
+ else:
+ return False
+
+ def _fastFindChild(self, pred, recursive=True):
+ """
+ Searches for an Accessible using methods from pyatspi.utils
+ """
+ if isinstance(pred, predicate.Predicate):
+ pred = pred.satisfiedByNode
+ if not recursive:
+ cIter = iter(self)
+ while True:
+ try:
+ child = cIter.next()
+ except StopIteration:
+ break
+ if child is not None:
+ if pred(child):
+ return child
+ else:
+ return pyatspi.utils.findDescendant(self, pred)
+
+ def findChild(self, pred, recursive=True, debugName=None,
+ retry=True, requireResult=True):
+ """
+ Search for a node satisyfing the predicate, returning a Node.
+
+ If retry is True (the default), it makes multiple attempts,
+ backing off and retrying on failure, and eventually raises a
+ descriptive exception if the search fails.
+
+ If retry is False, it gives up after one attempt.
+
+ If requireResult is True (the default), an exception is raised after all
+ attempts have failed. If it is false, the function simply returns None.
+ """
+ def describeSearch(parent, pred, recursive, debugName):
+ """
+ Internal helper function
+ """
+ if recursive:
+ noun = "descendent"
+ else:
+ noun = "child"
+ if debugName is None:
+ debugName = pred.describeSearchResult()
+ return "%s of %s: %s" % (noun, parent.getLogString(), debugName)
+
+ assert isinstance(pred, predicate.Predicate)
+ numAttempts = 0
+ while numAttempts < config.searchCutoffCount:
+ if numAttempts >= config.searchWarningThreshold or config.debugSearching:
+ logger.log("searching for %s (attempt %i)" %
+ (describeSearch(self, pred, recursive, debugName), numAttempts))
+
+ result = self._fastFindChild(pred.satisfiedByNode, recursive)
+ if result:
+ assert isinstance(result, Node)
+ if debugName:
+ result.debugName = debugName
+ else:
+ result.debugName = pred.describeSearchResult()
+ return result
+ else:
+ if not retry:
+ break
+ numAttempts += 1
+ if config.debugSearching or config.debugSleep:
+ logger.log("sleeping for %f" %
+ config.searchBackoffDuration)
+ sleep(config.searchBackoffDuration)
+ if requireResult:
+ raise SearchError(describeSearch(self, pred, recursive, debugName))
+
+ # The canonical "search for multiple" method:
+ def findChildren(self, pred, recursive=True, isLambda=False):
+ """
+ Find all children/descendents satisfying the predicate.
+ """
+ if isLambda is True:
+ nodes = self.findChildren(predicate.GenericPredicate(), recursive=recursive)
+ result = []
+ for node in nodes:
+ try:
+ if pred(node):
+ result.append(node)
+ except:
+ pass
+ return result
+ if isinstance(pred, predicate.Predicate):
+ pred = pred.satisfiedByNode
+ if not recursive:
+ cIter = iter(self)
+ result = []
+ while True:
+ try:
+ child = cIter.next()
+ except StopIteration:
+ break
+ if child is not None and pred(child):
+ result.append(child)
+ return result
+ else:
+ descendants = []
+ while True:
+ try:
+ descendants = pyatspi.utils.findAllDescendants(self, pred)
+ break
+ except (GLib.GError, TypeError):
+ continue
+ return descendants
+
+ # The canonical "search above this node" method:
+ def findAncestor(self, pred):
+ """
+ Search up the ancestry of this node, returning the first Node
+ satisfying the predicate, or None.
+ """
+ assert isinstance(pred, predicate.Predicate)
+ candidate = self.parent
+ while candidate is not None:
+ if candidate.satisfies(pred):
+ return candidate
+ else:
+ candidate = candidate.parent
+ # Not found:
+ return None
+
+ # Various wrapper/helper search methods:
+ def child(self, name='', roleName='', description='', label='', recursive=True, retry=True, debugName=None):
+ """
+ Finds a child satisying the given criteria.
+
+ This is implemented using findChild, and hence will automatically retry
+ if no such child is found, and will eventually raise an exception. It
+ also logs the search.
+ """
+ return self.findChild(predicate.GenericPredicate(name=name, roleName=roleName, description=description, label=label), recursive=recursive, retry=retry, debugName=debugName)
+
+ def isChild(self, name='', roleName='', description='', label='', recursive=True, retry=False, debugName=None):
+ """
+ Determines whether a child satisying the given criteria exists.
+
+ This is implemented using findChild, but will not automatically retry
+ if no such child is found. To make the function retry multiple times set retry to True.
+ Returns a boolean value depending on whether the child was eventually found. Similar to
+ 'child', yet it catches SearchError exception to provide for False results, will raise
+ any other exceptions. It also logs the search.
+ """
+ found = True
+ try:
+ self.findChild(
+ predicate.GenericPredicate(
+ name=name, roleName=roleName, description=description, label=label),
+ recursive=recursive, retry=retry, debugName=debugName)
+ except SearchError:
+ found = False
+ return found
+
+ def menu(self, menuName, recursive=True):
+ """
+ Search below this node for a menu with the given name.
+
+ This is implemented using findChild, and hence will automatically retry
+ if no such child is found, and will eventually raise an exception. It
+ also logs the search.
+ """
+ return self.findChild(predicate.IsAMenuNamed(menuName=menuName), recursive)
+
+ def menuItem(self, menuItemName, recursive=True):
+ """
+ Search below this node for a menu item with the given name.
+
+ This is implemented using findChild, and hence will automatically retry
+ if no such child is found, and will eventually raise an exception. It
+ also logs the search.
+ """
+ return self.findChild(predicate.IsAMenuItemNamed(menuItemName=menuItemName), recursive)
+
+ def textentry(self, textEntryName, recursive=True):
+ """
+ Search below this node for a text entry with the given name.
+
+ This is implemented using findChild, and hence will automatically retry
+ if no such child is found, and will eventually raise an exception. It
+ also logs the search.
+ """
+ return self.findChild(predicate.IsATextEntryNamed(textEntryName=textEntryName), recursive)
+
+ def button(self, buttonName, recursive=True):
+ """
+ Search below this node for a button with the given name.
+
+ This is implemented using findChild, and hence will automatically retry
+ if no such child is found, and will eventually raise an exception. It
+ also logs the search.
+ """
+ return self.findChild(predicate.IsAButtonNamed(buttonName=buttonName), recursive)
+
+ def childLabelled(self, labelText, recursive=True):
+ """
+ Search below this node for a child labelled with the given text.
+
+ This is implemented using findChild, and hence will automatically retry
+ if no such child is found, and will eventually raise an exception. It
+ also logs the search.
+ """
+ return self.findChild(predicate.IsLabelledAs(labelText), recursive)
+
+ def childNamed(self, childName, recursive=True):
+ """
+ Search below this node for a child with the given name.
+
+ This is implemented using findChild, and hence will automatically retry
+ if no such child is found, and will eventually raise an exception. It
+ also logs the search.
+ """
+ return self.findChild(predicate.IsNamed(childName), recursive)
+
+ def tab(self, tabName, recursive=True):
+ """
+ Search below this node for a tab with the given name.
+
+ This is implemented using findChild, and hence will automatically retry
+ if no such child is found, and will eventually raise an exception. It
+ also logs the search.
+ """
+ return self.findChild(predicate.IsATabNamed(tabName=tabName), recursive)
+
+ def getUserVisibleStrings(self):
+ """
+ Get all user-visible strings in this node and its descendents.
+
+ (Could be implemented as an attribute)
+ """
+ result = []
+ if self.name:
+ result.append(self.name)
+ if self.description:
+ result.append(self.description)
+ try:
+ children = self.children
+ except Exception:
+ return result
+ for child in children:
+ result.extend(child.getUserVisibleStrings())
+ return result
+
+ def blink(self):
+ """
+ Blink, baby!
+ """
+ if not self.extents:
+ return False
+ else:
+ (x, y, w, h) = self.extents
+ Blinker(x, y, w, h)
+ return True
+
+
+class LinkAnchor(object):
+
+ """
+ Class storing info about an anchor within an Accessibility.Hyperlink, which
+ is in turn stored within an Accessibility.Hypertext.
+ """
+
+ def __init__(self, node, hypertext, linkIndex, anchorIndex):
+ self.node = node
+ self.hypertext = hypertext
+ self.linkIndex = linkIndex
+ self.anchorIndex = anchorIndex
+
+ @property
+ def link(self):
+ return self.hypertext.getLink(self.linkIndex)
+
+ @property
+ def URI(self):
+ return self.link.getURI(self.anchorIndex)
+
+
+class Root (Node):
+
+ """
+ FIXME:
+ """
+
+ def applications(self):
+ """
+ Get all applications.
+ """
+ return root.findChildren(predicate.GenericPredicate(
+ roleName="application"), recursive=False)
+
+ def application(self, appName, retry=True):
+ """
+ Gets an application by name, returning an Application instance
+ or raising an exception.
+
+ This is implemented using findChild, and hence will automatically retry
+ if no such child is found, and will eventually raise an exception. It
+ also logs the search.
+ """
+ return root.findChild(predicate.IsAnApplicationNamed(appName), recursive=False, retry=retry)
+
+
+class Application (Node):
+
+ def dialog(self, dialogName, recursive=False):
+ """
+ Search below this node for a dialog with the given name,
+ returning a Window instance.
+
+ This is implemented using findChild, and hence will automatically retry
+ if no such child is found, and will eventually raise an exception. It
+ also logs the search.
+
+ FIXME: should this method activate the dialog?
+ """
+ return self.findChild(predicate.IsADialogNamed(dialogName=dialogName), recursive)
+
+ def window(self, windowName, recursive=False):
+ """
+ Search below this node for a window with the given name,
+ returning a Window instance.
+
+ This is implemented using findChild, and hence will automatically retry
+ if no such child is found, and will eventually raise an exception. It
+ also logs the search.
+
+ FIXME: this bit isn't true:
+ The window will be automatically activated (raised and focused
+ by the window manager) if wnck bindings are available.
+ """
+ result = self.findChild(
+ predicate.IsAWindowNamed(windowName=windowName), recursive)
+ # FIXME: activate the WnckWindow ?
+ # if gotWnck:
+ # result.activate()
+ return result
+
+ def getWnckApplication(self): # pragma: no cover
+ """
+ Get the wnck.Application instance for this application, or None
+
+ Currently implemented via a hack: requires the app to have a
+ window, and looks up the application of that window
+
+ wnck.Application can give you the pid, the icon, etc
+
+ FIXME: untested
+ """
+ window = self.child(roleName='frame')
+ if window:
+ wnckWindow = window.getWnckWindow()
+ return wnckWindow.get_application()
+
+
+class Window (Node):
+
+ def getWnckWindow(self): # pragma: no cover
+ """
+ Get the wnck.Window instance for this window, or None
+ """
+ # FIXME: this probably needs rewriting:
+ screen = Wnck.screen_get_default()
+
+ # You have to force an update before any of the wnck methods
+ # do anything:
+ screen.force_update()
+
+ for wnckWindow in screen.get_windows():
+ # FIXME: a dubious hack: search by window title:
+ if wnckWindow.get_name() == self.name:
+ return wnckWindow
+
+ def activate(self): # pragma: no cover
+ """
+ Activates the wnck.Window associated with this Window.
+
+ FIXME: doesn't yet work
+ """
+ wnckWindow = self.getWnckWindow()
+ # Activate it with a timestamp of 0; this may confuse
+ # alt-tabbing through windows etc:
+ # FIXME: is there a better way of getting a timestamp?
+ # gdk_x11_get_server_time (), with a dummy window
+ wnckWindow.activate(0)
+
+
+class Wizard (Window):
+
+ """
+ Note that the buttons of a GnomeDruid were not accessible until
+ recent versions of libgnomeui. This is
+ http://bugzilla.gnome.org/show_bug.cgi?id=157936
+ and is fixed in gnome-2.10 and gnome-2.12 (in CVS libgnomeui);
+ there's a patch attached to that bug.
+
+ This bug is known to affect FC3; fixed in FC5
+ """
+
+ def __init__(self, node, debugName=None):
+ Node.__init__(self, node)
+ if debugName:
+ self.debugName = debugName
+ logger.log("%s is on '%s' page" % (self, self.getPageTitle()))
+
+ def currentPage(self):
+ """
+ Get the current page of this wizard
+
+ FIXME: this is currently a hack, supporting only GnomeDruid
+ """
+ pageHolder = self.child(roleName='panel')
+ for child in pageHolder.children:
+ # current child has SHOWING state set, we hope:
+ # print child
+ # print child.showing
+ if child.showing:
+ return child
+ raise "Unable to determine current page of %s" % self
+
+ def getPageTitle(self):
+ """
+ Get the string title of the current page of this wizard
+
+ FIXME: this is currently a total hack, supporting only GnomeDruid
+ """
+ currentPage = self.currentPage()
+ return currentPage.child(roleName='panel').child(roleName='panel').child(roleName='label', recursive=False).text
+
+ def clickForward(self):
+ """
+ Click on the 'Forward' button to advance to next page of wizard.
+
+ It will log the title of the new page that is reached.
+
+ FIXME: what if it's Next rather than Forward ???
+
+ This will only work if your libgnomeui has accessible buttons;
+ see above.
+ """
+ fwd = self.child("Forward")
+ fwd.click()
+
+ # Log the new wizard page; it's helpful when debugging scripts
+ logger.log("%s is now on '%s' page" % (self, self.getPageTitle()))
+ # FIXME disabled for now (can't get valid page titles)
+
+ def clickApply(self):
+ """
+ Click on the 'Apply' button to advance to next page of wizard.
+ FIXME: what if it's Finish rather than Apply ???
+
+ This will only work if your libgnomeui has accessible buttons;
+ see above.
+ """
+ fwd = self.child("Apply")
+ fwd.click()
+
+ # FIXME: debug logging?
+
+Accessibility.Accessible.__bases__ = (
+ Application, Root, Node,) + Accessibility.Accessible.__bases__
+
+try:
+ root = pyatspi.Registry.getDesktop(0)
+ root.debugName = 'root'
+except Exception: # pragma: no cover
+ # Warn if AT-SPI's desktop object doesn't show up.
+ logger.log(
+ "Error: AT-SPI's desktop is not visible. Do you have accessibility enabled?")
+
+# Check that there are applications running. Warn if none are.
+children = root.children
+if not children: # pragma: no cover
+ logger.log(
+ "Warning: AT-SPI's desktop is visible but it has no children. Are you running any AT-SPI-aware applications?")
+del children
+
+import os
+# sniff also imports from tree and we don't want to run this code from
+# sniff itself
+if not os.path.exists('/tmp/sniff_running.lock'):
+ if not os.path.exists('/tmp/sniff_refresh.lock'): # may have already been locked by dogtail.procedural
+ # tell sniff not to use auto-refresh while script using this module is
+ # running
+ sniff_lock = Lock(lockname='sniff_refresh.lock', randomize=False)
+ try:
+ sniff_lock.lock()
+ except OSError: # pragma: no cover
+ pass # lock was already present from other script instance or leftover from killed instance
+ # lock should unlock automatically on script exit.
+
+# Convenient place to set some debug variables:
+#config.debugSearching = True
+#config.absoluteNodePaths = True
+#config.logDebugToFile = False