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
88 try:
89 import wnck
90 gotWnck = True
91 except ImportError:
92
93
94 gotWnck = False
95
96 haveWarnedAboutChildrenLimit = False
97
100
102 """
103 The widget is not sensitive.
104 """
105 message = "Cannot %s %s. It is not sensitive."
108
111
113 """
114 The widget does not support the requested action.
115 """
116 message = "Cannot do '%s' action on %s"
118 self.actionName = actionName
119 self.node = node
120
123
125 """
126 Class representing an action that can be performed on a specific node
127 """
128
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
157
173
174
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
184 try: len(self.user_data)
185 except (AttributeError, TypeError): self.user_data = {}
186
188 self.__setupUserData()
189 return self.user_data.get('debugName', None)
190
194
195 debugName = property(_getDebugName, _setDebugName, doc = \
196 "debug name assigned during search operations")
197
198
199
200
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
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
260
261
271
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
296 combovalue = property(_getComboValue, _setComboValue, doc = \
297 """The value (as a string) currently selected in the combo box.""")
298
299
300
301
302
304 try: return self.user_data['linkAnchor'].URI
305 except (KeyError, AttributeError): raise NotImplementedError
306 URI = property(_getURI)
307
308
309
310
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
321
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
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
347
348
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
369 try: return self.queryComponent().contains(x, y, pyatspi.DESKTOP_COORDS)
370 except NotImplementedError: return False
371
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
385 "Attempts to set the keyboard focus to this Accessible."
386 return self.queryComponent().grabFocus()
387
388 - def blink(self, count=2):
389 """
390 Blink, baby!
391 """
392 if not self.extents: return False
393 else:
394 (x, y, w, h) = self.extents
395 from utils import Blinker
396 blinkData = Blinker(x, y, w, h, count)
397 return True
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
421
422
423
424
425
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
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
463
465 sensitive = property(_isSensitive, doc = \
466 "Is the Accessible sensitive (i.e. not greyed out)?")
467
469 showing = property(_isShowing)
470
472 focusable = property(_isFocusable, doc = \
473 "Is the Accessible capable of having keyboard focus?")
474
476 focused = property(_isFocused, doc = \
477 "Does the Accessible have keyboard focus?")
478
480 checked = property(_isChecked, doc = \
481 "Is the Accessible a checked checkbox?")
482
483
484
485
486
488 """
489 Selects all children.
490 """
491 result = self.querySelection().selectAll()
492 doDelay()
493 return result
494
496 """
497 Deselects all selected children.
498 """
499 result = self.querySelection().clearSelection()
500 doDelay()
501 return result
502
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
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
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
535 selectedChildren = property(_getSelectedChildren, doc = \
536 "Returns a list of children that are selected.")
537
538
539
540
541
543 try: return self.queryValue().getCurrentValue()
544 except NotImplementedError: pass
545
547 self.setCurrentValue(value)
548
549 value = property(_getValue, _setValue)
550
552 try: return self.queryValue().getMinimumValue()
553 except NotImplementedError: pass
554 minValue = property(_getMinValue)
555
557 try: return self.queryValue().getMinimumIncrement
558 except NotImplementedError: pass
559 minValueIncrement = property(_getMinValueIncrement)
560
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
593
594
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
612
613 - def dump (self, type = 'plain', fileName = None):
614 import dump
615 dumper = getattr (dump, type)
616 dumper (self, fileName)
617
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
655 return path.SearchPath()
656
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
675
676 while not self.__nodeIsIdentifiable(ancestor):
677 ancestor = ancestor.parent
678 isRecursive = True
679
680
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
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
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
796
797
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
811 return None
812
813
814
815 - def child (self, name = '', roleName = '', description= '', label = '', recursive=True, debugName=None):
824
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
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
864
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
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
912
913 - def blink(self, count = 2):
914 """
915 Blink, baby!
916 """
917 if not self.extents:
918 return False
919 else:
920 (x, y, w, h) = self.extents
921 blinkData = Blinker(x, y, w, h, count)
922 return True
923
924
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
938 return self.hypertext.getLink(self.linkIndex)
939 link = property(_getLink)
940
942 return self.link.getURI(self.anchorIndex)
943 URI = property(_getURI)
944
945
947 """
948 FIXME:
949 """
956
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
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
997
998
999 return result
1000
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
1021 """
1022 Get the wnck.Window instance for this window, or None
1023 """
1024
1025 screen = wnck.screen_get_default()
1026
1027
1028
1029 screen.force_update()
1030
1031 for wnckWindow in screen.get_windows():
1032
1033 if wnckWindow.get_name()==self.name:
1034 return wnckWindow
1035
1037 """
1038 Activates the wnck.Window associated with this Window.
1039
1040 FIXME: doesn't yet work
1041 """
1042 wnckWindow = self.getWnckWindow()
1043
1044
1045
1046
1047 wnckWindow.activate(0)
1048
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):
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
1074
1075
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
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
1104 logger.log("%s is now on '%s' page"%(self, self.getPageTitle()))
1105
1106
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
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
1129 logger.log("Error: AT-SPI's desktop is not visible. Do you have accessibility enabled?")
1130
1131
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
1138
1139
1140
1141