~ubuntu-branches/ubuntu/utopic/dogtail/utopic

« back to all changes in this revision

Viewing changes to dogtail/tree.py

  • Committer: Bazaar Package Importer
  • Author(s): Daniel Holbach
  • Date: 2006-12-21 13:33:47 UTC
  • mfrom: (1.2.1 upstream)
  • mto: This revision was merged to the branch mainline in revision 5.
  • Revision ID: james.westby@ubuntu.com-20061221133347-xo9jg11afp5plcka
Tags: upstream-0.6.1
ImportĀ upstreamĀ versionĀ 0.6.1

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
#!/usr/bin/env python
2
1
"""Makes some sense of the AT-SPI API
3
2
 
4
3
The tree API handles various things for you:
25
24
by the Predicate class.
26
25
 
27
26
Dogtail implements a high-level searching system, for finding a node (or
28
 
nodes) satisfying whatever criteria you are interested in.      It does this with
 
27
nodes) satisfying whatever criteria you are interested in.      It does this with
29
28
a 'backoff and retry' algorithm. This fixes most timing problems e.g. when a
30
29
dialog is in the process of opening but hasn't yet done so.
31
30
 
37
36
all of this process in the debug log by setting 'config.debugSearching' to True
38
37
 
39
38
We also automatically add a short delay after each action
40
 
('config.defaultDelay' gives the time in seconds).      We'd hoped that the search
 
39
('config.defaultDelay' gives the time in seconds).      We'd hoped that the search
41
40
backoff and retry code would eliminate the need for this, but unfortunately we
42
 
still run into timing issues.    For example, Evolution (and probably most
 
41
still run into timing issues.    For example, Evolution (and probably most
43
42
other apps) set things up on new dialogs and wizard pages as they appear, and
44
43
we can run into 'setting wars' where the app resets the widgetry to defaults
45
44
after our script has already filled out the desired values, and so we lose our
53
52
Unfortunately, some applications do not set up the 'sensitive' state
54
53
correctly on their buttons (e.g. Epiphany on form buttons in a web page). The
55
54
current workaround for this is to set config.ensureSensitivity=False, which
56
 
disables the sensitivity testing. 
 
55
disables the sensitivity testing.
57
56
 
58
57
Authors: Zack Cerza <zcerza@redhat.com>, David Malcolm <dmalcolm@redhat.com>
59
58
"""
61
60
David Malcolm <dmalcolm@redhat.com>
62
61
"""
63
62
 
 
63
from utils import checkForA11y
 
64
checkForA11y()
 
65
 
64
66
import re
65
67
import predicate
66
68
from datetime import datetime
68
70
from config import config
69
71
from utils import doDelay
70
72
from utils import Blinker
 
73
import rawinput
71
74
import path
72
75
 
73
76
from logging import debugLogger as logger
74
77
 
75
78
try:
76
 
        import atspi
 
79
    import atspi
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"
80
83
 
81
84
# We optionally import the bindings for libwnck.
82
85
try:
83
 
        import wnck
84
 
        gotWnck = True
 
86
    import wnck
 
87
    gotWnck = True
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."
88
 
        gotWnck = False
89
 
 
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."
 
91
    gotWnck = False
 
92
 
 
93
class SearchError(Exception):
 
94
    pass
 
95
 
 
96
class ReadOnlyError(TypeError):
 
97
    """
 
98
    This attribute is not writeable.
 
99
    """
 
100
    message = "Cannot set %s. It is read-only."
 
101
    def __init__(self, attr):
 
102
        self.attr = attr
 
103
 
 
104
    def __str__(self):
 
105
        return self.message % self.attr
91
106
 
92
107
class NotSensitiveError(Exception):
93
 
        """
94
 
        The widget is not sensitive.
95
 
        """
96
 
        message = "Cannot %s %s. It is not sensitive."
97
 
        def __init__(self, action):
98
 
                self.action = action
99
 
        
100
 
        def __str__(self):
101
 
                return self.message % (self.action.name, self.action.node.getLogString())
 
108
    """
 
109
    The widget is not sensitive.
 
110
    """
 
111
    message = "Cannot %s %s. It is not sensitive."
 
112
    def __init__(self, action):
 
113
        self.action = action
 
114
 
 
115
    def __str__(self):
 
116
        return self.message % (self.action.name, self.action.node.getLogString())
102
117
 
103
118
class ActionNotSupported(Exception):
104
 
        """
105
 
        The widget does not support the requested action.
106
 
        """
107
 
        message = "Cannot do '%s' action on %s"
108
 
        def __init__(self, actionName, node):
109
 
                self.actionName = actionName
110
 
                self.node = node
111
 
        
112
 
        def __str__(self):
113
 
                return self.message % (self.actionName, self.node.getLogString())
 
119
    """
 
120
    The widget does not support the requested action.
 
121
    """
 
122
    message = "Cannot do '%s' action on %s"
 
123
    def __init__(self, actionName, node):
 
124
        self.actionName = actionName
 
125
        self.node = node
 
126
 
 
127
    def __str__(self):
 
128
        return self.message % (self.actionName, self.node.getLogString())
114
129
 
115
130
class Action:
116
 
        """
117
 
        Class representing an action that can be performed on a specific node
118
 
        """
119
 
        # Valid types of actions we know about. Feel free to add any you see.
120
 
        types = ('click',
121
 
                 'press',
122
 
                 'release',
123
 
                 'activate',
124
 
                 'jump',
125
 
                 'check',
126
 
                 'dock',
127
 
                 'undock',
128
 
                 'open',
129
 
                 'menu')
130
 
        
131
 
        def __init__ (self, node, action, index):
132
 
                self.node = node
133
 
                self.__action = action
134
 
                self.__index = index
135
 
 
136
 
        def __getattr__ (self, attr):
137
 
                if attr == "name":
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
144
 
 
145
 
        def __str__ (self):
146
 
                """
147
 
                Plain-text representation of the Action.
148
 
                """
149
 
                string = "Action node='%s' name='%s' description='%s' keybinding='%s'" % (self.node, self.name, self.description, self.keyBinding)
150
 
                return string
151
 
 
152
 
        def do (self):
153
 
                """
154
 
                Performs the given tree.Action, with appropriate delays and logging.
155
 
                """
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
161
 
                        else:
162
 
                                nSE = NotSensitiveError(self)
163
 
                                print "Warning: " + str(nSE)
164
 
                if config.blinkOnActions: self.node.blink()
165
 
                result = self.__action.doAction (self.__index)
166
 
                doDelay()
167
 
                return result
 
131
    """
 
132
    Class representing an action that can be performed on a specific node
 
133
    """
 
134
    # Valid types of actions we know about. Feel free to add any you see.
 
135
    types = ('click',
 
136
             'press',
 
137
             'release',
 
138
             'activate',
 
139
             'jump',
 
140
             'check',
 
141
             'dock',
 
142
             'undock',
 
143
             'open',
 
144
             'menu')
 
145
 
 
146
    def __init__ (self, node, action, index):
 
147
        self.node = node
 
148
        self.__action = action
 
149
        self.__index = index
 
150
 
 
151
    def __getattr__ (self, attr):
 
152
        if attr == "name":
 
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
 
159
 
 
160
    def __str__ (self):
 
161
        """
 
162
        Plain-text representation of the Action.
 
163
        """
 
164
        string = "Action node='%s' name='%s' description='%s' keybinding='%s'" % (self.node, self.name, self.description, self.keyBinding)
 
165
        return string
 
166
 
 
167
    def do (self):
 
168
        """
 
169
        Performs the given tree.Action, with appropriate delays and logging.
 
170
        """
 
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
 
176
            else:
 
177
                nSE = NotSensitiveError(self)
 
178
                logger.log("Warning: " + str(nSE))
 
179
        if config.blinkOnActions: self.node.blink()
 
180
        result = self.__action.doAction (self.__index)
 
181
        doDelay()
 
182
        return result
168
183
 
169
184
class Node:
170
 
        """
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.
174
 
 
175
 
        Node instances have various attributes synthesized, to make it easy to
176
 
        get and the underlying accessible data. Many more attributes can be
177
 
        added as desired.
178
 
 
179
 
        'name' (read-only string):
180
 
        Wraps Accessible_getName on the Node's underlying Accessible
181
 
        
182
 
        'roleName' (read-only string):
183
 
        Wraps Accessible_getRoleName on the Node's underlying Accessible
184
 
        
185
 
        'role' (read-only atspi role enum):
186
 
        Wraps Accessible_getRole on the Node's underlying Accessible
187
 
 
188
 
        'description' (read-only string):
