1#!/usr/bin/python
2"""
3Step file creator/editor.
4
5@copyright: Red Hat Inc 2009
6@author: mgoldish@redhat.com (Michael Goldish)
7@version: "20090401"
8"""
9
10import pygtk, gtk, os, glob, shutil, sys, logging
11import common, ppm_utils
12pygtk.require('2.0')
13
14
15# General utilities
16
17def corner_and_size_clipped(startpoint, endpoint, limits):
18    c0 = startpoint[:]
19    c1 = endpoint[:]
20    if c0[0] < 0:
21        c0[0] = 0
22    if c0[1] < 0:
23        c0[1] = 0
24    if c1[0] < 0:
25        c1[0] = 0
26    if c1[1] < 0:
27        c1[1] = 0
28    if c0[0] > limits[0] - 1:
29        c0[0] = limits[0] - 1
30    if c0[1] > limits[1] - 1:
31        c0[1] = limits[1] - 1
32    if c1[0] > limits[0] - 1:
33        c1[0] = limits[0] - 1
34    if c1[1] > limits[1] - 1:
35        c1[1] = limits[1] - 1
36    return ([min(c0[0], c1[0]),
37             min(c0[1], c1[1])],
38            [abs(c1[0] - c0[0]) + 1,
39             abs(c1[1] - c0[1]) + 1])
40
41
42def key_event_to_qemu_string(event):
43    keymap = gtk.gdk.keymap_get_default()
44    keyvals = keymap.get_entries_for_keycode(event.hardware_keycode)
45    keyval = keyvals[0][0]
46    keyname = gtk.gdk.keyval_name(keyval)
47
48    dict = { "Return": "ret",
49             "Tab": "tab",
50             "space": "spc",
51             "Left": "left",
52             "Right": "right",
53             "Up": "up",
54             "Down": "down",
55             "F1": "f1",
56             "F2": "f2",
57             "F3": "f3",
58             "F4": "f4",
59             "F5": "f5",
60             "F6": "f6",
61             "F7": "f7",
62             "F8": "f8",
63             "F9": "f9",
64             "F10": "f10",
65             "F11": "f11",
66             "F12": "f12",
67             "Escape": "esc",
68             "minus": "minus",
69             "equal": "equal",
70             "BackSpace": "backspace",
71             "comma": "comma",
72             "period": "dot",
73             "slash": "slash",
74             "Insert": "insert",
75             "Delete": "delete",
76             "Home": "home",
77             "End": "end",
78             "Page_Up": "pgup",
79             "Page_Down": "pgdn",
80             "Menu": "menu",
81             "semicolon": "0x27",
82             "backslash": "0x2b",
83             "apostrophe": "0x28",
84             "grave": "0x29",
85             "less": "0x2b",
86             "bracketleft": "0x1a",
87             "bracketright": "0x1b",
88             "Super_L": "0xdc",
89             "Super_R": "0xdb",
90             }
91
92    if ord('a') <= keyval <= ord('z') or ord('0') <= keyval <= ord('9'):
93        str = keyname
94    elif keyname in dict.keys():
95        str = dict[keyname]
96    else:
97        return ""
98
99    if event.state & gtk.gdk.CONTROL_MASK:
100        str = "ctrl-" + str
101    if event.state & gtk.gdk.MOD1_MASK:
102        str = "alt-" + str
103    if event.state & gtk.gdk.SHIFT_MASK:
104        str = "shift-" + str
105
106    return str
107
108
109class StepMakerWindow:
110
111    # Constructor
112
113    def __init__(self):
114        # Window
115        self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
116        self.window.set_title("Step Maker Window")
117        self.window.connect("delete-event", self.delete_event)
118        self.window.connect("destroy", self.destroy)
119        self.window.set_default_size(600, 800)
120
121        # Main box (inside a frame which is inside a VBox)
122        self.menu_vbox = gtk.VBox()
123        self.window.add(self.menu_vbox)
124        self.menu_vbox.show()
125
126        frame = gtk.Frame()
127        frame.set_border_width(10)
128        frame.set_shadow_type(gtk.SHADOW_NONE)
129        self.menu_vbox.pack_end(frame)
130        frame.show()
131
132        self.main_vbox = gtk.VBox(spacing=10)
133        frame.add(self.main_vbox)
134        self.main_vbox.show()
135
136        # EventBox
137        self.scrolledwindow = gtk.ScrolledWindow()
138        self.scrolledwindow.set_policy(gtk.POLICY_AUTOMATIC,
139                                       gtk.POLICY_AUTOMATIC)
140        self.scrolledwindow.set_shadow_type(gtk.SHADOW_NONE)
141        self.main_vbox.pack_start(self.scrolledwindow)
142        self.scrolledwindow.show()
143
144        table = gtk.Table(1, 1)
145        self.scrolledwindow.add_with_viewport(table)
146        table.show()
147        table.realize()
148
149        self.event_box = gtk.EventBox()
150        table.attach(self.event_box, 0, 1, 0, 1, gtk.EXPAND, gtk.EXPAND)
151        self.event_box.show()
152        self.event_box.realize()
153
154        # Image
155        self.image = gtk.Image()
156        self.event_box.add(self.image)
157        self.image.show()
158
159        # Data VBox
160        self.data_vbox = gtk.VBox(spacing=10)
161        self.main_vbox.pack_start(self.data_vbox, expand=False)
162        self.data_vbox.show()
163
164        # User VBox
165        self.user_vbox = gtk.VBox(spacing=10)
166        self.main_vbox.pack_start(self.user_vbox, expand=False)
167        self.user_vbox.show()
168
169        # Screendump ID HBox
170        box = gtk.HBox(spacing=10)
171        self.data_vbox.pack_start(box)
172        box.show()
173
174        label = gtk.Label("Screendump ID:")
175        box.pack_start(label, False)
176        label.show()
177
178        self.entry_screendump = gtk.Entry()
179        self.entry_screendump.set_editable(False)
180        box.pack_start(self.entry_screendump)
181        self.entry_screendump.show()
182
183        label = gtk.Label("Time:")
184        box.pack_start(label, False)
185        label.show()
186
187        self.entry_time = gtk.Entry()
188        self.entry_time.set_editable(False)
189        self.entry_time.set_width_chars(10)
190        box.pack_start(self.entry_time, False)
191        self.entry_time.show()
192
193        # Comment HBox
194        box = gtk.HBox(spacing=10)
195        self.data_vbox.pack_start(box)
196        box.show()
197
198        label = gtk.Label("Comment:")
199        box.pack_start(label, False)
200        label.show()
201
202        self.entry_comment = gtk.Entry()
203        box.pack_start(self.entry_comment)
204        self.entry_comment.show()
205
206        # Sleep HBox
207        box = gtk.HBox(spacing=10)
208        self.data_vbox.pack_start(box)
209        box.show()
210
211        self.check_sleep = gtk.CheckButton("Sleep:")
212        self.check_sleep.connect("toggled", self.event_check_sleep_toggled)
213        box.pack_start(self.check_sleep, False)
214        self.check_sleep.show()
215
216        self.spin_sleep = gtk.SpinButton(gtk.Adjustment(0, 0, 50000, 1, 10, 0),
217                                         climb_rate=0.0)
218        box.pack_start(self.spin_sleep, False)
219        self.spin_sleep.show()
220
221        # Barrier HBox
222        box = gtk.HBox(spacing=10)
223        self.data_vbox.pack_start(box)
224        box.show()
225
226        self.check_barrier = gtk.CheckButton("Barrier:")
227        self.check_barrier.connect("toggled", self.event_check_barrier_toggled)
228        box.pack_start(self.check_barrier, False)
229        self.check_barrier.show()
230
231        vbox = gtk.VBox()
232        box.pack_start(vbox)
233        vbox.show()
234
235        self.label_barrier_region = gtk.Label("Region:")
236        self.label_barrier_region.set_alignment(0, 0.5)
237        vbox.pack_start(self.label_barrier_region)
238        self.label_barrier_region.show()
239
240        self.label_barrier_md5sum = gtk.Label("MD5:")
241        self.label_barrier_md5sum.set_alignment(0, 0.5)
242        vbox.pack_start(self.label_barrier_md5sum)
243        self.label_barrier_md5sum.show()
244
245        self.label_barrier_timeout = gtk.Label("Timeout:")
246        box.pack_start(self.label_barrier_timeout, False)
247        self.label_barrier_timeout.show()
248
249        self.spin_barrier_timeout = gtk.SpinButton(gtk.Adjustment(0, 0, 50000,
250                                                                  1, 10, 0),
251                                                                 climb_rate=0.0)
252        box.pack_start(self.spin_barrier_timeout, False)
253        self.spin_barrier_timeout.show()
254
255        self.check_barrier_optional = gtk.CheckButton("Optional")
256        box.pack_start(self.check_barrier_optional, False)
257        self.check_barrier_optional.show()
258
259        # Keystrokes HBox
260        box = gtk.HBox(spacing=10)
261        self.data_vbox.pack_start(box)
262        box.show()
263
264        label = gtk.Label("Keystrokes:")
265        box.pack_start(label, False)
266        label.show()
267
268        frame = gtk.Frame()
269        frame.set_shadow_type(gtk.SHADOW_IN)
270        box.pack_start(frame)
271        frame.show()
272
273        self.text_buffer = gtk.TextBuffer()
274        self.entry_keys = gtk.TextView(self.text_buffer)
275        self.entry_keys.set_wrap_mode(gtk.WRAP_WORD)
276        self.entry_keys.connect("key-press-event", self.event_key_press)
277        frame.add(self.entry_keys)
278        self.entry_keys.show()
279
280        self.check_manual = gtk.CheckButton("Manual")
281        self.check_manual.connect("toggled", self.event_manual_toggled)
282        box.pack_start(self.check_manual, False)
283        self.check_manual.show()
284
285        button = gtk.Button("Clear")
286        button.connect("clicked", self.event_clear_clicked)
287        box.pack_start(button, False)
288        button.show()
289
290        # Mouse click HBox
291        box = gtk.HBox(spacing=10)
292        self.data_vbox.pack_start(box)
293        box.show()
294
295        label = gtk.Label("Mouse action:")
296        box.pack_start(label, False)
297        label.show()
298
299        self.button_capture = gtk.Button("Capture")
300        box.pack_start(self.button_capture, False)
301        self.button_capture.show()
302
303        self.check_mousemove = gtk.CheckButton("Move: ...")
304        box.pack_start(self.check_mousemove, False)
305        self.check_mousemove.show()
306
307        self.check_mouseclick = gtk.CheckButton("Click: ...")
308        box.pack_start(self.check_mouseclick, False)
309        self.check_mouseclick.show()
310
311        self.spin_sensitivity = gtk.SpinButton(gtk.Adjustment(1, 1, 100, 1, 10,
312                                                              0),
313                                                              climb_rate=0.0)
314        box.pack_end(self.spin_sensitivity, False)
315        self.spin_sensitivity.show()
316
317        label = gtk.Label("Sensitivity:")
318        box.pack_end(label, False)
319        label.show()
320
321        self.spin_latency = gtk.SpinButton(gtk.Adjustment(10, 1, 500, 1, 10, 0),
322                                           climb_rate=0.0)
323        box.pack_end(self.spin_latency, False)
324        self.spin_latency.show()
325
326        label = gtk.Label("Latency:")
327        box.pack_end(label, False)
328        label.show()
329
330        self.handler_event_box_press = None
331        self.handler_event_box_release = None
332        self.handler_event_box_scroll = None
333        self.handler_event_box_motion = None
334        self.handler_event_box_expose = None
335
336        self.window.realize()
337        self.window.show()
338
339        self.clear_state()
340
341    # Utilities
342
343    def message(self, text, title):
344        dlg = gtk.MessageDialog(self.window,
345                gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
346                gtk.MESSAGE_INFO,
347                gtk.BUTTONS_CLOSE,
348                title)
349        dlg.set_title(title)
350        dlg.format_secondary_text(text)
351        response = dlg.run()
352        dlg.destroy()
353
354
355    def question_yes_no(self, text, title):
356        dlg = gtk.MessageDialog(self.window,
357                gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
358                gtk.MESSAGE_QUESTION,
359                gtk.BUTTONS_YES_NO,
360                title)
361        dlg.set_title(title)
362        dlg.format_secondary_text(text)
363        response = dlg.run()
364        dlg.destroy()
365        if response == gtk.RESPONSE_YES:
366            return True
367        return False
368
369
370    def inputdialog(self, text, title, default_response=""):
371        # Define a little helper function
372        def inputdialog_entry_activated(entry):
373            dlg.response(gtk.RESPONSE_OK)
374
375        # Create the dialog
376        dlg = gtk.MessageDialog(self.window,
377                gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
378                gtk.MESSAGE_QUESTION,
379                gtk.BUTTONS_OK_CANCEL,
380                title)
381        dlg.set_title(title)
382        dlg.format_secondary_text(text)
383
384        # Create an entry widget
385        entry = gtk.Entry()
386        entry.set_text(default_response)
387        entry.connect("activate", inputdialog_entry_activated)
388        dlg.vbox.pack_start(entry)
389        entry.show()
390
391        # Run the dialog
392        response = dlg.run()
393        dlg.destroy()
394        if response == gtk.RESPONSE_OK:
395            return entry.get_text()
396        return None
397
398
399    def filedialog(self, title=None, default_filename=None):
400        chooser = gtk.FileChooserDialog(title=title, parent=self.window,
401                                        action=gtk.FILE_CHOOSER_ACTION_OPEN,
402                buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OPEN,
403                         gtk.RESPONSE_OK))
404        chooser.resize(700, 500)
405        if default_filename:
406            chooser.set_filename(os.path.abspath(default_filename))
407        filename = None
408        response = chooser.run()
409        if response == gtk.RESPONSE_OK:
410            filename = chooser.get_filename()
411        chooser.destroy()
412        return filename
413
414
415    def redirect_event_box_input(self, press=None, release=None, scroll=None,
416                                 motion=None, expose=None):
417        if self.handler_event_box_press != None: \
418        self.event_box.disconnect(self.handler_event_box_press)
419        if self.handler_event_box_release != None: \
420        self.event_box.disconnect(self.handler_event_box_release)
421        if self.handler_event_box_scroll != None: \
422        self.event_box.disconnect(self.handler_event_box_scroll)
423        if self.handler_event_box_motion != None: \
424        self.event_box.disconnect(self.handler_event_box_motion)
425        if self.handler_event_box_expose != None: \
426        self.event_box.disconnect(self.handler_event_box_expose)
427        self.handler_event_box_press = None
428        self.handler_event_box_release = None
429        self.handler_event_box_scroll = None
430        self.handler_event_box_motion = None
431        self.handler_event_box_expose = None
432        if press != None: self.handler_event_box_press = \
433        self.event_box.connect("button-press-event", press)
434        if release != None: self.handler_event_box_release = \
435        self.event_box.connect("button-release-event", release)
436        if scroll != None: self.handler_event_box_scroll = \
437        self.event_box.connect("scroll-event", scroll)
438        if motion != None: self.handler_event_box_motion = \
439        self.event_box.connect("motion-notify-event", motion)
440        if expose != None: self.handler_event_box_expose = \
441        self.event_box.connect_after("expose-event", expose)
442
443
444    def get_keys(self):
445        return self.text_buffer.get_text(
446                self.text_buffer.get_start_iter(),
447                self.text_buffer.get_end_iter())
448
449
450    def add_key(self, key):
451        text = self.get_keys()
452        if len(text) > 0 and text[-1] != ' ':
453            text += " "
454        text += key
455        self.text_buffer.set_text(text)
456
457
458    def clear_keys(self):
459        self.text_buffer.set_text("")
460
461
462    def update_barrier_info(self):
463        if self.barrier_selected:
464            self.label_barrier_region.set_text("Selected region: Corner: " + \
465                                            str(tuple(self.barrier_corner)) + \
466                                            " Size: " + \
467                                            str(tuple(self.barrier_size)))
468        else:
469            self.label_barrier_region.set_text("No region selected.")
470        self.label_barrier_md5sum.set_text("MD5: " + self.barrier_md5sum)
471
472
473    def update_mouse_click_info(self):
474        if self.mouse_click_captured:
475            self.check_mousemove.set_label("Move: " + \
476                                           str(tuple(self.mouse_click_coords)))
477            self.check_mouseclick.set_label("Click: button %d" %
478                                            self.mouse_click_button)
479        else:
480            self.check_mousemove.set_label("Move: ...")
481            self.check_mouseclick.set_label("Click: ...")
482
483
484    def clear_state(self, clear_screendump=True):
485        # Recording time
486        self.entry_time.set_text("unknown")
487        if clear_screendump:
488            # Screendump
489            self.clear_image()
490        # Screendump ID
491        self.entry_screendump.set_text("")
492        # Comment
493        self.entry_comment.set_text("")
494        # Sleep
495        self.check_sleep.set_active(True)
496        self.check_sleep.set_active(False)
497        self.spin_sleep.set_value(10)
498        # Barrier
499        self.clear_barrier_state()
500        # Keystrokes
501        self.check_manual.set_active(False)
502        self.clear_keys()
503        # Mouse actions
504        self.check_mousemove.set_sensitive(False)
505        self.check_mouseclick.set_sensitive(False)
506        self.check_mousemove.set_active(False)
507        self.check_mouseclick.set_active(False)
508        self.mouse_click_captured = False
509        self.mouse_click_coords = [0, 0]
510        self.mouse_click_button = 0
511        self.update_mouse_click_info()
512
513
514    def clear_barrier_state(self):
515        self.check_barrier.set_active(True)
516        self.check_barrier.set_active(False)
517        self.check_barrier_optional.set_active(False)
518        self.spin_barrier_timeout.set_value(10)
519        self.barrier_selection_started = False
520        self.barrier_selected = False
521        self.barrier_corner0 = [0, 0]
522        self.barrier_corner1 = [0, 0]
523        self.barrier_corner = [0, 0]
524        self.barrier_size = [0, 0]
525        self.barrier_md5sum = ""
526        self.update_barrier_info()
527
528
529    def set_image(self, w, h, data):
530        (self.image_width, self.image_height, self.image_data) = (w, h, data)
531        self.image.set_from_pixbuf(gtk.gdk.pixbuf_new_from_data(
532            data, gtk.gdk.COLORSPACE_RGB, False, 8,
533            w, h, w*3))
534        hscrollbar = self.scrolledwindow.get_hscrollbar()
535        hscrollbar.set_range(0, w)
536        vscrollbar = self.scrolledwindow.get_vscrollbar()
537        vscrollbar.set_range(0, h)
538
539
540    def set_image_from_file(self, filename):
541        if not ppm_utils.image_verify_ppm_file(filename):
542            logging.warning("set_image_from_file: Warning: received invalid"
543                            "screendump file")
544            return self.clear_image()
545        (w, h, data) = ppm_utils.image_read_from_ppm_file(filename)
546        self.set_image(w, h, data)
547
548
549    def clear_image(self):
550        self.image.clear()
551        self.image_width = 0
552        self.image_height = 0
553        self.image_data = ""
554
555
556    def update_screendump_id(self, data_dir):
557        if not self.image_data:
558            return
559        # Find a proper ID for the screendump
560        scrdump_md5sum = ppm_utils.image_md5sum(self.image_width,
561                                                self.image_height,
562                                                self.image_data)
563        scrdump_id = ppm_utils.find_id_for_screendump(scrdump_md5sum, data_dir)
564        if not scrdump_id:
565            # Not found; generate one
566            scrdump_id = ppm_utils.generate_id_for_screendump(scrdump_md5sum,
567                                                              data_dir)
568        self.entry_screendump.set_text(scrdump_id)
569
570
571    def get_step_lines(self, data_dir=None):
572        if self.check_barrier.get_active() and not self.barrier_selected:
573            self.message("No barrier region selected.", "Error")
574            return
575
576        str = "step"
577
578        # Add step recording time
579        if self.entry_time.get_text():
580            str += " " + self.entry_time.get_text()
581
582        str += "\n"
583
584        # Add screendump line
585        if self.image_data:
586            str += "screendump %s\n" % self.entry_screendump.get_text()
587
588        # Add comment
589        if self.entry_comment.get_text():
590            str += "# %s\n" % self.entry_comment.get_text()
591
592        # Add sleep line
593        if self.check_sleep.get_active():
594            str += "sleep %d\n" % self.spin_sleep.get_value()
595
596        # Add barrier_2 line
597        if self.check_barrier.get_active():
598            str += "barrier_2 %d %d %d %d %s %d" % (
599                    self.barrier_size[0], self.barrier_size[1],
600                    self.barrier_corner[0], self.barrier_corner[1],
601                    self.barrier_md5sum, self.spin_barrier_timeout.get_value())
602            if self.check_barrier_optional.get_active():
603                str += " optional"
604            str += "\n"
605
606        # Add "Sending keys" comment
607        keys_to_send = self.get_keys().split()
608        if keys_to_send:
609            str += "# Sending keys: %s\n" % self.get_keys()
610
611        # Add key and var lines
612        for key in keys_to_send:
613            if key.startswith("$"):
614                varname = key[1:]
615                str += "var %s\n" % varname
616            else:
617                str += "key %s\n" % key
618
619        # Add mousemove line
620        if self.check_mousemove.get_active():
621            str += "mousemove %d %d\n" % (self.mouse_click_coords[0],
622                                          self.mouse_click_coords[1])
623
624        # Add mouseclick line
625        if self.check_mouseclick.get_active():
626            dict = { 1 : 1,
627                     2 : 2,
628                     3 : 4 }
629            str += "mouseclick %d\n" % dict[self.mouse_click_button]
630
631        # Write screendump and cropped screendump image files
632        if data_dir and self.image_data:
633            # Create the data dir if it doesn't exist
634            if not os.path.exists(data_dir):
635                os.makedirs(data_dir)
636            # Get the full screendump filename
637            scrdump_filename = os.path.join(data_dir,
638                                            self.entry_screendump.get_text())
639            # Write screendump file if it doesn't exist
640            if not os.path.exists(scrdump_filename):
641                try:
642                    ppm_utils.image_write_to_ppm_file(scrdump_filename,
643                                                      self.image_width,
644                                                      self.image_height,
645                                                      self.image_data)
646                except IOError:
647                    self.message("Could not write screendump file.", "Error")
648
649            #if self.check_barrier.get_active():
650            #    # Crop image to get the cropped screendump
651            #    (cw, ch, cdata) = ppm_utils.image_crop(
652            #            self.image_width, self.image_height, self.image_data,
653            #            self.barrier_corner[0], self.barrier_corner[1],
654            #            self.barrier_size[0], self.barrier_size[1])
655            #    cropped_scrdump_md5sum = ppm_utils.image_md5sum(cw, ch, cdata)
656            #    cropped_scrdump_filename = \
657            #    ppm_utils.get_cropped_screendump_filename(scrdump_filename,
658            #                                            cropped_scrdump_md5sum)
659            #    # Write cropped screendump file
660            #    try:
661            #        ppm_utils.image_write_to_ppm_file(cropped_scrdump_filename,
662            #                                          cw, ch, cdata)
663            #    except IOError:
664            #        self.message("Could not write cropped screendump file.",
665            #                     "Error")
666
667        return str
668
669    def set_state_from_step_lines(self, str, data_dir, warn=True):
670        self.clear_state()
671
672        for line in str.splitlines():
673            words = line.split()
674            if not words:
675                continue
676
677            if line.startswith("#") \
678                    and not self.entry_comment.get_text() \
679                    and not line.startswith("# Sending keys:") \
680                    and not line.startswith("# ----"):
681                self.entry_comment.set_text(line.strip("#").strip())
682
683            elif words[0] == "step":
684                if len(words) >= 2:
685                    self.entry_time.set_text(words[1])
686
687            elif words[0] == "screendump":
688                self.entry_screendump.set_text(words[1])
689                self.set_image_from_file(os.path.join(data_dir, words[1]))
690
691            elif words[0] == "sleep":
692                self.spin_sleep.set_value(int(words[1]))
693                self.check_sleep.set_active(True)
694
695            elif words[0] == "key":
696                self.add_key(words[1])
697
698            elif words[0] == "var":
699                self.add_key("$%s" % words[1])
700
701            elif words[0] == "mousemove":
702                self.mouse_click_captured = True
703                self.mouse_click_coords = [int(words[1]), int(words[2])]
704                self.update_mouse_click_info()
705
706            elif words[0] == "mouseclick":
707                self.mouse_click_captured = True
708                self.mouse_click_button = int(words[1])
709                self.update_mouse_click_info()
710
711            elif words[0] == "barrier_2":
712                # Get region corner and size from step lines
713                self.barrier_corner = [int(words[3]), int(words[4])]
714                self.barrier_size = [int(words[1]), int(words[2])]
715                # Get corner0 and corner1 from step lines
716                self.barrier_corner0 = self.barrier_corner
717                self.barrier_corner1 = [self.barrier_corner[0] +
718                                        self.barrier_size[0] - 1,
719                                        self.barrier_corner[1] +
720                                        self.barrier_size[1] - 1]
721                # Get the md5sum
722                self.barrier_md5sum = words[5]
723                # Pretend the user selected the region with the mouse
724                self.barrier_selection_started = True
725                self.barrier_selected = True
726                # Update label widgets according to region information
727                self.update_barrier_info()
728                # Check the barrier checkbutton
729                self.check_barrier.set_active(True)
730                # Set timeout value
731                self.spin_barrier_timeout.set_value(int(words[6]))
732                # Set 'optional' checkbutton state
733                self.check_barrier_optional.set_active(words[-1] == "optional")
734                # Update the image widget
735                self.event_box.queue_draw()
736
737                if warn:
738                    # See if the computed md5sum matches the one recorded in
739                    # the file
740                    computed_md5sum = ppm_utils.get_region_md5sum(
741                            self.image_width, self.image_height,
742                            self.image_data, self.barrier_corner[0],
743                            self.barrier_corner[1], self.barrier_size[0],
744                            self.barrier_size[1])
745                    if computed_md5sum != self.barrier_md5sum:
746                        self.message("Computed MD5 sum (%s) differs from MD5"
747                                     " sum recorded in steps file (%s)" %
748                                     (computed_md5sum, self.barrier_md5sum),
749                                     "Warning")
750
751    # Events
752
753    def delete_event(self, widget, event):
754        pass
755
756    def destroy(self, widget):
757        gtk.main_quit()
758
759    def event_check_barrier_toggled(self, widget):
760        if self.check_barrier.get_active():
761            self.redirect_event_box_input(
762                    self.event_button_press,
763                    self.event_button_release,
764                    None,
765                    None,
766                    self.event_expose)
767            self.event_box.queue_draw()
768            self.event_box.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.CROSSHAIR))
769            self.label_barrier_region.set_sensitive(True)
770            self.label_barrier_md5sum.set_sensitive(True)
771            self.label_barrier_timeout.set_sensitive(True)
772            self.spin_barrier_timeout.set_sensitive(True)
773            self.check_barrier_optional.set_sensitive(True)
774        else:
775            self.redirect_event_box_input()
776            self.event_box.queue_draw()
777            self.event_box.window.set_cursor(None)
778            self.label_barrier_region.set_sensitive(False)
779            self.label_barrier_md5sum.set_sensitive(False)
780            self.label_barrier_timeout.set_sensitive(False)
781            self.spin_barrier_timeout.set_sensitive(False)
782            self.check_barrier_optional.set_sensitive(False)
783
784    def event_check_sleep_toggled(self, widget):
785        if self.check_sleep.get_active():
786            self.spin_sleep.set_sensitive(True)
787        else:
788            self.spin_sleep.set_sensitive(False)
789
790    def event_manual_toggled(self, widget):
791        self.entry_keys.grab_focus()
792
793    def event_clear_clicked(self, widget):
794        self.clear_keys()
795        self.entry_keys.grab_focus()
796
797    def event_expose(self, widget, event):
798        if not self.barrier_selection_started:
799            return
800        (corner, size) = corner_and_size_clipped(self.barrier_corner0,
801                                                 self.barrier_corner1,
802                                                 self.event_box.size_request())
803        gc = self.event_box.window.new_gc(line_style=gtk.gdk.LINE_DOUBLE_DASH,
804                                          line_width=1)
805        gc.set_foreground(gc.get_colormap().alloc_color("red"))
806        gc.set_background(gc.get_colormap().alloc_color("dark red"))
807        gc.set_dashes(0, (4, 4))
808        self.event_box.window.draw_rectangle(
809                gc, False,
810                corner[0], corner[1],
811                size[0]-1, size[1]-1)
812
813    def event_drag_motion(self, widget, event):
814        old_corner1 = self.barrier_corner1
815        self.barrier_corner1 = [int(event.x), int(event.y)]
816        (corner, size) = corner_and_size_clipped(self.barrier_corner0,
817                                                 self.barrier_corner1,
818                                                 self.event_box.size_request())
819        (old_corner, old_size) = corner_and_size_clipped(self.barrier_corner0,
820                                                         old_corner1,
821                                                  self.event_box.size_request())
822        corner0 = [min(corner[0], old_corner[0]), min(corner[1], old_corner[1])]
823        corner1 = [max(corner[0] + size[0], old_corner[0] + old_size[0]),
824                   max(corner[1] + size[1], old_corner[1] + old_size[1])]
825        size = [corner1[0] - corner0[0] + 1,
826                corner1[1] - corner0[1] + 1]
827        self.event_box.queue_draw_area(corner0[0], corner0[1], size[0], size[1])
828
829    def event_button_press(self, widget, event):
830        (corner, size) = corner_and_size_clipped(self.barrier_corner0,
831                                                 self.barrier_corner1,
832                                                 self.event_box.size_request())
833        self.event_box.queue_draw_area(corner[0], corner[1], size[0], size[1])
834        self.barrier_corner0 = [int(event.x), int(event.y)]
835        self.barrier_corner1 = [int(event.x), int(event.y)]
836        self.redirect_event_box_input(
837                self.event_button_press,
838                self.event_button_release,
839                None,
840                self.event_drag_motion,
841                self.event_expose)
842        self.barrier_selection_started = True
843
844    def event_button_release(self, widget, event):
845        self.redirect_event_box_input(
846                self.event_button_press,
847                self.event_button_release,
848                None,
849                None,
850                self.event_expose)
851        (self.barrier_corner, self.barrier_size) = \
852        corner_and_size_clipped(self.barrier_corner0, self.barrier_corner1,
853                                self.event_box.size_request())
854        self.barrier_md5sum = ppm_utils.get_region_md5sum(
855                self.image_width, self.image_height, self.image_data,
856                self.barrier_corner[0], self.barrier_corner[1],
857                self.barrier_size[0], self.barrier_size[1])
858        self.barrier_selected = True
859        self.update_barrier_info()
860
861    def event_key_press(self, widget, event):
862        if self.check_manual.get_active():
863            return False
864        str = key_event_to_qemu_string(event)
865        self.add_key(str)
866        return True
867
868
869class StepEditor(StepMakerWindow):
870    ui = '''<ui>
871    <menubar name="MenuBar">
872        <menu action="File">
873            <menuitem action="Open"/>
874            <separator/>
875            <menuitem action="Quit"/>
876        </menu>
877        <menu action="Edit">
878            <menuitem action="CopyStep"/>
879            <menuitem action="DeleteStep"/>
880        </menu>
881        <menu action="Insert">
882            <menuitem action="InsertNewBefore"/>
883            <menuitem action="InsertNewAfter"/>
884            <separator/>
885            <menuitem action="InsertStepsBefore"/>
886            <menuitem action="InsertStepsAfter"/>
887        </menu>
888        <menu action="Tools">
889            <menuitem action="CleanUp"/>
890        </menu>
891    </menubar>
892</ui>'''
893
894    # Constructor
895
896    def __init__(self, filename=None):
897        StepMakerWindow.__init__(self)
898
899        self.steps_filename = None
900        self.steps = []
901
902        # Create a UIManager instance
903        uimanager = gtk.UIManager()
904
905        # Add the accelerator group to the toplevel window
906        accelgroup = uimanager.get_accel_group()
907        self.window.add_accel_group(accelgroup)
908
909        # Create an ActionGroup
910        actiongroup = gtk.ActionGroup('StepEditor')
911
912        # Create actions
913        actiongroup.add_actions([
914            ('Quit', gtk.STOCK_QUIT, '_Quit', None, 'Quit the Program',
915             self.quit),
916            ('Open', gtk.STOCK_OPEN, '_Open', None, 'Open steps file',
917             self.open_steps_file),
918            ('CopyStep', gtk.STOCK_COPY, '_Copy current step...', "",
919             'Copy current step to user specified position', self.copy_step),
920            ('DeleteStep', gtk.STOCK_DELETE, '_Delete current step', "",
921             'Delete current step', self.event_remove_clicked),
922            ('InsertNewBefore', gtk.STOCK_ADD, '_New step before current', "",
923             'Insert new step before current step', self.insert_before),
924            ('InsertNewAfter', gtk.STOCK_ADD, 'N_ew step after current', "",
925             'Insert new step after current step', self.insert_after),
926            ('InsertStepsBefore', gtk.STOCK_ADD, '_Steps before current...',
927             "", 'Insert steps (from file) before current step',
928             self.insert_steps_before),
929            ('InsertStepsAfter', gtk.STOCK_ADD, 'Steps _after current...', "",
930             'Insert steps (from file) after current step',
931             self.insert_steps_after),
932            ('CleanUp', gtk.STOCK_DELETE, '_Clean up data directory', "",
933             'Move unused PPM files to a backup directory', self.cleanup),
934            ('File', None, '_File'),
935            ('Edit', None, '_Edit'),
936            ('Insert', None, '_Insert'),
937            ('Tools', None, '_Tools')
938            ])
939
940        def create_shortcut(name, callback, keyname):
941            # Create an action
942            action = gtk.Action(name, None, None, None)
943            # Connect a callback to the action
944            action.connect("activate", callback)
945            actiongroup.add_action_with_accel(action, keyname)
946            # Have the action use accelgroup
947            action.set_accel_group(accelgroup)
948            # Connect the accelerator to the action
949            action.connect_accelerator()
950
951        create_shortcut("Next", self.event_next_clicked, "Page_Down")
952        create_shortcut("Previous", self.event_prev_clicked, "Page_Up")
953
954        # Add the actiongroup to the uimanager
955        uimanager.insert_action_group(actiongroup, 0)
956
957        # Add a UI description
958        uimanager.add_ui_from_string(self.ui)
959
960        # Create a MenuBar
961        menubar = uimanager.get_widget('/MenuBar')
962        self.menu_vbox.pack_start(menubar, False)
963
964        # Remember the Edit menu bar for future reference
965        self.menu_edit = uimanager.get_widget('/MenuBar/Edit')
966        self.menu_edit.set_sensitive(False)
967
968        # Remember the Insert menu bar for future reference
969        self.menu_insert = uimanager.get_widget('/MenuBar/Insert')
970        self.menu_insert.set_sensitive(False)
971
972        # Remember the Tools menu bar for future reference
973        self.menu_tools = uimanager.get_widget('/MenuBar/Tools')
974        self.menu_tools.set_sensitive(False)
975
976        # Next/Previous HBox
977        hbox = gtk.HBox(spacing=10)
978        self.user_vbox.pack_start(hbox)
979        hbox.show()
980
981        self.button_first = gtk.Button(stock=gtk.STOCK_GOTO_FIRST)
982        self.button_first.connect("clicked", self.event_first_clicked)
983        hbox.pack_start(self.button_first)
984        self.button_first.show()
985
986        #self.button_prev = gtk.Button("<< Previous")
987        self.button_prev = gtk.Button(stock=gtk.STOCK_GO_BACK)
988        self.button_prev.connect("clicked", self.event_prev_clicked)
989        hbox.pack_start(self.button_prev)
990        self.button_prev.show()
991
992        self.label_step = gtk.Label("Step:")
993        hbox.pack_start(self.label_step, False)
994        self.label_step.show()
995
996        self.entry_step_num = gtk.Entry()
997        self.entry_step_num.connect("activate", self.event_entry_step_activated)
998        self.entry_step_num.set_width_chars(3)
999        hbox.pack_start(self.entry_step_num, False)
1000        self.entry_step_num.show()
1001
1002        #self.button_next = gtk.Button("Next >>")
1003        self.button_next = gtk.Button(stock=gtk.STOCK_GO_FORWARD)
1004        self.button_next.connect("clicked", self.event_next_clicked)
1005        hbox.pack_start(self.button_next)
1006        self.button_next.show()
1007
1008        self.button_last = gtk.Button(stock=gtk.STOCK_GOTO_LAST)
1009        self.button_last.connect("clicked", self.event_last_clicked)
1010        hbox.pack_start(self.button_last)
1011        self.button_last.show()
1012
1013        # Save HBox
1014        hbox = gtk.HBox(spacing=10)
1015        self.user_vbox.pack_start(hbox)
1016        hbox.show()
1017
1018        self.button_save = gtk.Button("_Save current step")
1019        self.button_save.connect("clicked", self.event_save_clicked)
1020        hbox.pack_start(self.button_save)
1021        self.button_save.show()
1022
1023        self.button_remove = gtk.Button("_Delete current step")
1024        self.button_remove.connect("clicked", self.event_remove_clicked)
1025        hbox.pack_start(self.button_remove)
1026        self.button_remove.show()
1027
1028        self.button_replace = gtk.Button("_Replace screendump")
1029        self.button_replace.connect("clicked", self.event_replace_clicked)
1030        hbox.pack_start(self.button_replace)
1031        self.button_replace.show()
1032
1033        # Disable unused widgets
1034        self.button_capture.set_sensitive(False)
1035        self.spin_latency.set_sensitive(False)
1036        self.spin_sensitivity.set_sensitive(False)
1037
1038        # Disable main vbox because no steps file is loaded
1039        self.main_vbox.set_sensitive(False)
1040
1041        # Set title
1042        self.window.set_title("Step Editor")
1043
1044    # Events
1045
1046    def delete_event(self, widget, event):
1047        # Make sure the step is saved (if the user wants it to be)
1048        self.verify_save()
1049
1050    def event_first_clicked(self, widget):
1051        if not self.steps:
1052            return
1053        # Make sure the step is saved (if the user wants it to be)
1054        self.verify_save()
1055        # Go to first step
1056        self.set_step(0)
1057
1058    def event_last_clicked(self, widget):
1059        if not self.steps:
1060            return
1061        # Make sure the step is saved (if the user wants it to be)
1062        self.verify_save()
1063        # Go to last step
1064        self.set_step(len(self.steps) - 1)
1065
1066    def event_prev_clicked(self, widget):
1067        if not self.steps:
1068            return
1069        # Make sure the step is saved (if the user wants it to be)
1070        self.verify_save()
1071        # Go to previous step
1072        index = self.current_step_index - 1
1073        if self.steps:
1074            index = index % len(self.steps)
1075        self.set_step(index)
1076
1077    def event_next_clicked(self, widget):
1078        if not self.steps:
1079            return
1080        # Make sure the step is saved (if the user wants it to be)
1081        self.verify_save()
1082        # Go to next step
1083        index = self.current_step_index + 1
1084        if self.steps:
1085            index = index % len(self.steps)
1086        self.set_step(index)
1087
1088    def event_entry_step_activated(self, widget):
1089        if not self.steps:
1090            return
1091        step_index = self.entry_step_num.get_text()
1092        if not step_index.isdigit():
1093            return
1094        step_index = int(step_index) - 1
1095        if step_index == self.current_step_index:
1096            return
1097        self.verify_save()
1098        self.set_step(step_index)
1099
1100    def event_save_clicked(self, widget):
1101        if not self.steps:
1102            return
1103        self.save_step()
1104
1105    def event_remove_clicked(self, widget):
1106        if not self.steps:
1107            return
1108        if not self.question_yes_no("This will modify the steps file."
1109                                    " Are you sure?", "Remove step?"):
1110            return
1111        # Remove step
1112        del self.steps[self.current_step_index]
1113        # Write changes to file
1114        self.write_steps_file(self.steps_filename)
1115        # Move to previous step
1116        self.set_step(self.current_step_index)
1117
1118    def event_replace_clicked(self, widget):
1119        if not self.steps:
1120            return
1121        # Let the user choose a screendump file
1122        current_filename = os.path.join(self.steps_data_dir,
1123                                        self.entry_screendump.get_text())
1124        filename = self.filedialog("Choose PPM image file",
1125                                   default_filename=current_filename)
1126        if not filename:
1127            return
1128        if not ppm_utils.image_verify_ppm_file(filename):
1129            self.message("Not a valid PPM image file.", "Error")
1130            return
1131        self.clear_image()
1132        self.clear_barrier_state()
1133        self.set_image_from_file(filename)
1134        self.update_screendump_id(self.steps_data_dir)
1135
1136    # Menu actions
1137
1138    def open_steps_file(self, action):
1139        # Make sure the step is saved (if the user wants it to be)
1140        self.verify_save()
1141        # Let the user choose a steps file
1142        current_filename = self.steps_filename
1143        filename = self.filedialog("Open steps file",
1144                                   default_filename=current_filename)
1145        if not filename:
1146            return
1147        self.set_steps_file(filename)
1148
1149    def quit(self, action):
1150        # Make sure the step is saved (if the user wants it to be)
1151        self.verify_save()
1152        # Quit
1153        gtk.main_quit()
1154
1155    def copy_step(self, action):
1156        if not self.steps:
1157            return
1158        self.verify_save()
1159        self.set_step(self.current_step_index)
1160        # Get the desired position
1161        step_index = self.inputdialog("Copy step to position:",
1162                                      "Copy step",
1163                                      str(self.current_step_index + 2))
1164        if not step_index:
1165            return
1166        step_index = int(step_index) - 1
1167        # Get the lines of the current step
1168        step = self.steps[self.current_step_index]
1169        # Insert new step at position step_index
1170        self.steps.insert(step_index, step)
1171        # Go to new step
1172        self.set_step(step_index)
1173        # Write changes to disk
1174        self.write_steps_file(self.steps_filename)
1175
1176    def insert_before(self, action):
1177        if not self.steps_filename:
1178            return
1179        if not self.question_yes_no("This will modify the steps file."
1180                                    " Are you sure?", "Insert new step?"):
1181            return
1182        self.verify_save()
1183        step_index = self.current_step_index
1184        # Get the lines of a blank step
1185        self.clear_state()
1186        step = self.get_step_lines()
1187        # Insert new step at position step_index
1188        self.steps.insert(step_index, step)
1189        # Go to new step
1190        self.set_step(step_index)
1191        # Write changes to disk
1192        self.write_steps_file(self.steps_filename)
1193
1194    def insert_after(self, action):
1195        if not self.steps_filename:
1196            return
1197        if not self.question_yes_no("This will modify the steps file."
1198                                    " Are you sure?", "Insert new step?"):
1199            return
1200        self.verify_save()
1201        step_index = self.current_step_index + 1
1202        # Get the lines of a blank step
1203        self.clear_state()
1204        step = self.get_step_lines()
1205        # Insert new step at position step_index
1206        self.steps.insert(step_index, step)
1207        # Go to new step
1208        self.set_step(step_index)
1209        # Write changes to disk
1210        self.write_steps_file(self.steps_filename)
1211
1212    def insert_steps(self, filename, index):
1213        # Read the steps file
1214        (steps, header) = self.read_steps_file(filename)
1215
1216        data_dir = ppm_utils.get_data_dir(filename)
1217        for step in steps:
1218            self.set_state_from_step_lines(step, data_dir, warn=False)
1219            step = self.get_step_lines(self.steps_data_dir)
1220
1221        # Insert steps into self.steps
1222        self.steps[index:index] = steps
1223        # Write changes to disk
1224        self.write_steps_file(self.steps_filename)
1225
1226    def insert_steps_before(self, action):
1227        if not self.steps_filename:
1228            return
1229        # Let the user choose a steps file
1230        current_filename = self.steps_filename
1231        filename = self.filedialog("Choose steps file",
1232                                   default_filename=current_filename)
1233        if not filename:
1234            return
1235        self.verify_save()
1236
1237        step_index = self.current_step_index
1238        # Insert steps at position step_index
1239        self.insert_steps(filename, step_index)
1240        # Go to new steps
1241        self.set_step(step_index)
1242
1243    def insert_steps_after(self, action):
1244        if not self.steps_filename:
1245            return
1246        # Let the user choose a steps file
1247        current_filename = self.steps_filename
1248        filename = self.filedialog("Choose steps file",
1249                                   default_filename=current_filename)
1250        if not filename:
1251            return
1252        self.verify_save()
1253
1254        step_index = self.current_step_index + 1
1255        # Insert new steps at position step_index
1256        self.insert_steps(filename, step_index)
1257        # Go to new steps
1258        self.set_step(step_index)
1259
1260    def cleanup(self, action):
1261        if not self.steps_filename:
1262            return
1263        if not self.question_yes_no("All unused PPM files will be moved to a"
1264                                    " backup directory. Are you sure?",
1265                                    "Clean up data directory?"):
1266            return
1267        # Remember the current step index
1268        current_step_index = self.current_step_index
1269        # Get the backup dir
1270        backup_dir = os.path.join(self.steps_data_dir, "backup")
1271        # Create it if it doesn't exist
1272        if not os.path.exists(backup_dir):
1273            os.makedirs(backup_dir)
1274        # Move all files to the backup dir
1275        for filename in glob.glob(os.path.join(self.steps_data_dir,
1276                                               "*.[Pp][Pp][Mm]")):
1277            shutil.move(filename, backup_dir)
1278        # Get the used files back
1279        for step in self.steps:
1280            self.set_state_from_step_lines(step, backup_dir, warn=False)
1281            self.get_step_lines(self.steps_data_dir)
1282        # Remove the used files from the backup dir
1283        used_files = os.listdir(self.steps_data_dir)
1284        for filename in os.listdir(backup_dir):
1285            if filename in used_files:
1286                os.unlink(os.path.join(backup_dir, filename))
1287        # Restore step index
1288        self.set_step(current_step_index)
1289        # Inform the user
1290        self.message("All unused PPM files may be found at %s." %
1291                     os.path.abspath(backup_dir),
1292                     "Clean up data directory")
1293
1294    # Methods
1295
1296    def read_steps_file(self, filename):
1297        steps = []
1298        header = ""
1299
1300        file = open(filename, "r")
1301        for line in file.readlines():
1302            words = line.split()
1303            if not words:
1304                continue
1305            if line.startswith("# ----"):
1306                continue
1307            if words[0] == "step":
1308                steps.append("")
1309            if steps:
1310                steps[-1] += line
1311            else:
1312                header += line
1313        file.close()
1314
1315        return (steps, header)
1316
1317    def set_steps_file(self, filename):
1318        try:
1319            (self.steps, self.header) = self.read_steps_file(filename)
1320        except (TypeError, IOError):
1321            self.message("Cannot read file %s." % filename, "Error")
1322            return
1323
1324        self.steps_filename = filename
1325        self.steps_data_dir = ppm_utils.get_data_dir(filename)
1326        # Go to step 0
1327        self.set_step(0)
1328
1329    def set_step(self, index):
1330        # Limit index to legal boundaries
1331        if index < 0:
1332            index = 0
1333        if index > len(self.steps) - 1:
1334            index = len(self.steps) - 1
1335
1336        # Enable the menus
1337        self.menu_edit.set_sensitive(True)
1338        self.menu_insert.set_sensitive(True)
1339        self.menu_tools.set_sensitive(True)
1340
1341        # If no steps exist...
1342        if self.steps == []:
1343            self.current_step_index = index
1344            self.current_step = None
1345            # Set window title
1346            self.window.set_title("Step Editor -- %s" %
1347                                  os.path.basename(self.steps_filename))
1348            # Set step entry widget text
1349            self.entry_step_num.set_text("")
1350            # Clear the state of all widgets
1351            self.clear_state()
1352            # Disable the main vbox
1353            self.main_vbox.set_sensitive(False)
1354            return
1355
1356        self.current_step_index = index
1357        self.current_step = self.steps[index]
1358        # Set window title
1359        self.window.set_title("Step Editor -- %s -- step %d" %
1360                              (os.path.basename(self.steps_filename),
1361                               index + 1))
1362        # Set step entry widget text
1363        self.entry_step_num.set_text(str(self.current_step_index + 1))
1364        # Load the state from the step lines
1365        self.set_state_from_step_lines(self.current_step, self.steps_data_dir)
1366        # Enable the main vbox
1367        self.main_vbox.set_sensitive(True)
1368        # Make sure the step lines in self.current_step are identical to the
1369        # output of self.get_step_lines
1370        self.current_step = self.get_step_lines()
1371
1372    def verify_save(self):
1373        if not self.steps:
1374            return
1375        # See if the user changed anything
1376        if self.get_step_lines() != self.current_step:
1377            if self.question_yes_no("Step contents have been modified."
1378                                    " Save step?", "Save changes?"):
1379                self.save_step()
1380
1381    def save_step(self):
1382        lines = self.get_step_lines(self.steps_data_dir)
1383        if lines != None:
1384            self.steps[self.current_step_index] = lines
1385            self.current_step = lines
1386            self.write_steps_file(self.steps_filename)
1387
1388    def write_steps_file(self, filename):
1389        file = open(filename, "w")
1390        file.write(self.header)
1391        for step in self.steps:
1392            file.write("# " + "-" * 32 + "\n")
1393            file.write(step)
1394        file.close()
1395
1396
1397if __name__ == "__main__":
1398    se = StepEditor()
1399    if len(sys.argv) > 1:
1400        se.set_steps_file(sys.argv[1])
1401    gtk.main()
1402