普通文本  |  1402行  |  50.23 KB

#!/usr/bin/python
"""
Step file creator/editor.

@copyright: Red Hat Inc 2009
@author: mgoldish@redhat.com (Michael Goldish)
@version: "20090401"
"""

import pygtk, gtk, os, glob, shutil, sys, logging
import common, ppm_utils
pygtk.require('2.0')


# General utilities

def corner_and_size_clipped(startpoint, endpoint, limits):
    c0 = startpoint[:]
    c1 = endpoint[:]
    if c0[0] < 0:
        c0[0] = 0
    if c0[1] < 0:
        c0[1] = 0
    if c1[0] < 0:
        c1[0] = 0
    if c1[1] < 0:
        c1[1] = 0
    if c0[0] > limits[0] - 1:
        c0[0] = limits[0] - 1
    if c0[1] > limits[1] - 1:
        c0[1] = limits[1] - 1
    if c1[0] > limits[0] - 1:
        c1[0] = limits[0] - 1
    if c1[1] > limits[1] - 1:
        c1[1] = limits[1] - 1
    return ([min(c0[0], c1[0]),
             min(c0[1], c1[1])],
            [abs(c1[0] - c0[0]) + 1,
             abs(c1[1] - c0[1]) + 1])


def key_event_to_qemu_string(event):
    keymap = gtk.gdk.keymap_get_default()
    keyvals = keymap.get_entries_for_keycode(event.hardware_keycode)
    keyval = keyvals[0][0]
    keyname = gtk.gdk.keyval_name(keyval)

    dict = { "Return": "ret",
             "Tab": "tab",
             "space": "spc",
             "Left": "left",
             "Right": "right",
             "Up": "up",
             "Down": "down",
             "F1": "f1",
             "F2": "f2",
             "F3": "f3",
             "F4": "f4",
             "F5": "f5",
             "F6": "f6",
             "F7": "f7",
             "F8": "f8",
             "F9": "f9",
             "F10": "f10",
             "F11": "f11",
             "F12": "f12",
             "Escape": "esc",
             "minus": "minus",
             "equal": "equal",
             "BackSpace": "backspace",
             "comma": "comma",
             "period": "dot",
             "slash": "slash",
             "Insert": "insert",
             "Delete": "delete",
             "Home": "home",
             "End": "end",
             "Page_Up": "pgup",
             "Page_Down": "pgdn",
             "Menu": "menu",
             "semicolon": "0x27",
             "backslash": "0x2b",
             "apostrophe": "0x28",
             "grave": "0x29",
             "less": "0x2b",
             "bracketleft": "0x1a",
             "bracketright": "0x1b",
             "Super_L": "0xdc",
             "Super_R": "0xdb",
             }

    if ord('a') <= keyval <= ord('z') or ord('0') <= keyval <= ord('9'):
        str = keyname
    elif keyname in dict.keys():
        str = dict[keyname]
    else:
        return ""

    if event.state & gtk.gdk.CONTROL_MASK:
        str = "ctrl-" + str
    if event.state & gtk.gdk.MOD1_MASK:
        str = "alt-" + str
    if event.state & gtk.gdk.SHIFT_MASK:
        str = "shift-" + str

    return str