189
 
        Wraps Accessible_getDescription on the Node's underlying Accessible
190
 
        
191
 
        'parent' (read-only Node instance):
192
 
        A Node instance wrapping the parent, or None. Wraps Accessible_getParent
193
 
 
194
 
        'children' (read-only list of Node instances):
195
 
        The children of this node, wrapping getChildCount and getChildAtIndex 
196
 
 
197
 
        'text' (string):
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.
206
 
 
207
 
        'passwordText' (write-only string):
208
 
        See documentation of 'text' attribute above.
209
 
        
210
 
        'caretOffset' (read/write int):
211
 
        For instances wrapping AccessibleText, the location of the text caret,
212
 
        expressed as an offset in characters.
213
 
 
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.
217
 
 
218
 
        'stateSet' (read-only StateSet instance):
219
 
        Wraps Accessible_getStateSet; a set of boolean state flags
220
 
        
221
 
        'relations' (read-only list of atspi.Relation instances):
222
 
        Wraps Accessible_getRelationSet
223
 
 
224
 
        'labellee' (read-only list of Node instances):
225
 
        The node(s) that this node is a label for. Generated from 'relations'.
226
 
 
227
 
        'labeller' (read-only list of Node instances):
228
 
        The node(s) that is/are a label for this node. Generated from
229
 
        'relations'.
230
 
 
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.
235
 
 
236
 
        'showing' (read-only boolean):
237
 
        Generated from stateSet based on presence of atspi.SPI_STATE_SHOWING
238
 
 
239
 
        'actions' (read-only list of Action instances):
240
 
        Generated from Accessible_getAction and AccessibleAction_getNActions
241
 
 
242
 
        For each action that is supported by a node, a method is hooked up,
243
 
        this can include the following list:
244
 
        'click'
245
 
        'press'
246
 
        'release'
247
 
        'activate'
248
 
        'jump'
249
 
        'check'
250
 
        'dock'
251
 
        'undock'
252
 
        'open'
253
 
        'menu'
254
 
        
255
 
        'extents' (readonly tuple):
256
 
        For instances wrapping a Component, the (x,y,w,h) screen extents of the
257
 
        component.
258
 
 
259
 
        'position' (readonly tuple):
260
 
        For instances wrapping a Component, the (x,y) screen position of the
261
 
        component.
262
 
 
263
 
        'size' (readonly tuple):
264
 
        For instances wrapping a Component, the (w,h) screen size of the component.
265
 
 
266
 
        'grabFocus':
267
 
        For instances wrapping a Component, attempt to set the keyboard input focus 
268
 
        to that Node.
269
 
 
270
 
        'toolkit' (readonly string):
271
 
        For instances wrapping an application, the name of the toolkit.
272
 
        
273
 
        'version'
274
 
        For instances wrapping an application.
275
 
        
276
 
        'ID'
277
 
        For instances wrapping an application.
278
 
        
279
 
        'pause' (function)
280
 
        'resume' (function)
281
 
        For instances wrapping an application; probably don't work
282
 
        """
283
 
 
284
 
        #Valid types of AT-SPI objects we wrap.
285
 
        contained = ('__accessible', '__action', '__component', '__text', '__editableText')
286
 
 
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)
295
 
                else:
296
 
                        raise "Unknown Node initializer"
297
 
                assert self.__accessible
298
 
 
299
 
                # Swallow the Action object, if it exists
300
 
                self.__action = self.__accessible.getAction()
301
 
                if self.__action is not None:
302
 
                        def doAction(name):
303
 
                                """
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
306
 
                                action.
307
 
                                """
308
 
                                actions = self.actions
309
 
                                if actions.has_key(name):
310
 
                                        return actions[name].do()
311
 
                                else:
312
 
                                        raise ActionNotSupported(name, self)
313
 
                        self.doAction = doAction
314
 
 
315
 
                # Swallow the Component object, if it exists
316
 
                component = self.__accessible.getComponent()
317
 
                if component is not None:
318
 
                        self.__component = component
319
 
                        
320
 
                        def grabFocus():
321
 
                                self.__component.grabFocus()
322
 
                        self.grabFocus = grabFocus
323
 
 
324
 
                        def rawClick(button = 1):
325
 
                                """
326
 
                                Generates a raw mouse click event whether or not the Node has a 'click' action, using the specified button.
327
 
                                1 is left,
328
 
                                2 is middle,
329
 
                                3 is right.
330
 
                                """
331
 
                                import rawinput
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
340
 
 
341
 
                        def rawType(text):
342
 
                                """
343
 
                                Generates raw keyboard events to type text into the Node.
344
 
                                """
345
 
                                import rawinput
346
 
                                if config.debugSearching: logger.log("Typing text '%s' into %s"%(text, self.getLogString()))
347
 
                                self.grabFocus()
348
 
                                rawinput.typeText(text)
349
 
                        self.rawType = rawType
350
 
                        
351
 
                # Swallow the Text object, if it exists
352
 
                text = self.__accessible.getText()
353
 
                if text is not None: 
354
 
                        self.__text = text
355
 
                        self.addSelection = self.__text.addSelection
356
 
                        self.getNSelections = self.__text.getNSelections
357
 
                        self.removeSelection = self.__text.removeSelection
358
 
                        self.setSelection = self.__text.setSelection
359
 
 
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
365
 
 
366
 
                # Swallow the Hypertext object, if it exists
367
 
                hypertext = self.__accessible.getHypertext()
368
 
                if hypertext is not None:
369
 
                        self.__hypertext = hypertext
370
 
 
371
 
                # Add more objects here. Nobody uses them yet, so I haven't.
372
 
                # You also need to change the __getattr__ and __setattr__ functions.
373
 
 
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
379
 
 
380
 
        def __getattr__ (self, attr):
381
 
                if False: pass
382
 
                
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
396
 
                
397
 
                # Attributes from the Accessible object
398
 
                elif attr == "name":
399
 
                        return self.__accessible.getName()
400
 
                elif attr == "role":
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":
407
 
                        try:
408
 
                                parentAcc = self.__accessible.getParent ()
409
 
                                if parentAcc:
410
 
                                        parent = Node (parentAcc)
411
 
                                        return parent
412
 
                                else:
413
 
                                        return None
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
419
 
                        children = []
420
 
                        for i in xrange (self.__accessible.getChildCount ()):
421
 
                                children.append (Node (self.__accessible.getChildAtIndex (i)))
422
 
                        # Attributes from the Hypertext object
423
 
                        try:
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
428
 
                        else:
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)
448
 
                        
449
 
                
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)
455
 
                
456
 
                # Attributes from the Action object
457
 
                elif attr == "actions":
458
 
                        actions = {}
459
 
                        for i in xrange (self.__action.getNActions ()):
460
 
                                action = (Action (self, self.__action, i))
461
 
                                actions[action.name] = action
462
 
                        if actions: 
463
 
                                return actions
464
 
                        else:
465
 
                                raise AttributeError, attr
466
 
 
467
 
                # Attributes from the Component object
468
 
                elif attr == "extents":
469
 
                        return self.__component.getExtents ()
470
 
                elif attr == "position":
471
 
                        return self.__component.getPosition ()
472
 
                elif attr == "size":
473
 
                        return self.__component.getSize ()
474
 
 
475
 
                # Attributes from the Text object
476
 
                elif attr == "text":
477
 
                        return self.__text.getText (0, 32767)
478
 
                elif attr == "caretOffset":
479
 
                        return self.__text.getCaretOffset ()
480
 
 
481
 
                # Attributes from the Component object
482
 
                elif attr == "extents":
483
 
                        return self.__component.getExtents ()
484
 
 
485
 
                else: raise AttributeError, attr
486
 
 
487
 
        def __setattr__ (self, attr, value):
488
 
                if False: pass
489
 
                
490
 
                # Are we swallowing an AT-SPI object?
491
 
                elif attr.replace('_Node', '') in self.contained:
492
 
                        self.__dict__[attr] = value
493
 
 
494
 
                # Attributes from the Text object
495
 
                elif attr=="caretOffset":
496
 
                        self.__text.setCaretOffset(value)
497
 
 
498
 
                # Attributes from the EditableText object
499
 
                elif attr=="text":
500
 
                        """
501
 
                        Set the text of the node to the given value, with
502
 
                        appropriate delays and logging, then test the result:
