68
70
from config import config
69
71
from utils import doDelay
70
72
from utils import Blinker
73
76
from logging import debugLogger as logger
77
80
except ImportError:
78
# If atspi can't be imported, fail.
79
raise ImportError, "Error importing the AT-SPI bindings"
81
# If atspi can't be imported, fail.
82
raise ImportError, "Error importing the AT-SPI bindings"
81
84
# We optionally import the bindings for libwnck.
85
88
except ImportError:
86
# Skip this warning, since the functionality is almost entirely nonworking anyway.
87
#print "Warning: Dogtail could not import the Python bindings for libwnck. Window-manager manipulation will not be available."
90
SearchError = "Couldn't find"
89
# Skip this warning, since the functionality is almost entirely nonworking anyway.
90
#print "Warning: Dogtail could not import the Python bindings for libwnck. Window-manager manipulation will not be available."
93
class SearchError(Exception):
96
class ReadOnlyError(TypeError):
98
This attribute is not writeable.
100
message = "Cannot set %s. It is read-only."
101
def __init__(self, attr):
105
return self.message % self.attr
92
107
class NotSensitiveError(Exception):
94
The widget is not sensitive.
96
message = "Cannot %s %s. It is not sensitive."
97
def __init__(self, action):
101
return self.message % (self.action.name, self.action.node.getLogString())
109
The widget is not sensitive.
111
message = "Cannot %s %s. It is not sensitive."
112
def __init__(self, action):
116
return self.message % (self.action.name, self.action.node.getLogString())
103
118
class ActionNotSupported(Exception):
105
The widget does not support the requested action.
107
message = "Cannot do '%s' action on %s"
108
def __init__(self, actionName, node):
109
self.actionName = actionName
113
return self.message % (self.actionName, self.node.getLogString())
120
The widget does not support the requested action.
122
message = "Cannot do '%s' action on %s"
123
def __init__(self, actionName, node):
124
self.actionName = actionName
128
return self.message % (self.actionName, self.node.getLogString())
117
Class representing an action that can be performed on a specific node
119
# Valid types of actions we know about. Feel free to add any you see.
131
def __init__ (self, node, action, index):
133
self.__action = action
136
def __getattr__ (self, attr):
138
return self.__action.getName (self.__index).lower()
139
elif attr == "description":
140
return self.__action.getDescription (self.__index)
141
elif attr == "keyBinding":
142
return self.__action.getKeyBinding (self.__index)
143
else: raise AttributeError, attr
147
Plain-text representation of the Action.
149
string = "Action node='%s' name='%s' description='%s' keybinding='%s'" % (self.node, self.name, self.description, self.keyBinding)
154
Performs the given tree.Action, with appropriate delays and logging.
156
assert isinstance(self, Action)
157
logger.log("%s on %s"%(self.name, self.node.getLogString()))
158
if not self.node.sensitive:
159
if config.ensureSensitivity:
160
raise NotSensitiveError, self
162
nSE = NotSensitiveError(self)
163
print "Warning: " + str(nSE)
164
if config.blinkOnActions: self.node.blink()
165
result = self.__action.doAction (self.__index)
132
Class representing an action that can be performed on a specific node
134
# Valid types of actions we know about. Feel free to add any you see.
146
def __init__ (self, node, action, index):
148
self.__action = action
151
def __getattr__ (self, attr):
153
return self.__action.getName (self.__index).lower()
154
elif attr == "description":
155
return self.__action.getDescription (self.__index)
156
elif attr == "keyBinding":
157
return self.__action.getKeyBinding (self.__index)
158
else: raise AttributeError, attr
162
Plain-text representation of the Action.
164
string = "Action node='%s' name='%s' description='%s' keybinding='%s'" % (self.node, self.name, self.description, self.keyBinding)
169
Performs the given tree.Action, with appropriate delays and logging.
171
assert isinstance(self, Action)
172
logger.log("%s on %s"%(self.name, self.node.getLogString()))
173
if not self.node.sensitive:
174
if config.ensureSensitivity:
175
raise NotSensitiveError, self
177
nSE = NotSensitiveError(self)
178
logger.log("Warning: " + str(nSE))
179
if config.blinkOnActions: self.node.blink()
180
result = self.__action.doAction (self.__index)
171
A node in the tree of UI elements. It wraps an Accessible and
172
exposes its useful members. It also has a debugName which is set
173
up automatically when doing searches.
175
Node instances have various attributes synthesized, to make it easy to
176
get and the underlying accessible data. Many more attributes can be
179
'name' (read-only string):
180
Wraps Accessible_getName on the Node's underlying Accessible
182
'roleName' (read-only string):
183
Wraps Accessible_getRoleName on the Node's underlying Accessible
185
'role' (read-only atspi role enum):
186
Wraps Accessible_getRole on the Node's underlying Accessible
188
'description' (read-only string):
189
Wraps Accessible_getDescription on the Node's underlying Accessible
191
'parent' (read-only Node instance):
192
A Node instance wrapping the parent, or None. Wraps Accessible_getParent
194
'children' (read-only list of Node instances):
195
The children of this node, wrapping getChildCount and getChildAtIndex
198
For instances wrapping AccessibleText, the text. This is read-only,
199
unless the instance wraps an AccessibleEditableText. In this case, you
200
can write values to the attribute. This will get logged in the debug
201
log, and a delay will be added. After the delay, the content of the
202
node will be checked to ensure that it has the expected value. If it
203
does not, an exception will be raised. This does not work for password
204
dialogs (since all we get back are * characters). In this case, set
205
the passwordText attribute instead.
207
'passwordText' (write-only string):
208
See documentation of 'text' attribute above.
210
'caretOffset' (read/write int):
211
For instances wrapping AccessibleText, the location of the text caret,
212
expressed as an offset in characters.
214
'combovalue' (write-only string):
215
For comboboxes. Write to this attribute to set the combobox to the
216
given value, with appropriate delays and logging.
218
'stateSet' (read-only StateSet instance):
219
Wraps Accessible_getStateSet; a set of boolean state flags
221
'relations' (read-only list of atspi.Relation instances):
222
Wraps Accessible_getRelationSet
224
'labellee' (read-only list of Node instances):
225
The node(s) that this node is a label for. Generated from 'relations'.
227
'labeller' (read-only list of Node instances):
228
The node(s) that is/are a label for this node. Generated from
231
'sensitive' (read-only boolean):
232
Is this node sensitive (i.e. not greyed out). Generated from stateSet
233
based on presence of atspi.SPI_STATE_SENSITIVE
234
Not all applications/toolkits seem to properly set this up.
236
'showing' (read-only boolean):
237
Generated from stateSet based on presence of atspi.SPI_STATE_SHOWING
239
'actions' (read-only list of Action instances):
240
Generated from Accessible_getAction and AccessibleAction_getNActions
242
For each action that is supported by a node, a method is hooked up,
243
this can include the following list:
255
'extents' (readonly tuple):
256
For instances wrapping a Component, the (x,y,w,h) screen extents of the
259
'position' (readonly tuple):
260
For instances wrapping a Component, the (x,y) screen position of the
263
'size' (readonly tuple):
264
For instances wrapping a Component, the (w,h) screen size of the component.
267
For instances wrapping a Component, attempt to set the keyboard input focus
270
'toolkit' (readonly string):
271
For instances wrapping an application, the name of the toolkit.
274
For instances wrapping an application.
277
For instances wrapping an application.
281
For instances wrapping an application; probably don't work
284
#Valid types of AT-SPI objects we wrap.
285
contained = ('__accessible', '__action', '__component', '__text', '__editableText')
287
def __init__ (self, initializer):
288
self.__hideChildren = False
289
self.debugName = None
290
if isinstance(initializer, atspi.Accessible):
291
self.__accessible = initializer
292
elif isinstance(initializer, Node):
293
self.__accessible = initializer.__accessible
294
self.debugName = getattr(initializer, 'debugName', None)
296
raise "Unknown Node initializer"
297
assert self.__accessible
299
# Swallow the Action object, if it exists
300
self.__action = self.__accessible.getAction()
301
if self.__action is not None:
304
Performs the tree.Action with the given name, with appropriate delays and logging,
305
or raises the ActionNotSupported exception if the node doesn't support that particular
308
actions = self.actions
309
if actions.has_key(name):
310
return actions[name].do()
312
raise ActionNotSupported(name, self)
313
self.doAction = doAction
315
# Swallow the Component object, if it exists
316
component = self.__accessible.getComponent()
317
if component is not None:
318
self.__component = component
321
self.__component.grabFocus()
322
self.grabFocus = grabFocus
324
def rawClick(button = 1):
326
Generates a raw mouse click event whether or not the Node has a 'click' action, using the specified button.
332
extents = self.extents
333
position = (extents[0], extents[1])
334
size = (extents[2], extents[3])
335
clickX = position[0] + 0.5 * size[0]
336
clickY = position[1] + 0.5 * size[1]
337
if config.debugSearching: logger.log("raw click on %s %s at (%s,%s)"%(self.name, self.getLogString(), str(clickX), str(clickY)))
338
rawinput.click(clickX, clickY, button)
339
self.rawClick = rawClick
343
Generates raw keyboard events to type text into the Node.
346
if config.debugSearching: logger.log("Typing text '%s' into %s"%(text, self.getLogString()))
348
rawinput.typeText(text)
349
self.rawType = rawType
351
# Swallow the Text object, if it exists
352
text = self.__accessible.getText()
355
self.addSelection = self.__text.addSelection
356
self.getNSelections = self.__text.getNSelections
357
self.removeSelection = self.__text.removeSelection
358
self.setSelection = self.__text.setSelection
360
# Swallow the EditableText object, if it exists
361
editableText = self.__accessible.getEditableText()
362
if editableText is not None:
363
self.__editableText = editableText
364
self.setAttributes = self.__editableText.setAttributes
366
# Swallow the Hypertext object, if it exists
367
hypertext = self.__accessible.getHypertext()
368
if hypertext is not None:
369
self.__hypertext = hypertext
371
# Add more objects here. Nobody uses them yet, so I haven't.
372
# You also need to change the __getattr__ and __setattr__ functions.
374
# It'd be nice to know if two objects are "identical". However, the
375
# approach below does not work, since atspi.Accessible doesn't know
376
# how to check if two cspi.Accessible objects are "identical" either. :(
377
#def __cmp__ (self, node):
378
# return self.__accessible == node.__accessible
380
def __getattr__ (self, attr):
383
# Attributes from Applications
384
# (self.__accessible will be an Application and not an Accessible)
385
elif attr == "toolkit" and type(self.__accessible) == atspi.Application:
386
return self.__accessible.getToolkit()
387
elif attr == "version" and type(self.__accessible) == atspi.Application:
388
return self.__accessible.getVersion()
389
elif attr == "ID" and type(self.__accessible) == atspi.Application:
390
return self.__accessible.getID()
391
# These two appear to be useless, so they're lazily bound. :)
392
elif attr == "pause" and type(self.__accessible) == atspi.Application:
393
return self.__accessible.pause
394
elif attr == "resume" and type(self.__accessible) == atspi.Application:
395
return self.__accessible.resume
397
# Attributes from the Accessible object
399
return self.__accessible.getName()
401
return self.__accessible.getRole()
402
elif attr == "roleName":
403
return self.__accessible.getRoleName()
404
elif attr == "description":
405
return self.__accessible.getDescription()
406
elif attr == "parent":
408
parentAcc = self.__accessible.getParent ()
410
parent = Node (parentAcc)
414
except AttributeError:
415
# Wrap the AttributeError to be more informative.
416
raise AttributeError, attr
417
elif attr == "children":
418
if self.__hideChildren: raise AttributeError, attr
420
for i in xrange (self.__accessible.getChildCount ()):
421
children.append (Node (self.__accessible.getChildAtIndex (i)))
422
# Attributes from the Hypertext object
424
for i in range(self.__hypertext.getNLinks()):
425
children.append(Link(self, self.__hypertext.getLink(i), i))
426
except AttributeError: pass
427
if children: return children
429
raise AttributeError, attr
430
elif attr == "stateSet":
431
return self.__accessible.getStateSet()
432
elif attr == "relations":
433
return self.__accessible.getRelationSet()
434
elif attr == "labellee":
435
# Find the nodes that this node is a label for:
436
# print self.relations
437
for relation in self.relations:
438
if relation.getRelationType() == atspi.SPI_RELATION_LABEL_FOR:
439
targets = relation.getTargets ()
440
return apply(Node, targets)
441
elif attr == "labeller":
442
# Find the nodes that are a label for this node:
443
# print self.relations
444
for relation in self.relations:
445
if relation.getRelationType() == atspi.SPI_RELATION_LABELED_BY:
446
targets = relation.getTargets ()
447
return apply(Node, targets)
450
# Attributes synthesized from the Accessible's stateSet:
451
elif attr == "sensitive":
452
return self.__accessible.getStateSet().contains(atspi.SPI_STATE_SENSITIVE)
453
elif attr == "showing":
454
return self.__accessible.getStateSet().contains(atspi.SPI_STATE_SHOWING)
456
# Attributes from the Action object
457
elif attr == "actions":
459
for i in xrange (self.__action.getNActions ()):
460
action = (Action (self, self.__action, i))
461
actions[action.name] = action
465
raise AttributeError, attr
467
# Attributes from the Component object
468
elif attr == "extents":
469
return self.__component.getExtents ()
470
elif attr == "position":
471
return self.__component.getPosition ()
473
return self.__component.getSize ()
475
# Attributes from the Text object
477
return self.__text.getText (0, 32767)
478
elif attr == "caretOffset":
479
return self.__text.getCaretOffset ()
481
# Attributes from the Component object
482
elif attr == "extents":
483
return self.__component.getExtents ()
485
else: raise AttributeError, attr
487
def __setattr__ (self, attr, value):
490
# Are we swallowing an AT-SPI object?
491
elif attr.replace('_Node', '') in self.contained:
492
self.__dict__[attr] = value
494
# Attributes from the Text object
495
elif attr=="caretOffset":
496
self.__text.setCaretOffset(value)
498
# Attributes from the EditableText object
501
Set the text of the node to the given value, with
502
appropriate delays and logging, then test the result:
504
if config.debugSearching: logger.log("Setting text of %s to '%s'"%(self.getLogString(), value))
505
self.__editableText.setTextContents (value)
508
#resultText = self.text
509
#if resultText != value:
510
# raise "%s failed to have its text set to '%s'; value is '%s'"%(self.getLogString(), value, resultText)
512
elif attr=='passwordText':
514
Set the text of the node to the given value, with
515
appropriate delays and logging. We can't test the
516
result, we'd only get * characters back.
518
logger.log("Setting text %s to password '%s'"%(self.getLogString(), value))
519
self.__editableText.setTextContents (value)
522
elif attr=="combovalue":
524
Set the combobox to the given value, with appropriate delays and
527
logger.log("Setting combobox %s to '%s'"%(self.getLogString(), value))
528
self.childNamed(childName=value).click()
531
# FIXME: should we doing stuff like in the clause above???
532
self.__dict__[attr] = value
534
def typeText(self, string):
536
Type the given text into the node, with appropriate delays and
539
FIXME: Doesn't work well at the moment
541
logger.log("Typing text into %s: '%s'"%(self.getLogString(), string))
543
# Non-working implementation
544
# Unfortunately, the call to AccessibleText_setCaretOffset fails for Evolution's gtkhtml composer for some reason
546
print "caret offset: %s"%self.caretOffset
547
self.__editableText.insertText (self.caretOffset, text)
548
self.caretOffset+=len(string) # FIXME: is this correct?
549
print "new caret offset: %s"%self.caretOffset
551
# Another non-working implementation
552
# Focus the node and inject and keyboard event:
553
# Unfortunately, this doesn't work well with Evolution either
557
ev = atspi.EventGenerator()
558
ev.injectKeyboardString (string)
560
# This approach partially works:
568
If debugName is set on this Node, returns debugName surrounded
570
Otherwise, returns a plain-text representation of the most
571
important attributes of the underlying Accessible.
574
return "{" + self.debugName + "}"
577
try: string = string + " roleName='%s'" % self.roleName
578
except AttributeError: pass
579
string = string + " name='%s' description='%s'" % (self.name, self.description)
580
try: string = string + " text='%s'" % self.text
581
except AttributeError: pass
584
def getLogString(self):
586
Get a string describing this node for the logs,
587
respecting the config.absoluteNodePaths boolean.
589
if config.absoluteNodePaths:
590
return self.getAbsoluteSearchPath()
594
def satisfies (self, pred):
596
Does this node satisfy the given predicate?
598
# the logic is handled by the predicate:
599
assert isinstance(pred, predicate.Predicate)
600
return pred.satisfiedByNode(self)
602
def dump (self, type = 'plain'):
604
dumper = getattr (dump, type)
607
def getAbsoluteSearchPath(self):
609
FIXME: this needs rewriting...
610
Generate a SearchPath instance giving the 'best'
611
way to find the Accessible wrapped by this node again, starting
612
at the root and applying each search in turn.
614
This is somewhat analagous to an absolute path in a filesystem,
615
except that some of searches may be recursive, rather than just
616
searching direct children.
618
Used by the recording framework for identifying nodes in a
619
persistent way, independent of the style of script being
622
FIXME: try to ensure uniqueness
623
FIXME: need some heuristics to get 'good' searches, whatever
626
if config.debugSearchPaths:
627
print "getAbsoluteSearchPath(%s)"%self
629
if self.roleName=='application':
630
result =path.SearchPath()
631
result.append(predicate.IsAnApplicationNamed(self.name), False)
635
(ancestor, pred, isRecursive) = self.getRelativeSearch()
636
if config.debugSearchPaths:
637
print "got ancestor: %s"%ancestor
639
ancestorPath = ancestor.getAbsoluteSearchPath()
640
ancestorPath.append(pred, isRecursive)
643
# This should be the root node:
644
return path.SearchPath()
646
def getRelativeSearch(self):
648
Get a (ancestorNode, predicate, isRecursive) triple that identifies the
649
best way to find this Node uniquely.
650
FIXME: or None if no such search exists?
651
FIXME: may need to make this more robust
652
FIXME: should this be private?
654
if config.debugSearchPaths:
655
print "getRelativeSearchPath(%s)"%self
661
ancestor = self.parent
663
# iterate up ancestors until you reach an identifiable one,
664
# setting the search to be isRecursive if need be:
665
while not self.__nodeIsIdentifiable(ancestor):
666
ancestor = ancestor.parent
669
# Pick the most appropriate predicate for finding this node:
671
if self.labellee.name:
672
return (ancestor, predicate.IsLabelledAs(self.labellee.name), isRecursive)
674
if self.roleName=='menu':
675
return (ancestor, predicate.IsAMenuNamed(self.name), isRecursive)
676
elif self.roleName=='menu item' or self.roleName=='check menu item':
677
return (ancestor, predicate.IsAMenuItemNamed(self.name), isRecursive)
678
elif self.roleName=='text':
679
return (ancestor, predicate.IsATextEntryNamed(self.name), isRecursive)
680
elif self.roleName=='push button':
681
return (ancestor, predicate.IsAButtonNamed(self.name), isRecursive)
682
elif self.roleName=='frame':
683
return (ancestor, predicate.IsAWindowNamed(self.name), isRecursive)
684
elif self.roleName=='dialog':
685
return (ancestor, predicate.IsADialogNamed(self.name), isRecursive)
687
pred = predicate.GenericPredicate(name=self.name, roleName=self.roleName)
688
return (ancestor, pred, isRecursive)
690
def __nodeIsIdentifiable(self, ancestor):
691
if ancestor.labellee:
695
elif not ancestor.parent:
701
return self.__component.grabFocus ()
703
# The canonical search method:
704
def findChild(self, pred, recursive = True, debugName = None, retry = True, requireResult = True):
706
Search for a node satisyfing the predicate, returning a Node.
708
If retry is True (the default), it makes multiple attempts,
709
backing off and retrying on failure, and eventually raises a
710
descriptive exception if the search fails.
712
If retry is False, it gives up after one attempt.
714
If requireResult is True (the default), an exception is raised after all
715
attempts have failed. If it is false, the function simply returns None.
717
FIXME: make multiple attempts?
720
def findFirstChildSatisfying (parent, pred, recursive = True):
722
Internal helper function that does a one-shot search, recursing if need be.
724
# print "findFirstChildSatisfying(%s, %s, recursive=%s)"%(parent, pred,recursive)
725
assert isinstance(pred, predicate.Predicate)
727
try: children = parent.children
730
for child in children:
732
if child.satisfies(pred):
736
child = findFirstChildSatisfying(child, pred, recursive)
737
if child: return child
738
# ...on to next child
740
def describeSearch (parent, pred, recursive, debugName):
742
Internal helper function
749
if debugName == None:
750
debugName = pred.describeSearchResult()
752
return "%s of %s: %s"%(noun, parent.getLogString(), debugName)
754
assert isinstance(pred, predicate.Predicate)
756
while numAttempts<config.searchCutoffCount:
757
if numAttempts>=config.searchWarningThreshold or config.debugSearching:
758
print "searching for %s (attempt %i)"%(describeSearch(self, pred, recursive, debugName), numAttempts)
760
result = findFirstChildSatisfying(self, pred, recursive)
762
assert isinstance(result, Node)
764
result.debugName = debugName
766
result.debugName = pred.describeSearchResult()
771
if config.debugSearching or config.debugSleep:
772
print "sleeping for %f"%config.searchBackoffDuration
773
sleep(config.searchBackoffDuration)
775
raise SearchError, describeSearch(self, pred, recursive, debugName)
777
# The canonical "search for multiple" method:
778
def findChildren(self, pred, recursive = True):
780
Find all children/descendents satisfying the predicate.
782
assert isinstance(pred, predicate.Predicate)
786
try: children = self.children
789
for child in children:
790
if child.satisfies(pred): selfList.append(child)
792
childList = child.findChildren(pred, recursive)
794
for child in childList:
795
selfList.append(child)
796
# ...on to next child
798
if selfList: return selfList
800
# The canonical "search above this node" method:
801
def findAncestor (self, pred):
803
Search up the ancestry of this node, returning the first Node
804
satisfying the predicate, or None.
806
assert isinstance(pred, predicate.Predicate)
807
candidate = self.parent
808
while candidate != None:
809
if candidate.satisfies(pred):
812
candidate = candidate.parent
817
# Various wrapper/helper search methods:
818
def child (self, name = '', roleName = '', description= '', label = '', recursive=True, debugName=None):
820
Finds a child satisying the given criteria.
822
This is implemented using findChild, and hence will automatically retry
823
if no such child is found, and will eventually raise an exception. It
824
also logs the search.
826
return self.findChild (predicate.GenericPredicate(name = name, roleName = roleName, description= description, label = label), recursive = recursive, debugName=debugName)
828
# FIXME: does this clash with the "menu" action
829
def menu(self, menuName, recursive=True):
831
Search below this node for a menu with the given name.
833
This is implemented using findChild, and hence will automatically retry
834
if no such child is found, and will eventually raise an exception. It
835
also logs the search.
837
return self.findChild (predicate.IsAMenuNamed(menuName=menuName), recursive)
839
def menuItem(self, menuItemName, recursive=True):
841
Search below this node for a menu item with the given name.
843
This is implemented using findChild, and hence will automatically retry
844
if no such child is found, and will eventually raise an exception. It
845
also logs the search.
847
return self.findChild (predicate.IsAMenuItemNamed(menuItemName=menuItemName), recursive)
849
def textentry(self, textEntryName, recursive=True):
851
Search below this node for a text entry with the given name.
853
This is implemented using findChild, and hence will automatically retry
854
if no such child is found, and will eventually raise an exception. It
855
also logs the search.
857
return self.findChild (predicate.IsATextEntryNamed(textEntryName=textEntryName), recursive)
859
def button(self, buttonName, recursive=True):
861
Search below this node for a button with the given name.
863
This is implemented using findChild, and hence will automatically retry
864
if no such child is found, and will eventually raise an exception. It
865
also logs the search.
867
return self.findChild (predicate.IsAButtonNamed(buttonName=buttonName), recursive)
869
def childLabelled(self, labelText, recursive=True):
871
Search below this node for a child labelled with the given text.
873
This is implemented using findChild, and hence will automatically retry
874
if no such child is found, and will eventually raise an exception. It
875
also logs the search.
877
return self.findChild (predicate.IsLabelled(labelText), recursive)
879
def childNamed(self, childName, recursive=True):
881
Search below this node for a child with the given name.
883
This is implemented using findChild, and hence will automatically retry
884
if no such child is found, and will eventually raise an exception. It
885
also logs the search.
887
return self.findChild (predicate.IsNamed(childName), recursive)
889
def tab(self, tabName, recursive=True):
891
Search below this node for a tab with the given name.
893
This is implemented using findChild, and hence will automatically retry
894
if no such child is found, and will eventually raise an exception. It
895
also logs the search.
897
return self.findChild (predicate.IsATabNamed(tabName=tabName), recursive)
899
def getUserVisibleStrings(self):
901
Get all user-visible strings in this node and its descendents.
903
(Could be implemented as an attribute)
907
result.append(self.name)
909
result.append(self.description)
911
children = self.children
912
except: return result
913
for child in children:
914
result.extend(child.getUserVisibleStrings())
917
def blink(self, count = 2):
922
(x, y, w, h) = self.extents
923
blinkData = Blinker(x, y, w, h, count)
925
except AttributeError:
930
If the Node supports an action called "click", do it, with appropriate delays and logging.
931
Otherwise, raise an ActionNotSupported exception.
933
Note that this is just a shortcut to doAction('click'), as it is by far the most-used
934
action. To do any other action such as 'activate' or 'open', you must use doAction().
936
if self.__action is not None:
937
return self.doAction('click')
938
raise ActionNotSupported('click', self)
186
A node in the tree of UI elements. It wraps an Accessible and
187
exposes its useful members. It also has a debugName which is set
188
up automatically when doing searches.
190
Node instances have various attributes synthesized, to make it easy to
191
get and the underlying accessible data. Many more attributes can be
194
'name' (read-only string):
195
Wraps Accessible_getName on the Node's underlying Accessible
197
'roleName' (read-only string):
198
Wraps Accessible_getRoleName on the Node's underlying Accessible
200
'role' (read-only atspi role enum):
201
Wraps Accessible_getRole on the Node's underlying Accessible
203
'description' (read-only string):
204
Wraps Accessible_getDescription on the Node's underlying Accessible
206
'parent' (read-only Node instance):
207
A Node instance wrapping the parent, or None. Wraps Accessible_getParent
209
'children' (read-only list of Node instances):
210
The children of this node, wrapping getChildCount and getChildAtIndex
213
For instances wrapping AccessibleText, the text. This is read-only,
214
unless the instance wraps an AccessibleEditableText. In this case, you
215
can write values to the attribute. This will get logged in the debug
216
log, and a delay will be added. After the delay, the content of the
217
node will be checked to ensure that it has the expected value. If it
218
does not, an exception will be raised. This does not work for password
219
dialogs (since all we get back are * characters). In this case, set
220
the passwordText attribute instead.
222
'passwordText' (write-only string):
223
See documentation of 'text' attribute above.
225
'caretOffset' (read/write int):
226
For instances wrapping AccessibleText, the location of the text caret,
227
expressed as an offset in characters.
229
'combovalue' (write-only string):
230
For comboboxes. Write to this attribute to set the combobox to the
231
given value, with appropriate delays and logging.
233
'stateSet' (read-only StateSet instance):
234
Wraps Accessible_getStateSet; a set of boolean state flags
236
'relations' (read-only list of atspi.Relation instances):
237
Wraps Accessible_getRelationSet
239
'labellee' (read-only list of Node instances):
240
The node(s) that this node is a label for. Generated from 'relations'.
242
'labeller' (read-only list of Node instances):
243
The node(s) that is/are a label for this node. Generated from
246
'checked' (read-only boolean):
247
Is this node checked? Only valid for checkboxes. Generated from stateSet
248
based on presence of atspi.SPI_STATE_CHECKED.
250
'sensitive' (read-only boolean):
251
Is this node sensitive (i.e. not greyed out). Generated from stateSet
252
based on presence of atspi.SPI_STATE_SENSITIVE
253
Not all applications/toolkits seem to properly set this up.
255
'showing' (read-only boolean):
256
Generated from stateSet based on presence of atspi.SPI_STATE_SHOWING
258
'focusable' (read-only boolean):
259
Generated from stateSet based on presence of atspi.SPI_STATE_FOCUSABLE
261
'focused' (read-only boolean):
262
Generated from stateSet based on presence of atspi.SPI_STATE_FOCUSED
264
'actions' (read-only list of Action instances):
265
Generated from Accessible_getAction and AccessibleAction_getNActions
267
For each action that is supported by a node, a method is hooked up,
268
this can include the following list:
280
'extents' (readonly tuple):
281
For instances wrapping a Component, the (x,y,w,h) screen extents of the
284
'position' (readonly tuple):
285
For instances wrapping a Component, the (x,y) screen position of the
288
'size' (readonly tuple):
289
For instances wrapping a Component, the (w,h) screen size of the component.
292
For instances wrapping a Component, attempt to set the keyboard input focus
295
'toolkit' (readonly string):
296
For instances wrapping an application, the name of the toolkit.
299
For instances wrapping an application.
302
For instances wrapping an application.
306
For instances wrapping an application; probably don't work
309
#Valid types of AT-SPI objects we wrap.
310
contained = ('__accessible', '__action', '__component', '__text', '__editableText', '__selection')
312
def __init__ (self, initializer):
313
self.__hideChildren = False
314
self.debugName = None
315
if isinstance(initializer, atspi.Accessible):
316
self.__accessible = initializer
317
elif isinstance(initializer, Node):
318
self.__accessible = initializer.__accessible
319
self.debugName = getattr(initializer, 'debugName', None)
321
raise "Unknown Node initializer"
322
assert self.__accessible
324
# Swallow the Action object, if it exists
325
self.__action = self.__accessible.getAction()
326
if self.__action is not None:
329
Performs the tree.Action with the given name, with appropriate delays and logging,
330
or raises the ActionNotSupported exception if the node doesn't support that particular
333
actions = self.actions
334
if actions.has_key(name):
335
return actions[name].do()
337
raise ActionNotSupported(name, self)
338
self.doAction = doAction
340
# Swallow the Component object, if it exists
341
self.__component = self.__accessible.getComponent()
342
if self.__component is not None:
344
self.__component.grabFocus()
346
self.grabFocus = grabFocus
348
def rawClick(button = 1):
350
Generates a raw mouse click event whether or not the Node has a 'click' action, using the specified button.
355
extents = self.extents
356
position = (extents[0], extents[1])
357
size = (extents[2], extents[3])
358
clickX = position[0] + 0.5 * size[0]
359
clickY = position[1] + 0.5 * size[1]
360
if config.debugSearching:
361
logger.log("raw click on %s %s at (%s,%s)"%(self.name, self.getLogString(), str(clickX), str(clickY)))
362
rawinput.click(clickX, clickY, button)
363
self.rawClick = rawClick
365
# Swallow the Text object, if it exists
366
self.__text = self.__accessible.getText()
367
if self.__text is not None:
368
self.addSelection = self.__text.addSelection
369
self.getNSelections = self.__text.getNSelections
370
self.removeSelection = self.__text.removeSelection
371
self.setSelection = self.__text.setSelection
373
# Swallow the EditableText object, if it exists
374
self.__editableText = self.__accessible.getEditableText()
375
if self.__editableText is not None:
376
self.setAttributes = self.__editableText.setAttributes
378
# Swallow the Hypertext object, if it exists
379
self.__hypertext = self.__accessible.getHypertext()
381
# Swallow the Selection object, if it exists
382
self.__selection = self.__accessible.getSelection()
383
if self.__selection is not None:
386
Selects all children.
388
return self.__selection.selectAll()
389
self.selectAll = selectAll
393
Deselects all selected children.
395
return self.__selection.clearSelection()
396
self.deselectAll = deselectAll
398
# Implement select() for children of nodes with Selection interfaces.
400
if parent and parent._Node__selection:
403
Selects the node, relative to its siblings.
405
return self.parent._Node__selection.selectChild(self.indexInParent)
410
Deselects the node, relative to its siblings.
414
for i in range(self.indexInParent):
415
if parent.children[i].isSelected:
417
return parent._Node__selection.deselectSelectedChild(selectedIndex)
418
self.deselect = select
420
# Add more objects here. Nobody uses them yet, so I haven't.
421
# You also need to change the __getattr__ and __setattr__ functions.
423
# It'd be nice to know if two objects are "identical". However, the
424
# approach below does not work, since atspi.Accessible doesn't know
425
# how to check if two cspi.Accessible objects are "identical" either. :(
426
#def __cmp__ (self, node):
427
# return self.__accessible == node.__accessible
429
def __getattr__ (self, attr):
432
# Attributes from Applications
433
# (self.__accessible will be an Application and not an Accessible)
434
elif attr == "toolkit" and type(self.__accessible) == atspi.Application:
435
return self.__accessible.getToolkit()
436
elif attr == "version" and type(self.__accessible) == atspi.Application:
437
return self.__accessible.getVersion()
438
elif attr == "ID" and type(self.__accessible) == atspi.Application:
439
return self.__accessible.getID()
440
# These two appear to be useless, so they're lazily bound. :)
441
elif attr == "pause" and type(self.__accessible) == atspi.Application:
442
return self.__accessible.pause
443
elif attr == "resume" and type(self.__accessible) == atspi.Application:
444
return self.__accessible.resume
446
# Attributes from the Accessible object
448
return self.__accessible.getName()
450
return self.__accessible.getRole()
451
elif attr == "roleName":
452
return self.__accessible.getRoleName()
453
elif attr == "description":
454
return self.__accessible.getDescription()
455
elif attr == "parent":
456
parentAcc = self.__accessible.getParent ()
458
parent = Node (parentAcc)
460
elif attr =="indexInParent":
461
return self.__accessible.getIndexInParent()
462
elif attr == "children":
463
if self.__hideChildren: return
465
for i in xrange (self.__accessible.getChildCount ()):
466
if isinstance(self, Root):
467
try: a = self.__accessible.getChildAtIndex (i)
468
except atspi.SpiException:
470
logger.log(traceback.format_exc())
471
else: a = self.__accessible.getChildAtIndex (i)
472
# Workaround to GNOME bug #321273
473
# http://bugzilla.gnome.org/show_bug.cgi?id=321273
474
if a is not None: children.append (Node (a))
475
# Attributes from the Hypertext object
477
for i in range(self.__hypertext.getNLinks()):
478
children.append(Link(self, self.__hypertext.getLink(i), i))
480
elif attr == "stateSet":
481
return self.__accessible.getStateSet()
482
elif attr == "relations":
483
return self.__accessible.getRelationSet()
484
elif attr == "labellee":
485
# Find the nodes that this node is a label for:
486
# print self.relations
487
for relation in self.relations:
488
if relation.getRelationType() == atspi.SPI_RELATION_LABEL_FOR:
489
targets = relation.getTargets ()
490
return apply(Node, targets)
491
elif attr == "labeller":
492
# Find the nodes that are a label for this node:
493
# print self.relations
494
for relation in self.relations:
495
if relation.getRelationType() == atspi.SPI_RELATION_LABELED_BY:
496
targets = relation.getTargets ()
497
return apply(Node, targets)
499
# Attributes synthesized from the Accessible's stateSet:
500
elif attr == "sensitive":
501
return self.__accessible.getStateSet().contains(atspi.SPI_STATE_SENSITIVE)
502
elif attr == "showing":
503
return self.__accessible.getStateSet().contains(atspi.SPI_STATE_SHOWING)
504
elif attr == "focusable":
505
return self.__accessible.getStateSet().contains(atspi.SPI_STATE_FOCUSABLE)
506
elif attr == "focused":
507
return self.__accessible.getStateSet().contains(atspi.SPI_STATE_FOCUSED)
508
elif attr == "checked":
509
return self.__accessible.getStateSet().contains(atspi.SPI_STATE_CHECKED)
511
# Attributes from the Action object
512
elif attr == "actions":
515
for i in xrange (self.__action.getNActions ()):
516
action = (Action (self, self.__action, i))
517
actions[action.name] = action
520
# Attributes from the Component object
521
elif attr == "extents":
523
return self.__component.getExtents()
524
elif attr == "position":
526
return self.__component.getPosition()
529
# This always returns [0, 0]
530
#return self.__component.getSize()
531
extents = self.__component.getExtents()
532
size = (extents[2], extents[3])
535
# Attributes from the Text object
538
return self.__text.getText(0, 32767)
539
elif attr == "caretOffset":
541
return self.__text.getCaretOffset()
543
# Attributes from the Selection object
544
elif attr == "isSelected":
546
if parent and parent._Node__selection:
547
return self.parent._Node__selection.isChildSelected(self.indexInParent)
548
elif attr == "selectedChildren":
549
if self.__hideChildren:
551
selectedChildren = []
553
if parent and self.parent._Node__selection:
554
for i in xrange(self.__selection.getNSelectedChildren()):
555
selectedChildren.append(Node(self.__selection.getSelectedChild(i)))
556
return selectedChildren
558
else: raise AttributeError, attr
560
def __setattr__ (self, attr, value):
563
# Are we swallowing an AT-SPI object?
564
elif attr.replace('_Node', '') in self.contained:
565
self.__dict__[attr] = value
567
# Read-only attributes from Applications
568
# (self.__accessible will be an Application and not an Accessible)
569
elif attr in ["toolkit", "version", "ID"]:
570
raise ReadOnlyError, attr
572
# Read-only attributes from the Accessible object
573
elif attr in ["name", "role", "roleName", "description", "parent",
574
"indexInParent", "children", "stateSet", "relations",
575
"labellee", "labeller"]:
576
raise ReadOnlyError, attr
578
# Read-only attributes synthesized from the Accessible's stateSet:
579
elif attr in ["sensitive", "showing", "focusable", "focused", "checked"]:
580
raise ReadOnlyError, attr
582
# Read-only attributes from the Action object
583
elif attr == "actions":
584
raise ReadOnlyError, attr
586
# Attributes from the Component object
587
elif attr in ["extents", "position", "size"]:
588
raise ReadOnlyError, attr
590
# Attributes from the Text object
591
elif attr=="caretOffset":
593
raise ReadOnlyError, attr
594
self.__text.setCaretOffset(value)
596
# Attributes from the EditableText object
599
Set the text of the node to the given value, with
600
appropriate delays and logging, then test the result:
602
if not self.__editableText:
603
raise ReadOnlyError, attr
604
if config.debugSearching: logger.log("Setting text of %s to '%s'"%(self.getLogString(), value))
605
self.__editableText.setTextContents (value)
608
#resultText = self.text
609
#if resultText != value:
610
# raise "%s failed to have its text set to '%s'; value is '%s'"%(self.getLogString(), value, resultText)
612
elif attr=='passwordText':
614
Set the text of the node to the given value, with
615
appropriate delays and logging. We can't test the
616
result, we'd only get * characters back.
618
if not self.__editableText:
619
raise ReadOnlyError, attr
620
logger.log("Setting text %s to password '%s'"%(self.getLogString(), value))
621
self.__editableText.setTextContents (value)
624
elif attr=="combovalue":
626
Set the combobox to the given value, with appropriate delays and
629
logger.log("Setting combobox %s to '%s'"%(self.getLogString(), value))
630
self.childNamed(childName=value).click()
633
# FIXME: should we doing stuff like in the clause above???
634
self.__dict__[attr] = value
636
def typeText(self, string):
638
Type the given text into the node, with appropriate delays and
641
logger.log("Typing text into %s: '%s'"%(self.getLogString(), string))
643
# Non-working implementation
644
# Unfortunately, the call to AccessibleText_setCaretOffset fails for Evolution's gtkhtml composer for some reason
646
logger.log("caret offset: %s" % self.caretOffset)
647
self.__editableText.insertText (self.caretOffset, text)
648
self.caretOffset+=len(string) # FIXME: is this correct?
649
logger.log("new caret offset: %s" % self.caretOffset)
653
try: self.grabFocus()
654
except: logger.log("Node is focusable but I can't grabFocus!")
655
rawinput.typeText(string)
657
logger.log("Node is not focusable; falling back to setting text")
661
def keyCombo(self, comboString):
662
if config.debugSearching: logger.log("Pressing keys '%s' into %s"%(combo, self.getLogString()))
665
try: self.grabFocus()
666
except: logger.log("Node is focusable but I can't grabFocus!")
667
else: logger.log("Node is not focusable; trying key combo anyway")
668
rawinput.keyCombo(comboString)
672
If debugName is set on this Node, returns debugName surrounded
674
Otherwise, returns a plain-text representation of the most
675
important attributes of the underlying Accessible.
678
return "{" + self.debugName + "}"
681
string = string + " roleName='%s'" % self.roleName
682
string = string + " name='%s' description='%s'" % (self.name, self.description)
683
if self.text is not None:
684
string = string + " text='%s'" % self.text
687
def getLogString(self):
689
Get a string describing this node for the logs,
690
respecting the config.absoluteNodePaths boolean.
692
if config.absoluteNodePaths:
693
return self.getAbsoluteSearchPath()
697
def satisfies (self, pred):
699
Does this node satisfy the given predicate?
701
# the logic is handled by the predicate:
702
assert isinstance(pred, predicate.Predicate)
703
return pred.satisfiedByNode(self)
705
def dump (self, type = 'plain'):
707
dumper = getattr (dump, type)
710
def getAbsoluteSearchPath(self):
712
FIXME: this needs rewriting...
713
Generate a SearchPath instance giving the 'best'
714
way to find the Accessible wrapped by this node again, starting
715
at the root and applying each search in turn.
717
This is somewhat analagous to an absolute path in a filesystem,
718
except that some of searches may be recursive, rather than just
719
searching direct children.
721
Used by the recording framework for identifying nodes in a
722
persistent way, independent of the style of script being
725
FIXME: try to ensure uniqueness
726
FIXME: need some heuristics to get 'good' searches, whatever
729
if config.debugSearchPaths:
730
logger.log("getAbsoluteSearchPath(%s)" % self)
732
if self.roleName=='application':
733
result =path.SearchPath()
734
result.append(predicate.IsAnApplicationNamed(self.name), False)
738
(ancestor, pred, isRecursive) = self.getRelativeSearch()
739
if config.debugSearchPaths:
740
logger.log("got ancestor: %s" % ancestor)
742
ancestorPath = ancestor.getAbsoluteSearchPath()
743
ancestorPath.append(pred, isRecursive)
746
# This should be the root node:
747
return path.SearchPath()
749
def getRelativeSearch(self):
751
Get a (ancestorNode, predicate, isRecursive) triple that identifies the
752
best way to find this Node uniquely.
753
FIXME: or None if no such search exists?
754
FIXME: may need to make this more robust
755
FIXME: should this be private?
757
if config.debugSearchPaths:
758
logger.log("getRelativeSearchPath(%s)" % self)
764
ancestor = self.parent
766
# iterate up ancestors until you reach an identifiable one,
767
# setting the search to be isRecursive if need be:
768
while not self.__nodeIsIdentifiable(ancestor):
769
ancestor = ancestor.parent
772
# Pick the most appropriate predicate for finding this node:
774
if self.labellee.name:
775
return (ancestor, predicate.IsLabelledAs(self.labellee.name), isRecursive)
777
if self.roleName=='menu':
778
return (ancestor, predicate.IsAMenuNamed(self.name), isRecursive)
779
elif self.roleName=='menu item' or self.roleName=='check menu item':
780
return (ancestor, predicate.IsAMenuItemNamed(self.name), isRecursive)
781
elif self.roleName=='text':
782
return (ancestor, predicate.IsATextEntryNamed(self.name), isRecursive)
783
elif self.roleName=='push button':
784
return (ancestor, predicate.IsAButtonNamed(self.name), isRecursive)
785
elif self.roleName=='frame':
786
return (ancestor, predicate.IsAWindowNamed(self.name), isRecursive)
787
elif self.roleName=='dialog':
788
return (ancestor, predicate.IsADialogNamed(self.name), isRecursive)
790
pred = predicate.GenericPredicate(name=self.name, roleName=self.roleName)
791
return (ancestor, pred, isRecursive)
793
def __nodeIsIdentifiable(self, ancestor):
794
if ancestor.labellee:
798
elif not ancestor.parent:
803
# The canonical search method:
804
def findChild(self, pred, recursive = True, debugName = None, retry = True, requireResult = True):
806
Search for a node satisyfing the predicate, returning a Node.
808
If retry is True (the default), it makes multiple attempts,
809
backing off and retrying on failure, and eventually raises a
810
descriptive exception if the search fails.
812
If retry is False, it gives up after one attempt.
814
If requireResult is True (the default), an exception is raised after all
815
attempts have failed. If it is false, the function simply returns None.
817
FIXME: make multiple attempts?
820
def findFirstChildSatisfying (parent, pred, recursive = True):
822
Internal helper function that does a one-shot search, recursing if need be.
824
# print "findFirstChildSatisfying(%s, %s, recursive=%s)"%(parent, pred,recursive)
825
assert isinstance(pred, predicate.Predicate)
827
try: children = parent.children
830
for child in children:
832
if child.satisfies(pred):
836
child = findFirstChildSatisfying(child, pred, recursive)
837
if child: return child
838
# ...on to next child
840
def describeSearch (parent, pred, recursive, debugName):
842
Internal helper function
849
if debugName == None:
850
debugName = pred.describeSearchResult()
852
return "%s of %s: %s"%(noun, parent.getLogString(), debugName)
854
assert isinstance(pred, predicate.Predicate)
856
while numAttempts<config.searchCutoffCount:
857
if numAttempts>=config.searchWarningThreshold or config.debugSearching:
858
logger.log("searching for %s (attempt %i)" % (describeSearch(self, pred, recursive, debugName), numAttempts))
860
result = findFirstChildSatisfying(self, pred, recursive)
862
assert isinstance(result, Node)
864
result.debugName = debugName
866
result.debugName = pred.describeSearchResult()
871
if config.debugSearching or config.debugSleep:
872
logger.log("sleeping for %f" % config.searchBackoffDuration)
873
sleep(config.searchBackoffDuration)
875
raise SearchError(describeSearch(self, pred, recursive, debugName))
877
# The canonical "search for multiple" method:
878
def findChildren(self, pred, recursive = True):
880
Find all children/descendents satisfying the predicate.
882
assert isinstance(pred, predicate.Predicate)
886
try: children = self.children
889
for child in children:
890
if child.satisfies(pred): selfList.append(child)
892
childList = child.findChildren(pred, recursive)
894
for child in childList:
895
selfList.append(child)
896
# ...on to next child
898
if selfList: return selfList
900
# The canonical "search above this node" method:
901
def findAncestor (self, pred):
903
Search up the ancestry of this node, returning the first Node
904
satisfying the predicate, or None.
906
assert isinstance(pred, predicate.Predicate)
907
candidate = self.parent
908
while candidate != None:
909
if candidate.satisfies(pred):
912
candidate = candidate.parent
917
# Various wrapper/helper search methods:
918
def child (self, name = '', roleName = '', description= '', label = '', recursive=True, debugName=None):
920
Finds a child satisying the given criteria.
922
This is implemented using findChild, and hence will automatically retry
923
if no such child is found, and will eventually raise an exception. It
924
also logs the search.
926
return self.findChild (predicate.GenericPredicate(name = name, roleName = roleName, description= description, label = label), recursive = recursive, debugName=debugName)
928
# FIXME: does this clash with the "menu" action
929
def menu(self, menuName, recursive=True):
931
Search below this node for a menu with the given name.
933
This is implemented using findChild, and hence will automatically retry
934
if no such child is found, and will eventually raise an exception. It
935
also logs the search.
937
return self.findChild (predicate.IsAMenuNamed(menuName=menuName), recursive)
939
def menuItem(self, menuItemName, recursive=True):
941
Search below this node for a menu item with the given name.
943
This is implemented using findChild, and hence will automatically retry
944
if no such child is found, and will eventually raise an exception. It
945
also logs the search.
947
return self.findChild (predicate.IsAMenuItemNamed(menuItemName=menuItemName), recursive)
949
def textentry(self, textEntryName, recursive=True):
951
Search below this node for a text entry with the given name.
953
This is implemented using findChild, and hence will automatically retry
954
if no such child is found, and will eventually raise an exception. It
955
also logs the search.
957
return self.findChild (predicate.IsATextEntryNamed(textEntryName=textEntryName), recursive)
959
def button(self, buttonName, recursive=True):
961
Search below this node for a button with the given name.
963
This is implemented using findChild, and hence will automatically retry
964
if no such child is found, and will eventually raise an exception. It
965
also logs the search.
967
return self.findChild (predicate.IsAButtonNamed(buttonName=buttonName), recursive)
969
def childLabelled(self, labelText, recursive=True):
971
Search below this node for a child labelled with the given text.
973
This is implemented using findChild, and hence will automatically retry
974
if no such child is found, and will eventually raise an exception. It
975
also logs the search.
977
return self.findChild (predicate.IsLabelled(labelText), recursive)
979
def childNamed(self, childName, recursive=True):
981
Search below this node for a child with the given name.
983
This is implemented using findChild, and hence will automatically retry
984
if no such child is found, and will eventually raise an exception. It
985
also logs the search.
987
return self.findChild (predicate.IsNamed(childName), recursive)
989
def tab(self, tabName, recursive=True):
991
Search below this node for a tab with the given name.
993
This is implemented using findChild, and hence will automatically retry
994
if no such child is found, and will eventually raise an exception. It
995
also logs the search.
997
return self.findChild (predicate.IsATabNamed(tabName=tabName), recursive)
999
def getUserVisibleStrings(self):
1001
Get all user-visible strings in this node and its descendents.
1003
(Could be implemented as an attribute)
1007
result.append(self.name)
1008
if self.description:
1009
result.append(self.description)
1011
children = self.children
1012
except: return result
1013
for child in children:
1014
result.extend(child.getUserVisibleStrings())
1017
def blink(self, count = 2):
1021
if not self.extents:
1024
(x, y, w, h) = self.extents
1025
blinkData = Blinker(x, y, w, h, count)
1031
If the Node supports an action called "click", do it, with appropriate delays and logging.
1032
Otherwise, raise an ActionNotSupported exception.
1034
Note that this is just a shortcut to doAction('click'), as it is by far the most-used
1035
action. To do any other action such as 'activate' or 'open', you must use doAction().
1037
if self.__action is not None:
1038
return self.doAction('click')
1039
raise ActionNotSupported('click', self)
940
1041
class Link(Node):
942
Class representing a hyperlink
944
contained = ('__hyperlink', '__node')
946
def __init__(self, node, hyperlink, index):
947
self.debugName = None
949
self.__hyperlink = hyperlink
951
self.__node = Node(self.__hyperlink.getObject(self.__index))
952
# Somehow, if we allow the children to be seen, things get weird.
953
self.__node.__hideChildren = True
955
def __getattr__(self, name):
958
# Note: This doesn't seem to work. It usually just causes python to hang.
959
return self.__hyperlink.getURI(self.__index)
961
if name == 'children':
962
raise AttributeError, name
964
result = getattr(self.__node, name)
966
except AttributeError:
967
raise AttributeError, name
969
def __setattr__(self, name, value):
970
self.__dict__[name] = value
1043
Class representing a hyperlink
1045
contained = ('__hyperlink', '__node')
1047
def __init__(self, node, hyperlink, index):
1048
self.debugName = None
1050
self.__hyperlink = hyperlink
1051
self.__index = index
1052
self.__node = Node(self.__hyperlink.getObject(self.__index))
1053
# Somehow, if we allow the children to be seen, things get weird.
1054
self.__node.__hideChildren = True
1056
def __getattr__(self, name):
1059
# Note: This doesn't seem to work. It usually just causes python to hang.
1060
return self.__hyperlink.getURI(self.__index)
1062
if name == 'children':
1065
result = getattr(self.__node, name)
1067
except AttributeError:
1068
raise AttributeError, name
1070
def __setattr__(self, name, value):
1071
self.__dict__[name] = value
972
1073
class Root (Node):
976
def applications(self):
978
Get all applications.
980
return root.findAllChildrenSatisfying(predicate.GenericPredicate(roleName="application"), recursive=False)
982
def application(self, appName):
984
Gets an application by name, returning an Application instance
985
or raising an exception.
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.
991
return Application(root.findChild(predicate.IsAnApplicationNamed(appName),recursive=False))
1077
def applications(self):
1079
Get all applications.
1081
return root.findAllChildrenSatisfying(predicate.GenericPredicate(roleName="application"), recursive=False)
1083
def application(self, appName):
1085
Gets an application by name, returning an Application instance
1086
or raising an exception.
1088
This is implemented using findChild, and hence will automatically retry
1089
if no such child is found, and will eventually raise an exception. It
1090
also logs the search.
1092
return Application(root.findChild(predicate.IsAnApplicationNamed(appName),recursive=False))
993
1094
class Application (Node):
994
def dialog(self, dialogName, recursive=False):
996
Search below this node for a dialog with the given name,
997
returning a Window instance.
999
This is implemented using findChild, and hence will automatically retry
1000
if no such child is found, and will eventually raise an exception. It
1001
also logs the search.
1003
FIXME: should this method activate the dialog?
1005
return self.findChild(predicate.IsADialogNamed(dialogName=dialogName), recursive)
1007
def window(self, windowName, recursive=False):
1009
Search below this node for a window with the given name,
1010
returning a Window instance.
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.
1016
FIXME: this bit isn't true:
1017
The window will be automatically activated (raised and focused
1018
by the window manager) if wnck bindings are available.
1020
result = Window(self.findChild (predicate.IsAWindowNamed(windowName=windowName), recursive))
1021
# FIXME: activate the WnckWindow ?
1026
def getWnckApplication(self):
1028
Get the wnck.Application instance for this application, or None
1030
Currently implemented via a hack: requires the app to have a
1031
window, and looks up the application of that window
1033
wnck.Application can give you the pid, the icon, etc
1037
window = child(roleName='frame')
1039
wnckWindow = window.getWnckWindow()
1040
return wnckWindow.get_application()
1095
def dialog(self, dialogName, recursive=False):
1097
Search below this node for a dialog with the given name,
1098
returning a Window instance.
1100
This is implemented using findChild, and hence will automatically retry
1101
if no such child is found, and will eventually raise an exception. It
1102
also logs the search.
1104
FIXME: should this method activate the dialog?
1106
return self.findChild(predicate.IsADialogNamed(dialogName=dialogName), recursive)
1108
def window(self, windowName, recursive=False):
1110
Search below this node for a window with the given name,
1111
returning a Window instance.
1113
This is implemented using findChild, and hence will automatically retry
1114
if no such child is found, and will eventually raise an exception. It
1115
also logs the search.
1117
FIXME: this bit isn't true:
1118
The window will be automatically activated (raised and focused
1119
by the window manager) if wnck bindings are available.
1121
result = Window(self.findChild (predicate.IsAWindowNamed(windowName=windowName), recursive))
1122
# FIXME: activate the WnckWindow ?
1127
def getWnckApplication(self):
1129
Get the wnck.Application instance for this application, or None
1131
Currently implemented via a hack: requires the app to have a
1132
window, and looks up the application of that window
1134
wnck.Application can give you the pid, the icon, etc
1138
window = child(roleName='frame')
1140
wnckWindow = window.getWnckWindow()
1141
return wnckWindow.get_application()
1044
1145
class Window (Node):
1045
def getWnckWindow(self):
1047
Get the wnck.Window instance for this window, or None
1049
# FIXME: this probably needs rewriting:
1050
screen = wnck.screen_get_default()
1052
# You have to force an update before any of the wnck methods
1054
screen.force_update()
1056
for wnckWindow in screen.get_windows():
1057
# FIXME: a dubious hack: search by window title:
1058
if wnckWindow.get_name()==self.name:
1063
Activates the wnck.Window associated with this Window.
1065
FIXME: doesn't yet work
1067
wnckWindow = self.getWnckWindow()
1068
# Activate it with a timestamp of 0; this may confuse
1069
# alt-tabbing through windows etc:
1070
# FIXME: is there a better way of getting a timestamp?
1071
# gdk_x11_get_server_time (), with a dummy window
1072
wnckWindow.activate(0)
1146
def getWnckWindow(self):
1148
Get the wnck.Window instance for this window, or None
1150
# FIXME: this probably needs rewriting:
1151
screen = wnck.screen_get_default()
1153
# You have to force an update before any of the wnck methods
1155
screen.force_update()
1157
for wnckWindow in screen.get_windows():
1158
# FIXME: a dubious hack: search by window title:
1159
if wnckWindow.get_name()==self.name:
1164
Activates the wnck.Window associated with this Window.
1166
FIXME: doesn't yet work
1168
wnckWindow = self.getWnckWindow()
1169
# Activate it with a timestamp of 0; this may confuse
1170
# alt-tabbing through windows etc:
1171
# FIXME: is there a better way of getting a timestamp?
1172
# gdk_x11_get_server_time (), with a dummy window
1173
wnckWindow.activate(0)
1074
1175
class Wizard (Window):
1076
Note that the buttons of a GnomeDruid were not accessible until
1077
recent versions of libgnomeui. This is
1078
http://bugzilla.gnome.org/show_bug.cgi?id=157936
1079
and is fixed in gnome-2.10 and gnome-2.12 (in CVS libgnomeui);
1080
there's a patch attached to that bug.
1082
This bug is known to affect FC3; fixed in FC5
1084
def __init__(self, node, debugName=None):
1085
Node.__init__(self, node)
1087
self.debugName = debugName
1088
logger.log("%s is on '%s' page"%(self, self.getPageTitle()))
1090
def currentPage(self):
1092
Get the current page of this wizard
1094
FIXME: this is currently a hack, supporting only GnomeDruid
1096
pageHolder = self.child(roleName='panel')
1097
for child in pageHolder.children:
1098
# current child has SHOWING state set, we hope:
1100
#print child.showing
1103
raise "Unable to determine current page of %s"%self
1105
def getPageTitle(self):
1107
Get the string title of the current page of this wizard
1109
FIXME: this is currently a total hack, supporting only GnomeDruid
1111
currentPage = self.currentPage()
1112
return currentPage.child(roleName='panel').child(roleName='panel').child(roleName='label', recursive=False).text
1114
def clickForward(self):
1116
Click on the 'Forward' button to advance to next page of wizard.
1118
It will log the title of the new page that is reached.
1120
FIXME: what if it's Next rather than Forward ???
1122
This will only work if your libgnomeui has accessible buttons;
1125
fwd = self.child("Forward")
1128
# Log the new wizard page; it's helpful when debugging scripts
1129
logger.log("%s is now on '%s' page"%(self, self.getPageTitle()))
1130
# FIXME disabled for now (can't get valid page titles)
1132
def clickApply(self):
1134
Click on the 'Apply' button to advance to next page of wizard.
1135
FIXME: what if it's Finish rather than Apply ???
1137
This will only work if your libgnomeui has accessible buttons;
1140
fwd = self.child("Apply")
1143
# FIXME: debug logging?
1177
Note that the buttons of a GnomeDruid were not accessible until
1178
recent versions of libgnomeui. This is
1179
http://bugzilla.gnome.org/show_bug.cgi?id=157936
1180
and is fixed in gnome-2.10 and gnome-2.12 (in CVS libgnomeui);
1181
there's a patch attached to that bug.
1183
This bug is known to affect FC3; fixed in FC5
1185
def __init__(self, node, debugName=None):
1186
Node.__init__(self, node)
1188
self.debugName = debugName
1189
logger.log("%s is on '%s' page"%(self, self.getPageTitle()))
1191
def currentPage(self):
1193
Get the current page of this wizard
1195
FIXME: this is currently a hack, supporting only GnomeDruid
1197
pageHolder = self.child(roleName='panel')
1198
for child in pageHolder.children:
1199
# current child has SHOWING state set, we hope:
1201
#print child.showing
1204
raise "Unable to determine current page of %s"%self
1206
def getPageTitle(self):
1208
Get the string title of the current page of this wizard
1210
FIXME: this is currently a total hack, supporting only GnomeDruid
1212
currentPage = self.currentPage()
1213
return currentPage.child(roleName='panel').child(roleName='panel').child(roleName='label', recursive=False).text
1215
def clickForward(self):
1217
Click on the 'Forward' button to advance to next page of wizard.
1219
It will log the title of the new page that is reached.
1221
FIXME: what if it's Next rather than Forward ???
1223
This will only work if your libgnomeui has accessible buttons;
1226
fwd = self.child("Forward")
1229
# Log the new wizard page; it's helpful when debugging scripts
1230
logger.log("%s is now on '%s' page"%(self, self.getPageTitle()))
1231
# FIXME disabled for now (can't get valid page titles)
1233
def clickApply(self):
1235
Click on the 'Apply' button to advance to next page of wizard.
1236
FIXME: what if it's Finish rather than Apply ???
1238
This will only work if your libgnomeui has accessible buttons;
1241
fwd = self.child("Apply")
1244
# FIXME: debug logging?
1146
root = Root (atspi.registry.getDesktop ())
1147
root.debugName= 'root'
1247
root = Root (atspi.registry.getDesktop ())
1248
root.debugName= 'root'
1148
1249
except AssertionError:
1149
# Warn if AT-SPI's desktop object doesn't show up.
1150
print "Error: AT-SPI's desktop is not visible. Do you have accessibility enabled?"
1153
# Check that there are applications running. Warn if none are.
1154
test = root.children
1156
except AttributeError:
1157
print "Warning: AT-SPI's desktop is visible but it has no children. Are you running any AT-SPI-aware applications?"
1159
# This is my poor excuse for a unit test.
1160
if __name__ == '__main__':
1162
f=root.findChild(name="File", roleName="menu")
1163
case = tc.TCNumber()
1164
case.compare("File menu text", 4, len(f.text), "float")
1250
# Warn if AT-SPI's desktop object doesn't show up.
1251
logger.log("Error: AT-SPI's desktop is not visible. Do you have accessibility enabled?")
1253
# Check that there are applications running. Warn if none are.
1254
children = root.children
1256
logger.log("Warning: AT-SPI's desktop is visible but it has no children. Are you running any AT-SPI-aware applications?")
1167
1259
# Convenient place to set some debug variables:
1168
1260
#config.debugSearching = True