Package dogtail :: Module tree
[hide private]
[frames] | no frames]

Source Code for Module dogtail.tree

   1  """Makes some sense of the AT-SPI API 
   2   
   3  The tree API handles various things for you: 
   4      - fixes most timing issues 
   5      - can automatically generate (hopefully) highly-readable logs of what the 
   6  script is doing 
   7      - traps various UI malfunctions, raising exceptions for them (again, 
   8  hopefully improving the logs) 
   9   
  10  The most important class is Node. Each Node is an element of the desktop UI. 
  11  There is a tree of nodes, starting at 'root', with applications as its 
  12  children, with the top-level windows and dialogs as their children. The various 
  13  widgets that make up the UI appear as descendents in this tree. All of these 
  14  elements (root, the applications, the windows, and the widgets) are represented 
  15  as instances of Node in a tree (provided that the program of interest is 
  16  correctly exporting its user-interface to the accessibility system). The Node 
  17  class is a mixin for Accessible and the various Accessible interfaces. 
  18   
  19  The Action class represents an action that the accessibility layer exports as 
  20  performable on a specific node, such as clicking on it. It's a wrapper around 
  21  Accessibility.Action. 
  22   
  23  We often want to look for a node, based on some criteria, and this is provided 
  24  by the Predicate class. 
  25   
  26  Dogtail implements a high-level searching system, for finding a node (or 
  27  nodes) satisfying whatever criteria you are interested in. It does this with 
  28  a 'backoff and retry' algorithm. This fixes most timing problems e.g. when a 
  29  dialog is in the process of opening but hasn't yet done so. 
  30   
  31  If a search fails, it waits 'config.searchBackoffDuration' seconds, and then 
  32  tries again, repeatedly. After several failed attempts (determined by 
  33  config.searchWarningThreshold) it will start sending warnings about the search 
  34  to the debug log. If it still can't succeed after 'config.searchCutoffCount' 
  35  attempts, it raises an exception containing details of the search. You can see 
  36  all of this process in the debug log by setting 'config.debugSearching' to True 
  37   
  38  We also automatically add a short delay after each action 
  39  ('config.defaultDelay' gives the time in seconds). We'd hoped that the search 
  40  backoff and retry code would eliminate the need for this, but unfortunately we 
  41  still run into timing issues. For example, Evolution (and probably most 
  42  other apps) set things up on new dialogs and wizard pages as they appear, and 
  43  we can run into 'setting wars' where the app resets the widgetry to defaults 
  44  after our script has already filled out the desired values, and so we lose our 
  45  values. So we give the app time to set the widgetry up before the rest of the 
  46  script runs. 
  47   
  48  The classes trap various UI malfunctions and raise exceptions that better 
  49  describe what went wrong. For example, they detects attempts to click on an 
  50  insensitive UI element and raise a specific exception for this. 
  51   
  52  Unfortunately, some applications do not set up the 'sensitive' state 
  53  correctly on their buttons (e.g. Epiphany on form buttons in a web page). The 
  54  current workaround for this is to set config.ensureSensitivity=False, which 
  55  disables the sensitivity testing. 
  56   
  57  Authors: Zack Cerza <zcerza@redhat.com>, David Malcolm <dmalcolm@redhat.com> 
  58  """ 
  59  __author__ = """Zack Cerza <zcerza@redhat.com>, 
  60  David Malcolm <dmalcolm@redhat.com> 
  61  """ 
  62   
  63  from config import config 
  64  if config.checkForA11y: 
  65      from utils import checkForA11y 
  66      checkForA11y() 
  67   
  68  import re 
  69  import predicate 
  70  from datetime import datetime 
  71  from time import sleep 
  72  from utils import doDelay 
  73  from utils import Blinker 
  74  import rawinput 
  75  import path 
  76   
  77  from logging import debugLogger as logger 
  78   
  79  try: 
  80      import pyatspi 
  81      import Accessibility 
  82  except ImportError: 
  83      raise ImportError, "Error importing the AT-SPI bindings" 
  84   
  85  from CORBA import COMM_FAILURE, OBJECT_NOT_EXIST 
  86   
  87  # We optionally import the bindings for libwnck. 
  88  try: 
  89      import wnck 
  90      gotWnck = True 
  91  except ImportError: 
  92      # Skip this warning, since the functionality is almost entirely nonworking anyway. 
  93      #print "Warning: Dogtail could not import the Python bindings for libwnck. Window-manager manipulation will not be available." 
  94      gotWnck = False 
  95   
  96  haveWarnedAboutChildrenLimit = False 
  97   