503
 
                        """
504
 
                        if config.debugSearching: logger.log("Setting text of %s to '%s'"%(self.getLogString(), value))
505
 
                        self.__editableText.setTextContents (value)
506
 
                        doDelay()
507
 
 
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)
511
 
 
512
 
                elif attr=='passwordText':
513
 
                        """
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.
517
 
                        """
518
 
                        logger.log("Setting text %s to password '%s'"%(self.getLogString(), value))
519
 
                        self.__editableText.setTextContents (value)
520
 
                        doDelay()
521
 
                        
522
 
                elif attr=="combovalue":
523
 
                        """
524
 
                        Set the combobox to the given value, with appropriate delays and
525
 
                        logging.
526
 
                        """
527
 
                        logger.log("Setting combobox %s to '%s'"%(self.getLogString(), value))
528
 
                        self.childNamed(childName=value).click()
529
 
                        doDelay()
530
 
                else:
531
 
                        # FIXME: should we doing stuff like in the clause above???
532
 
                        self.__dict__[attr] = value
533
 
 
534
 
        def typeText(self, string):
535
 
                """
536
 
                Type the given text into the node, with appropriate delays and
537
 
                logging.
538
 
 
539
 
                FIXME: Doesn't work well at the moment
540
 
                """
541
 
                logger.log("Typing text into %s: '%s'"%(self.getLogString(), string))
542
 
 
543
 
                # Non-working implementation
544
 
                # Unfortunately, the call to AccessibleText_setCaretOffset fails for Evolution's gtkhtml composer for some reason               
545
 
                if False:
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
550
 
 
551
 
                # Another non-working implementation
552
 
                # Focus the node and inject and keyboard event:
553
 
                # Unfortunately, this doesn't work well with Evolution either
554
 
                if False:
555
 
                        self.grabFocus()
556
 
                        doDelay()
557
 
                        ev = atspi.EventGenerator()
558
 
                        ev.injectKeyboardString (string)
559
 
 
560
 
                # This approach partially works:
561
 
                if True:
562
 
                        self.text += string
563
 
                
564
 
                doDelay()
565
 
 
566
 
        def __str__ (self):
567
 
                """
568
 
                If debugName is set on this Node, returns debugName surrounded
569
 
                in curly braces.
570
 
                Otherwise, returns a plain-text representation of the most
571
 
                important attributes of the underlying Accessible.
572
 
                """
573
 
                if self.debugName:
574
 
                        return "{" + self.debugName + "}"
575
 
                else:
576
 
                        string = "Node"
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
582
 
                        return string
583
 
 
584
 
        def getLogString(self):
585
 
                """
586
 
                Get a string describing this node for the logs,
587
 
                respecting the config.absoluteNodePaths boolean.
588
 
                """
589
 
                if config.absoluteNodePaths:
590
 
                        return self.getAbsoluteSearchPath()
591
 
                else:
592
 
                        return str(self)
593
 
                
594
 
        def satisfies (self, pred):
595
 
                """
596
 
                Does this node satisfy the given predicate?
597
 
                """
598
 
                # the logic is handled by the predicate:
599
 
                assert isinstance(pred, predicate.Predicate)
600
 
                return pred.satisfiedByNode(self)
601
 
 
602
 
        def dump (self, type = 'plain'):
603
 
                import dump
604
 
                dumper = getattr (dump, type)
605
 
                dumper (self)
606
 
 
607
 
        def getAbsoluteSearchPath(self):
608
 
                """
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.
613
 
 
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.
617
 
 
618
 
                Used by the recording framework for identifying nodes in a
619
 
                persistent way, independent of the style of script being
620
 
                written.
621
 
                
622
 
                FIXME: try to ensure uniqueness
623
 
                FIXME: need some heuristics to get 'good' searches, whatever
624
 
                that means
625
 
                """
626
 
                if config.debugSearchPaths:
627
 
                        print "getAbsoluteSearchPath(%s)"%self
628
 
                        
629
 
                if self.roleName=='application':
630
 
                        result =path.SearchPath()
631
 
                        result.append(predicate.IsAnApplicationNamed(self.name), False)
632
 
                        return result
633
 
                else:
634
 
                        if self.parent:
635
 
                                (ancestor, pred, isRecursive) = self.getRelativeSearch()
636
 
                                if config.debugSearchPaths:
637
 
                                        print "got ancestor: %s"%ancestor
638
 
                                
639
 
                                ancestorPath = ancestor.getAbsoluteSearchPath()
640
 
                                ancestorPath.append(pred, isRecursive)
641
 
                                return ancestorPath
642
 
                        else:
643
 
                                # This should be the root node:
644
 
                                return path.SearchPath()
645
 
 
646
 
        def getRelativeSearch(self):
647
 
                """
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?
653
 
                """
654
 
                if config.debugSearchPaths:
655
 
                        print "getRelativeSearchPath(%s)"%self
656
 
                        
657
 
                assert self
658
 
                assert self.parent
659
 
 
660
 
                isRecursive = False
661
 
                ancestor = self.parent
662
 
 
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
667
 
                        isRecursive = True
668
 
 
669
 
                # Pick the most appropriate predicate for finding this node:
670
 
                if self.labellee:
671
 
                        if self.labellee.name:
672
 
                                return (ancestor, predicate.IsLabelledAs(self.labellee.name), isRecursive)
673
 
 
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)
686
 
                else:
687
 
                        pred = predicate.GenericPredicate(name=self.name, roleName=self.roleName)
688
 
                        return (ancestor, pred, isRecursive)
689
 
 
690
 
        def __nodeIsIdentifiable(self, ancestor):
691
 
                if ancestor.labellee:
692
 
                        return True
693
 
                elif ancestor.name:
694
 
                        return True
695
 
                elif not ancestor.parent:
696
 
                        return True
697
 
                else:
698
 
                        return False
699
 
 
700
 
        def grabFocus(self):
701
 
                return self.__component.grabFocus ()
702
 
 
703
 
        # The canonical search method:
704
 
        def findChild(self, pred, recursive = True, debugName = None, retry = True, requireResult = True):
705
 
                """
706
 
                Search for a node satisyfing the predicate, returning a Node.
707
 
 
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.
711
 
 
712
 
                If retry is False, it gives up after one attempt.
713
 
                
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.
716
 
        
717
 
                FIXME: make multiple attempts?
718
 
                """
719
 
                
720
 
                def findFirstChildSatisfying (parent, pred, recursive = True):
721
 
                        """
722
 
                        Internal helper function that does a one-shot search, recursing if need be.
723
 
                        """
724
 
                        # print "findFirstChildSatisfying(%s, %s, recursive=%s)"%(parent, pred,recursive)
725
 
                        assert isinstance(pred, predicate.Predicate)
726
 
                
727
 
                        try: children = parent.children
728
 
                        except: return None
729
 
 
730
 
                        for child in children:
731
 
                                # print child
732
 
                                if child.satisfies(pred):
733
 
                                        return child
734
 
                                else:
735
 
                                        if recursive:
736
 
                                                child = findFirstChildSatisfying(child, pred, recursive)
737
 
                                                if child: return child
738
 
                                # ...on to next child
739
 
 
740
 
                def describeSearch (parent, pred, recursive, debugName):
741
 
                        """
742
 
                        Internal helper function
743
 
                        """
744
 
                        if recursive:
745
 
                                noun = "descendent"
746
 
                        else:
747
 
                                noun = "child"
748
 
 
749
 
                        if debugName == None:
750
 
                                debugName = pred.describeSearchResult()
751
 
 
752
 
                        return "%s of %s: %s"%(noun, parent.getLogString(), debugName)
753
 
                
754
 
                assert isinstance(pred, predicate.Predicate)
755
 
                numAttempts = 0
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)
759
 
                        
760
 
                        result = findFirstChildSatisfying(self, pred, recursive)
761
 
                        if result:
762
 
                                assert isinstance(result, Node)
763
 
                                if debugName:
764
 
                                        result.debugName = debugName
765
 
                                else:
766
 
                                        result.debugName = pred.describeSearchResult()
767
 
                                return result
768
 
                        else:
769
 
                                if not retry: break
770
 
                                numAttempts+=1
771
 
                                if config.debugSearching or config.debugSleep:
772
 
                                        print "sleeping for %f"%config.searchBackoffDuration
773
 
                                sleep(config.searchBackoffDuration)
774
 
                if requireResult:
775
 
                        raise SearchError, describeSearch(self, pred, recursive, debugName)
776
 
 
777
 
        # The canonical "search for multiple" method:
778
 
        def findChildren(self, pred, recursive = True):
779
 
                """
780
 
                Find all children/descendents satisfying the predicate.
