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

« back to all changes in this revision

Viewing changes to recorder/dogtail-recorder

  • 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
__author__ = 'David Malcolm <dmalcolm@redhat.com>, Zack Cerza <zcerza@redhat.com>'
 
3
appName = 'Script Recorder'
 
4
import os
 
5
os.environ['GTK_MODULES']=''
 
6
 
 
7
from dogtail.utils import checkForA11yInteractively
 
8
checkForA11yInteractively()
 
9
 
 
10
import atspi
 
11
import dogtail.tree
 
12
import gtk.gdk
 
13
import dogtail.rawinput
 
14
import re
 
15
 
 
16
# Begin GUI code
 
17
import threading
 
18
class PlaybackThread(threading.Thread):
 
19
    def __init__(self, script):
 
20
        threading.Thread.__init__(self)
 
21
        self.script = script
 
22
 
 
23
    def run(self):
 
24
        exec self.script
 
25
 
 
26
import gobject
 
27
import gnome
 
28
import gtk.glade
 
29
 
 
30
try:
 
31
    import gtksourceview
 
32
    useGtkSourceView = True
 
33
except:
 
34
    useGtkSourceView = False
 
35
 
 
36
def createSourceView():
 
37
    langManager = gtksourceview.SourceLanguagesManager()
 
38
    lang = langManager.get_language_from_mime_type("text/x-python")
 
39
    buffer = gtksourceview.SourceBuffer()
 
40
    sourceView = gtksourceview.SourceView(buffer)
 
41
    buffer.set_language(lang)
 
42
    buffer.set_highlight(True)
 
43
    return sourceView
 
44
 
 
45
class RecorderGUI(gnome.Program):
 
46
    def __init__(self):
 
47
        gnome.Program.__init__(self)
 
48
        appAuthors = ['Zack Cerza <zcerza@redhat.com>']
 
49
        program = gnome.program_init(appName, '0.1')
 
50
 
 
51
        if os.path.exists('recorder.glade'):
 
52
            x = gtk.glade.XML('recorder.glade')
 
53
        else:
 
54
            import sys
 
55
            exec_root = sys.argv[0].split("/bin/")[0]
 
56
            if exec_root[0] is not '/':
 
57
                exec_root = "/usr"
 
58
            x = gtk.glade.XML(exec_root + '/share/dogtail/glade/recorder.glade')
 
59
 
 
60
        self.window = x.get_widget('window')
 
61
 
 
62
        try:
 
63
            self.window.set_icon_from_file('../icons/dogtail-head.svg')
 
64
        except:
 
65
            self.window.set_icon_from_file('/usr/share/icons/hicolor/scalable/apps/dogtail-head.svg')
 
66
 
 
67
        self.recordButton = x.get_widget('recordButton')
 
68
        self.playButton = x.get_widget('playButton')
 
69
        self.playButton.set_sensitive(False)
 
70
        self.clearButton = x.get_widget('clearButton')
 
71
        self.clearButton.set_sensitive(False)
 
72
        self.saveButton = x.get_widget('saveButton')
 
73
        self.saveButton.set_sensitive(False)
 
74
        if useGtkSourceView:
 
75
            oldTextView = x.get_widget('scriptTextView')
 
76
            parent = oldTextView.get_parent()
 
77
            parent.remove(oldTextView)
 
78
            sourceView = createSourceView()
 
79
            parent.add(sourceView)
 
80
            sourceView.show()
 
81
            self.scriptTextView = sourceView
 
82
        else:
 
83
            self.scriptTextView = x.get_widget('scriptTextView')
 
84
        self.scriptTextView.set_editable(False)
 
85
        #self.writerComboBox = x.get_widget('writerComboBox')
 
86
        #self.writerComboBox.set_active(0)
 
87
        #self.writerComboBox.set_sensitive(True)
 
88
 
 
89
        # The following line added because self.writerComboBox is gone:
 
90
        recorder.writerClass = ProceduralScriptWriter
 
91
 
 
92
        self.connectSignals()
 
93
 
 
94
        self.window.show_all()
 
95
        gtk.main()
 
96
 
 
97
    def connectSignals(self):
 
98
        #self.writerComboBox.connect('changed', self.setWriterClass)
 
99
        self.recordButton.connect('clicked', self.toggleRecording, self.scriptTextView)
 
100
        self.playButton.connect('clicked', self.playScript, self.scriptTextView)
 
101
        self.clearButton.connect('clicked', self.clearScript, self.scriptTextView)
 
102
        self.saveButton.connect('clicked', self.saveScript)
 
103
        self.window.connect('delete_event', self.quit)
 
104
 
 
105
    def setWriterClass (self, comboBox):
 
106
        selected = comboBox.get_active_text()
 
107
        if selected == "Procedural":
 