class StepMakerWindow:

    # Constructor

    def __init__(self):
        # Window
        self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
        self.window.set_title("Step Maker Window")
        self.window.connect("delete-event", self.delete_event)
        self.window.connect("destroy", self.destroy)
        self.window.set_default_size(600, 800)

        # Main box (inside a frame which is inside a VBox)
        self.menu_vbox = gtk.VBox()
        self.window.add(self.menu_vbox)
        self.menu_vbox.show()

        frame = gtk.Frame()
        frame.set_border_width(10)
        frame.set_shadow_type(gtk.SHADOW_NONE)
        self.menu_vbox.pack_end(frame)
        frame.show()

        self.main_vbox = gtk.VBox(spacing=10)
        frame.add(self.main_vbox)
        self.main_vbox.show()

        # EventBox
        self.scrolledwindow = gtk.ScrolledWindow()
        self.scrolledwindow.set_policy(gtk.POLICY_AUTOMATIC,
                                       gtk.POLICY_AUTOMATIC)
        self.scrolledwindow.set_shadow_type(gtk.SHADOW_NONE)
        self.main_vbox.pack_start(self.scrolledwindow)
        self.scrolledwindow.show()

        table = gtk.Table(1, 1)
        self.scrolledwindow.add_with_viewport(table)
        table.show()
        table.realize()

        self.event_box = gtk.EventBox()
        table.attach(self.event_box, 0, 1, 0, 1, gtk.EXPAND, gtk.EXPAND)
        self.event_box.show()
        self.event_box.realize()

        # Image
        self.image = gtk.Image()
        self.event_box.add(self.image)
        self.image.show()

        # Data VBox
        self.data_vbox = gtk.VBox(spacing=10)
        self.main_vbox.pack_start(self.data_vbox, expand=False)
        self.data_vbox.show()

        # User VBox
        self.user_vbox = gtk.VBox(spacing=10)
        self.main_vbox.pack_start(self.user_vbox, expand=False)
        self.user_vbox.show()

        # Screendump ID HBox
        box = gtk.HBox(spacing=10)
        self.data_vbox.pack_start(box)
        box.show()

        label = gtk.Label("Screendump ID:")
        box.pack_start(label, False)
        label.show()

        self.entry_screendump = gtk.Entry()
        self.entry_screendump.set_editable(False)
        box.pack_start(self.entry_screendump)
        self.entry_screendump.show()

        label = gtk.Label("Time:")
        box.pack_start(label, False)
        label.show()

        self.entry_time = gtk.Entry()
        self.entry_time.set_editable(False)
        self.entry_time.set_width_chars(10)
        box.pack_start(self.entry_time, False)
        self.entry_time.show()

        # Comment HBox
        box = gtk.HBox(spacing=10)
        self.data_vbox.pack_start(box)
        box.show()

        label = gtk.Label("Comment:")
        box.pack_start(label, False)
        label.show()

        self.entry_comment = gtk.Entry()
        box.pack_start(self.entry_comment)
        self.entry_comment.show()

        # Sleep HBox
        box = gtk.HBox(spacing=10)
        self.data_vbox.pack_start(box)
        box.show()

        self.check_sleep = gtk.CheckButton("Sleep:")
        self.check_sleep.connect("toggled", self.event_check_sleep_toggled)
        box.pack_start(self.check_sleep, False)
        self.check_sleep.show()

        self.spin_sleep = gtk.SpinButton(gtk.Adjustment(0, 0, 50000, 1, 10, 0),
                                         climb_rate=0.0)
        box.pack_start(self.spin_sleep, False)
        self.spin_sleep.show()

        # Barrier HBox
        box = gtk.HBox(spacing=10)
        self.data_vbox.pack_start(box)
        box.show()

        self.check_barrier = gtk.CheckButton("Barrier:")
        self.check_barrier.connect("toggled", self.event_check_barrier_toggled)
        box.pack_start(self.check_barrier, False)
        self.check_barrier.show()

        vbox = gtk.VBox()
        box.pack_start(vbox)
        vbox.show()

        self.label_barrier_region = gtk.Label("Region:")
        self.label_barrier_region.set_alignment(0, 0.5)
        vbox.pack_start(self.label_barrier_region)
        self.label_barrier_region.show()

        self.label_barrier_md5sum = gtk.Label("MD5:")
        self.label_barrier_md5sum.set_alignment(0, 0.5)
        vbox.pack_start(self.label_barrier_md5sum)
        self.label_barrier_md5sum.show()

        self.label_barrier_timeout = gtk.Label("Timeout:")
        box.pack_start(self.label_barrier_timeout, False)
        self.label_barrier_timeout.show()

        self.spin_barrier_timeout = gtk.SpinButton(gtk.Adjustment(0, 0, 50000,
                                                                  1, 10, 0),
                                                                 climb_rate=0.0)
        box.pack_start(self.spin_barrier_timeout, False)
        self.spin_barrier_timeout.show()

        self.check_barrier_optional = gtk.CheckButton("Optional")
        box.pack_start(self.check_barrier_optional, False)
        self.check_barrier_optional.show()

        # Keystrokes HBox
        box = gtk.HBox(spacing=10)
        self.data_vbox.pack_start(box)
        box.show()

        label = gtk.Label("Keystrokes:")
        box.pack_start(label, False)
        label.show()

        frame = gtk.Frame()
        frame.set_shadow_type(gtk.SHADOW_IN)
        box.pack_start(frame)
        frame.show()

        self.text_buffer = gtk.TextBuffer()
        self.entry_keys = gtk.TextView(self.text_buffer)
        self.entry_keys.set_wrap_mode(gtk.WRAP_WORD)
        self.entry_keys.connect("key-press-event", self.event_key_press)
        frame.add(self.entry_keys)
        self.entry_keys.show()

        self.check_manual = gtk.CheckButton("Manual")
        self.check_manual.connect("toggled", self.event_manual_toggled)
        box.pack_start(self.check_manual, False)
        self.check_manual.show()

        button = gtk.Button("Clear")
        button.connect("clicked", self.event_clear_clicked)
        box.pack_start(button, False)
        button.show()

        # Mouse click HBox
        box = gtk.HBox(spacing=10)
        self.data_vbox.pack_start(box)
        box.show()

        label = gtk.Label("Mouse action:")
        box.pack_start(label, False)
        label.show()

        self.button_capture = gtk.Button("Capture")
        box.pack_start(self.button_capture, False)
        self.button_capture.show()

        self.check_mousemove = gtk.CheckButton("Move: ...")
        box.pack_start(self.check_mousemove, False)
        self.check_mousemove.show()

        self.check_mouseclick = gtk.CheckButton("Click: ...")
        box.pack_start(self.check_mouseclick, False)
        self.check_mouseclick.show()

        self.spin_sensitivity = gtk.SpinButton(gtk.Adjustment(1, 1, 100, 1, 10,
                                                              0),
                                                              climb_rate=0.0)
        box.pack_end(self.spin_sensitivity, False)
        self.spin_sensitivity.show()

        label = gtk.Label("Sensitivity:")
        box.pack_end(label, False)
        label.show()

        self.spin_latency = gtk.SpinButton(gtk.Adjustment(10, 1, 500, 1, 10, 0),
                                           climb_rate=0.0)
        box.pack_end(self.spin_latency, False)
        self.spin_latency.show()

        label = gtk.Label("Latency:")
        box.pack_end(label, False)
        label.show()

        self.handler_event_box_press = None
        self.handler_event_box_release = None
        self.handler_event_box_scroll = None
        self.handler_event_box_motion = None
        self.handler_event_box_expose = None

        self.window.realize()
        self.window.show()

        self.clear_state()

    # Utilities

    def message(self, text, title):
        dlg = gtk.MessageDialog(self.window,
                gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
                gtk.MESSAGE_INFO,
                gtk.BUTTONS_CLOSE,
                title)
        dlg.set_title(title)
        dlg.format_secondary_text(text)
        response = dlg.run()
        dlg.destroy()


    def question_yes_no(self, text, title):
        dlg = gtk.MessageDialog(self.window,
                gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
                gtk.MESSAGE_QUESTION,
                gtk.BUTTONS_YES_NO,
                title)
        dlg.set_title(title)
        dlg.format_secondary_text(text)
        response = dlg.run()
        dlg.destroy()
        if response == gtk.RESPONSE_YES:
            return True
        return False


    def inputdialog(self, text, title, default_response=""):
        # Define a little helper function
        def inputdialog_entry_activated(entry):
            dlg.response(gtk.RESPONSE_OK)

        # Create the dialog
        dlg = gtk.MessageDialog(self.window,
                gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
                gtk.MESSAGE_QUESTION,
                gtk.BUTTONS_OK_CANCEL,
                title)
        dlg.set_title(title)
        dlg.format_secondary_text(text)

        # Create an entry widget
        entry = gtk.Entry()
        entry.set_text(default_response)
        entry.connect("activate", inputdialog_entry_activated)
        dlg.vbox.pack_start(entry)
        entry.show()

        # Run the dialog
        response = dlg.run()
        dlg.destroy()
        if response == gtk.RESPONSE_OK:
            return entry.get_text()
        return None


    def filedialog(self, title=None, default_filename=None):
        chooser = gtk.FileChooserDialog(title=title, parent=self.window,
                                        action=gtk.FILE_CHOOSER_ACTION_OPEN,
                buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OPEN,
                         gtk.RESPONSE_OK))
        chooser.resize(700, 500)
        if default_filename:
            chooser.set_filename(os.path.abspath(default_filename))
        filename = None
        response = chooser.run()
        if response == gtk.RESPONSE_OK:
            filename = chooser.get_filename()
        chooser.destroy()
        return filename


    def redirect_event_box_input(self, press=None, release=None, scroll=None,
                                 motion=None, expose=None):
        if self.handler_event_box_press != None: \
        self.event_box.disconnect(self.handler_event_box_press)
        if self.handler_event_box_release != None: \
        self.event_box.disconnect(self.handler_event_box_release)
        if self.handler_event_box_scroll != None: \
        self.event_box.disconnect(self.handler_event_box_scroll)
        if self.handler_event_box_motion != None: \
        self.event_box.disconnect(self.handler_event_box_motion)
        if self.handler_event_box_expose != None: \
        self.event_box.disconnect(self.handler_event_box_expose)
        self.handler_event_box_press = None
        self.handler_event_box_release = None
        self.handler_event_box_scroll = None
        self.handler_event_box_motion = None
        self.handler_event_box_expose = None
        if press != None: self.handler_event_box_press = \
        self.event_box.connect("button-press-event", press)
        if release != None: self.handler_event_box_release = \
        self.event_box.connect("button-release-event", release)
        if scroll != None: self.handler_event_box_scroll = \
        self.event_box.connect("scroll-event", scroll)
        if motion != None: self.handler_event_box_motion = \
        self.event_box.connect("motion-notify-event", motion)
        if expose != None: self.handler_event_box_expose = \
        self.event_box.connect_after("expose-event", expose)


    def get_keys(self):
        return self.text_buffer.get_text(
                self.text_buffer.get_start_iter(),
                self.text_buffer.get_end_iter())


    def add_key(self, key):
        text = self.get_keys()
        if len(text) > 0 and text[-1] != ' ':
            text += " "
        text += key
        self.text_buffer.set_text(text)


    def clear_keys(self):
        self.text_buffer.set_text("")


    def update_barrier_info(self):
        if self.barrier_selected:
            self.label_barrier_region.set_text("Selected region: Corner: " + \
                                            str(tuple(self.barrier_corner)) + \
                                            " Size: " + \
                                            str(tuple(self.barrier_size)))
        else:
            self.label_barrier_region.set_text("No region selected.")
        self.label_barrier_md5sum.set_text("MD5: " + self.barrier_md5sum)


    def update_mouse_click_info(self):
        if self.mouse_click_captured:
            self.check_mousemove.set_label("Move: " + \
                                           str(tuple(self.mouse_click_coords)))
            self.check_mouseclick.set_label("Click: button %d" %
                                            self.mouse_click_button)
        else:
            self.check_mousemove.set_label("Move: ...")
            self.check_mouseclick.set_label("Click: ...")


    def clear_state(self, clear_screendump=True):
        # Recording time
        self.entry_time.set_text("unknown")
        if clear_screendump:
            # Screendump
            self.clear_image()
        # Screendump ID
        self.entry_screendump.set_text("")
        # Comment
        self.entry_comment.set_text("")
        # Sleep
        self.check_sleep.set_active(True)
        self.check_sleep.set_active(False)
        self.spin_sleep.set_value(10)
        # Barrier
        self.clear_barrier_state()
        # Keystrokes
        self.check_manual.set_active(False)
        self.clear_keys()
        # Mouse actions
        self.check_mousemove.set_sensitive(False)
        self.check_mouseclick.set_sensitive(False)
        self.check_mousemove.set_active(False)
        self.check_mouseclick.set_active(False)
        self.mouse_click_captured = False
        self.mouse_click_coords = [0, 0]
        self.mouse_click_button = 0
        self.update_mouse_click_info()


    def clear_barrier_state(self):
        self.check_barrier.set_active(True)
        self.check_barrier.set_active(False)
        self.check_barrier_optional.set_active(False)
        self.spin_barrier_timeout.set_value(10)
        self.barrier_selection_started = False
        self.barrier_selected = False
        self.barrier_corner0 = [0, 0]
        self.barrier_corner1 = [0, 0]
        self.barrier_corner = [0, 0]
        self.barrier_size = [0, 0]
        self.barrier_md5sum = ""
        self.update_barrier_info()


    def set_image(self, w, h, data):
        (self.image_width, self.image_height, self.image_data) = (w, h, data)
        self.image.set_from_pixbuf(gtk.gdk.pixbuf_new_from_data(
            data, gtk.gdk.COLORSPACE_RGB, False, 8,
            w, h, w*3))
        hscrollbar = self.scrolledwindow.get_hscrollbar()
        hscrollbar.set_range(0, w)
        vscrollbar = self.scrolledwindow.get_vscrollbar()
        vscrollbar.set_range(0, h)


    def set_image_from_file(self, filename):
        if not ppm_utils.image_verify_ppm_file(filename):
            logging.warning("set_image_from_file: Warning: received invalid"
                            "screendump file")
            return self.clear_image()
        (w, h, data) = ppm_utils.image_read_from_ppm_file(filename)
        self.set_image(w, h, data)


    def clear_image(self):
        self.image.clear()
        self.image_width = 0
        self.image_height = 0
        self.image_data = ""


    def update_screendump_id(self, data_dir):
        if not self.image_data:
            return
        # Find a proper ID for the screendump
        scrdump_md5sum = ppm_utils.image_md5sum(self.image_width,
                                                self.image_height,
                                                self.image_data)
        scrdump_id = ppm_utils.find_id_for_screendump(scrdump_md5sum, data_dir)
        if not scrdump_id:
            # Not found; generate one
            scrdump_id = ppm_utils.generate_id_for_screendump(scrdump_md5sum,
                                                              data_dir)
        self.entry_screendump.set_text(scrdump_id)


    def get_step_lines(self, data_dir=None):
        if self.check_barrier.get_active() and not self.barrier_selected:
            self.message("No barrier region selected.", "Error")
            return

        str = "step"

        # Add step recording time
        if self.entry_time.get_text():
            str += " " + self.entry_time.get_text()

        str += "\n"

        # Add screendump line
        if self.image_data:
            str += "screendump %s\n" % self.entry_screendump.get_text()

        # Add comment
        if self.entry_comment.get_text():
            str += "# %s\n" % self.entry_comment.get_text()

        # Add sleep line
        if self.check_sleep.get_active():
            str += "sleep %d\n" % self.spin_sleep.get_value()

        # Add barrier_2 line
        if self.check_barrier.get_active():
            str += "barrier_2 %d %d %d %d %s %d" % (
                    self.barrier_size[0], self.barrier_size[1],
                    self.barrier_corner[0], self.barrier_corner[1],
                    self.barrier_md5sum, self.spin_barrier_timeout.get_value())
            if self.check_barrier_optional.get_active():
                str += " optional"
            str += "\n"

        # Add "Sending keys" comment
        keys_to_send = self.get_keys().split()
        if keys_to_send:
            str += "# Sending keys: %s\n" % self.get_keys()

        # Add key and var lines
        for key in keys_to_send:
            if key.startswith("$"):
                varname = key[1:]
                str += "var %s\n" % varname
            else:
                str += "key %s\n" % key

        # Add mousemove line
        if self.check_mousemove.get_active():
            str += "mousemove %d %d\n" % (self.mouse_click_coords[0],
                                          self.mouse_click_coords[1])

        # Add mouseclick line
        if self.check_mouseclick.get_active():
            dict = { 1 : 1,
                     2 : 2,
                     3 : 4 }
            str += "mouseclick %d\n" % dict[self.mouse_click_button]

        # Write screendump and cropped screendump image files
        if data_dir and self.image_data:
            # Create the data dir if it doesn't exist
            if not os.path.exists(data_dir):
                os.makedirs(data_dir)
            # Get the full screendump filename
            scrdump_filename = os.path.join(data_dir,
                                            self.entry_screendump.get_text())
            # Write screendump file if it doesn't exist
            if not os.path.exists(scrdump_filename):
                try:
                    ppm_utils.image_write_to_ppm_file(scrdump_filename,
                                                      self.image_width,
                                                      self.image_height,
                                                      self.image_data)
                except IOError:
                    self.message("Could not write screendump file.", "Error")

            #if self.check_barrier.get_active():
            #    # Crop image to get the cropped screendump
            #    (cw, ch, cdata) = ppm_utils.image_crop(
            #            self.image_width, self.image_height, self.image_data,
            #            self.barrier_corner[0], self.barrier_corner[1],
            #            self.barrier_size[0], self.barrier_size[1])
            #    cropped_scrdump_md5sum = ppm_utils.image_md5sum(cw, ch, cdata)
            #    cropped_scrdump_filename = \
            #    ppm_utils.get_cropped_screendump_filename(scrdump_filename,
            #                                            cropped_scrdump_md5sum)
            #    # Write cropped screendump file
            #    try:
            #        ppm_utils.image_write_to_ppm_file(cropped_scrdump_filename,
            #                                          cw, ch, cdata)
            #    except IOError:
            #        self.message("Could not write cropped screendump file.",
            #                     "Error")

        return str

    def set_state_from_step_lines(self, str, data_dir, warn=True):
        self.clear_state()

        for line in str.splitlines():
            words = line.split()
            if not words:
                continue

            if line.startswith("#") \
                    and not self.entry_comment.get_text() \
                    and not line.startswith("# Sending keys:") \
                    and not line.startswith("# ----"):
                self.entry_comment.set_text(line.strip("#").strip())

            elif words[0] == "step":
                if len(words) >= 2:
                    self.entry_time.set_text(words[1])

            elif words[0] == "screendump":
                self.entry_screendump.set_text(words[1])
                self.set_image_from_file(os.path.join(data_dir, words[1]))

            elif words[0] == "sleep":
                self.spin_sleep.set_value(int(words[1]))
                self.check_sleep.set_active(True)

            elif words[0] == "key":
                self.add_key(words[1])

            elif words[0] == "var":
                self.add_key("$%s" % words[1])

            elif words[0] == "mousemove":
                self.mouse_click_captured = True
                self.mouse_click_coords = [int(words[1]), int(words[2])]
                self.update_mouse_click_info()

            elif words[0] == "mouseclick":
                self.mouse_click_captured = True
                self.mouse_click_button = int(words[1])
                self.update_mouse_click_info()

            elif words[0] == "barrier_2":
                # Get region corner and size from step lines
                self.barrier_corner = [int(words[3]), int(words[4])]
                self.barrier_size = [int(words[1]), int(words[2])]
                # Get corner0 and corner1 from step lines
                self.barrier_corner0 = self.barrier_corner
                self.barrier_corner1 = [self.barrier_corner[0] +
                                        self.barrier_size[0] - 1,
                                        self.barrier_corner[1] +
                                        self.barrier_size[1] - 1]
                # Get the md5sum
                self.barrier_md5sum = words[5]
                # Pretend the user selected the region with the mouse
                self.barrier_selection_started = True
                self.barrier_selected = True
                # Update label widgets according to region information
                self.update_barrier_info()
                # Check the barrier checkbutton
                self.check_barrier.set_active(True)
                # Set timeout value
                self.spin_barrier_timeout.set_value(int(words[6]))
                # Set 'optional' checkbutton state
                self.check_barrier_optional.set_active(words[-1] == "optional")
                # Update the image widget
                self.event_box.queue_draw()

                if warn:
                    # See if the computed md5sum matches the one recorded in
                    # the file
                    computed_md5sum = ppm_utils.get_region_md5sum(
                            self.image_width, self.image_height,
                            self.image_data, self.barrier_corner[0],
                            self.barrier_corner[1], self.barrier_size[0],
                            self.barrier_size[1])
                    if computed_md5sum != self.barrier_md5sum:
                        self.message("Computed MD5 sum (%s) differs from MD5"
                                     " sum recorded in steps file (%s)" %
                                     (computed_md5sum, self.barrier_md5sum),
                                     "Warning")

    # Events

    def delete_event(self, widget, event):
        pass

    def destroy(self, widget):
        gtk.main_quit()

    def event_check_barrier_toggled(self, widget):
        if self.check_barrier.get_active():
            self.redirect_event_box_input(
                    self.event_button_press,
                    self.event_button_release,
                    None,
                    None,
                    self.event_expose)
            self.event_box.queue_draw()
            self.event_box.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.CROSSHAIR))
            self.label_barrier_region.set_sensitive(True)
            self.label_barrier_md5sum.set_sensitive(True)
            self.label_barrier_timeout.set_sensitive(True)
            self.spin_barrier_timeout.set_sensitive(True)
            self.check_barrier_optional.set_sensitive(True)
        else:
            self.redirect_event_box_input()
            self.event_box.queue_draw()
            self.event_box.window.set_cursor(None)
            self.label_barrier_region.set_sensitive(False)
            self.label_barrier_md5sum.set_sensitive(False)
            self.label_barrier_timeout.set_sensitive(False)
            self.spin_barrier_timeout.set_sensitive(False)
            self.check_barrier_optional.set_sensitive(False)

    def event_check_sleep_toggled(self, widget):
        if self.check_sleep.get_active():
            self.spin_sleep.set_sensitive(True)
        else:
            self.spin_sleep.set_sensitive(False)

    def event_manual_toggled(self, widget):
        self.entry_keys.grab_focus()

    def event_clear_clicked(self, widget):
        self.clear_keys()
        self.entry_keys.grab_focus()

    def event_expose(self, widget, event):
        if not self.barrier_selection_started:
            return
        (corner, size) = corner_and_size_clipped(self.barrier_corner0,
                                                 self.barrier_corner1,
                                                 self.event_box.size_request())
        gc = self.event_box.window.new_gc(line_style=gtk.gdk.LINE_DOUBLE_DASH,
                                          line_width=1)
        gc.set_foreground(gc.get_colormap().alloc_color("red"))
        gc.set_background(gc.get_colormap().alloc_color("dark red"))
        gc.set_dashes(0, (4, 4))
        self.event_box.window.draw_rectangle(
                gc, False,
                corner[0], corner[1],
                size[0]-1, size[1]-1)

    def event_drag_motion(self, widget, event):
        old_corner1 = self.barrier_corner1
        self.barrier_corner1 = [int(event.x), int(event.y)]
        (corner, size) = corner_and_size_clipped(self.barrier_corner0,
                                                 self.barrier_corner1,
                                                 self.event_box.size_request())
        (old_corner, old_size) = corner_and_size_clipped(self.barrier_corner0,
                                                         old_corner1,
                                                  self.event_box.size_request())
        corner0 = [min(corner[0], old_corner[0]), min(corner[1], old_corner[1])]
        corner1 = [max(corner[0] + size[0], old_corner[0] + old_size[0]),
                   max(corner[1] + size[1], old_corner[1] + old_size[1])]
        size = [corner1[0] - corner0[0] + 1,
                corner1[1] - corner0[1] + 1]
        self.event_box.queue_draw_area(corner0[0], corner0[1], size[0], size[1])

    def event_button_press(self, widget, event):
        (corner, size) = corner_and_size_clipped(self.barrier_corner0,
                                                 self.barrier_corner1,
                                                 self.event_box.size_request())
        self.event_box.queue_draw_area(corner[0], corner[1], size[0], size[1])
        self.barrier_corner0 = [int(event.x), int(event.y)]
        self.barrier_corner1 = [int(event.x), int(event.y)]
        self.redirect_event_box_input(
                self.event_button_press,
                self.event_button_release,
                None,
                self.event_drag_motion,
                self.event_expose)
        self.barrier_selection_started = True

    def event_button_release(self, widget, event):
        self.redirect_event_box_input(
                self.event_button_press,
                self.event_button_release,
                None,
                None,
                self.event_expose)
        (self.barrier_corner, self.barrier_size) = \
        corner_and_size_clipped(self.barrier_corner0, self.barrier_corner1,
                                self.event_box.size_request())
        self.barrier_md5sum = ppm_utils.get_region_md5sum(
                self.image_width, self.image_height, self.image_data,
                self.barrier_corner[0], self.barrier_corner[1],
                self.barrier_size[0], self.barrier_size[1])
        self.barrier_selected = True
        self.update_barrier_info()

    def event_key_press(self, widget, event):
        if self.check_manual.get_active():
            return False
        str = key_event_to_qemu_string(event)
        self.add_key(str)
        return True