781
 
                """
782
 
                assert isinstance(pred, predicate.Predicate)
783
 
 
784
 
                selfList = []
785
 
 
786
 
                try: children = self.children
787
 
                except: return None
788
 
 
789
 
                for child in children:
790
 
                        if child.satisfies(pred): selfList.append(child)
791
 
                        if recursive:
792
 
                                childList = child.findChildren(pred, recursive)
793
 
                                if childList:
794
 
                                        for child in childList:
795
 
                                                selfList.append(child)
796
 
                        # ...on to next child
797
 
 
798
 
                if selfList: return selfList
799
 
 
800
 
        # The canonical "search above this node" method:
801
 
        def findAncestor (self, pred):
802
 
                """
803
 
                Search up the ancestry of this node, returning the first Node
804
 
                satisfying the predicate, or None.
805
 
                """
806
 
                assert isinstance(pred, predicate.Predicate)
807
 
                candidate = self.parent
808
 
                while candidate != None:
809
 
                        if candidate.satisfies(pred):
810
 
                                return candidate
811
 
                        else:
812
 
                                candidate = candidate.parent
813
 
                # Not found:
814
 
                return None
815
 
        
816
 
 
817
 
        # Various wrapper/helper search methods:
818
 
        def child (self, name = '', roleName = '', description= '', label = '', recursive=True, debugName=None):
819
 
                """
820
 
                Finds a child satisying the given criteria.
821
 
 
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.
825
 
                """
826
 
                return self.findChild (predicate.GenericPredicate(name = name, roleName = roleName, description= description, label = label), recursive = recursive, debugName=debugName)
827
 
 
828
 
        # FIXME: does this clash with the "menu" action
829
 
        def menu(self, menuName, recursive=True):
830
 
                """
831
 
                Search below this node for a menu with the given name.
832
 
 
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.
836
 
                """
837
 
                return self.findChild (predicate.IsAMenuNamed(menuName=menuName), recursive)
838
 
 
839
 
        def menuItem(self, menuItemName, recursive=True):
840
 
                """
841
 
                Search below this node for a menu item with the given name.
842
 
                
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.
846
 
                """
847
 
                return self.findChild (predicate.IsAMenuItemNamed(menuItemName=menuItemName), recursive)
848
 
 
849
 
        def textentry(self, textEntryName, recursive=True):
850
 
                """
851
 
                Search below this node for a text entry with the given name.
852
 
 
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.
856
 
                """
857
 
                return self.findChild (predicate.IsATextEntryNamed(textEntryName=textEntryName), recursive)
858
 
 
859
 
        def button(self, buttonName, recursive=True):
860
 
                """
861
 
                Search below this node for a button with the given name.
862
 
 
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.
866
 
                """
867
 
                return self.findChild (predicate.IsAButtonNamed(buttonName=buttonName), recursive)
868
 
 
869
 
        def childLabelled(self, labelText, recursive=True):
870
 
                """
871
 
                Search below this node for a child labelled with the given text.
872
 
 
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.
876
 
                """
877
 
                return self.findChild (predicate.IsLabelled(labelText), recursive)
878
 
 
879
 
        def childNamed(self, childName, recursive=True):
880
 
                """
881
 
                Search below this node for a child with the given name.
882
 
 
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.
886
 
                """
887
 
                return self.findChild (predicate.IsNamed(childName), recursive)
888
 
 
889
 
        def tab(self, tabName, recursive=True):
890
 
                """
891
 
                Search below this node for a tab with the given name.
892
 
 
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.
896
 
                """
897
 
                return self.findChild (predicate.IsATabNamed(tabName=tabName), recursive)
898
 
 
899
 
        def getUserVisibleStrings(self):
900
 
                """
901
 
                Get all user-visible strings in this node and its descendents.
902
 
                
903
 
                (Could be implemented as an attribute)
904
 
                """
905
 
                result=[]
906
 
                if self.name:
907
 
                        result.append(self.name)
908
 
                if self.description:
909
 
                        result.append(self.description)
910
 
                try: 
911
 
                        children = self.children
912
 
                except: return result
913
 
                for child in children:
914
 
                                result.extend(child.getUserVisibleStrings())
915
 
                return result
916
 
 
917
 
        def blink(self, count = 2):
918
 
                """
919
 
                Blink, baby!
920
 
                """
921
 
                try:
922
 
                        (x, y, w, h) = self.extents
923
 
                        blinkData = Blinker(x, y, w, h, count)
924
 
                        return True
925
 
                except AttributeError:
926
 
                        return False
927
 
 
928
 
        def click(self):
929
 
                """
930
 
                If the Node supports an action called "click", do it, with appropriate delays and logging.
931
 
                Otherwise, raise an ActionNotSupported exception. 
932
 
 
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().
935
 
                """
936
 
                if self.__action is not None:
937
 
                        return self.doAction('click')
938
 
                raise ActionNotSupported('click', self)
 
185
    """
 
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.
 
189
 
 
190
    Node instances have various attributes synthesized, to make it easy to
 
191
    get and the underlying accessible data. Many more attributes can be
 
192
    added as desired.
 
193
 
 
194
    'name' (read-only string):
 
195
    Wraps Accessible_getName on the Node's underlying Accessible
 
196
 
 
197
    'roleName' (read-only string):
 
198
    Wraps Accessible_getRoleName on the Node's underlying Accessible
 
199
 
 
200
    'role' (read-only atspi role enum):
 
201
    Wraps Accessible_getRole on the Node's underlying Accessible
 
202
 
 
203
    'description' (read-only string):
 
204
    Wraps Accessible_getDescription on the Node's underlying Accessible
 
205
 
 
206
    'parent' (read-only Node instance):
 
207
    A Node instance wrapping the parent, or None. Wraps Accessible_getParent
 
208
 
 
209
    'children' (read-only list of Node instances):
 
210
    The children of this node, wrapping getChildCount and getChildAtIndex
 
211
 
 
212
    'text' (string):
 
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.
 
221
 
 
222
    'passwordText' (write-only string):
 
223
    See documentation of 'text' attribute above.
 
224
 
 
225
    'caretOffset' (read/write int):
 
226
    For instances wrapping AccessibleText, the location of the text caret,
 
227
    expressed as an offset in characters.
 
228
 
 
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.
 
232
 
 
233
    'stateSet' (read-only StateSet instance):
 
234
    Wraps Accessible_getStateSet; a set of boolean state flags
 
235
 
 
236
    'relations' (read-only list of atspi.Relation instances):
 
237
    Wraps Accessible_getRelationSet
 
238
 
 
239
    'labellee' (read-only list of Node instances):
 
240
    The node(s) that this node is a label for. Generated from 'relations'.
 
241
 
 
242
    'labeller' (read-only list of Node instances):
 
243
    The node(s) that is/are a label for this node. Generated from
 
244
    'relations'.
 
245
 
 
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.
 
249
 
 
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.
 
254
 
 
255
    'showing' (read-only boolean):
 
256
    Generated from stateSet based on presence of atspi.SPI_STATE_SHOWING
 
257
 
 
258
    'focusable' (read-only boolean):
 
259
    Generated from stateSet based on presence of atspi.SPI_STATE_FOCUSABLE
 
260
 
 
261
    'focused' (read-only boolean):
 
262
    Generated from stateSet based on presence of atspi.SPI_STATE_FOCUSED
 
263
 
 
264
    'actions' (read-only list of Action instances):
 
265
    Generated from Accessible_getAction and AccessibleAction_getNActions
 
266
 
 
267
    For each action that is supported by a node, a method is hooked up,
 
268
    this can include the following list:
 
269
    'click'
 
270
    'press'
 
271
    'release'
 
272
    'activate'
 
273
    'jump'
 
274
    'check'
 
275
    'dock'
 
276
    'undock'
 
277
    'open'
 
278
    'menu'
 
279
 
 
280
    'extents' (readonly tuple):
 
281
    For instances wrapping a Component, the (x,y,w,h) screen extents of the
 
282
    component.
 
283
 
 
284
    'position' (readonly tuple):
 
285
    For instances wrapping a Component, the (x,y) screen position of the
 
286
    component.
 
287
 
 
288
    'size' (readonly tuple):
 
289
    For instances wrapping a Component, the (w,h) screen size of the component.
 
290
 
 
291
    'grabFocus':
 
292
    For instances wrapping a Component, attempt to set the keyboard input focus
 
293
    to that Node.
 
294
 
 
295
    'toolkit' (readonly string):
 
296
    For instances wrapping an application, the name of the toolkit.
 
297
 
 
298
    'version'
 
299
    For instances wrapping an application.
 
300
 
 
301
    'ID'
 
302
    For instances wrapping an application.
 
303
 
 
304
    'pause' (function)
 
305
    'resume' (function)
 
306
    For instances wrapping an application; probably don't work
 