108
            recorder.writerClass = ProceduralScriptWriter
 
109
        elif selected == "Object-Oriented":
 
110
            recorder.writerClass = OOScriptWriter
 
111
        else:
 
112
            print selected, "isn't a ScriptWriter, but it is selected. How?"
 
113
 
 
114
    def toggleRecording(self, recordButton = None, scriptTextView = None):
 
115
        label = self.recordButton.get_label()
 
116
        recordID = 'gtk-media-record'
 
117
        stopID = 'gtk-media-stop'
 
118
        
 
119
        if label == recordID:
 
120
            #self.writerComboBox.set_sensitive(False)
 
121
            self.playButton.set_sensitive(False)
 
122
            self.clearButton.set_sensitive(False)
 
123
            self.saveButton.set_sensitive(False)
 
124
            self.scriptTextView.set_editable(False)
 
125
            self.recordButton.set_label(stopID)
 
126
            recorder.startRecording(self.recordButton, self.scriptTextView)
 
127
        
 
128
        elif label == stopID:
 
129
            #self.writerComboBox.set_sensitive(True)
 
130
            self.playButton.set_sensitive(True)
 
131
            self.clearButton.set_sensitive(True)
 
132
            self.saveButton.set_sensitive(True)
 
133
            self.scriptTextView.set_editable(True)
 
134
            self.recordButton.set_label(recordID)
 
135
            recorder.stopRecording(self.recordButton, self.scriptTextView)
 
136
 
 
137
    def stopRecording(self):
 
138
        self.recordButton.set_label('gtk-media-stop')
 
139
        self.toggleRecording()
 
140
 
 
141
    def playScript(self, button = None, scriptTextView = None):
 
142
        self.recordButton.set_sensitive(False)
 
143
        self.playButton.set_sensitive(False)
 
144
        self.scriptTextView.set_editable(False)
 
145
 
 
146
        buffer = self.scriptTextView.get_buffer()
 
147
        startIter = buffer.get_start_iter()
 
148
        endIter = buffer.get_end_iter()
 
149
        scriptText = buffer.get_text(startIter, endIter)
 
150
 
 
151
        self.playbackThread = PlaybackThread(scriptText)
 
152
        self.playbackThread.start()
 
153
        self.playbackThread.join()
 
154
 
 
155
        self.playButton.set_sensitive(True)
 
156
        self.recordButton.set_sensitive(True)
 
157
        self.scriptTextView.set_editable(True)
 
158
 
 
159
    def clearScript(self, button = None, scriptTextView = None):
 
160
        self.scriptTextView.get_buffer().set_text('')
 
161
        self.clearButton.set_sensitive(False)
 
162
        self.saveButton.set_sensitive(False)
 
163
 
 
164
    def saveScript(self, button):
 
165
        """
 
166
        Brings up a file chooser dialog asking where to save the script.
 
167
        """
 
168
        self.saveFileChooser = gtk.FileChooserDialog("Save Script...", None, \
 
169
            gtk.FILE_CHOOSER_ACTION_SAVE, (gtk.STOCK_CANCEL, \
 
170
            gtk.RESPONSE_CANCEL, gtk.STOCK_SAVE, gtk.RESPONSE_OK))
 
171
        self.saveFileChooser.set_default_response(gtk.RESPONSE_OK)
 
172
 
 
173
        # Why this isn't default, I do not understand.
 
174
        self.saveFileChooser.set_do_overwrite_confirmation(True)
 
175
 
 
176
        filter = gtk.FileFilter()
 
177
        filter.set_name('Python files')
 
178
        filter.add_pattern('*.py')
 
179
        self.saveFileChooser.add_filter(filter)
 
180
        filter = gtk.FileFilter()
 
181
        filter.set_name('All Files')
 
182
        filter.add_pattern('*')
 
183
        self.saveFileChooser.add_filter(filter)
 
184
 
 
185
        response = self.saveFileChooser.run()
 
186
        if response == gtk.RESPONSE_OK:
 
187
            fileName = self.saveFileChooser.get_filename()
 
188
            # Append a .py to the file name if necessary
 
189
            if fileName[-3:] != '.py':
 
190
                fileName += '.py'
 
191
            file = open(fileName, 'w')
 
192
            buffer = self.scriptTextView.get_buffer()
 
193
            startIter = buffer.get_start_iter()
 
194
            endIter = buffer.get_end_iter()
 
195
            scriptText = buffer.get_text(startIter, endIter)
 
196
            file.write(scriptText)
 
197
            file.close()
 
198
        self.saveFileChooser.destroy()
 
199
 
 
200
    def quit(self, *args):
 
201
        self.stopRecording()
 
202
        gtk.main_quit()
 
203
 
 
204
# End GUI code
 
