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 predicate 
  69  from time import sleep 
  70  from utils import doDelay 
  71  from utils import Blinker 
  72  from utils import Lock 
  73  import rawinput 
  74  import path 
  75  from __builtin__ import xrange 
  76   
  77  from logging import debugLogger as logger 
  78   
  79  try: 
  80      import pyatspi 
  81      import Accessibility 
  82  except ImportError:  # pragma: no cover 
  83      raise ImportError("Error importing the AT-SPI bindings") 
  84   
  85  # We optionally import the bindings for libWnck. 
  86  try: 
  87      from gi.repository import Wnck 
  88      gotWnck = True  # pragma: no cover 
  89  except ImportError: 
  90      # Skip this warning, since the functionality is almost entirely nonworking anyway. 
  91      # print "Warning: Dogtail could not import the Python bindings for 
  92      # libwnck. Window-manager manipulation will not be available." 
  93      gotWnck = False 
  94   
  95  from gi.repository import GLib 
  96   
  97  haveWarnedAboutChildrenLimit = False 
98 99 100 -class SearchError(Exception):
101 pass
102
103 104 -class NotSensitiveError(Exception):
105 106 """ 107 The widget is not sensitive. 108 """ 109 message = "Cannot %s %s. It is not sensitive." 110
111 - def __init__(self, action):
112 self.action = action
113
114 - def __str__(self):
115 return self.message % (self.action.name, self.action.node.getLogString())
116
117 118 -class ActionNotSupported(Exception):
119 120 """ 121 The widget does not support the requested action. 122 """ 123 message = "Cannot do '%s' action on %s" 124
125 - def __init__(self, actionName, node):
126 self.actionName = actionName 127 self.node = node
128
129 - def __str__(self):
130 return self.message % (self.actionName, self.node.getLogString())
131
132 133 -class Action(object):
134 135 """ 136 Class representing an action that can be performed on a specific node 137 """ 138 # Valid types of actions we know about. Feel free to add any you see. 139 types = ('click', 140 'press', 141 'release', 142 'activate', 143 'jump', 144 'check', 145 'dock', 146 'undock', 147 'open', 148 'menu') 149
150 - def __init__(self, node, action, index):
151 self.node = node 152 self.__action = action 153 self.__index = index
154 155 @property
156 - def name(self):
157 return self.__action.getName(self.__index)
158 159 @property
160 - def description(self):
161 return self.__action.getDescription(self.__index)
162 163 @property
164 - def keyBinding(self):
165 return self.__action.getKeyBinding(self.__index)
166
167 - def __str__(self):
168 return "[action | %s | %s ]" % \ 169 (self.name, self.keyBinding)
170
171 - def do(self):
172 """ 173 Performs the given tree.Action, with appropriate delays and logging. 174 """ 175 logger.log("%s on %s" % (self.name, self.node.getLogString())) 176 if not self.node.sensitive: 177 if config.ensureSensitivity: 178 raise NotSensitiveError(self) 179 else: 180 nSE = NotSensitiveError(self) 181 logger.log("Warning: " + str(nSE)) 182 if config.blinkOnActions: 183 self.node.blink() 184 result = self.__action.doAction(self.__index) 185 doDelay(config.actionDelay) 186 return result
187
188 189 -class Node(object):
190 191 """ 192 A node in the tree of UI elements. This class is mixed in with 193 Accessibility.Accessible to both make it easier to use and to add 194 additional functionality. It also has a debugName which is set up 195 automatically when doing searches. 196 """ 197
198 - def __setupUserData(self):
199 try: 200 len(self.user_data) 201 except (AttributeError, TypeError): 202 self.user_data = {}
203
204 - def debugName():
205 doc = "debug name assigned during search operations" 206 207 def fget(self): 208 self.__setupUserData() 209 return self.user_data.get('debugName', None)
210 211 def fset(self, debugName): 212 self.__setupUserData() 213 self.user_data['debugName'] = debugName
214 215 return property(**locals()) 216 debugName = debugName() 217 # 218 # Accessible 219 # 220 221 @property
222 - def dead(self):
223 """Is the node dead (defunct) ?""" 224 try: 225 if self.roleName == 'invalid': 226 return True 227 self.role 228 self.name 229 if len(self) > 0: 230 self[0] 231 except: 232 return True 233 return False
234 235 @property
236 - def children(self):
237 """a list of this Accessible's children""" 238 if self.parent and self.parent.roleName == 'hyper link': 239 print(self.parent.role) 240 return [] 241 children = [] 242 childCount = self.childCount 243 if childCount > config.childrenLimit: 244 global haveWarnedAboutChildrenLimit 245 if not haveWarnedAboutChildrenLimit: 246 logger.log("Only returning %s children. You may change " 247 "config.childrenLimit if you wish. This message will only" 248 " be printed once." % str(config.childrenLimit)) 249 haveWarnedAboutChildrenLimit = True 250 childCount = config.childrenLimit 251 for i in range(childCount): 252 # Workaround for GNOME bug #465103 253 # also solution for GNOME bug #321273 254 try: 255 child = self[i] 256 except LookupError: 257 child = None 258 if child: 259 children.append(child) 260 261 invalidChildren = childCount - len(children) 262 if invalidChildren and config.debugSearching: 263 logger.log("Skipped %s invalid children of %s" % 264 (invalidChildren, str(self))) 265 try: 266 ht = self.queryHypertext() 267 for li in range(ht.getNLinks()): 268 link = ht.getLink(li) 269 for ai in range(link.nAnchors): 270 child = link.getObject(ai) 271 child.__setupUserData() 272 child.user_data['linkAnchor'] = \ 273 LinkAnchor(node=child, 274 hypertext=ht, 275 linkIndex=li, 276 anchorIndex=ai) 277 children.append(child) 278 except (NotImplementedError, AttributeError): 279 pass 280 281 return children
282 283 roleName = property(Accessibility.Accessible.getRoleName) 284 285 role = property(Accessibility.Accessible.getRole) 286 287 indexInParent = property(Accessibility.Accessible.getIndexInParent) 288 289 # 290 # Action 291 # 292 293 # Needed to be renamed from doAction due to conflicts 294 # with 'Accessibility.Accessible.doAction' in gtk3 branch
295 - def doActionNamed(self, name):
296 """ 297 Perform the action with the specified name. For a list of actions 298 supported by this instance, check the 'actions' property. 299 """ 300 actions = self.actions 301 if name in actions: 302 return actions[name].do() 303 raise ActionNotSupported(name, self)
304 305 @property
306 - def actions(self):
307 """ 308 A dictionary of supported action names as keys, with Action objects as 309 values. Common action names include: 310 311 'click' 'press' 'release' 'activate' 'jump' 'check' 'dock' 'undock' 312 'open' 'menu' 313 """ 314 actions = {} 315 try: 316 action = self.queryAction() 317 for i in range(action.nActions): 318 a = Action(self, action, i) 319 actions[action.getName(i)] = a 320 finally: 321 return actions
322
323 - def combovalue():
324 doc = "The value (as a string) currently selected in the combo box." 325 326 def fget(self): 327 return self.name
328 329 def fset(self, value): 330 logger.log("Setting combobox %s to '%s'" % (self.getLogString(), 331 value)) 332 self.childNamed(childName=value).doActionNamed('click') 333 doDelay() 334 335 return property(**locals()) 336 combovalue = combovalue() 337 # 338 # Hypertext and Hyperlink 339 # 340 341 @property
342 - def URI(self):
343 try: 344 return self.user_data['linkAnchor'].URI 345 except (KeyError, AttributeError): 346 raise NotImplementedError
347 348 # 349 # Text and EditableText 350 #
351 - def text():
352 doc = """For instances with an AccessibleText interface, the text as a 353 string. This is read-only, unless the instance also has an 354 AccessibleEditableText interface. In this case, you can write values 355 to the attribute. This will get logged in the debug log, and a delay 356 will be added. 357 358 If this instance corresponds to a password entry, use the passwordText 359 property instead.""" 360 361 def fget(self): 362 try: 363 return self.queryText().getText(0, -1) 364 except NotImplementedError: 365 return None
366 367 def fset(self, text): 368 try: 369 if config.debugSearching: 370 msg = "Setting text of %s to %s" 371 # Let's not get too crazy if 'text' is really large... 372 # FIXME: Sometimes the next line screws up Unicode strings. 373 if len(text) > 140: 374 txt = text[:134] + " [...]" 375 else: 376 txt = text 377 logger.log(msg % (self.getLogString(), "'%s'" % txt)) 378 self.queryEditableText().setTextContents(text) 379 except NotImplementedError: 380 raise AttributeError("can't set attribute") 381 382 return property(**locals()) 383 text = text() 384
385 - def caretOffset():
386 387 def fget(self): 388 """For instances with an AccessibleText interface, the caret 389 offset as an integer.""" 390 return self.queryText().caretOffset
391 392 def fset(self, offset): 393 return self.queryText().setCaretOffset(offset) 394 395 return property(**locals()) 396 caretOffset = caretOffset() 397 398 # 399 # Component 400 # 401 402 @property
403 - def position(self):
404 """A tuple containing the position of the Accessible: (x, y)""" 405 return self.queryComponent().getPosition(pyatspi.DESKTOP_COORDS)
406 407 @property
408 - def size(self):
409 """A tuple containing the size of the Accessible: (w, h)""" 410 return self.queryComponent().getSize()
411 412 @property
413 - def extents(self):
414 """A tuple containing the location and size of the Accessible: 415 (x, y, w, h)""" 416 try: 417 ex = self.queryComponent().getExtents(pyatspi.DESKTOP_COORDS) 418 return (ex.x, ex.y, ex.width, ex.height) 419 except NotImplementedError: 420 return None
421
422 - def contains(self, x, y):
423 try: 424 return self.queryComponent().contains(x, y, pyatspi.DESKTOP_COORDS) 425 except NotImplementedError: 426 return False
427
428 - def getChildAtPoint(self, x, y):
429 node = self 430 while True: 431 try: 432 child = node.queryComponent().getAccessibleAtPoint(x, y, 433 pyatspi.DESKTOP_COORDS) 434 if child and child.contains(x, y): 435 node = child 436 else: 437 break 438 except NotImplementedError: 439 break 440 if node and node.contains(x, y): 441 return node 442 else: 443 return None
444
445 - def grabFocus(self):
446 "Attempts to set the keyboard focus to this Accessible." 447 return self.queryComponent().grabFocus()
448 449 # def blink(self, count=2): 450 #""" 451 # Blink, baby! 452 #""" 453 # if not self.extents: return False 454 # else: 455 #(x, y, w, h) = self.extents 456 #from utils import Blinker 457 #blinkData = Blinker(x, y, w, h, count) 458 # return True 459
460 - def click(self, button=1):
461 """ 462 Generates a raw mouse click event, using the specified button. 463 - 1 is left, 464 - 2 is middle, 465 - 3 is right. 466 """ 467 logger.log("Clicking on %s" % self.getLogString()) 468 clickX = self.position[0] + self.size[0] / 2 469 clickY = self.position[1] + self.size[1] / 2 470 if config.debugSearching: 471 logger.log("raw click on %s %s at (%s,%s)" % 472 (self.name, self.getLogString(), str(clickX), str(clickY))) 473 rawinput.click(clickX, clickY, button)
474
475 - def doubleClick(self, button=1):
476 """ 477 Generates a raw mouse double-click event, using the specified button. 478 """ 479 clickX = self.position[0] + self.size[0] / 2 480 clickY = self.position[1] + self.size[1] / 2 481 if config.debugSearching: 482 logger.log("raw click on %s %s at (%s,%s)" % 483 (self.name, self.getLogString(), str(clickX), str(clickY))) 484 rawinput.doubleClick(clickX, clickY, button)
485
486 - def point(self, mouseDelay=None):
487 """ 488 Move mouse cursor to the center of the widget. 489 """ 490 pointX = self.position[0] + self.size[0] / 2 491 pointY = self.position[1] + self.size[1] / 2 492 logger.log("Pointing on %s %s at (%s,%s)" % 493 (self.name, self.getLogString(), str(pointX), str(pointY))) 494 rawinput.registry.generateMouseEvent(pointX, pointY, 'abs') 495 if mouseDelay: 496 doDelay(mouseDelay) 497 else: 498 doDelay()
499 500 # 501 # RelationSet 502 # 503 @property
504 - def labeler(self):
505 """'labeller' (read-only list of Node instances): 506 The node(s) that is/are a label for this node. Generated from 507 'relations'. 508 """ 509 relationSet = self.getRelationSet() 510 for relation in relationSet: 511 if relation.getRelationType() == pyatspi.RELATION_LABELLED_BY: 512 if relation.getNTargets() == 1: 513 return relation.getTarget(0) 514 targets = [] 515 for i in range(relation.getNTargets()): 516 targets.append(relation.getTarget(i)) 517 return targets
518 labeller = labeler 519 520 @property
521 - def labelee(self):
522 """'labellee' (read-only list of Node instances): 523 The node(s) that this node is a label for. Generated from 'relations'. 524 """ 525 relationSet = self.getRelationSet() 526 for relation in relationSet: 527 if relation.getRelationType() == pyatspi.RELATION_LABEL_FOR: 528 if relation.getNTargets() == 1: 529 return relation.getTarget(0) 530 targets = [] 531 for i in range(relation.getNTargets()): 532 targets.append(relation.getTarget(i)) 533 return targets
534 labellee = labelee 535 536 # 537 # StateSet 538 # 539 @property
540 - def sensitive(self):
541 """Is the Accessible sensitive (i.e. not greyed out)?""" 542 return self.getState().contains(pyatspi.STATE_SENSITIVE)
543 544 @property
545 - def showing(self):
546 return self.getState().contains(pyatspi.STATE_SHOWING)
547 548 @property
549 - def focusable(self):
550 """Is the Accessible capable of having keyboard focus?""" 551 return self.getState().contains(pyatspi.STATE_FOCUSABLE)
552 553 @property
554 - def focused(self):
555 """Does the Accessible have keyboard focus?""" 556 return self.getState().contains(pyatspi.STATE_FOCUSED)
557 558 @property
559 - def checked(self):
560 """Is the Accessible a checked checkbox?""" 561 return self.getState().contains(pyatspi.STATE_CHECKED)
562 563 @property
564 - def isChecked(self):
565 """Is the Accessible a checked checkbox? Compatibility property, same as Node.checked.""" 566 return self.checked
567 568 # 569 # Selection 570 # 571
572 - def selectAll(self):
573 """Selects all children.""" 574 result = self.querySelection().selectAll() 575 doDelay() 576 return result
577
578 - def deselectAll(self):
579 """Deselects all selected children.""" 580 result = self.querySelection().clearSelection() 581 doDelay() 582 return result
583
584 - def select(self):
585 """Selects the Accessible.""" 586 try: 587 parent = self.parent 588 except AttributeError: 589 raise NotImplementedError 590 result = parent.querySelection().selectChild(self.indexInParent) 591 doDelay() 592 return result
593
594 - def deselect(self):
595 """Deselects the Accessible.""" 596 try: 597 parent = self.parent 598 except AttributeError: 599 raise NotImplementedError 600 result = parent.querySelection().deselectChild(self.indexInParent) 601 doDelay() 602 return result
603 604 @property
605 - def isSelected(self):
606 """Is the Accessible selected? Compatibility property, same as Node.selected.""" 607 try: 608 parent = self.parent 609 except AttributeError: 610 raise NotImplementedError 611 return parent.querySelection().isChildSelected(self.indexInParent)
612 613 @property
614 - def selected(self):
615 """Is the Accessible selected?""" 616 return self.isSelected
617 618 @property
619 - def selectedChildren(self):
620 """Returns a list of children that are selected.""" 621 # TODO: hideChildren for Hyperlinks? 622 selection = self.querySelection() 623 selectedChildren = [] 624 for i in xrange(selection.nSelectedChildren): 625 selectedChildren.append(selection.getSelectedChild(i))
626 627 # 628 # Value 629 # 630
631 - def value():
632 doc = "The value contained by the AccessibleValue interface." 633 634 def fget(self): 635 try: 636 return self.queryValue().currentValue 637 except NotImplementedError: 638 pass
639 640 def fset(self, value): 641 self.queryValue().currentValue = value 642 643 return property(**locals()) 644 value = value() 645 646 @property
647 - def minValue(self):
648 """The minimum value of self.value""" 649 try: 650 return self.queryValue().minimumValue 651 except NotImplementedError: 652 pass
653 654 @property
655 - def minValueIncrement(self):
656 """The minimum value increment of self.value""" 657 try: 658 return self.queryValue().minimumIncrement 659 except NotImplementedError: 660 pass
661 662 @property
663 - def maxValue(self):
664 """The maximum value of self.value""" 665 try: 666 return self.queryValue().maximumValue 667 except NotImplementedError: 668 pass
669
670 - def typeText(self, string):
671 """ 672 Type the given text into the node, with appropriate delays and 673 logging. 674 """ 675 logger.log("Typing text into %s: '%s'" % (self.getLogString(), string)) 676 677 if self.focusable: 678 if not self.focused: 679 try: 680 self.grabFocus() 681 except Exception: 682 logger.log("Node is focusable but I can't grabFocus!") 683 rawinput.typeText(string) 684 else: 685 logger.log("Node is not focusable; falling back to inserting text") 686 et = self.queryEditableText() 687 et.insertText(self.caretOffset, string, len(string)) 688 self.caretOffset += len(string) 689 doDelay()
690
691 - def keyCombo(self, comboString):
692 if config.debugSearching: 693 logger.log("Pressing keys '%s' into %s" % 694 (comboString, self.getLogString())) 695 if self.focusable: 696 if not self.focused: 697 try: 698 self.grabFocus() 699 except Exception: 700 logger.log("Node is focusable but I can't grabFocus!") 701 else: 702 logger.log("Node is not focusable; trying key combo anyway") 703 rawinput.keyCombo(comboString)
704
705 - def getLogString(self):
706 """ 707 Get a string describing this node for the logs, 708 respecting the config.absoluteNodePaths boolean. 709 """ 710 if config.absoluteNodePaths: 711 return self.getAbsoluteSearchPath() 712 else: 713 return str(self)
714
715 - def satisfies(self, pred):
716 """ 717 Does this node satisfy the given predicate? 718 """ 719 # the logic is handled by the predicate: 720 assert isinstance(pred, predicate.Predicate) 721 return pred.satisfiedByNode(self)
722
723 - def dump(self, type='plain', fileName=None):
724 import dump 725 dumper = getattr(dump, type) 726 dumper(self, fileName)
727
728 - def getAbsoluteSearchPath(self):
729 """ 730 FIXME: this needs rewriting... 731 Generate a SearchPath instance giving the 'best' 732 way to find the Accessible wrapped by this node again, starting 733 at the root and applying each search in turn. 734 735 This is somewhat analagous to an absolute path in a filesystem, 736 except that some of searches may be recursive, rather than just 737 searching direct children. 738 739 Used by the recording framework for identifying nodes in a 740 persistent way, independent of the style of script being 741 written. 742 743 FIXME: try to ensure uniqueness 744 FIXME: need some heuristics to get 'good' searches, whatever 745 that means 746 """ 747 if config.debugSearchPaths: 748 logger.log("getAbsoluteSearchPath(%s)" % self) 749 750 if self.roleName == 'application': 751 result = path.SearchPath() 752 result.append(predicate.IsAnApplicationNamed(self.name), False) 753 return result 754 else: 755 if self.parent: 756 (ancestor, pred, isRecursive) = self.getRelativeSearch() 757 if config.debugSearchPaths: 758 logger.log("got ancestor: %s" % ancestor) 759 760 ancestorPath = ancestor.getAbsoluteSearchPath() 761 ancestorPath.append(pred, isRecursive) 762 return ancestorPath 763 else: 764 # This should be the root node: 765 return path.SearchPath()
766
767 - def getRelativeSearch(self):
768 """ 769 Get a (ancestorNode, predicate, isRecursive) triple that identifies the 770 best way to find this Node uniquely. 771 FIXME: or None if no such search exists? 772 FIXME: may need to make this more robust 773 FIXME: should this be private? 774 """ 775 if config.debugSearchPaths: 776 logger.log("getRelativeSearchPath(%s)" % self) 777 778 assert self 779 assert self.parent 780 781 isRecursive = False 782 ancestor = self.parent 783 784 # iterate up ancestors until you reach an identifiable one, 785 # setting the search to be isRecursive if need be: 786 while not self.__nodeIsIdentifiable(ancestor): 787 ancestor = ancestor.parent 788 isRecursive = True 789 790 # Pick the most appropriate predicate for finding this node: 791 if self.labellee: 792 if self.labellee.name: 793 return (ancestor, predicate.IsLabelledAs(self.labellee.name), isRecursive) 794 795 if self.roleName == 'menu': 796 return (ancestor, predicate.IsAMenuNamed(self.name), isRecursive) 797 elif self.roleName == 'menu item' or self.roleName == 'check menu item': 798 return (ancestor, predicate.IsAMenuItemNamed(self.name), isRecursive) 799 elif self.roleName == 'text': 800 return (ancestor, predicate.IsATextEntryNamed(self.name), isRecursive) 801 elif self.roleName == 'push button': 802 return (ancestor, predicate.IsAButtonNamed(self.name), isRecursive) 803 elif self.roleName == 'frame': 804 return (ancestor, predicate.IsAWindowNamed(self.name), isRecursive) 805 elif self.roleName == 'dialog': 806 return (ancestor, predicate.IsADialogNamed(self.name), isRecursive) 807 else: 808 pred = predicate.GenericPredicate( 809 name=self.name, roleName=self.roleName) 810 return (ancestor, pred, isRecursive)
811
812 - def __nodeIsIdentifiable(self, ancestor):
813 if ancestor.labellee: 814 return True 815 elif ancestor.name: 816 return True 817 elif not ancestor.parent: 818 return True 819 else: 820 return False
821
822 - def _fastFindChild(self, pred, recursive=True):
823 """ 824 Searches for an Accessible using methods from pyatspi.utils 825 """ 826 if isinstance(pred, predicate.Predicate): 827 pred = pred.satisfiedByNode 828 if not recursive: 829 cIter = iter(self) 830 while True: 831 try: 832 child = cIter.next() 833 except StopIteration: 834 break 835 if child is not None: 836 if pred(child): 837 return child 838 else: 839 return pyatspi.utils.findDescendant(self, pred)
840
841 - def findChild(self, pred, recursive=True, debugName=None, 842 retry=True, requireResult=True):
843 """ 844 Search for a node satisyfing the predicate, returning a Node. 845 846 If retry is True (the default), it makes multiple attempts, 847 backing off and retrying on failure, and eventually raises a 848 descriptive exception if the search fails. 849 850 If retry is False, it gives up after one attempt. 851 852 If requireResult is True (the default), an exception is raised after all 853 attempts have failed. If it is false, the function simply returns None. 854 """ 855 def describeSearch(parent, pred, recursive, debugName): 856 """ 857 Internal helper function 858 """ 859 if recursive: 860 noun = "descendent" 861 else: 862 noun = "child" 863 if debugName is None: 864 debugName = pred.describeSearchResult() 865 return "%s of %s: %s" % (noun, parent.getLogString(), debugName)
866 867 assert isinstance(pred, predicate.Predicate) 868 numAttempts = 0 869 while numAttempts < config.searchCutoffCount: 870 if numAttempts >= config.searchWarningThreshold or config.debugSearching: 871 logger.log("searching for %s (attempt %i)" % 872 (describeSearch(self, pred, recursive, debugName), numAttempts)) 873 874 result = self._fastFindChild(pred.satisfiedByNode, recursive) 875 if result: 876 assert isinstance(result, Node) 877 if debugName: 878 result.debugName = debugName 879 else: 880 result.debugName = pred.describeSearchResult() 881 return result 882 else: 883 if not retry: 884 break 885 numAttempts += 1 886 if config.debugSearching or config.debugSleep: 887 logger.log("sleeping for %f" % 888 config.searchBackoffDuration) 889 sleep(config.searchBackoffDuration) 890 if requireResult: 891 raise SearchError(describeSearch(self, pred, recursive, debugName)) 892 893 # The canonical "search for multiple" method:
894 - def findChildren(self, pred, recursive=True, isLambda=False):
895 """ 896 Find all children/descendents satisfying the predicate. 897 """ 898 if isLambda is True: 899 nodes = self.findChildren(predicate.GenericPredicate(), recursive=recursive) 900 result = [] 901 for node in nodes: 902 try: 903 if pred(node): 904 result.append(node) 905 except: 906 pass 907 return result 908 if isinstance(pred, predicate.Predicate): 909 pred = pred.satisfiedByNode 910 if not recursive: 911 cIter = iter(self) 912 result = [] 913 while True: 914 try: 915 child = cIter.next() 916 except StopIteration: 917 break 918 if child is not None and pred(child): 919 result.append(child) 920 return result 921 else: 922 descendants = [] 923 while True: 924 try: 925 descendants = pyatspi.utils.findAllDescendants(self, pred) 926 break 927 except (GLib.GError, TypeError): 928 continue 929 return descendants
930 931 # The canonical "search above this node" method:
932 - def findAncestor(self, pred):
933 """ 934 Search up the ancestry of this node, returning the first Node 935 satisfying the predicate, or None. 936 """ 937 assert isinstance(pred, predicate.Predicate) 938 candidate = self.parent 939 while candidate is not None: 940 if candidate.satisfies(pred): 941 return candidate 942 else: 943 candidate = candidate.parent 944 # Not found: 945 return None
946 947 # Various wrapper/helper search methods:
948 - def child(self, name='', roleName='', description='', label='', recursive=True, retry=True, debugName=None):
949 """ 950 Finds a child satisying the given criteria. 951 952 This is implemented using findChild, and hence will automatically retry 953 if no such child is found, and will eventually raise an exception. It 954 also logs the search. 955 """ 956 return self.findChild(predicate.GenericPredicate(name=name, roleName=roleName, description=description, label=label), recursive=recursive, retry=retry, debugName=debugName)
957
958 - def isChild(self, name='', roleName='', description='', label='', recursive=True, retry=False, debugName=None):
959 """ 960 Determines whether a child satisying the given criteria exists. 961 962 This is implemented using findChild, but will not automatically retry 963 if no such child is found. To make the function retry multiple times set retry to True. 964 Returns a boolean value depending on whether the child was eventually found. Similar to 965 'child', yet it catches SearchError exception to provide for False results, will raise 966 any other exceptions. It also logs the search. 967 """ 968 found = True 969 try: 970 self.findChild( 971 predicate.GenericPredicate( 972 name=name, roleName=roleName, description=description, label=label), 973 recursive=recursive, retry=retry, debugName=debugName) 974 except SearchError: 975 found = False 976 return found
977
978 - def menu(self, menuName, recursive=True):
979 """ 980 Search below this node for a menu with the given name. 981 982 This is implemented using findChild, and hence will automatically retry 983 if no such child is found, and will eventually raise an exception. It 984 also logs the search. 985 """ 986 return self.findChild(predicate.IsAMenuNamed(menuName=menuName), recursive)
987
988 - def menuItem(self, menuItemName, recursive=True):
989 """ 990 Search below this node for a menu item with the given name. 991 992 This is implemented using findChild, and hence will automatically retry 993 if no such child is found, and will eventually raise an exception. It 994 also logs the search. 995 """ 996 return self.findChild(predicate.IsAMenuItemNamed(menuItemName=menuItemName), recursive)
997
998 - def textentry(self, textEntryName, recursive=True):
999 """ 1000 Search below this node for a text entry with the given name. 1001 1002 This is implemented using findChild, and hence will automatically retry 1003 if no such child is found, and will eventually raise an exception. It 1004 also logs the search. 1005 """ 1006 return self.findChild(predicate.IsATextEntryNamed(textEntryName=textEntryName), recursive)
1007
1008 - def button(self, buttonName, recursive=True):
1009 """ 1010 Search below this node for a button with the given name. 1011 1012 This is implemented using findChild, and hence will automatically retry 1013 if no such child is found, and will eventually raise an exception. It 1014 also logs the search. 1015 """ 1016 return self.findChild(predicate.IsAButtonNamed(buttonName=buttonName), recursive)
1017
1018 - def childLabelled(self, labelText, recursive=True):
1019 """ 1020 Search below this node for a child labelled with the given text. 1021 1022 This is implemented using findChild, and hence will automatically retry 1023 if no such child is found, and will eventually raise an exception. It 1024 also logs the search. 1025 """ 1026 return self.findChild(predicate.IsLabelledAs(labelText), recursive)
1027
1028 - def childNamed(self, childName, recursive=True):
1029 """ 1030 Search below this node for a child with the given name. 1031 1032 This is implemented using findChild, and hence will automatically retry 1033 if no such child is found, and will eventually raise an exception. It 1034 also logs the search. 1035 """ 1036 return self.findChild(predicate.IsNamed(childName), recursive)
1037
1038 - def tab(self, tabName, recursive=True):
1039 """ 1040 Search below this node for a tab with the given name. 1041 1042 This is implemented using findChild, and hence will automatically retry 1043 if no such child is found, and will eventually raise an exception. It 1044 also logs the search. 1045 """ 1046 return self.findChild(predicate.IsATabNamed(tabName=tabName), recursive)
1047
1048 - def getUserVisibleStrings(self):
1049 """ 1050 Get all user-visible strings in this node and its descendents. 1051 1052 (Could be implemented as an attribute) 1053 """ 1054 result = [] 1055 if self.name: 1056 result.append(self.name) 1057 if self.description: 1058 result.append(self.description) 1059 try: 1060 children = self.children 1061 except Exception: 1062 return result 1063 for child in children: 1064 result.extend(child.getUserVisibleStrings()) 1065 return result
1066 1077
1078 1079 -class LinkAnchor(object):
1080 1081 """ 1082 Class storing info about an anchor within an Accessibility.Hyperlink, which 1083 is in turn stored within an Accessibility.Hypertext. 1084 """ 1085
1086 - def __init__(self, node, hypertext, linkIndex, anchorIndex):
1087 self.node = node 1088 self.hypertext = hypertext 1089 self.linkIndex = linkIndex 1090 self.anchorIndex = anchorIndex
1091 1092 @property 1095 1096 @property
1097 - def URI(self):
1098 return self.link.getURI(self.anchorIndex)
1099
1100 1101 -class Root (Node):
1102 1103 """ 1104 FIXME: 1105 """ 1106
1107 - def applications(self):
1108 """ 1109 Get all applications. 1110 """ 1111 return root.findChildren(predicate.GenericPredicate( 1112 roleName="application"), recursive=False)
1113
1114 - def application(self, appName, retry=True):
1115 """ 1116 Gets an application by name, returning an Application instance 1117 or raising an exception. 1118 1119 This is implemented using findChild, and hence will automatically retry 1120 if no such child is found, and will eventually raise an exception. It 1121 also logs the search. 1122 """ 1123 return root.findChild(predicate.IsAnApplicationNamed(appName), recursive=False, retry=retry)
1124
1125 1126 -class Application (Node):
1127
1128 - def dialog(self, dialogName, recursive=False):
1129 """ 1130 Search below this node for a dialog with the given name, 1131 returning a Window instance. 1132 1133 This is implemented using findChild, and hence will automatically retry 1134 if no such child is found, and will eventually raise an exception. It 1135 also logs the search. 1136 1137 FIXME: should this method activate the dialog? 1138 """ 1139 return self.findChild(predicate.IsADialogNamed(dialogName=dialogName), recursive)
1140
1141 - def window(self, windowName, recursive=False):
1142 """ 1143 Search below this node for a window with the given name, 1144 returning a Window instance. 1145 1146 This is implemented using findChild, and hence will automatically retry 1147 if no such child is found, and will eventually raise an exception. It 1148 also logs the search. 1149 1150 FIXME: this bit isn't true: 1151 The window will be automatically activated (raised and focused 1152 by the window manager) if wnck bindings are available. 1153 """ 1154 result = self.findChild( 1155 predicate.IsAWindowNamed(windowName=windowName), recursive) 1156 # FIXME: activate the WnckWindow ? 1157 # if gotWnck: 1158 # result.activate() 1159 return result
1160
1161 - def getWnckApplication(self): # pragma: no cover
1162 """ 1163 Get the wnck.Application instance for this application, or None 1164 1165 Currently implemented via a hack: requires the app to have a 1166 window, and looks up the application of that window 1167 1168 wnck.Application can give you the pid, the icon, etc 1169 1170 FIXME: untested 1171 """ 1172 window = self.child(roleName='frame') 1173 if window: 1174 wnckWindow = window.getWnckWindow() 1175 return wnckWindow.get_application()
1176
1177 1178 -class Window (Node):
1179
1180 - def getWnckWindow(self): # pragma: no cover
1181 """ 1182 Get the wnck.Window instance for this window, or None 1183 """ 1184 # FIXME: this probably needs rewriting: 1185 screen = Wnck.screen_get_default() 1186 1187 # You have to force an update before any of the wnck methods 1188 # do anything: 1189 screen.force_update() 1190 1191 for wnckWindow in screen.get_windows(): 1192 # FIXME: a dubious hack: search by window title: 1193 if wnckWindow.get_name() == self.name: 1194 return wnckWindow
1195
1196 - def activate(self): # pragma: no cover
1197 """ 1198 Activates the wnck.Window associated with this Window. 1199 1200 FIXME: doesn't yet work 1201 """ 1202 wnckWindow = self.getWnckWindow() 1203 # Activate it with a timestamp of 0; this may confuse 1204 # alt-tabbing through windows etc: 1205 # FIXME: is there a better way of getting a timestamp? 1206 # gdk_x11_get_server_time (), with a dummy window 1207 wnckWindow.activate(0) 1208
1209 1210 -class Wizard (Window):
1211 1212 """ 1213 Note that the buttons of a GnomeDruid were not accessible until 1214 recent versions of libgnomeui. This is 1215 http://bugzilla.gnome.org/show_bug.cgi?id=157936 1216 and is fixed in gnome-2.10 and gnome-2.12 (in CVS libgnomeui); 1217 there's a patch attached to that bug. 1218 1219 This bug is known to affect FC3; fixed in FC5 1220 """ 1221
1222 - def __init__(self, node, debugName=None):
1223 Node.__init__(self, node) 1224 if debugName: 1225 self.debugName = debugName 1226 logger.log("%s is on '%s' page" % (self, self.getPageTitle()))
1227
1228 - def currentPage(self):
1229 """ 1230 Get the current page of this wizard 1231 1232 FIXME: this is currently a hack, supporting only GnomeDruid 1233 """ 1234 pageHolder = self.child(roleName='panel') 1235 for child in pageHolder.children: 1236 # current child has SHOWING state set, we hope: 1237 # print child 1238 # print child.showing 1239 if child.showing: 1240 return child 1241 raise "Unable to determine current page of %s" % self
1242
1243 - def getPageTitle(self):
1244 """ 1245 Get the string title of the current page of this wizard 1246 1247 FIXME: this is currently a total hack, supporting only GnomeDruid 1248 """ 1249 currentPage = self.currentPage() 1250 return currentPage.child(roleName='panel').child(roleName='panel').child(roleName='label', recursive=False).text
1251
1252 - def clickForward(self):
1253 """ 1254 Click on the 'Forward' button to advance to next page of wizard. 1255 1256 It will log the title of the new page that is reached. 1257 1258 FIXME: what if it's Next rather than Forward ??? 1259 1260 This will only work if your libgnomeui has accessible buttons; 1261 see above. 1262 """ 1263 fwd = self.child("Forward") 1264 fwd.click() 1265 1266 # Log the new wizard page; it's helpful when debugging scripts 1267 logger.log("%s is now on '%s' page" % (self, self.getPageTitle()))
1268 # FIXME disabled for now (can't get valid page titles) 1269
1270 - def clickApply(self):
1271 """ 1272 Click on the 'Apply' button to advance to next page of wizard. 1273 FIXME: what if it's Finish rather than Apply ??? 1274 1275 This will only work if your libgnomeui has accessible buttons; 1276 see above. 1277 """ 1278 fwd = self.child("Apply") 1279 fwd.click()
1280 1281 # FIXME: debug logging? 1282 1283 Accessibility.Accessible.__bases__ = ( 1284 Application, Root, Node,) + Accessibility.Accessible.__bases__ 1285 1286 try: 1287 root = pyatspi.Registry.getDesktop(0) 1288 root.debugName = 'root' 1289 except Exception: # pragma: no cover 1290 # Warn if AT-SPI's desktop object doesn't show up. 1291 logger.log( 1292 "Error: AT-SPI's desktop is not visible. Do you have accessibility enabled?") 1293 1294 # Check that there are applications running. Warn if none are. 1295 children = root.children 1296 if not children: # pragma: no cover 1297 logger.log( 1298 "Warning: AT-SPI's desktop is visible but it has no children. Are you running any AT-SPI-aware applications?") 1299 del children 1300 1301 import os 1302 # sniff also imports from tree and we don't want to run this code from 1303 # sniff itself 1304 if not os.path.exists('/tmp/sniff_running.lock'): 1305 if not os.path.exists('/tmp/sniff_refresh.lock'): # may have already been locked by dogtail.procedural 1306 # tell sniff not to use auto-refresh while script using this module is 1307 # running 1308 sniff_lock = Lock(lockname='sniff_refresh.lock', randomize=False) 1309 try: 1310 sniff_lock.lock() 1311 except OSError: # pragma: no cover 1312 pass # lock was already present from other script instance or leftover from killed instance 1313 # lock should unlock automatically on script exit. 1314 1315 # Convenient place to set some debug variables: 1316 #config.debugSearching = True 1317 #config.absoluteNodePaths = True 1318 #config.logDebugToFile = False 1319