307
    """
 
308
 
 
309
    #Valid types of AT-SPI objects we wrap.
 
310
    contained = ('__accessible', '__action', '__component', '__text', '__editableText', '__selection')
 
311
 
 
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)
 
320
        else:
 
321
            raise "Unknown Node initializer"
 
322
        assert self.__accessible
 
323
 
 
324
        # Swallow the Action object, if it exists
 
325
        self.__action = self.__accessible.getAction()
 
326
        if self.__action is not None:
 
327
            def doAction(name):
 
328
                """
 
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
 
331
                action.
 
332
                """
 
333
                actions = self.actions
 
334
                if actions.has_key(name):
 
335
                    return actions[name].do()
 
336
                else:
 
337
                    raise ActionNotSupported(name, self)
 
338
            self.doAction = doAction
 
339
 
 
340
        # Swallow the Component object, if it exists
 
341
        self.__component = self.__accessible.getComponent()
 
342
        if self.__component is not None:
 
343
            def grabFocus():
 
344
                self.__component.grabFocus()
 
345
                doDelay()
 
346
            self.grabFocus = grabFocus
 
347
 
 
348
            def rawClick(button = 1):
 
349
                """
 
350
                Generates a raw mouse click event whether or not the Node has a 'click' action, using the specified button.
 
351
                1 is left,
 
352
                2 is middle,
 
353
                3 is right.
 
354
                """
 
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
 
364
 
 
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
 
372
 
 
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
 
377
 
 
378
        # Swallow the Hypertext object, if it exists
 
379
        self.__hypertext = self.__accessible.getHypertext()
 
380
 
 
381
        # Swallow the Selection object, if it exists
 
382
        self.__selection = self.__accessible.getSelection()
 
383
        if self.__selection is not None:
 
384
            def selectAll():
 
385
                """
 
386
                Selects all children.
 
387
                """
 
388
                return self.__selection.selectAll()
 
389
            self.selectAll = selectAll
 
390
 
 
391
            def deselectAll():
 
392
                """
 
393
                Deselects all selected children.
 
394
                """
 
395
                return self.__selection.clearSelection()
 
396
            self.deselectAll = deselectAll
 
397
 
 
398
        # Implement select() for children of nodes with Selection interfaces.
 
399
        parent = self.parent
 
400
        if parent and parent._Node__selection:
 
401
            def select():
 
402
                """
 
403
                Selects the node, relative to its siblings.
 
404
                """
 
405
                return self.parent._Node__selection.selectChild(self.indexInParent)
 
406
            self.select = select
 
407
 
 
408
            def deselect():
 
409
                """
 
410
                Deselects the node, relative to its siblings.
 
411
                """
 
412
                selectedIndex = 0
 
413
                parent = self.parent
 
414
                for i in range(self.indexInParent):
 
415
                    if parent.children[i].isSelected:
 
416
                        selectedIndex+=1
 
417
                return parent._Node__selection.deselectSelectedChild(selectedIndex)
 
418
            self.deselect = select
 
419
 
 
420
        # Add more objects here. Nobody uses them yet, so I haven't.
 
421
        # You also need to change the __getattr__ and __setattr__ functions.
 
422
 
 
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
 
428
 
 
429
    def __getattr__ (self, attr):
 
430
        if False: pass
 
431
 
 
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
 
445
 
 
446
        # Attributes from the Accessible object
 
447
        elif attr == "name":
 
448
            return self.__accessible.getName()
 
449
        elif attr == "role":
 
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 ()
 
457
            if parentAcc:
 
458
                parent = Node (parentAcc)
 
459
                return parent
 
460
        elif attr =="indexInParent":
 
461
            return self.__accessible.getIndexInParent()
 
462
        elif attr == "children":
 
463
            if self.__hideChildren: return
 
464
            children = []
 
465
            for i in xrange (self.__accessible.getChildCount ()):
 
466
                if isinstance(self, Root):
 
467
                    try: a = self.__accessible.getChildAtIndex (i)
 
468
                    except atspi.SpiException:
 
469
                        import traceback
 
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
 
476
            if self.__hypertext:
 
477
                for i in range(self.__hypertext.getNLinks()):
 
478
                    children.append(Link(self, self.__hypertext.getLink(i), i))
 
479
            return children
 
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)
 
498
 
 
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)
 
510
 
 
511
        # Attributes from the Action object
 
512
        elif attr == "actions":
 
513
            actions = {}
 
514
            if self.__action:
 
515
                for i in xrange (self.__action.getNActions ()):
 
516
                    action = (Action (self, self.__action, i))
 
517
                    actions[action.name] = action
 
518
            return actions
 
519
 
 
520
        # Attributes from the Component object
 
521
        elif attr == "extents":
 
522
            if self.__component:
 
523
                return self.__component.getExtents()
 
524
        elif attr == "position":
 
525
            if self.__component:
 
526
                return self.__component.getPosition()
 
527
        elif attr == "size":
 
528
            if self.__component:
 
529
                # This always returns [0, 0]
 
530
                #return self.__component.getSize()
 
531
                extents = self.__component.getExtents()
 
532
                size = (extents[2], extents[3])
 
533
                return size
 
534
 
 
535
        # Attributes from the Text object
 
536
        elif attr == "text":
 
537
            if self.__text:
 
538
                return self.__text.getText(0, 32767)
 
539
        elif attr == "caretOffset":
 
540
            if self.__text:
 
541
                return self.__text.getCaretOffset()
 
542
 
 
543
        # Attributes from the Selection object
 
544
        elif attr == "isSelected":
 
545
            parent = self.parent
 
546
            if parent and parent._Node__selection:
 
547
                return self.parent._Node__selection.isChildSelected(self.indexInParent)
 
548
        elif attr == "selectedChildren":
 
549
            if self.__hideChildren:
 
550
                return
 
551
            selectedChildren = []
 
552
            parent = self.parent
 
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
 
557
 
 
558
        else: raise AttributeError, attr
 
559
 
 
560
    def __setattr__ (self, attr, value):
 
561
        if False: pass
 
562
 
 
563
        # Are we swallowing an AT-SPI object?
 
564
        elif attr.replace('_Node', '') in self.contained:
 
565
            self.__dict__[attr] = value
 
566
 
 
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
 
571
 
 
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
 
577
 
 
578
        # Read-only attributes synthesized from the Accessible's stateSet:
 
579
        elif attr in ["sensitive", "showing", "focusable", "focused", "checked"]:
 
580
            raise ReadOnlyError, attr
 
581
 
 
582
        # Read-only attributes from the Action object
 
583
        elif attr == "actions":
 
584
            raise ReadOnlyError, attr
 
585
 
 
586
        # Attributes from the Component object
 
587
        elif attr in ["extents", "position", "size"]:
 
588
            raise ReadOnlyError, attr
 
589
 
 
590
        # Attributes from the Text object
 
591
        elif attr=="caretOffset":
 
592
            if not self.__text:
 
593
                raise ReadOnlyError, attr
 
594
            self.__text.setCaretOffset(value)
 
595
 
 
596
        # Attributes from the EditableText object
 
597
        elif attr=="text":
 
598
            """
 
599
            Set the text of the node to the given value, with
 
600
            appropriate delays and logging, then test the result:
 
601
            """
 
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)
 
606
            doDelay()
 
607
 
 
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)
 
611
 
 
612
        elif attr=='passwordText':
 
613
            """
 
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.
 
617
            """
 
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)
 
622
            doDelay()
 
623
 
 
624
        elif attr=="combovalue":
 
625
            """
 
626
            Set the combobox to the given value, with appropriate delays and
 
627
            logging.
 
628
            """
 
629
            logger.log("Setting combobox %s to '%s'"%(self.getLogString(), value))
 
630
            self.childNamed(childName=value).click()
 
631
            doDelay()
 
632
        else:
 
633
            # FIXME: should we doing stuff like in the clause above???
 
634
            self.__dict__[attr] = value
 
635
 
 
636
    def typeText(self, string):
 
637
        """
 
638
        Type the given text into the node, with appropriate delays and
 
639
        logging.
 