205
 
 
206
def logEvent(event):
 
207
    source = event.source
 
208
    if isinstance(source, atspi.Accessible):
 
209
        sourceStr = " source:%s"%(str(dogtail.tree.Node(source)))
 
210
    else:
 
211
        sourceStr = ""
 
212
    print "Got event: %s%s"%(event.type, sourceStr)
 
213
 
 
214
class ScriptWriter:
 
215
    """
 
216
    Abstract Writer subclass which writes out Python scripts
 
217
    """
 
218
    def __init__(self, scriptTextView = None):
 
219
        self.scriptBuffer = ""
 
220
        self.scriptTextView = scriptTextView
 
221
        self.debug = False
 
222
 
 
223
    def recordLine(self, string):
 
224
        print string
 
225
        if self.scriptTextView:
 
226
            buffer = self.scriptTextView.get_buffer()
 
227
            iter = buffer.get_end_iter()
 
228
            if buffer.get_line_count() > 1:
 
229
                string = '\n' + string
 
230
            buffer.insert(iter, string)
 
231
            
 
232
            # Scroll to the end
 
233
            iter = buffer.get_end_iter()
 
234
            mark = buffer.create_mark('end', iter, True)
 
235
            self.scriptTextView.scroll_mark_onscreen(mark)
 
236
            buffer.delete_mark(mark)
 
237
        else:
 
238
            self.scriptBuffer += '\n' + string
 
239
 
 
240
    def recordClick(self, node):
 
241
        raise NotImplementedError
 
242
 
 
243
    def recordTyping(self, string, type, node):
 
244
        raise NotImplementedError
 
245
 
 
246
    def recordKeyCombo(self, string, type, node):
 
247
        raise NotImplementedError
 
248
 
 
249
class OOScriptWriter(ScriptWriter):
 
250
    """
 
251
    Concrete Writer subclass which writes out Python scripts in an object-oriented
 
252
    style
 
253
    """
 
254
    def __init__(self, scriptTextView = None):
 
255
        ScriptWriter.__init__(self, scriptTextView)
 
256
 
 
257
        self.debugVariables = False
 
258
 
 
259
        self.recordLine("#!/usr/bin/python\nfrom dogtail.tree import *\n")
 
260
 
 
261
        # maintain a dict from variable names to search paths
 
262
        self.variables = {}
 
263
 
 
264
    def generateVariableName(self, predicate):
 
265
        # Ensure uniqueness
 
266
        result = predicate.makeScriptVariableName()
 
267
        if result in self.variables:
 
268
            # This variable name is already in use; need to append a number:
 
269
            index = 1
 
270
            while result+str(index) in self.variables:
 
271
                index+=1
 
272
            return result+str(index)
 
273
        else:
 
274
            return result
 
275
 
 
276
    def printVariables(self):
 
277
        # debug hook
 
278
        print "variables:"
 
279
        for (varName, varAbsPath) in self.variables.iteritems():
 
280
            print "varName:%s -> absPath:%s"%(varName, varAbsPath)
 
281
 
 
282
    def generateAbsSearchPathMethodCall(self, absSearchPath):
 
283
        """
 
284
        Generates a method call that identifies the absolute search path,
 
285
        optimizing away prefixes where possible with variable names.
 
286
        """
 
287
        # We optimize away the longest common absolute path prefix, i.e. the
 
288
        # shortest relative path suffix:
 
289
        if self.debug:
 
290
            print "*******************"
 
291
            print "generateAbsSearchPathMethodCall for %s"%absSearchPath
 
292
            self.printVariables()
 
293
 
 
294
        shortestRelativePath = None
 
295
        for (varName, varAbsPath) in self.variables.iteritems():
 
296
            relPath = varAbsPath.getRelativePath(absSearchPath)
 
297
            if relPath:
 
298
                if shortestRelativePath:
 
299
                    if relPath.length() < shortestRelativePath[2].length():
 
300
                        shortestRelativePath = (varName, varAbsPath, relPath)
 
301
                else:
 
302
                    shortestRelativePath = (varName, varAbsPath, relPath)
 
303
 
 
304
        if self.debug:
 
305
            if shortestRelativePath:
 
306
                (varName, varAbsPath, relPath) = shortestRelativePath
 
307
                print "shortestRelativePath: (%s, %s, %s)"%(varName, varAbsPath, relPath)
 
308
            else:
 
309
                print "shortestRelativePath: None"
 
310
            print "*******************"
 
311
 
 
312
        if shortestRelativePath:
 
313
            (varName, varAbsPath, relPath) = shortestRelativePath
 
314
            return varName+relPath.makeScriptMethodCall()
 
315
        else:
 
316
            # Have to specify it as an absolute path:
 
317
            return "root"+absSearchPath.makeScriptMethodCall()
 