class StepEditor(StepMakerWindow):
    ui = '''<ui>
    <menubar name="MenuBar">
        <menu action="File">
            <menuitem action="Open"/>
            <separator/>
            <menuitem action="Quit"/>
        </menu>
        <menu action="Edit">
            <menuitem action="CopyStep"/>
            <menuitem action="DeleteStep"/>
        </menu>
        <menu action="Insert">
            <menuitem action="InsertNewBefore"/>
            <menuitem action="InsertNewAfter"/>
            <separator/>
            <menuitem action="InsertStepsBefore"/>
            <menuitem action="InsertStepsAfter"/>
        </menu>
        <menu action="Tools">
            <menuitem action="CleanUp"/>
        </menu>
    </menubar>
</ui>'''

    # Constructor

    def __init__(self, filename=None):
        StepMakerWindow.__init__(self)

        self.steps_filename = None
        self.steps = []

        # Create a UIManager instance
        uimanager = gtk.UIManager()

        # Add the accelerator group to the toplevel window
        accelgroup = uimanager.get_accel_group()
        self.window.add_accel_group(accelgroup)

        # Create an ActionGroup
        actiongroup = gtk.ActionGroup('StepEditor')

        # Create actions
        actiongroup.add_actions([
            ('Quit', gtk.STOCK_QUIT, '_Quit', None, 'Quit the Program',
             self.quit),
            ('Open', gtk.STOCK_OPEN, '_Open', None, 'Open steps file',
             self.open_steps_file),
            ('CopyStep', gtk.STOCK_COPY, '_Copy current step...', "",
             'Copy current step to user specified position', self.copy_step),
            ('DeleteStep', gtk.STOCK_DELETE, '_Delete current step', "",
             'Delete current step', self.event_remove_clicked),
            ('InsertNewBefore', gtk.STOCK_ADD, '_New step before current', "",
             'Insert new step before current step', self.insert_before),
            ('InsertNewAfter', gtk.STOCK_ADD, 'N_ew step after current', "",
             'Insert new step after current step', self.insert_after),
            ('InsertStepsBefore', gtk.STOCK_ADD, '_Steps before current...',
             "", 'Insert steps (from file) before current step',
             self.insert_steps_before),
            ('InsertStepsAfter', gtk.STOCK_ADD, 'Steps _after current...', "",
             'Insert steps (from file) after current step',
             self.insert_steps_after),
            ('CleanUp', gtk.STOCK_DELETE, '_Clean up data directory', "",
             'Move unused PPM files to a backup directory', self.cleanup),
            ('File', None, '_File'),
            ('Edit', None, '_Edit'),
            ('Insert', None, '_Insert'),
            ('Tools', None, '_Tools')
            ])

        def create_shortcut(name, callback, keyname):
            # Create an action
            action = gtk.Action(name, None, None, None)
            # Connect a callback to the action
            action.connect("activate", callback)
            actiongroup.add_action_with_accel(action, keyname)
            # Have the action use accelgroup
            action.set_accel_group(accelgroup)
            # Connect the accelerator to the action
            action.connect_accelerator()

        create_shortcut("Next", self.event_next_clicked, "Page_Down")
        create_shortcut("Previous", self.event_prev_clicked, "Page_Up")

        # Add the actiongroup to the uimanager
        uimanager.insert_action_group(actiongroup, 0)

        # Add a UI description
        uimanager.add_ui_from_string(self.ui)

        # Create a MenuBar
        menubar = uimanager.get_widget('/MenuBar')
        self.menu_vbox.pack_start(menubar, False)

        # Remember the Edit menu bar for future reference
        self.menu_edit = uimanager.get_widget('/MenuBar/Edit')
        self.menu_edit.set_sensitive(False)

        # Remember the Insert menu bar for future reference
        self.menu_insert = uimanager.get_widget('/MenuBar/Insert')
        self.menu_insert.set_sensitive(False)

        # Remember the Tools menu bar for future reference
        self.menu_tools = uimanager.get_widget('/MenuBar/Tools')
        self.menu_tools.set_sensitive(False)

        # Next/Previous HBox
        hbox = gtk.HBox(spacing=10)
        self.user_vbox.pack_start(hbox)
        hbox.show()

        self.button_first = gtk.Button(stock=gtk.STOCK_GOTO_FIRST)
        self.button_first.connect("clicked", self.event_first_clicked)
        hbox.pack_start(self.button_first)
        self.button_first.show()

        #self.button_prev = gtk.Button("<< Previous")
        self.button_prev = gtk.Button(stock=gtk.STOCK_GO_BACK)
        self.button_prev.connect("clicked", self.event_prev_clicked)
        hbox.pack_start(self.button_prev)
        self.button_prev.show()

        self.label_step = gtk.Label("Step:")
        hbox.pack_start(self.label_step, False)
        self.label_step.show()

        self.entry_step_num = gtk.Entry()
        self.entry_step_num.connect("activate", self.event_entry_step_activated)
        self.entry_step_num.set_width_chars(3)
        hbox.pack_start(self.entry_step_num, False)
        self.entry_step_num.show()

        #self.button_next = gtk.Button("Next >>")
        self.button_next = gtk.Button(stock=gtk.STOCK_GO_FORWARD)
        self.button_next.connect("clicked", self.event_next_clicked)
        hbox.pack_start(self.button_next)
        self.button_next.show()

        self.button_last = gtk.Button(stock=gtk.STOCK_GOTO_LAST)
        self.button_last.connect("clicked", self.event_last_clicked)
        hbox.pack_start(self.button_last)
        self.button_last.show()

        # Save HBox
        hbox = gtk.HBox(spacing=10)
        self.user_vbox.pack_start(hbox)
        hbox.show()

        self.button_save = gtk.Button("_Save current step")
        self.button_save.connect("clicked", self.event_save_clicked)
        hbox.pack_start(self.button_save)
        self.button_save.show()

        self.button_remove = gtk.Button("_Delete current step")
        self.button_remove.connect("clicked", self.event_remove_clicked)
        hbox.pack_start(self.button_remove)
        self.button_remove.show()

        self.button_replace = gtk.Button("_Replace screendump")
        self.button_replace.connect("clicked", self.event_replace_clicked)
        hbox.pack_start(self.button_replace)
        self.button_replace.show()

        # Disable unused widgets
        self.button_capture.set_sensitive(False)
        self.spin_latency.set_sensitive(False)
        self.spin_sensitivity.set_sensitive(False)

        # Disable main vbox because no steps file is loaded
        self.main_vbox.set_sensitive(False)

        # Set title
        self.window.set_title("Step Editor")

    # Events

    def delete_event(self, widget, event):
        # Make sure the step is saved (if the user wants it to be)
        self.verify_save()

    def event_first_clicked(self, widget):
        if not self.steps:
            return
        # Make sure the step is saved (if the user wants it to be)
        self.verify_save()
        # Go to first step
        self.set_step(0)

    def event_last_clicked(self, widget):
        if not self.steps:
            return
        # Make sure the step is saved (if the user wants it to be)
        self.verify_save()
        # Go to last step
        self.set_step(len(self.steps) - 1)

    def event_prev_clicked(self, widget):
        if not self.steps:
            return
        # Make sure the step is saved (if the user wants it to be)
        self.verify_save()
        # Go to previous step
        index = self.current_step_index - 1
        if self.steps:
            index = index % len(self.steps)
        self.set_step(index)

    def event_next_clicked(self, widget):
        if not self.steps:
            return
        # Make sure the step is saved (if the user wants it to be)
        self.verify_save()
        # Go to next step
        index = self.current_step_index + 1
        if self.steps:
            index = index % len(self.steps)
        self.set_step(index)

    def event_entry_step_activated(self, widget):
        if not self.steps:
            return
        step_index = self.entry_step_num.get_text()
        if not step_index.isdigit():
            return
        step_index = int(step_index) - 1
        if step_index == self.current_step_index:
            return
        self.verify_save()
        self.set_step(step_index)

    def event_save_clicked(self, widget):
        if not self.steps:
            return
        self.save_step()

    def event_remove_clicked(self, widget):
        if not self.steps:
            return
        if not self.question_yes_no("This will modify the steps file."
                                    " Are you sure?", "Remove step?"):
            return
        # Remove step
        del self.steps[self.current_step_index]
        # Write changes to file
        self.write_steps_file(self.steps_filename)
        # Move to previous step
        self.set_step(self.current_step_index)

    def event_replace_clicked(self, widget):
        if not self.steps:
            return
        # Let the user choose a screendump file
        current_filename = os.path.join(self.steps_data_dir,
                                        self.entry_screendump.get_text())
        filename = self.filedialog("Choose PPM image file",
                                   default_filename=current_filename)
        if not filename:
            return
        if not ppm_utils.image_verify_ppm_file(filename):
            self.message("Not a valid PPM image file.", "Error")
            return
        self.clear_image()
        self.clear_barrier_state()
        self.set_image_from_file(filename)
        self.update_screendump_id(self.steps_data_dir)

    # Menu actions

    def open_steps_file(self, action):
        # Make sure the step is saved (if the user wants it to be)
        self.verify_save()
        # Let the user choose a steps file
        current_filename = self.steps_filename
        filename = self.filedialog("Open steps file",
                                   default_filename=current_filename)
        if not filename:
            return
        self.set_steps_file(filename)

    def quit(self, action):
        # Make sure the step is saved (if the user wants it to be)
        self.verify_save()
        # Quit
        gtk.main_quit()

    def copy_step(self, action):
        if not self.steps:
            return
        self.verify_save()
        self.set_step(self.current_step_index)
        # Get the desired position
        step_index = self.inputdialog("Copy step to position:",
                                      "Copy step",
                                      str(self.current_step_index + 2))
        if not step_index:
            return
        step_index = int(step_index) - 1
        # Get the lines of the current step
        step = self.steps[self.current_step_index]
        # Insert new step at position step_index
        self.steps.insert(step_index, step)
        # Go to new step
        self.set_step(step_index)
        # Write changes to disk
        self.write_steps_file(self.steps_filename)

    def insert_before(self, action):
        if not self.steps_filename:
            return
        if not self.question_yes_no("This will modify the steps file."
                                    " Are you sure?", "Insert new step?"):
            return
        self.verify_save()
        step_index = self.current_step_index
        # Get the lines of a blank step
        self.clear_state()
        step = self.get_step_lines()
        # Insert new step at position step_index
        self.steps.insert(step_index, step)
        # Go to new step
        self.set_step(step_index)
        # Write changes to disk
        self.write_steps_file(self.steps_filename)

    def insert_after(self, action):
        if not self.steps_filename:
            return
        if not self.question_yes_no("This will modify the steps file."
                                    " Are you sure?", "Insert new step?"):
            return
        self.verify_save()
        step_index = self.current_step_index + 1
        # Get the lines of a blank step
        self.clear_state()
        step = self.get_step_lines()
        # Insert new step at position step_index
        self.steps.insert(step_index, step)
        # Go to new step
        self.set_step(step_index)
        # Write changes to disk
        self.write_steps_file(self.steps_filename)

    def insert_steps(self, filename, index):
        # Read the steps file
        (steps, header) = self.read_steps_file(filename)

        data_dir = ppm_utils.get_data_dir(filename)
        for step in steps:
            self.set_state_from_step_lines(step, data_dir, warn=False)
            step = self.get_step_lines(self.steps_data_dir)

        # Insert steps into self.steps
        self.steps[index:index] = steps
        # Write changes to disk
        self.write_steps_file(self.steps_filename)

    def insert_steps_before(self, action):
        if not self.steps_filename:
            return
        # Let the user choose a steps file
        current_filename = self.steps_filename
        filename = self.filedialog("Choose steps file",
                                   default_filename=current_filename)
        if not filename:
            return
        self.verify_save()

        step_index = self.current_step_index
        # Insert steps at position step_index
        self.insert_steps(filename, step_index)
        # Go to new steps
        self.set_step(step_index)

    def insert_steps_after(self, action):
        if not self.steps_filename:
            return
        # Let the user choose a steps file
        current_filename = self.steps_filename
        filename = self.filedialog("Choose steps file",
                                   default_filename=current_filename)
        if not filename:
            return
        self.verify_save()

        step_index = self.current_step_index + 1
        # Insert new steps at position step_index
        self.insert_steps(filename, step_index)
        # Go to new steps
        self.set_step(step_index)

    def cleanup(self, action):
        if not self.steps_filename:
            return
        if not self.question_yes_no("All unused PPM files will be moved to a"
                                    " backup directory. Are you sure?",
                                    "Clean up data directory?"):
            return
        # Remember the current step index
        current_step_index = self.current_step_index
        # Get the backup dir
        backup_dir = os.path.join(self.steps_data_dir, "backup")
        # Create it if it doesn't exist
        if not os.path.exists(backup_dir):
            os.makedirs(backup_dir)
        # Move all files to the backup dir
        for filename in glob.glob(os.path.join(self.steps_data_dir,
                                               "*.[Pp][Pp][Mm]")):
            shutil.move(filename, backup_dir)
        # Get the used files back
        for step in self.steps:
            self.set_state_from_step_lines(step, backup_dir, warn=False)
            self.get_step_lines(self.steps_data_dir)
        # Remove the used files from the backup dir
        used_files = os.listdir(self.steps_data_dir)
        for filename in os.listdir(backup_dir):
            if filename in used_files:
                os.unlink(os.path.join(backup_dir, filename))
        # Restore step index
        self.set_step(current_step_index)
        # Inform the user
        self.message("All unused PPM files may be found at %s." %
                     os.path.abspath(backup_dir),
                     "Clean up data directory")

    # Methods

    def read_steps_file(self, filename):
        steps = []
        header = ""

        file = open(filename, "r")
        for line in file.readlines():
            words = line.split()
            if not words:
                continue
            if line.startswith("# ----"):
                continue
            if words[0] == "step":
                steps.append("")
            if steps:
                steps[-1] += line
            else:
                header += line
        file.close()

        return (steps, header)

    def set_steps_file(self, filename):
        try:
            (self.steps, self.header) = self.read_steps_file(filename)
        except (TypeError, IOError):
            self.message("Cannot read file %s." % filename, "Error")
            return

        self.steps_filename = filename
        self.steps_data_dir = ppm_utils.get_data_dir(filename)
        # Go to step 0
        self.set_step(0)

    def set_step(self, index):
        # Limit index to legal boundaries
        if index < 0:
            index = 0
        if index > len(self.steps) - 1:
            index = len(self.steps) - 1

        # Enable the menus
        self.menu_edit.set_sensitive(True)
        self.menu_insert.set_sensitive(True)
        self.menu_tools.set_sensitive(True)

        # If no steps exist...
        if self.steps == []:
            self.current_step_index = index
            self.current_step = None
            # Set window title
            self.window.set_title("Step Editor -- %s" %
                                  os.path.basename(self.steps_filename))
            # Set step entry widget text
            self.entry_step_num.set_text("")
            # Clear the state of all widgets
            self.clear_state()
            # Disable the main vbox
            self.main_vbox.set_sensitive(False)
            return

        self.current_step_index = index
        self.current_step = self.steps[index]
        # Set window title
        self.window.set_title("Step Editor -- %s -- step %d" %
                              (os.path.basename(self.steps_filename),
                               index + 1))
        # Set step entry widget text
        self.entry_step_num.set_text(str(self.current_step_index + 1))
        # Load the state from the step lines
        self.set_state_from_step_lines(self.current_step, self.steps_data_dir)
        # Enable the main vbox
        self.main_vbox.set_sensitive(True)
        # Make sure the step lines in self.current_step are identical to the
        # output of self.get_step_lines
        self.current_step = self.get_step_lines()

    def verify_save(self):
        if not self.steps:
            return
        # See if the user changed anything
        if self.get_step_lines() != self.current_step:
            if self.question_yes_no("Step contents have been modified."
                                    " Save step?", "Save changes?"):
                self.save_step()

    def save_step(self):
        lines = self.get_step_lines(self.steps_data_dir)
        if lines != None:
            self.steps[self.current_step_index] = lines
            self.current_step = lines
            self.write_steps_file(self.steps_filename)

    def write_steps_file(self, filename):
        file = open(filename, "w")
        file.write(self.header)
        for step in self.steps:
            file.write("# " + "-" * 32 + "\n")
            file.write(step)
        file.close()


if __name__ == "__main__":
    se = StepEditor()
    if len(sys.argv) > 1:
        se.set_steps_file(sys.argv[1])
    gtk.main()