640
        """
 
641
        logger.log("Typing text into %s: '%s'"%(self.getLogString(), string))
 
642
 
 
643
        # Non-working implementation
 
644
        # Unfortunately, the call to AccessibleText_setCaretOffset fails for Evolution's gtkhtml composer for some reason
 
645
        if False:
 
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)
 
650
 
 
651
        if self.focusable:
 
652
            if not self.focused:
 
653
                try: self.grabFocus()
 
654
                except: logger.log("Node is focusable but I can't grabFocus!")
 
655
            rawinput.typeText(string)
 
656
        else:
 
657
            logger.log("Node is not focusable; falling back to setting text")
 
658
            node.text += string
 
659
            doDelay()
 
660
 
 
661
    def keyCombo(self, comboString):
 
662
        if config.debugSearching: logger.log("Pressing keys '%s' into %s"%(combo, self.getLogString()))
 
663
        if self.focusable:
 
664
            if not self.focused:
 
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)
 
669
 
 
670
    def __str__ (self):
 
671
        """
 
672
        If debugName is set on this Node, returns debugName surrounded
 
673
        in curly braces.
 
674
        Otherwise, returns a plain-text representation of the most
 
675
        important attributes of the underlying Accessible.
 
676
        """
 
677
        if self.debugName:
 
678
            return "{" + self.debugName + "}"
 
679
        else:
 
680
            string = "Node"
 
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
 
685
            return string
 
686
 
 
687
    def getLogString(self):
 
688
        """
 
689
        Get a string describing this node for the logs,
 
690
        respecting the config.absoluteNodePaths boolean.
 
691
        """
 
692
        if config.absoluteNodePaths:
 
693
            return self.getAbsoluteSearchPath()
 
694
        else:
 
695
            return str(self)
 
696
 
 
697
    def satisfies (self, pred):
 
698
        """
 
699
        Does this node satisfy the given predicate?
 
700
        """
 
701
        # the logic is handled by the predicate:
 
702
        assert isinstance(pred, predicate.Predicate)
 
703
        return pred.satisfiedByNode(self)
 
704
 
 
705
    def dump (self, type = 'plain'):
 
706
        import dump
 
707
        dumper = getattr (dump, type)
 
708
        dumper (self)
 
709
 
 
710
    def getAbsoluteSearchPath(self):
 
711
        """
 
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.
 
716
 
 
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.
 
720
 
 
721
        Used by the recording framework for identifying nodes in a
 
722
        persistent way, independent of the style of script being
 
723
        written.
 
724
 
 
725
        FIXME: try to ensure uniqueness
 
726
        FIXME: need some heuristics to get 'good' searches, whatever
 
727
        that means
 
728
        """
 
729
        if config.debugSearchPaths:
 
730
            logger.log("getAbsoluteSearchPath(%s)" % self)
 
731
 
 
732
        if self.roleName=='application':
 
733
            result =path.SearchPath()
 
734
            result.append(predicate.IsAnApplicationNamed(self.name), False)
 
735
            return result
 
736
        else:
 
737
            if self.parent:
 
738
                (ancestor, pred, isRecursive) = self.getRelativeSearch()
 
739
                if config.debugSearchPaths:
 
740
                    logger.log("got ancestor: %s" % ancestor)
 
741
 
 
742
                ancestorPath = ancestor.getAbsoluteSearchPath()
 
743
                ancestorPath.append(pred, isRecursive)
 
744
                return ancestorPath
 
745
            else:
 
746
                # This should be the root node:
 
747
                return path.SearchPath()
 
748
 
 
749
    def getRelativeSearch(self):
 
750
        """
 
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?
 
756
        """
 
757
        if config.debugSearchPaths:
 
758
            logger.log("getRelativeSearchPath(%s)" % self)
 
759
 
 
760
        assert self
 
761
        assert self.parent
 
762
 
 
763
        isRecursive = False
 
764
        ancestor = self.parent
 
765
 
 
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
 
770
            isRecursive = True
 
771
 
 
772
        # Pick the most appropriate predicate for finding this node:
 
773
        if self.labellee:
 
774
            if self.labellee.name:
 
775
                return (ancestor, predicate.IsLabelledAs(self.labellee.name), isRecursive)
 
776
 
 
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)
 
789
        else:
 
790
            pred = predicate.GenericPredicate(name=self.name, roleName=self.roleName)
 
791
            return (ancestor, pred, isRecursive)
 
792
 
 
793
    def __nodeIsIdentifiable(self, ancestor):
 
794
        if ancestor.labellee:
 
795
            return True
 
796
        elif ancestor.name:
 
797
            return True
 
798
        elif not ancestor.parent:
 
799
            return True
 
800
        else:
 
801
            return False
 
802
 
 
803
    # The canonical search method:
 
804
    def findChild(self, pred, recursive = True, debugName = None, retry = True, requireResult = True):
 
805
        """
 
806
        Search for a node satisyfing the predicate, returning a Node.
 
807
 
 
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.
 
811
 
 
812
        If retry is False, it gives up after one attempt.
 
813
 
 
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.
 
816
 
 
817
        FIXME: make multiple attempts?
 
818
        """
 
819
 
 
820
        def findFirstChildSatisfying (parent, pred, recursive = True):
 
821
            """
 
822
            Internal helper function that does a one-shot search, recursing if need be.
 
823
            """
 
824
            # print "findFirstChildSatisfying(%s, %s, recursive=%s)"%(parent, pred,recursive)
 
825
            assert isinstance(pred, predicate.Predicate)
 
826
 
 
827
            try: children = parent.children
 
828
            except: return None
 
829
 
 
830
            for child in children:
 
831
                # print child
 
832
                if child.satisfies(pred):
 
833
                    return child
 
834
                else:
 
835
                    if recursive:
 
836
                        child = findFirstChildSatisfying(child, pred, recursive)
 
837
                        if child: return child
 
838
                # ...on to next child
 
839
 
 
840
        def describeSearch (parent, pred, recursive, debugName):
 
841
            """
 
842
            Internal helper function
 
843
            """
 
844
            if recursive:
 
845
                noun = "descendent"
 
846
            else:
 
847
                noun = "child"
 
848
 
 
849
            if debugName == None:
 
850
                debugName = pred.describeSearchResult()
 
851
 
 
852
            return "%s of %s: %s"%(noun, parent.getLogString(), debugName)
 
853
 
 
854
        assert isinstance(pred, predicate.Predicate)
 
855
        numAttempts = 0
 
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))
 
859
 
 
860
            result = findFirstChildSatisfying(self, pred, recursive)
 
861
            if result:
 
862
                assert isinstance(result, Node)
 
863
                if debugName:
 
864
                    result.debugName = debugName
 
865
                else:
 
866
                    result.debugName = pred.describeSearchResult()
 
867
                return result
 
868
            else:
 
869
                if not retry: break
 
870
                numAttempts+=1
 
871
                if config.debugSearching or config.debugSleep:
 
872
                    logger.log("sleeping for %f" % config.searchBackoffDuration)
 
873
                sleep(config.searchBackoffDuration)
 
874
        if requireResult:
 
875
            raise SearchError(describeSearch(self, pred, recursive, debugName))
 
876
 
 
877
    # The canonical "search for multiple" method:
 
878
    def findChildren(self, pred, recursive = True):
 
879
        """
 
880
        Find all children/descendents satisfying the predicate.
 
881
        """
 
882
        assert isinstance(pred, predicate.Predicate)
 
883
 
 
884
        selfList = []
 
885
 
 
886
        try: children = self.children
 
887
        except: return None
 
888
 
 
889
        for child in children:
 
890
            if child.satisfies(pred): selfList.append(child)
 
891
            if recursive:
 
892
                childList = child.findChildren(pred, recursive)
 
893
                if childList:
 
894
                    for child in childList:
 
895
                        selfList.append(child)
 
896
            # ...on to next child
 
897
 
 
898
        if selfList: return selfList
 
899
 
 
900
    # The canonical "search above this node" method:
 
901
    def findAncestor (self, pred):
 
902
        """
 
903
        Search up the ancestry of this node, returning the first Node
 
904
        satisfying the predicate, or None.
 
905
        """
 
906
        assert isinstance(pred, predicate.Predicate)
 
907
        candidate = self.parent
 
908
        while candidate != None:
 
909
            if candidate.satisfies(pred):
 
910
                return candidate
 
911
            else:
 
912
                candidate = candidate.parent
 
913
        # Not found:
 
914
        return None
 
915
 
 
916
 
 
917
    # Various wrapper/helper search methods:
 
918
    def child (self, name = '', roleName = '', description= '', label = '', recursive=True, debugName=None):
 
919
        """
 
920
        Finds a child satisying the given criteria.
 
921
 
 
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.
 
925
        """
 
926
        return self.findChild (predicate.GenericPredicate(name = name, roleName = roleName, description= description, label = label), recursive = recursive, debugName=debugName)
 
927
 
 
928
    # FIXME: does this clash with the "menu" action
 
929
    def menu(self, menuName, recursive=True):
 
930
        """
 
931
        Search below this node for a menu with the given name.
 