318
 
 
319
    def recordClick(self, node):
 
320
        """
 
321
        Record a mouse click
 
322
        """
 
323
        if node == None: return
 
324
        searchPath = node.getAbsoluteSearchPath()
 
325
 
 
326
        if self.debug:
 
327
            print "----------------------------------"
 
328
            print "click on %s"%searchPath
 
329
            print "Full script would be: root%s"%searchPath.makeScriptMethodCall()
 
330
 
 
331
        # Generate variables for nodes likely to be referred to often (application, window)
 
332
        # FIXME: make this smarter?
 
333
        for i in [1,2,3]:
 
334
            if i<searchPath.length():
 
335
 
 
336
                prefixPath = searchPath.getPrefix(i)
 
337
 
 
338
                if self.debugVariables:
 
339
                    print "Considering: %s"%prefixPath
 
340
 
 
341
                if not prefixPath in self.variables.values():
 
342
                    if self.debugVariables:
 
343
                        print "It is not yet a variable"
 
344
                        self.printVariables()
 
345
 
 
346
                    predicate = prefixPath.getPredicate(i-1)
 
347
                    varName = predicate.makeScriptVariableName()
 
348
                    self.recordLine(varName+" = "+self.generateAbsSearchPathMethodCall(prefixPath))
 
349
                    self.variables[varName]=prefixPath
 
350
                else:
 
351
                    if self.debugVariables:
 
352
                        print "It is already a variable"
 
353
 
 
354
        result = self.generateAbsSearchPathMethodCall(searchPath)
 
355
        result +=".click()"
 
356
 
 
357
        if self.debug:
 
358
            print "----------------------------------"
 
359
 
 
360
        self.recordLine(result)
 
361
 
 
362
class ProceduralScriptWriter(ScriptWriter):
 
363
    """
 
364
    Concrete Writer subclass which writes out Python scripts in a procedural
 
365
    style
 
366
    """
 
367
    
 
368
    currentWidget = None
 
369
    currentDialog = None
 
370
    currentFrame = None
 
371
    currentApplication = None
 
372
 
 
373
    def __init__(self, scriptTextView = None):
 
374
        ScriptWriter.__init__(self, scriptTextView)
 
375
 
 
376
        self.recordLine("#!/usr/bin/python\nfrom dogtail.procedural import *\n")
 
377
    
 
378
    def setUpFocus(self, node):
 
379
        """
 
380
        Writes out the necessary focus.application() and focus.dialog() lines 
 
381
        to the script.
 
382
        """
 
383
        application = FakeNode.findAncestor(node, roleName = 'application')
 
384
        if application:
 
385
            needApp = True
 
386
            if self.currentApplication:
 
387
                if application == self.currentApplication: needApp = False
 
388
            if needApp:
 
389
                self.recordLine("focus.application('%s')" % application.name)
 
390
                self.currentApplication = application
 
391
        elif application == None:
 
392
            print "Warning: could not determine which application you are clicking or typing on."
 
393
            print "    It is most likely not reporting its toplevel Accessible as having a"
 
394
            print "    role name of 'application'. Please file a bug against it!"
 
395
 
 
396
        dialog = FakeNode.findAncestor(node, roleName = 'dialog')
 
397
        if dialog:
 
398
            needDialog = True
 
399
            if dialog == self.currentDialog: needDialog = False
 
400
            if needDialog:
 
401
                self.recordLine("focus.dialog('%s')" % dialog.name)
 
402
                self.currentDialog = dialog
 
403
        else:
 
404
            frame = FakeNode.findAncestor(node, roleName='frame')
 
405
            if frame:
 
406
                needFrame = True
 
407
                if frame == self.currentFrame: needFrame = False
 
408
                if needFrame:
 
409
                    self.recordLine("focus.frame('%s')" % frame.name)
 
410
                    self.currentFrame = frame
 
411
 
 
412
        
 
413
    def recordClick(self, node, button):
 
414
        if node == None: return False
 
415
 
 
416
        self.setUpFocus(node)
 
417
        
 
418
        widget = node
 
419
        #possibleActions = ['click', 'activate', 'open', 'menu']
 
420
        possibleActions = ['click', 'activate', 'menu']
 
421
        foundAnAction = False
 
422
        for action in possibleActions:
 
423
            if action in widget.actions.keys():
 
424
                if button == 1 and action == 'menu': break
 
425
                foundAnAction = True
 
426
                self.recordLine("%s('%s', roleName='%s')" % \
 
427
                        (action, widget.name.replace('\n','\\n'), widget.roleName))
 
428
                break
 
429
        if not foundAnAction: 
 
430
            if hasattr(widget, 'select') and button != 1:
 
431
                self.recordLine("select('%s', roleName='%s')" % \
 
432
                        (widget.name.replace('\n','\\n'), widget.roleName))
 
