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:
83 raise ImportError("Error importing the AT-SPI bindings")
84
85
86 try:
87 from gi.repository import Wnck
88 gotWnck = True
89 except ImportError:
90
91
92
93 gotWnck = False
94
95 from gi.repository import GLib
96
97 haveWarnedAboutChildrenLimit = False
102
105
106 """
107 The widget is not sensitive.
108 """
109 message = "Cannot %s %s. It is not sensitive."
110
113
116
119
120 """
121 The widget does not support the requested action.
122 """
123 message = "Cannot do '%s' action on %s"
124
126 self.actionName = actionName
127 self.node = node
128
131
134
135 """
136 Class representing an action that can be performed on a specific node
137 """
138
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
157 return self.__action.getName(self.__index)
158
159 @property
161 return self.__action.getDescription(self.__index)
162
163 @property
165 return self.__action.getKeyBinding(self.__index)
166
170
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
199 try:
200 len(self.user_data)
201 except (AttributeError, TypeError):
202 self.user_data = {}
203
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
219
220
221 @property
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
282
283 roleName = property(Accessibility.Accessible.getRoleName)
284
285 role = property(Accessibility.Accessible.getRole)
286
287 indexInParent = property(Accessibility.Accessible.getIndexInParent)
288
289
290
291
292
293
294
304
305 @property
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
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
339
340
341 @property
343 try:
344 return self.user_data['linkAnchor'].URI
345 except (KeyError, AttributeError):
346 raise NotImplementedError
347
348
349
350
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
372
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
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
400
401
402 @property
404 """A tuple containing the position of the Accessible: (x, y)"""
405 return self.queryComponent().getPosition(pyatspi.DESKTOP_COORDS)
406
407 @property
409 """A tuple containing the size of the Accessible: (w, h)"""
410 return self.queryComponent().getSize()
411
412 @property
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
423 try:
424 return self.queryComponent().contains(x, y, pyatspi.DESKTOP_COORDS)
425 except NotImplementedError:
426 return False
427
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
446 "Attempts to set the keyboard focus to this Accessible."
447 return self.queryComponent().grabFocus()
448
449
450
451
452
453
454
455
456
457
458
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
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
502
503 @property
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
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
538
539 @property
541 """Is the Accessible sensitive (i.e. not greyed out)?"""
542 return self.getState().contains(pyatspi.STATE_SENSITIVE)
543
544 @property
546 return self.getState().contains(pyatspi.STATE_SHOWING)
547
548 @property
550 """Is the Accessible capable of having keyboard focus?"""
551 return self.getState().contains(pyatspi.STATE_FOCUSABLE)
552
553 @property
555 """Does the Accessible have keyboard focus?"""
556 return self.getState().contains(pyatspi.STATE_FOCUSED)
557
558 @property
560 """Is the Accessible a checked checkbox?"""
561 return self.getState().contains(pyatspi.STATE_CHECKED)
562
563 @property
565 """Is the Accessible a checked checkbox? Compatibility property, same as Node.checked."""
566 return self.checked
567
568
569
570
571
573 """Selects all children."""
574 result = self.querySelection().selectAll()
575 doDelay()
576 return result
577
579 """Deselects all selected children."""
580 result = self.querySelection().clearSelection()
581 doDelay()
582 return result
583
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
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
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
615 """Is the Accessible selected?"""
616 return self.isSelected
617
618 @property
620 """Returns a list of children that are selected."""
621
622 selection = self.querySelection()
623 selectedChildren = []
624 for i in xrange(selection.nSelectedChildren):
625 selectedChildren.append(selection.getSelectedChild(i))
626
627
628
629
630
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
648 """The minimum value of self.value"""
649 try:
650 return self.queryValue().minimumValue
651 except NotImplementedError:
652 pass
653
654 @property
656 """The minimum value increment of self.value"""
657 try:
658 return self.queryValue().minimumIncrement
659 except NotImplementedError:
660 pass
661
662 @property
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
704
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
722
723 - def dump(self, type='plain', fileName=None):
724 import dump
725 dumper = getattr(dump, type)
726 dumper(self, fileName)
727
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
765 return path.SearchPath()
766
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
785
786 while not self.__nodeIsIdentifiable(ancestor):
787 ancestor = ancestor.parent
788 isRecursive = True
789
790
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
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
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
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
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
945 return None
946
947
948 - def child(self, name='', roleName='', description='', label='', recursive=True, retry=True, debugName=None):
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
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
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
1017
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
1066
1068 """
1069 Blink, baby!
1070 """
1071 if not self.extents:
1072 return False
1073 else:
1074 (x, y, w, h) = self.extents
1075 Blinker(x, y, w, h)
1076 return True
1077
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
1094 return self.hypertext.getLink(self.linkIndex)
1095
1096 @property
1098 return self.link.getURI(self.anchorIndex)
1099
1100
1101 -class Root (Node):
1102
1103 """
1104 FIXME:
1105 """
1106
1113
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
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
1157
1158
1159 return result
1160
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
1179
1181 """
1182 Get the wnck.Window instance for this window, or None
1183 """
1184
1185 screen = Wnck.screen_get_default()
1186
1187
1188
1189 screen.force_update()
1190
1191 for wnckWindow in screen.get_windows():
1192
1193 if wnckWindow.get_name() == self.name:
1194 return wnckWindow
1195
1197 """
1198 Activates the wnck.Window associated with this Window.
1199
1200 FIXME: doesn't yet work
1201 """
1202 wnckWindow = self.getWnckWindow()
1203
1204
1205
1206
1207 wnckWindow.activate(0)
1208
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):
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
1237
1238
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
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
1267 logger.log("%s is now on '%s' page" % (self, self.getPageTitle()))
1268
1269
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
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:
1290
1291 logger.log(
1292 "Error: AT-SPI's desktop is not visible. Do you have accessibility enabled?")
1293
1294
1295 children = root.children
1296 if not children:
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
1303
1304 if not os.path.exists('/tmp/sniff_running.lock'):
1305 if not os.path.exists('/tmp/sniff_refresh.lock'):
1306
1307
1308 sniff_lock = Lock(lockname='sniff_refresh.lock', randomize=False)
1309 try:
1310 sniff_lock.lock()
1311 except OSError:
1312 pass
1313
1314
1315
1316
1317
1318
1319