932
 
 
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.
 
936
        """
 
937
        return self.findChild (predicate.IsAMenuNamed(menuName=menuName), recursive)
 
938
 
 
939
    def menuItem(self, menuItemName, recursive=True):
 
940
        """
 
941
        Search below this node for a menu item with the given name.
 
942
 
 
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.
 
946
        """
 
947
        return self.findChild (predicate.IsAMenuItemNamed(menuItemName=menuItemName), recursive)
 
948
 
 
949
    def textentry(self, textEntryName, recursive=True):
 
950
        """
 
951
        Search below this node for a text entry with the given name.
 
952
 
 
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.
 
956
        """
 
957
        return self.findChild (predicate.IsATextEntryNamed(textEntryName=textEntryName), recursive)
 
958
 
 
959
    def button(self, buttonName, recursive=True):
 
960
        """
 
961
        Search below this node for a button with the given name.
 
962
 
 
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.
 
966
        """
 
967
        return self.findChild (predicate.IsAButtonNamed(buttonName=buttonName), recursive)
 
968
 
 
969
    def childLabelled(self, labelText, recursive=True):
 
970
        """
 
971
        Search below this node for a child labelled with the given text.
 
972
 
 
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.
 
976
        """
 
977
        return self.findChild (predicate.IsLabelled(labelText), recursive)
 
978
 
 
979
    def childNamed(self, childName, recursive=True):
 
980
        """
 
981
        Search below this node for a child with the given name.
 
982
 
 
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.
 
986
        """
 
987
        return self.findChild (predicate.IsNamed(childName), recursive)
 
988
 
 
989
    def tab(self, tabName, recursive=True):
 
990
        """
 
991
        Search below this node for a tab with the given name.
 
992
 
 
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.
 
996
        """
 
997
        return self.findChild (predicate.IsATabNamed(tabName=tabName), recursive)
 
998
 
 
999
    def getUserVisibleStrings(self):
 
1000
        """
 
1001
        Get all user-visible strings in this node and its descendents.
 
1002
 
 
1003
        (Could be implemented as an attribute)
 
1004
        """
 
1005
        result=[]
 
1006
        if self.name:
 
1007
            result.append(self.name)
 
1008
        if self.description:
 
1009
            result.append(self.description)
 
1010
        try:
 
1011
            children = self.children
 
1012
        except: return result
 
1013
        for child in children:
 
1014
            result.extend(child.getUserVisibleStrings())
 
1015
        return result
 
1016
 
 
1017
    def blink(self, count = 2):
 
1018
        """
 
1019
        Blink, baby!
 
1020
        """
 
1021
        if not self.extents:
 
1022
            return False
 
1023
        else:
 
1024
            (x, y, w, h) = self.extents
 
1025
            blinkData = Blinker(x, y, w, h, count)
 
1026
            return True
 
1027
 
 
1028
 
 
1029
    def click(self):
 
1030
        """
 
1031
        If the Node supports an action called "click", do it, with appropriate delays and logging.
 
1032
        Otherwise, raise an ActionNotSupported exception.
 
1033
 
 
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().
 
1036
        """
 
1037
        if self.__action is not None:
 
1038
            return self.doAction('click')
 
1039
        raise ActionNotSupported('click', self)
939
1040
 
940
1041
class Link(Node):
941
 
        """
942
 
        Class representing a hyperlink
943
 
        """
944
 
        contained = ('__hyperlink', '__node')
945
 
 
946
 
        def __init__(self, node, hyperlink, index):
947
 
                self.debugName = None
948
 
                self.parent = node
949
 
                self.__hyperlink = hyperlink
950
 
                self.__index = index
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
954
 
        
955
 
        def __getattr__(self, name):
956
 
                if False: pass
957
 
                elif name == 'URI':
958
 
                        # Note: This doesn't seem to work. It usually just causes python to hang.
959
 
                        return self.__hyperlink.getURI(self.__index)
960
 
                else: 
961
 
                        if name == 'children':
962
 
                                raise AttributeError, name
963
 
                        try: 
964
 
                                result = getattr(self.__node, name)
965
 
                                return result
966
 
                        except AttributeError: 
967
 
                                raise AttributeError, name
968
 
 
969
 
        def __setattr__(self, name, value):
970
 
                self.__dict__[name] = value
971
 
                
 
1042
    """
 
1043
    Class representing a hyperlink
 
1044
    """
 
1045
    contained = ('__hyperlink', '__node')
 
1046
 
 
1047
    def __init__(self, node, hyperlink, index):
 
1048
        self.debugName = None
 
1049
        self.parent = node
 
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
 
1055
 
 
1056
    def __getattr__(self, name):
 
1057
        if False: pass
 
1058
        elif name == 'URI':
 
1059
            # Note: This doesn't seem to work. It usually just causes python to hang.
 
1060
            return self.__hyperlink.getURI(self.__index)
 
1061
        else:
 
1062
            if name == 'children':
 
1063
                return
 
1064
            try:
 
1065
                result = getattr(self.__node, name)
 
1066
                return result
 
1067
            except AttributeError:
 
1068
                raise AttributeError, name
 
1069
 
 
1070
    def __setattr__(self, name, value):
 
1071
        self.__dict__[name] = value
 
1072
 
972
1073
class Root (Node):
973
 
        """
974
 
        FIXME:
975
 
        """
976
 
        def applications(self):
977
 
                """
978
 
                Get all applications.
979
 
                """
980
 
                return root.findAllChildrenSatisfying(predicate.GenericPredicate(roleName="application"), recursive=False)
981
 
 
982
 
        def application(self, appName):
983
 
                """
984
 
                Gets an application by name, returning an Application instance 
985
 
                or raising an exception.
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
 
                return Application(root.findChild(predicate.IsAnApplicationNamed(appName),recursive=False))
 
1074
    """
 
1075
    FIXME:
 
1076
    """
 
1077
    def applications(self):
 
1078
        """
 
1079
        Get all applications.
 
1080
        """
 
1081
        return root.findAllChildrenSatisfying(predicate.GenericPredicate(roleName="application"), recursive=False)
 
1082
 
 
1083
    def application(self, appName):
 
1084
        """
 
1085
        Gets an application by name, returning an Application instance
 
1086
        or raising an exception.
 
1087
 
 
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.
 
1091
        """
 
1092
        return Application(root.findChild(predicate.IsAnApplicationNamed(appName),recursive=False))
992
1093
 
993
1094
class Application (Node):
994
 
        def dialog(self, dialogName, recursive=False):
995
 
                """
996
 
                Search below this node for a dialog with the given name,
997
 
                returning a Window instance.
998
 
 
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.
1002
 
                
1003
 
                FIXME: should this method activate the dialog?
1004
 
                """
1005
 
                return self.findChild(predicate.IsADialogNamed(dialogName=dialogName), recursive)
1006
 
 
1007
 
        def window(self, windowName, recursive=False):
1008
 
                """
1009
 
                Search below this node for a window with the given name,
1010
 
                returning a Window instance.
1011
 
 
1012
 
                This is implemented using findChild, and hence will automatically retry
1013
 
                if no such child is found, and will eventually raise an exception. It
1014
 
                also logs the search.           
1015
 
 
1016
 
                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.
1019
 
                """
1020
 
                result = Window(self.findChild (predicate.IsAWindowNamed(windowName=windowName), recursive))
1021
 
                # FIXME: activate the WnckWindow ?
1022
 
                #if gotWnck:
1023
 
                #       result.activate()
1024
 
                return result
1025
 
 
1026
 
        def getWnckApplication(self):
1027
 
                """
1028
 
                Get the wnck.Application instance for this application, or None
1029
 
                
1030
 
                Currently implemented via a hack: requires the app to have a
1031
 
                window, and looks up the application of that window
1032
 
                
1033
 
                wnck.Application can give you the pid, the icon, etc
1034
 
 
1035
 
                FIXME: untested
1036
 
                """
1037
 
                window = child(roleName='frame')
1038
 
                if window:
1039
 
                        wnckWindow = window.getWnckWindow()
1040
 
                        return wnckWindow.get_application()
1041
 
                
1042
 
                
 
1095
    def dialog(self, dialogName, recursive=False):
 
1096
        """
 
1097
        Search below this node for a dialog with the given name,
 
1098
        returning a Window instance.
 
1099
 
 
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.
 
1103
 
 
1104
        FIXME: should this method activate the dialog?
 
1105
        """
 
1106
        return self.findChild(predicate.IsADialogNamed(dialogName=dialogName), recursive)
 
1107
 
 
1108
    def window(self, windowName, recursive=False):
 