433
            else:
 
434
                if button != 1: btn = ', button = ' + str(button)
 
435
                else: btn = ''
 
436
                s = "click('%s', roleName='%s', raw=True%s)" % \
 
437
                        (widget.name.replace('\n','\\n'), widget.roleName, btn)
 
438
                self.recordLine(s)
 
439
        self.currentWidget = widget
 
440
        
 
441
    def recordTyping(self, string, type, node):
 
442
        if not string: return
 
443
        self.setUpFocus(node)
 
444
        self.recordLine("type(\"" + string.replace('"','\\"') + "\")")
 
445
 
 
446
    def recordKeyCombo(self, string, type, node):
 
447
        if not string: return
 
448
        self.setUpFocus(node)
 
449
        self.recordLine("keyCombo(\"" + string + "\")")
 
450
 
 
451
class FakeNode(dogtail.tree.Node):
 
452
    """A "cached pseudo-copy" of a Node
 
453
 
 
454
    This class exists for cases where we know we're going to need information
 
455
    about a Node instance at a point in time where it's no longer safe to
 
456
    assume that the Accessible it wraps is still valid. It is designed to
 
457
    cache enough information to allow all of the necessary Node methods to
 
458
    execute properly and return something meaningful.
 
459
 
 
460
    As it is often necessary to know the Node instance's parent, it creates
 
461
    FakeNode instances of each and every one of its ancestors.
 
462
    """
 
463
    def __init__(self, node):
 
464
        if node == None: raise TypeError, node
 
465
 
 
466
        self.__node = node
 
467
        self.name = self.__node.name
 
468
        self.roleName = self.__node.roleName
 
469
        self.description = self.__node.description
 
470
        self.actions = self.__node.actions
 
471
        self.debugName = self.__node.debugName
 
472
 
 
473
        self.text = self.__node.text
 
474
 
 
475
        self.position = self.__node.position
 
476
 
 
477
        self.size = self.__node.size
 
478
 
 
479
        if node.parent: self.parent = FakeNode(self.__node.parent)
 
480
        else: self.parent = None
 
481
 
 
482
        if node.labellee: self.labellee = FakeNode(self.__node.labellee)
 
483
        else: self.labellee = None
 
484
 
 
485
    def __getattr__(self, name):
 
486
        raise AttributeError, name
 
487
 
 
488
    def __setattr__(self, name, value):
 
489
        self.__dict__[name] = value
 
490
 
 
491
    def __cmp__(self, otherNode):
 
492
        if not otherNode: return True
 
493
 
 
494
        nameMatch = False
 
495
        roleNameMatch = False
 
496
        descMatch = False
 
497
        nameMatch = otherNode.name == self.name
 
498
        roleNameMatch = otherNode.roleName == self.roleName
 
499
        descMatch = otherNode.description == self.description
 
500
        match = nameMatch and roleNameMatch and descMatch
 
501
        return not match
 
502
 
 
503
    def findAncestor(node, name = None, roleName = None, description = None):
 
504
        while node.parent:
 
505
            nameMatch = False
 
506
            roleNameMatch = False
 
507
            descMatch = False
 
508
            if name != None: nameMatch = node.parent.name == name
 
509
            else: nameMatch = True
 
510
            if roleName != None: roleNameMatch = node.parent.roleName == roleName
 
511
            else: roleNameMatch = True
 
512
            if description != None: 
 
513
                descMatch = node.parent.description == description
 
514
            else: descMatch = True
 
515
            match = nameMatch and roleNameMatch and descMatch
 
516
            if match: return node.parent
 
517
            node = node.parent
 
518
        return None
 
519
    findAncestor = staticmethod(findAncestor)
 
520
 
 
521
 
 
522
 
 
523
# Singleton EventRecorder
 
524
global recorder
 
525
 
 
526
class EventRecorder:
 
527
    """
 
528
    Event Recorder
 
529
    """
 
530
    modifiers = {
 
531
            atspi.Accessibility_MODIFIER_NUMLOCK: 'NumLock',
 
532
            atspi.Accessibility_MODIFIER_META3: 'Meta3',
 
533
            atspi.Accessibility_MODIFIER_META2: 'Meta2',
 
534
            atspi.Accessibility_MODIFIER_META: 'Meta',
 
535
            atspi.Accessibility_MODIFIER_ALT: 'Alt',
 
536
            atspi.Accessibility_MODIFIER_CONTROL: 'Control',
 
537
            atspi.Accessibility_MODIFIER_SHIFTLOCK: 'ShiftLock',
 
538
            atspi.Accessibility_MODIFIER_SHIFT: 'Shift' }
 
539
 
 
540
    def __init__(self, writerClass = ProceduralScriptWriter):
 