98 -class SearchError(Exception):
99 pass
100
101 -class NotSensitiveError(Exception):
102 """ 103 The widget is not sensitive. 104 """ 105 message = "Cannot %s %s. It is not sensitive."
106 - def __init__(self, action):
107 self.action = action
108
109 - def __str__(self):
110 return self.message % (self.action.name, self.action.node.getLogString())
111
112 -class ActionNotSupported(Exception):
113 """ 114 The widget does not support the requested action. 115 """ 116 message = "Cannot do '%s' action on %s"
117 - def __init__(self, actionName, node):
118 self.actionName = actionName 119 self.node = node
120
121 - def __str__(self):
122 return self.message % (self.actionName, self.node.getLogString())
123
124 -class Action:
125 """ 126 Class representing an action that can be performed on a specific node 127 """ 128 # Valid types of actions we know about. Feel free to add any you see. 129 types = ('click', 130 'press', 131 'release', 132 'activate', 133 'jump', 134 'check', 135 'dock', 136 'undock', 137 'open', 138 'menu') 139
140 - def __init__ (self, node, action, index):
141 self.node = node 142 self.__action = action 143 self.__index = index
144
145 - def _getName(self): return self.__action.getName(self.__index)
146 name = property(_getName) 147
148 - def _getDescription(self): return self.__action.getDescription(self.__index)
149 description = property(_getDescription) 150
151 - def _getKeyBinding(self): return self.__action.getKeyBinding(self.__index)
152 keyBinding = property(_getKeyBinding) 153
154 - def __str__ (self):
155 return "[action | %s | %s ]" % \ 156 (self.name, self.keyBinding)
157
158 - def do (self):
159 """ 160 Performs the given tree.Action, with appropriate delays and logging. 161 """ 162 logger.log("%s on %s"%(self.name, self.node.getLogString())) 163 if not self.node.sensitive: 164 if config.ensureSensitivity: 165 raise NotSensitiveError, self 166 else: 167 nSE = NotSensitiveError(self) 168 logger.log("Warning: " + str(nSE)) 169 if config.blinkOnActions: self.node.blink() 170 result = self.__action.doAction (self.__index) 171 doDelay(config.actionDelay) 172 return result
173 174
175 -class Node:
176 """ 177 A node in the tree of UI elements. This class is mixed in with 178 Accessibility.Accessible to both make it easier to use and to add 179 additional functionality. It also has a debugName which is set up 180 automatically when doing searches. 181 """ 182
183 - def __setupUserData(self):
184 try: len(self.user_data) 185 except (AttributeError, TypeError): self.user_data = {}
186
187 - def _getDebugName(self):
188 self.__setupUserData() 189 return self.user_data.get('debugName', None)
190
191 - def _setDebugName(self, debugName):
192 self.__setupUserData() 193 self.user_data['debugName'] = debugName
194 195 debugName = property(_getDebugName, _setDebugName, doc = \ 196 "debug name assigned during search operations") 197 198 ## 199 # Accessible 200 ##
201 - def _getDead(self):
202 try: 203 if self.roleName == 'invalid': return True 204 n = self.role 205 n = self.name 206 if len(self) > 0: n = self[0] 207 except (LookupError, COMM_FAILURE, OBJECT_NOT_EXIST): return True 208 return False
209 dead = property(_getDead, doc = "Is the node dead (defunct) ?") 210
211 - def _getChildren(self):
212 if self.parent and self.parent.roleName == 'hyper link': 213 print self.parent.role 214 return [] 215 children = [] 216 childCount = self.childCount 217 if childCount > config.childrenLimit: 218 global haveWarnedAboutChildrenLimit 219 if not haveWarnedAboutChildrenLimit: 220 logger.log("Only returning %s children. You may change config.childrenLimit if you wish. This message will only be printed once." % str(config.childrenLimit)) 221 haveWarnedAboutChildrenLimit = True 222 childCount = config.childrenLimit 223 for i in range(childCount): 224 # Workaround for GNOME bug #465103 225 # also solution for GNOME bug #321273 226 try: 227 child = self[i] 228 except LookupError: child = None 229 if child: children.append(child) 230 231 invalidChildren = childCount - len(children) 232 if invalidChildren and config.debugSearching: 233 logger.log("Skipped %s invalid children of %s" % \ 234 (invalidChildren, str(self))) 235 try: 236 ht = self.queryHypertext() 237 for li in range(ht.getNLinks()): 238 link = ht.getLink(li) 239 for ai in range(link.nAnchors): 240 child = link.getObject(ai) 241 child.__setupUserData() 242 child.user_data['linkAnchor'] = \ 243 LinkAnchor(node = child, \ 244 hypertext = ht, \ 245 linkIndex = li, \ 246 anchorIndex = ai ) 247 children.append(child) 248 except NotImplementedError: pass 249 250 return children
251 children = property(_getChildren, doc = \ 252 "a list of this Accessible's children") 253 roleName = property(Accessibility.Accessible.getRoleName) 254 role = property(Accessibility.Accessible.getRole) 255 256 indexInParent = property(Accessibility.Accessible.getIndexInParent) 257 258 ## 259 # Action 260 ## 261
262 - def doAction(self, name):
263 """ 264 Perform the action with the specified name. For a list of actions 265 supported by this instance, check the 'actions' property. 266 """ 267 actions = self.actions 268 if actions.has_key(name): 269 return actions[name].do() 270 raise ActionNotSupported(name, self)
271
272 - def _getActions(self):
273 actions = {} 274 try: 275 action = self.queryAction() 276 for i in range(action.nActions): 277 a = Action(self, action, i) 278 actions[action.getName(i)] = a 279 finally: 280 return actions
281 actions = property(_getActions, doc = \ 282 """ 283 A dictionary of supported action names as keys, with Action objects as 284 values. Common action names include: 285 286 'click' 'press' 'release' 'activate' 'jump' 'check' 'dock' 'undock' 287 'open' 'menu' 288 """) 289 290
291 - def _getComboValue(self): return self.name
292 - def _setComboValue(self, value):
293 logger.log("Setting combobox %s to '%s'"%(self.getLogString(), value)) 294 self.childNamed(childName=value).doAction('click') 295 doDelay()
296 combovalue = property(_getComboValue, _setComboValue, doc = \ 297 """The value (as a string) currently selected in the combo box.""") 298 299 ## 300 # Hypertext and Hyperlink 301 ## 302
303 - def _getURI(self):
304 try: return self.user_data['linkAnchor'].URI 305 except (KeyError, AttributeError): raise NotImplementedError
306 URI = property(_getURI) 307 308 309 ## 310 # Text and EditableText 311 ## 312
313 - def _getText(self):
314 try: return self.queryText().getText(0,-1) 315 except NotImplementedError: return None
316 - def _setText(self, text):
317 try: 318 if config.debugSearching: 319 msg = "Setting text of %s to %s" 320 # Let's not get too crazy if 'text' is really large... 321 # FIXME: Sometimes the next line screws up Unicode strings. 322 if len(text) > 140: txt = text[:134] + " [...]" 323 else: txt = text 324 logger.log(msg % (self.getLogString(), "'%s'" % txt)) 325 self.queryEditableText().setTextContents(text) 326 except NotImplementedError: raise AttributeError, "can't set attribute"
327 text = property(_getText, _setText, doc = \ 328 """For instances with an AccessibleText interface, the text as a 329 string. This is read-only, unless the instance also has an 330 AccessibleEditableText interface. In this case, you can write values 331 to the attribute. This will get logged in the debug log, and a delay 332 will be added. 333 334 If this instance corresponds to a password entry, use the passwordText 335 property instead.""") 336
337 - def _getCaretOffset(self): return self.queryText().caretOffset
338 - def _setCaretOffset(self, offset):
339 return self.queryText().setCaretOffset(offset)
340 caretOffset = property(_getCaretOffset, _setCaretOffset, doc = \ 341 """For instances with an AccessibleText interface, the caret offset 342 as an integer.""") 343 344 345 ## 346 # Component 347 ## 348
349 - def _getPosition(self):
350 return self.queryComponent().getPosition(pyatspi.DESKTOP_COORDS)
351 position = property(_getPosition, doc = \ 352 """A tuple containing the position of the Accessible: (x, y)""") 353
354 - def _getSize(self): return self.queryComponent().getSize()
355 size = property(_getSize, doc = \ 356 """A tuple containing the size of the Accessible: (w, h)""") 357
358 - def _getExtents(self):
359 try: 360 ex = self.queryComponent().getExtents(pyatspi.DESKTOP_COORDS) 361 return (ex.x, ex.y, ex.width, ex.height) 362 except NotImplementedError: return None
363 extents = property(_getExtents, doc = \ 364 """ 365 A tuple containing the location and size of the Accessible: (x, y, w, h) 366 """) 367
368 - def contains(self, x, y):
369 try: return self.queryComponent().contains(x, y, pyatspi.DESKTOP_COORDS) 370 except NotImplementedError: return False
371
372 - def getChildAtPoint(self, x, y):
373 node = self 374 while True: 375 try: 376 child = node.queryComponent().getAccessibleAtPoint(x, y, 377 pyatspi.DESKTOP_COORDS) 378 if child and child.contains(x, y): node = child 379 else: break 380 except NotImplementedError: break 381 if node and node.contains(x, y): return node 382 else: return None
383
384 - def grabFocus(self):
385 "Attempts to set the keyboard focus to this Accessible." 386 return self.queryComponent().grabFocus()
387 398
399 - def click(self, button = 1):
400 """ 401 Generates a raw mouse click event, using the specified button. 402 - 1 is left, 403 - 2 is middle, 404 - 3 is right. 405 """ 406 clickX = self.position[0] + self.size[0]/2 407 clickY = self.position[1] + self.size[1]/2 408 if config.debugSearching: 409 logger.log("raw click on %s %s at (%s,%s)"%(self.name, self.getLogString(), str(clickX), str(clickY))) 410 rawinput.click(clickX, clickY, button)
411
412 - def doubleClick(self, button = 1):
413 """ 414 Generates a raw mouse double-click event, using the specified button. 415 """ 416 clickX = self.position[0] + self.size[0]/2 417 clickY = self.position[1] + self.size[1]/2 418 if config.debugSearching: 419 logger.log("raw click on %s %s at (%s,%s)"%(self.name, self.getLogString(), str(clickX), str(clickY))) 420 rawinput.doubleClick(clickX, clickY, button)
421 422 423 ## 424 # RelationSet 425 ##
426 - def _labeler(self):
427 relationSet = self.getRelationSet() 428 for relation in relationSet: 429 if relation.getRelationType() == pyatspi.RELATION_LABELLED_BY: 430 if relation.getNTargets() == 1: 431 return relation.getTarget(0) 432 targets = [] 433 for i in range(relation.getNTargets()): 434 targets.append(relation.getTarget(i)) 435 return targets
436 labeler = property(_labeler, doc = \ 437 """ 438 'labeller' (read-only list of Node instances): 439 The node(s) that is/are a label for this node. Generated from 440 'relations'. 441 """) 442 labeller = property(_labeler, doc = "See labeler") 443
444 - def _labelee(self):
445 relationSet = self.getRelationSet() 446 for relation in relationSet: 447 if relation.getRelationType() == pyatspi.RELATION_LABEL_FOR: 448 if relation.getNTargets() == 1: 449 return relation.getTarget(0) 450 targets = [] 451 for i in range(relation.getNTargets()): 452 targets.append(relation.getTarget(i)) 453 return targets
454 labelee = property(_labelee, doc = \ 455 """ 456 'labellee' (read-only list of Node instances): 457 The node(s) that this node is a label for. Generated from 'relations'. 458 """) 459 labellee = property(_labelee, doc = "See labelee") 460 461 ## 462 # StateSet 463 ##
464 - def _isSensitive(self): return self.getState().contains(pyatspi.STATE_SENSITIVE)
465 sensitive = property(_isSensitive, doc = \ 466 "Is the Accessible sensitive (i.e. not greyed out)?") 467
468 - def _isShowing(self): return self.getState().contains(pyatspi.STATE_SHOWING)
469 showing = property(_isShowing) 470
471 - def _isFocusable(self): return self.getState().contains(pyatspi.STATE_FOCUSABLE)
472 focusable = property(_isFocusable, doc = \ 473 "Is the Accessible capable of having keyboard focus?") 474
475 - def _isFocused(self): return self.getState().contains(pyatspi.STATE_FOCUSED)
476 focused = property(_isFocused, doc = \ 477 "Does the Accessible have keyboard focus?") 478
479 - def _isChecked(self): return self.getState().contains(pyatspi.STATE_CHECKED)
480 checked = property(_isChecked, doc = \ 481 "Is the Accessible a checked checkbox?") 482 483 ## 484 # Selection 485 ## 486
487 - def selectAll(self):
488 """ 489 Selects all children. 490 """ 491 result = self.querySelection().selectAll() 492 doDelay() 493 return result
494
495 - def deselectAll(self):
496 """ 497 Deselects all selected children. 498 """ 499 result = self.querySelection().clearSelection() 500 doDelay() 501 return result
502
503 - def select(self):
504 """ 505 Selects the Accessible. 506 """ 507 try: parent = self.parent 508 except AttributeError: raise NotImplementedError 509 result = parent.querySelection().selectChild(self.indexInParent) 510 doDelay() 511 return result
512
513 - def deselect(self):
514 """ 515 Deselects the Accessible. 516 """ 517 try: parent = self.parent 518 except AttributeError: raise NotImplementedError 519 result = parent.querySelection().deselectChild(self.indexInParent) 520 doDelay() 521 return result
522
523 - def _getSelected(self):
524 try: parent = self.parent 525 except AttributeError: raise NotImplementedError 526 return parent.querySelection().isChildSelected(self.indexInParent)
527 isSelected = property(_getSelected, doc = "Is the Accessible selected?") 528
529 - def _getSelectedChildren(self):
530 #TODO: hideChildren for Hyperlinks? 531 selection = self.querySelection() 532 selectedChildren = [] 533 for i in xrange(selection.nSelectedChildren): 534 selectedChildren.append(selection.getSelectedChild(i))
535 selectedChildren = property(_getSelectedChildren, doc = \ 536 "Returns a list of children that are selected.") 537 538 ## 539 # Value 540 ## 541
542 - def _getValue(self):
543 try: return self.queryValue().getCurrentValue() 544 except NotImplementedError: pass
545
546 - def _setValue(self, value):
547 self.setCurrentValue(value)
548 549 value = property(_getValue, _setValue) 550
551 - def _getMinValue(self):
552 try: return self.queryValue().getMinimumValue() 553 except NotImplementedError: pass
554 minValue = property(_getMinValue) 555
556 - def _getMinValueIncrement(self):
557 try: return self.queryValue().getMinimumIncrement 558 except NotImplementedError: pass
559 minValueIncrement = property(_getMinValueIncrement) 560
561 - def _getMaxValue(self):
562 try: return self.queryValue().getMaximumValue() 563 except NotImplementedError: pass
564 maxValue = property(_getMaxValue) 565
566 - def typeText(self, string):
567 """ 568 Type the given text into the node, with appropriate delays and 569 logging. 570 """ 571 logger.log("Typing text into %s: '%s'"%(self.getLogString(), string)) 572 573 if self.focusable: 574 if not self.focused: 575 try: self.grabFocus() 576 except Exception: logger.log("Node is focusable but I can't grabFocus!") 577 rawinput.typeText(string) 578 else: 579 logger.log("Node is not focusable; falling back to inserting text") 580 et = self.queryEditableText() 581 et.insertText(self.caretOffset, string, len(string)) 582 self.caretOffset += len(string) 583 doDelay()
584
585 - def keyCombo(self, comboString):
586 if config.debugSearching: logger.log("Pressing keys '%s' into %s"%(combo, self.getLogString())) 587 if self.focusable: 588 if not self.focused: 589 try: self.grabFocus() 590 except Exception: logger.log("Node is focusable but I can't grabFocus!") 591 else: logger.log("Node is not focusable; trying key combo anyway") 592 rawinput.keyCombo(comboString)
593 594
595 - def getLogString(self):
596 """ 597 Get a string describing this node for the logs, 598 respecting the config.absoluteNodePaths boolean. 599 """ 600 if config.absoluteNodePaths: 601 return self.getAbsoluteSearchPath() 602 else: 603 return str(self)
604
605 - def satisfies (self, pred):
606 """ 607 Does this node satisfy the given predicate? 608 """ 609 # the logic is handled by the predicate: 610 assert isinstance(pred, predicate.Predicate) 611 return pred.satisfiedByNode(self)
612
613 - def dump (self, type = 'plain', fileName = None):
614 import dump 615 dumper = getattr (dump, type) 616 dumper (self, fileName)
617
618 - def getAbsoluteSearchPath(self):
619 """ 620 FIXME: this needs rewriting... 621 Generate a SearchPath instance giving the 'best' 622 way to find the Accessible wrapped by this node again, starting 623 at the root and applying each search in turn. 624 625 This is somewhat analagous to an absolute path in a filesystem, 626 except that some of searches may be recursive, rather than just 627 searching direct children. 628 629 Used by the recording framework for identifying nodes in a 630 persistent way, independent of the style of script being 631 written. 632 633 FIXME: try to ensure uniqueness 634 FIXME: need some heuristics to get 'good' searches, whatever 635 that means 636 """ 637 if config.debugSearchPaths: 638 logger.log("getAbsoluteSearchPath(%s)" % self) 639 640 if self.roleName=='application': 641 result =path.SearchPath() 642 result.append(predicate.IsAnApplicationNamed(self.name), False) 643 return result 644 else: 645 if self.parent: 646 (ancestor, pred, isRecursive) = self.getRelativeSearch() 647 if config.debugSearchPaths: 648 logger.log("got ancestor: %s" % ancestor) 649 650 ancestorPath = ancestor.getAbsoluteSearchPath() 651 ancestorPath.append(pred, isRecursive) 652 return ancestorPath 653 else: 654 # This should be the root node: 655 return path.SearchPath()
656
657 - def getRelativeSearch(self):
658 """ 659 Get a (ancestorNode, predicate, isRecursive) triple that identifies the 660 best way to find this Node uniquely. 661 FIXME: or None if no such search exists? 662 FIXME: may need to make this more robust 663 FIXME: should this be private? 664 """ 665 if config.debugSearchPaths: 666 logger.log("getRelativeSearchPath(%s)" % self) 667 668 assert self 669 assert self.parent 670 671 isRecursive = False 672 ancestor = self.parent 673 674 # iterate up ancestors until you reach an identifiable one, 675 # setting the search to be isRecursive if need be: 676 while not self.__nodeIsIdentifiable(ancestor): 677 ancestor = ancestor.parent 678 isRecursive = True 679 680 # Pick the most appropriate predicate for finding this node: 681 if self.labellee: 682 if self.labellee.name: 683 return (ancestor, predicate.IsLabelledAs(self.labellee.name), isRecursive) 684 685 if self.roleName=='menu': 686 return (ancestor, predicate.IsAMenuNamed(self.name), isRecursive) 687 elif self.roleName=='menu item' or self.roleName=='check menu item': 688 return (ancestor, predicate.IsAMenuItemNamed(self.name), isRecursive) 689 elif self.roleName=='text': 690 return (ancestor, predicate.IsATextEntryNamed(self.name), isRecursive) 691 elif self.roleName=='push button': 692 return (ancestor, predicate.IsAButtonNamed(self.name), isRecursive) 693 elif self.roleName=='frame': 694 return (ancestor, predicate.IsAWindowNamed(self.name), isRecursive) 695 elif self.roleName=='dialog': 696 return (ancestor, predicate.IsADialogNamed(self.name), isRecursive) 697 else: 698 pred = predicate.GenericPredicate(name=self.name, roleName=self.roleName) 699 return (ancestor, pred, isRecursive)
700
701 - def __nodeIsIdentifiable(self, ancestor):
702 if ancestor.labellee: 703 return True 704 elif ancestor.name: 705 return True 706 elif not ancestor.parent: 707 return True 708 else: 709 return False
710
711 - def _fastFindChild(self, pred, recursive = True):
712 """ 713 Searches for an Accessible using methods from pyatspi.utils 714 """ 715 if isinstance(pred, predicate.Predicate): pred = pred.satisfiedByNode 716 if not recursive: 717 cIter = iter(self) 718 while True: 719 try: child = cIter.next() 720 except StopIteration: break 721 if child is not None: 722 if pred(child): return child 723 else: return pyatspi.utils.findDescendant(self, pred)
724
725 - def findChild(self, pred, recursive = True, debugName = None, \ 726 retry = True, requireResult = True):
727 """ 728 Search for a node satisyfing the predicate, returning a Node. 729 730 If retry is True (the default), it makes multiple attempts, 731 backing off and retrying on failure, and eventually raises a 732 descriptive exception if the search fails. 733 734 If retry is False, it gives up after one attempt. 735 736 If requireResult is True (the default), an exception is raised after all 737 attempts have failed. If it is false, the function simply returns None. 738 """ 739 def describeSearch (parent, pred, recursive, debugName): 740 """ 741 Internal helper function 742 """ 743 if recursive: noun = "descendent" 744 else: noun = "child" 745 if debugName == None: debugName = pred.describeSearchResult() 746 return "%s of %s: %s"%(noun, parent.getLogString(), debugName)
747 748 assert isinstance(pred, predicate.Predicate) 749 numAttempts = 0 750 while numAttempts < config.searchCutoffCount: 751 if numAttempts >= config.searchWarningThreshold or config.debugSearching: 752 logger.log("searching for %s (attempt %i)" % \ 753 (describeSearch(self, pred, recursive, debugName), numAttempts)) 754 755 result = self._fastFindChild(pred.satisfiedByNode, recursive) 756 if result: 757 assert isinstance(result, Node) 758 if debugName: result.debugName = debugName 759 else: result.debugName = pred.describeSearchResult() 760 return result 761 else: 762 if not retry: break 763 numAttempts += 1 764 if config.debugSearching or config.debugSleep: 765 logger.log("sleeping for %f" % config.searchBackoffDuration) 766 sleep(config.searchBackoffDuration) 767 if requireResult: 768 raise SearchError(describeSearch(self, pred, recursive, debugName))
769 770 771 # The canonical "search for multiple" method:
772 - def findChildren(self, pred, recursive = True):
773 """ 774 Find all children/descendents satisfying the predicate. 775 """ 776 # Note: This function does not use 777 # pyatspi.utils.findAllDescendants() because that function 778 # cannot be run non-recursively. 779 assert isinstance(pred, predicate.Predicate) 780 781 selfList = [] 782 783 try: children = self.children 784 except Exception: return [] 785 786 for child in children: 787 if child.satisfies(pred): selfList.append(child) 788 if recursive: 789 childList = child.findChildren(pred, recursive) 790 if childList: 791 for child in childList: 792 selfList.append(child) 793 # ...on to next child 794 795 if selfList: return selfList
796 797 # The canonical "search above this node" method:
798 - def findAncestor (self, pred):
799 """ 800 Search up the ancestry of this node, returning the first Node 801 satisfying the predicate, or None. 802 """ 803 assert isinstance(pred, predicate.Predicate) 804 candidate = self.parent 805 while candidate != None: 806 if candidate.satisfies(pred): 807 return candidate 808 else: 809 candidate = candidate.parent 810 # Not found: 811 return None
812 813 814 # Various wrapper/helper search methods:
815 - def child (self, name = '', roleName = '', description= '', label = '', recursive=True, debugName=None):
816 """ 817 Finds a child satisying the given criteria. 818 819 This is implemented using findChild, and hence will automatically retry 820 if no such child is found, and will eventually raise an exception. It 821 also logs the search. 822 """ 823 return self.findChild (predicate.GenericPredicate(name = name, roleName = roleName, description= description, label = label), recursive = recursive, debugName=debugName)
824
825 - def menu(self, menuName, recursive=True):
826 """ 827 Search below this node for a menu with the given name. 828 829 This is implemented using findChild, and hence will automatically retry 830 if no such child is found, and will eventually raise an exception. It 831 also logs the search. 832 """ 833 return self.findChild (predicate.IsAMenuNamed(menuName=menuName), recursive)
834
835 - def menuItem(self, menuItemName, recursive=True):
836 """ 837 Search below this node for a menu item with the given name. 838 839 This is implemented using findChild, and hence will automatically retry 840 if no such child is found, and will eventually raise an exception. It 841 also logs the search. 842 """ 843 return self.findChild (predicate.IsAMenuItemNamed(menuItemName=menuItemName), recursive)
844
845 - def textentry(self, textEntryName, recursive=True):
846 """ 847 Search below this node for a text entry with the given name. 848 849 This is implemented using findChild, and hence will automatically retry 850 if no such child is found, and will eventually raise an exception. It 851 also logs the search. 852 """ 853 return self.findChild (predicate.IsATextEntryNamed(textEntryName=textEntryName), recursive)
854
855 - def button(self, buttonName, recursive=True):
856 """ 857 Search below this node for a button with the given name. 858 859 This is implemented using findChild, and hence will automatically retry 860 if no such child is found, and will eventually raise an exception. It 861 also logs the search. 862 """ 863 return self.findChild (predicate.IsAButtonNamed(buttonName=buttonName), recursive)
864
865 - def childLabelled(self, labelText, recursive=True):
866 """ 867 Search below this node for a child labelled with the given text. 868 869 This is implemented using findChild, and hence will automatically retry 870 if no such child is found, and will eventually raise an exception. It 871 also logs the search. 872 """ 873 return self.findChild (predicate.IsLabelledAs(labelText), recursive)
874
875 - def childNamed(self, childName, recursive=True):
876 """ 877 Search below this node for a child with the given name. 878 879 This is implemented using findChild, and hence will automatically retry 880 if no such child is found, and will eventually raise an exception. It 881 also logs the search. 882 """ 883 return self.findChild (predicate.IsNamed(childName), recursive)
884
885 - def tab(self, tabName, recursive=True):
886 """ 887 Search below this node for a tab with the given name. 888 889 This is implemented using findChild, and hence will automatically retry 890 if no such child is found, and will eventually raise an exception. It 891 also logs the search. 892 """ 893 return self.findChild (predicate.IsATabNamed(tabName=tabName), recursive)
894
895 - def getUserVisibleStrings(self):
896 """ 897 Get all user-visible strings in this node and its descendents. 898 899 (Could be implemented as an attribute) 900 """ 901 result=[] 902 if self.name: 903 result.append(self.name) 904 if self.description: 905 result.append(self.description) 906 try: 907 children = self.children 908 except Exception: return result 909 for child in children: 910 result.extend(child.getUserVisibleStrings()) 911 return result
912 923 924
925 -class LinkAnchor:
926 """ 927 Class storing info about an anchor within an Accessibility.Hyperlink, which 928 is in turn stored within an Accessibility.Hypertext. 929 """ 930
931 - def __init__(self, node, hypertext, linkIndex, anchorIndex):
932 self.node = node 933 self.hypertext = hypertext 934 self.linkIndex = linkIndex 935 self.anchorIndex = anchorIndex
936 939 link = property(_getLink) 940
941 - def _getURI(self):
942 return self.link.getURI(self.anchorIndex)
943 URI = property(_getURI)
944 945
946 -class Root (Node):
947 """ 948 FIXME: 949 """
950 - def applications(self):
951 """ 952 Get all applications. 953 """ 954 return root.findChildren(predicate.GenericPredicate( \ 955 roleName="application"), recursive=False)
956
957 - def application(self, appName):
958 """ 959 Gets an application by name, returning an Application instance 960 or raising an exception. 961 962 This is implemented using findChild, and hence will automatically retry 963 if no such child is found, and will eventually raise an exception. It 964 also logs the search. 965 """ 966 return root.findChild(predicate.IsAnApplicationNamed(appName),recursive=False)
967
968 -class Application (Node):
969 - def dialog(self, dialogName, recursive=False):
970 """ 971 Search below this node for a dialog with the given name, 972 returning a Window instance. 973 974 This is implemented using findChild, and hence will automatically retry 975 if no such child is found, and will eventually raise an exception. It 976 also logs the search. 977 978 FIXME: should this method activate the dialog? 979 """ 980 return self.findChild(predicate.IsADialogNamed(dialogName=dialogName), recursive)
981
982 - def window(self, windowName, recursive=False):
983 """ 984 Search below this node for a window with the given name, 985 returning a Window instance. 986 987 This is implemented using findChild, and hence will automatically retry 988 if no such child is found, and will eventually raise an exception. It 989 also logs the search. 990 991 FIXME: this bit isn't true: 992 The window will be automatically activated (raised and focused 993 by the window manager) if wnck bindings are available. 994 """ 995 result = self.findChild (predicate.IsAWindowNamed(windowName=windowName), recursive) 996 # FIXME: activate the WnckWindow ? 997 #if gotWnck: 998 # result.activate() 999 return result
1000
1001 - def getWnckApplication(self):
1002 """ 1003 Get the wnck.Application instance for this application, or None 1004 1005 Currently implemented via a hack: requires the app to have a 1006 window, and looks up the application of that window 1007 1008 wnck.Application can give you the pid, the icon, etc 1009 1010 FIXME: untested 1011 """ 1012 window = child(roleName='frame') 1013 if window: 1014 wnckWindow = window.getWnckWindow() 1015 return wnckWindow.get_application()
1016 1017 1018
1019 -class Window (Node):
1020 - def getWnckWindow(self):
1021 """ 1022 Get the wnck.Window instance for this window, or None 1023 """ 1024 # FIXME: this probably needs rewriting: 1025 screen = wnck.screen_get_default() 1026 1027 # You have to force an update before any of the wnck methods 1028 # do anything: 1029 screen.force_update() 1030 1031 for wnckWindow in screen.get_windows(): 1032 # FIXME: a dubious hack: search by window title: 1033 if wnckWindow.get_name()==self.name: 1034 return wnckWindow
1035
1036 - def activate(self):
1037 """ 1038 Activates the wnck.Window associated with this Window. 1039 1040 FIXME: doesn't yet work 1041 """ 1042 wnckWindow = self.getWnckWindow() 1043 # Activate it with a timestamp of 0; this may confuse 1044 # alt-tabbing through windows etc: 1045 # FIXME: is there a better way of getting a timestamp? 1046 # gdk_x11_get_server_time (), with a dummy window 1047 wnckWindow.activate(0)
1048
1049 -class Wizard (Window):
1050 """ 1051 Note that the buttons of a GnomeDruid were not accessible until 1052 recent versions of libgnomeui. This is 1053 http://bugzilla.gnome.org/show_bug.cgi?id=157936 1054 and is fixed in gnome-2.10 and gnome-2.12 (in CVS libgnomeui); 1055 there's a patch attached to that bug. 1056 1057 This bug is known to affect FC3; fixed in FC5 1058 """
1059 - def __init__(self, node, debugName=None):
1060 Node.__init__(self, node) 1061 if debugName: 1062 self.debugName = debugName 1063 logger.log("%s is on '%s' page"%(self, self.getPageTitle()))
1064
1065 - def currentPage(self):
1066 """ 1067 Get the current page of this wizard 1068 1069 FIXME: this is currently a hack, supporting only GnomeDruid 1070 """ 1071 pageHolder = self.child(roleName='panel') 1072 for child in pageHolder.children: 1073 # current child has SHOWING state set, we hope: 1074 #print child 1075 #print child.showing 1076 if child.showing: 1077 return child 1078 raise "Unable to determine current page of %s"%self
1079
1080 - def getPageTitle(self):
1081 """ 1082 Get the string title of the current page of this wizard 1083 1084 FIXME: this is currently a total hack, supporting only GnomeDruid 1085 """ 1086 currentPage = self.currentPage() 1087 return currentPage.child(roleName='panel').child(roleName='panel').child(roleName='label', recursive=False).text
1088
1089 - def clickForward(self):
1090 """ 1091 Click on the 'Forward' button to advance to next page of wizard. 1092 1093 It will log the title of the new page that is reached. 1094 1095 FIXME: what if it's Next rather than Forward ??? 1096 1097 This will only work if your libgnomeui has accessible buttons; 1098 see above. 1099 """ 1100 fwd = self.child("Forward") 1101 fwd.click() 1102 1103 # Log the new wizard page; it's helpful when debugging scripts 1104 logger.log("%s is now on '%s' page"%(self, self.getPageTitle()))
1105 # FIXME disabled for now (can't get valid page titles) 1106
1107 - def clickApply(self):
1108 """ 1109 Click on the 'Apply' button to advance to next page of wizard. 1110 FIXME: what if it's Finish rather than Apply ??? 1111 1112 This will only work if your libgnomeui has accessible buttons; 1113 see above. 1114 """ 1115 fwd = self.child("Apply") 1116 fwd.click()
1117 1118 # FIXME: debug logging? 1119 1120 Accessibility.Accessible.__bases__ = (Node,) + Accessibility.Accessible.__bases__ 1121 Accessibility.Application.__bases__ = (Application,) + Accessibility.Application.__bases__ 1122 Accessibility.Desktop.__bases__ = (Root,) + Accessibility.Desktop.__bases__ 1123 1124 try: 1125 root = pyatspi.Registry.getDesktop(0) 1126 root.debugName= 'root' 1127 except Exception: 1128 # Warn if AT-SPI's desktop object doesn't show up. 1129 logger.log("Error: AT-SPI's desktop is not visible. Do you have accessibility enabled?") 1130 1131 # Check that there are applications running. Warn if none are. 1132 children = root.children 1133 if not children: 1134 logger.log("Warning: AT-SPI's desktop is visible but it has no children. Are you running any AT-SPI-aware applications?") 1135 del children 1136 1137 # Convenient place to set some debug variables: 1138 #config.debugSearching = True 1139 #config.absoluteNodePaths = True 1140 #config.logDebugToFile = False 1141