1109
        """
 
1110
        Search below this node for a window with the given name,
 
1111
        returning a Window instance.
 
1112
 
 
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.
 
1116
 
 
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.
 
1120
        """
 
1121
        result = Window(self.findChild (predicate.IsAWindowNamed(windowName=windowName), recursive))
 
1122
        # FIXME: activate the WnckWindow ?
 
1123
        #if gotWnck:
 
1124
        #       result.activate()
 
1125
        return result
 
1126
 
 
1127
    def getWnckApplication(self):
 
1128
        """
 
1129
        Get the wnck.Application instance for this application, or None
 
1130
 
 
1131
        Currently implemented via a hack: requires the app to have a
 
1132
        window, and looks up the application of that window
 
1133
 
 
1134
        wnck.Application can give you the pid, the icon, etc
 
1135
 
 
1136
        FIXME: untested
 
1137
        """
 
1138
        window = child(roleName='frame')
 
1139
        if window:
 
1140
            wnckWindow = window.getWnckWindow()
 
1141
            return wnckWindow.get_application()
 
1142
 
 
1143
 
1043
1144
 
1044
1145
class Window (Node):
1045
 
        def getWnckWindow(self):
1046
 
                """
1047
 
                Get the wnck.Window instance for this window, or None
1048
 
                """
1049
 
                # FIXME: this probably needs rewriting:
1050
 
                screen = wnck.screen_get_default()
1051
 
                
1052
 
                # You have to force an update before any of the wnck methods
1053
 
                # do anything:
1054
 
                screen.force_update()
1055
 
                
1056
 
                for wnckWindow in screen.get_windows():
1057
 
                        # FIXME: a dubious hack: search by window title:
1058
 
                        if wnckWindow.get_name()==self.name:
1059
 
                                return wnckWindow
1060
 
 
1061
 
        def activate(self):
1062
 
                """
1063
 
                Activates the wnck.Window associated with this Window.
1064
 
 
1065
 
                FIXME: doesn't yet work
1066
 
                """
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):
 
1147
        """
 
1148
        Get the wnck.Window instance for this window, or None
 
1149
        """
 
1150
        # FIXME: this probably needs rewriting:
 
1151
        screen = wnck.screen_get_default()
 
1152
 
 
1153
        # You have to force an update before any of the wnck methods
 
1154
        # do anything:
 
1155
        screen.force_update()
 
1156
 
 
1157
        for wnckWindow in screen.get_windows():
 
1158
            # FIXME: a dubious hack: search by window title:
 
1159
            if wnckWindow.get_name()==self.name:
 
1160
                return wnckWindow
 
1161
 
 
1162
    def activate(self):
 
1163
        """
 
1164
        Activates the wnck.Window associated with this Window.
 
1165
 
 
1166
        FIXME: doesn't yet work
 
1167
        """
 
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)
1073
1174
 
1074
1175
class Wizard (Window):
1075
 
        """
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.
1081
 
 
1082
 
        This bug is known to affect FC3; fixed in FC5
1083
 
        """
1084
 
        def __init__(self, node, debugName=None):
1085
 
                Node.__init__(self, node)
1086
 
                if debugName:
1087
 
                        self.debugName = debugName
1088
 
                logger.log("%s is on '%s' page"%(self, self.getPageTitle()))
1089
 
 
1090
 
        def currentPage(self):
1091
 
                """
1092
 
                Get the current page of this wizard
1093
 
 
1094
 
                FIXME: this is currently a hack, supporting only GnomeDruid
1095
 
                """
1096
 
                pageHolder = self.child(roleName='panel')
1097
 
                for child in pageHolder.children:
1098
 
                        # current child has SHOWING state set, we hope:
1099
 
                        #print child
1100
 
                        #print child.showing
1101
 
                        if child.showing:
1102
 
                                return child
1103
 
                raise "Unable to determine current page of %s"%self
1104
 
 
1105
 
        def getPageTitle(self):
1106
 
                """
1107
 
                Get the string title of the current page of this wizard
1108
 
 
1109
 
                FIXME: this is currently a total hack, supporting only GnomeDruid
1110
 
                """
1111
 
                currentPage = self.currentPage()
1112
 
                return currentPage.child(roleName='panel').child(roleName='panel').child(roleName='label', recursive=False).text
1113
 
        
1114
 
        def clickForward(self):
1115
 
                """
1116
 
                Click on the 'Forward' button to advance to next page of wizard.
1117
 
 
1118
 
                It will log the title of the new page that is reached.
1119
 
 
1120
 
                FIXME: what if it's Next rather than Forward ???
1121
 
 
1122
 
                This will only work if your libgnomeui has accessible buttons;
1123
 
                see above.
1124
 
                """
1125
 
                fwd = self.child("Forward")
1126
 
                fwd.click()
1127
 
 
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)
1131
 
 
1132
 
        def clickApply(self):
1133
 
                """
1134
 
                Click on the 'Apply' button to advance to next page of wizard.
1135
 
                FIXME: what if it's Finish rather than Apply ???
1136
 
 
1137
 
                This will only work if your libgnomeui has accessible buttons;
1138
 
                see above.
1139
 
                """
1140
 
                fwd = self.child("Apply")
1141
 
                fwd.click()
1142
 
 
1143
 
                # FIXME: debug logging?
 
1176
    """
 
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.
 
1182
 
 
1183
    This bug is known to affect FC3; fixed in FC5
 
1184
    """
 
1185
    def __init__(self, node, debugName=None):
 
1186
        Node.__init__(self, node)
 
1187
        if debugName:
 
1188
            self.debugName = debugName
 
1189
        logger.log("%s is on '%s' page"%(self, self.getPageTitle()))
 
1190
 
 
1191
    def currentPage(self):
 
1192
        """
 
1193
        Get the current page of this wizard
 
1194
 
 
1195
        FIXME: this is currently a hack, supporting only GnomeDruid
 
1196
        """
 
1197
        pageHolder = self.child(roleName='panel')
 
1198
        for child in pageHolder.children:
 
1199
            # current child has SHOWING state set, we hope:
 
1200
            #print child
 
1201
            #print child.showing
 
1202
            if child.showing:
 
1203
                return child
 
1204
        raise "Unable to determine current page of %s"%self
 
1205
 
 
1206
    def getPageTitle(self):
 
1207
        """
 
1208
        Get the string title of the current page of this wizard
 
1209
 
 
1210
        FIXME: this is currently a total hack, supporting only GnomeDruid
 
1211
        """
 
1212
        currentPage = self.currentPage()
 
1213
        return currentPage.child(roleName='panel').child(roleName='panel').child(roleName='label', recursive=False).text
 
1214
 
 
1215
    def clickForward(self):
 
1216
        """
 
1217
        Click on the 'Forward' button to advance to next page of wizard.
 
1218
 
 
1219
        It will log the title of the new page that is reached.
 
1220
 
 
1221
        FIXME: what if it's Next rather than Forward ???
 
1222
 
 
1223
        This will only work if your libgnomeui has accessible buttons;
 
1224
        see above.
 
1225
        """
 
1226
        fwd = self.child("Forward")
 
1227
        fwd.click()
 
1228
 
 
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)
 
1232
 
 
1233
    def clickApply(self):
 
1234
        """
 
1235
        Click on the 'Apply' button to advance to next page of wizard.
 
1236
        FIXME: what if it's Finish rather than Apply ???
 
1237
 
 
1238
        This will only work if your libgnomeui has accessible buttons;
 
1239
        see above.
 
1240
        """
 
1241
        fwd = self.child("Apply")
 
1242
        fwd.click()
 
1243
 
 
1244
        # FIXME: debug logging?
1144
1245
 
1145
1246
try:
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?"
1151
 
 
1152
 
try:
1153
 
        # Check that there are applications running. Warn if none are.
1154
 
        test = root.children
1155
 
        del test
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?"
1158
 
 
1159
 
# This is my poor excuse for a unit test.
1160
 
if __name__ == '__main__':
1161
 
        import tc
1162
 
        f=root.findChild(name="File", roleName="menu")
1163
 
        case = tc.TCNumber()
1164
 
        case.compare("File menu text", 4, len(f.text), "float")
1165
 
        print case.result
 
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?")
 
1252
 
 
1253
# Check that there are applications running. Warn if none are.
 
1254
children = root.children
 
1255
if not children:
 
1256
    logger.log("Warning: AT-SPI's desktop is visible but it has no children. Are you running any AT-SPI-aware applications?")
 
1257
del children
1166
1258
 
1167
1259
# Convenient place to set some debug variables:
1168
1260
#config.debugSearching = True