541
        self.writer = None
 
542
        self.writerClass = writerClass
 
543
        self.lastFocusedNode = None
 
544
        self.lastSelectedNode = None
 
545
        self.lastPressedNode = None
 
546
        self.lastReleasedNode = None
 
547
        self.typedTextBuffer = ""
 
548
        self.lastTypedNode = None
 
549
        self.absoluteNodePaths = True
 
550
        self.listeners = []
 
551
 
 
552
        import gtk.keysyms
 
553
 
 
554
    def __registerEvents(self):
 
555
        # Only specific events are recorded:
 
556
        listeners = []
 
557
 
 
558
        # Focus events:
 
559
        listeners.append(atspi.EventListener(marshalOnFocus, ["focus:"]))
 
560
 
 
561
        # State Changed events:
 
562
        listeners.append(atspi.EventListener(marshalOnSelect, ["object:state-changed:selected"]))
 
563
 
 
564
        # Mouse button events:
 
565
        listeners.append(atspi.EventListener(marshalOnMouseButton, ["mouse:button"]))
 
566
 
 
567
        # Keystroke events:
 
568
        listeners.append(atspi.DeviceListener(marshalOnKeyPress))
 
569
 
 
570
        # Window creation:
 
571
        #listeners.append(atspi.EventListener(marshalOnWindowCreate, ["window:create"]))
 
572
 
 
573
        return listeners
 
574
 
 
575
    def startRecording(self, unused = None, scriptTextView = None):
 
576
        self.writer = self.writerClass(scriptTextView)
 
577
        self.listeners = self.__registerEvents()
 
578
        # set lastKeyPressTimeStamp to 1, which is an invalid value.
 
579
        self.lastKeyPressTimeStamp = 1
 
580
        atspi.event_main()
 
581
 
 
582
    def stopRecording(self, unused = None, scriptTextView = None):
 
583
        for listener in self.listeners:
 
584
            listener.deregister()
 
585
        atspi.event_quit()
 
586
        self.writer = None
 
587
 
 
588
    def onFocus(self, event):
 
589
        sourceNode = dogtail.tree.Node(event.source)
 
590
        sourceNode = FakeNode(sourceNode)
 
591
        #if sourceNode == self.lastPressedNode or \
 
592
        #        sourceNode == self.lastReleasedNode:
 
593
        #    sourceNode._FakeNode__node.blink()
 
594
        self.lastFocusedNode = sourceNode
 
595
 
 
596
    def onSelect(self, event):
 
597
        sourceNode = dogtail.tree.Node(event.source)
 
598
        sourceNode = FakeNode(sourceNode)
 
599
        self.lastSelectedNode = sourceNode
 
600
 
 
601
    def onMouseButton(self, event):
 
602
        #logEvent(event)
 
603
        self.writer.recordTyping(self.typedTextBuffer, "pressed", self.lastFocusedNode)
 
604
        self.typedTextBuffer = ""
 
605
 
 
606
        isPress = isRelease = False
 
607
        g=re.match('mouse:button:(\d)(p|r)', event.type).groups()
 
608
        button = int(g[0])
 
609
        if g[1] == 'p': isPress = True
 
610
        elif g[1] == 'r': isRelease = True
 
611
 
 
612
        # The source node is always "main" - which sucks. We have to detect
 
613
        # the real source ourselves.
 
614
 
 
615
        def detectByCoordinates(nodes, x, y):
 
616
            # From a list of nodes, find the smallest one that (x, y) is in
 
617
            def isCandidate(node, x, y):
 
618
                # If (x, y) is inside the node, return True
 
619
                if node and node.position:
 
620
                    #print "x,y: %s, %s" % (x, y)
 
621
                    #print "position: %s, size: %s" % (node.position, node.size)
 
622
                    if node.position[0] <= x <= (node.position[0] + node.size[0]) and \
 
623
                            node.position[1] <= y <= (node.position[1] + node.size[1]):
 
624
                        return True
 
625
            def getAllDescendants(node):
 
626
                result = []
 
627
                for child in node.children:
 
628
                    result.append(child)
 
629
                    result.extend(getAllDescendants(child))
 
630
                    #for grandChild in child.children:
 
631
                    #    result.append(grandChild)
 
632
                return result
 
633
            def smallestNode(nodes):
 
634
                # Return the node with the smallest area
 
635
                areas = {}
 
636
                for node in nodes:
 
637
                    area = node.size[0] * node.size[1]
 
638
                    if areas.get(area, None):
 
639
                        print "Two nodes are the same size?!"
 
640
                        print str(areas[area]) + "|" + str(node)
 
641
                    areas[area] = node
 
642
                if areas:
 
643
                    return areas[min(areas.keys())]
 
644
 
 
645
            detectedNode = None
 
646
            for node in nodes:
 
647
                if isCandidate(node, x, y):
 
648
                    detectedNode = node
 
649
                    # table children don't send focus signals, so we have to
 
650
                    # find the child itself.
 
651
                    if node.roleName == 'table':
 
652
                        detectedNode = None
 
653
                        if not hasattr(node, 'children'):
 
654
                            node = node._FakeNode__node
 
655
                        # getAllDescendants() is very expensive :(
 
656
                        possibleNodes = getAllDescendants(node)
 
657
                        probableNodes = [n for n in possibleNodes if \
 
658
                                isCandidate(n, x, y)]
 
659
                        detectedNode = smallestNode(probableNodes)
 
660
            return detectedNode
 
661
 
 
662
        x = event.detail1
 
663
        y = event.detail2
 
664
        if self.lastSelectedNode != self.lastFocusedNode:
 
665
            possibleNodes = (self.lastSelectedNode, self.lastFocusedNode)
 
666
        else:
 
667
            # If self.lastSelectedNode isn't meaningful, don't waste time on 
 
668
            # it with detectByCoordinates().
 
669
            possibleNodes = [self.lastFocusedNode]
 
670
        detectedNode = detectByCoordinates(possibleNodes, x, y)
 
671
        if detectedNode and ((detectedNode.name == appName and \
 
672
                detectedNode.roleName == 'frame') or \
 
673
                FakeNode.findAncestor(detectedNode, \
 
674
                roleName = 'frame', name = appName)):
 
675
            self.lastPressedNode = None
 
676
            self.lastReleasedNode = None
 
677
            return
 
678
        if detectedNode and not isinstance(detectedNode, FakeNode):
 
679
            detectedNode = FakeNode(detectedNode)
 
680
        if isPress: 
 
681
            self.lastPressedNode = detectedNode
 
682
        elif isRelease:
 
683
            self.lastReleasedNode = detectedNode
 
684
        
 
685
        if isRelease and detectedNode:
 
686
            self.writer.recordClick(detectedNode, button)
 
687
 
 
688
    def __checkModMask(fullMask, partMask, toCheck):
 
689
        result = False
 
690
        if fullMask == 0 or partMask == 0:
 
691
            return (partMask, result)
 
692
        if partMask - toCheck >= 0:
 
693
            partMask = partMask - toCheck
 
694
            result = True
 
695
        return (partMask, result)
 
696
    __checkModMask = staticmethod(__checkModMask)
 
697
 
 
698
    def __getModifiers(self, modMask, keyChar):
 
699
        partMask = modMask
 
700
        modsDict = {}
 
701
        keys = self.modifiers.keys()
 
702
        keys.sort()
 
703
        for i in keys[::-1]:
 
704
            (partMask, pressed) = self.__checkModMask(modMask, partMask, i)
 
705
            if pressed: modsDict[self.modifiers[i]] = i
 
706
        # Screw capslock.
 
707
        if modsDict.has_key('ShiftLock'):
 
708
            del modsDict['ShiftLock']
 
709
        # Shift+foo is not considered a key combo if foo is printable
 
710
        if modsDict.has_key('Shift') and not keyChar == '':
 
711
            del modsDict['Shift']
 
712
        return modsDict
 
713
 
 
714
    def onKeyPress(self, event):
 
715
        # The Fn key on my Thinkpad has a keyID of 0. Ignore it.
 
716
        if not event.keyID: return
 
717
        if event.type == atspi.SPI_KEY_PRESSED:
 
718
            type = "pressed"
 
719
        elif event.type == atspi.SPI_KEY_RELEASED:
 
720
            type = "released"
 
721
            return
 
722
 
 
723
        self.lastKeyPressTimeStamp = event.timeStamp
 
724
        if self.lastKeyPressTimeStamp < 0:
 
725
            elapsedTime = event.timeStamp - self.lastKeyPressTimeStamp
 
726
        else:
 
727
            elapsedTime = 0
 
728
 
 
729
        if self.lastFocusedNode:
 
730
            self.lastFocusedNode.text = self.lastFocusedNode._FakeNode__node.text
 
731
 
 
732
        keyString = dogtail.rawinput.keyStrings[event.keyID]
 
733
        keyChar = dogtail.rawinput.keySymToUniChar(event.keyID)
 
734
 
 
735
        # If the only key being pressed is a modifier, don't do anything.
 
736
        if keyString.startswith("Alt_") or keyString.startswith("Control_") \
 
737
                or keyString.startswith("Meta_") or \
 
738
                keyString.startswith("Shift") or keyString == 'Caps_Lock':
 
739
            isJustAMod = True
 
740
            return
 
741
        else:
 
742
            isJustAMod = False
 
743
 
 
744
        modsDict = self.__getModifiers(event.modifiers, keyChar)
 
745
 
 
746
        def buildModifierString(modsDict):
 
747
            s = ''
 
748
            if modsDict:
 
749
                for mod in modsDict.keys():
 
750
                    s = s + '<' + mod + '>'
 
751
            return s
 
752
 
 
753
        modString = buildModifierString(modsDict)
 
754
 
 
755
        # If modifiers are present, we're dealing with a key combo
 
756
        if modString: combo = True
 
757
        # Otherwise, we assume for a second that we're not.
 
758
        else: combo = False
 
759
        
 
760
        # If the key represents a printable character, use that.
 
761
        if keyChar != '': key = keyChar
 
762
        else:
 
763
            # Otherwise, use the keyString, e.g. 'Return'.
 
764
            key = keyString
 
765
            # We treat nonprintable characters like key combos.
 
766
            combo = True
 
767
 
 
768
        if not combo and self.lastTypedNode is not None and \
 
769
                (self.lastTypedNode != self.lastFocusedNode):
 
770
            #print "changed node, flushing"
 
771
            flush = True
 
772
        else: flush = False
 
773
 
 
774
        #print "%s ! %s ! %s" % (str(self.lastTypedNode), str(self.lastFocusedNode), self.typedTextBuffer)
 
775
 
 
776
        if combo:
 
777
            self.writer.recordTyping(self.typedTextBuffer, "pressed", self.lastFocusedNode)
 
778
            self.typedTextBuffer = ""
 
779
            self.writer.recordKeyCombo(modString + key, type, self.lastFocusedNode)
 
780
            self.lastTypedNode = self.lastFocusedNode
 
781
        elif flush:
 
782
            self.writer.recordTyping(self.typedTextBuffer, "pressed", self.lastTypedNode)
 
783
            self.typedTextBuffer = key
 
784
            self.lastTypedNode = self.lastFocusedNode
 
785
        else:
 
786
            if self.typedTextBuffer == "":
 
787
                self.lastTypedNode = self.lastFocusedNode
 
788
            self.typedTextBuffer = self.typedTextBuffer + key
 
789
        return
 
790
 
 
791
        # If we're using the keyString or have modifiers, flush 
 
792
        # self.typedTextBuffer by recording a line and also record the key 
 
793
        # combo.
 
794
        if modString or flush:
 
795
            if self.typedTextBuffer:
 
796
                self.writer.recordTyping(self.typedTextBuffer, "pressed", self.lastFocusedNode)
 
797
                if not combo:
 
798
                    self.typedTextBuffer = key
 
799
                else:
 
800
                    self.typedTextBuffer = ""
 
801
                self.lastTypedNode = self.lastFocusedNode
 
802
            if modString or combo:
 
803
                self.writer.recordKeyCombo(modString + key, type, self.lastFocusedNode)
 
804
        # Otherwise add to the buffer, and wait to flush it.
 
805
        else:
 
806
            if self.typedTextBuffer == "":
 
807
                self.lastTypedNode = self.lastFocusedNode
 
808
            self.typedTextBuffer = self.typedTextBuffer + key
 
809
 
 
810
    def onWindowCreate(self, event):
 
811
        # logEvent(event)
 
812
        sourceNode = dogtail.tree.Node(event.source)
 
813
        # print "Window creation: %s"%str(sourceNode)
 
814
 
 
815
    def getLogStringForNode(self, node):
 
816
        if self.absoluteNodePaths:
 
817
            return node.getAbsoluteSearchPath()
 
818
        else:
 
819
            return node
 
820
 
 
821
# Under construction.  These ought to be methods, but am having Python assertion
 
822
# failures in refcounting when trying to hook them up using lambda expressions; grrr...
 
823
import traceback
 
824
def marshalOnFocus(event):
 
825
    try: recorder.onFocus(event)
 
826
    except: traceback.print_exc()
 
827
 
 
828
def marshalOnSelect(event):
 
829
    try: recorder.onSelect(event)
 
830
    except: traceback.print_exc()
 
831
 
 
832
def marshalOnMouseButton(event):
 
833
    try: recorder.onMouseButton(event)
 
834
    except: traceback.print_exc()
 
835
 
 
836
def marshalOnKeyPress(event):
 
837
    try: recorder.onKeyPress(event)
 
838
    except: traceback.print_exc()
 
839
 
 
840
def marshalOnWindowCreate(event):
 
841
    try: recorder.onWindowCreate(event)
 
842
    except: traceback.print_exc()
 
843
 
 
844
recorder = EventRecorder()
 
845
recorderGUI = RecorderGUI()
 
846
#recorder.writer.debug = True
 
847
#recorder.writer.debugVariables = True
 
848
 
 
849
 
 
850
# vim: sw=4 ts=4 sts=4 et ai