#!/usr/bin/env python

""" Small GTK GUI to allow the user to enter domain names for user actions.
    Also allow hierarchical viewing of usecases.
    Tkinter users still need GTK for TextTest to work... """

import gtktoolkit, gtk, encodingutils, os, sys, logging, shutil, re
from optparse import OptionParser

from ordereddict import OrderedDict
from guishared import UIMapFileHandler, removeMarkup
from replayer import ShortcutManager, ReplayScript
from recorder import RecordScript
from definitions import __version__, waitCommandName
from xml.sax.saxutils import escape, unescape
import itertools

class EditorScriptEngine(gtktoolkit.ScriptEngine):
    @classmethod
    def getShortcuts(cls, storyTextHome=None):
        return [] # Don't use the application shortcuts for our own replaying...

class UseCaseEditor:
    enterTitle = "Enter Usecase names for auto-recorded actions"
    def __init__(self, fileName, interface, mapFiles):
        self.fileName = fileName
        self.editTitle = self.fileName
        if os.getenv("TEXTTEST_HOME"):
            self.editTitle = self.fileName.replace(os.getenv("TEXTTEST_HOME") + "/", "")
        self.interface = interface
        self.uiMapFileHandler = UIMapFileHandler(mapFiles)
        self.scriptEngine = EditorScriptEngine(uiMapFiles=[])
        self.initShortcutManager()
        self.allEntries = OrderedDict()
        self.allDescriptionWidgets = []
        self.popupSensitivities = {}
        self.createdShortcuts = []

    def initShortcutManager(self):
        self.shortcutManager = ShortcutManager()
        for shortcut in gtktoolkit.ScriptEngine.getShortcuts():
            self.shortcutManager.add(shortcut)

    def getAllCommands(self): 
        return [ line.strip() for line in encodingutils.openEncoded(self.fileName) ]

    def getNewUsecaseNames(self):
        return [ entry.get_text() for entry in self.allEntries.values() ]
    
    def getNewWidgetDescriptions(self):
        def get_text(widget):
            return removeMarkup(unescape(widget.get_active_text())) if hasattr(widget, "get_active_text") else widget.get_text()
        return map(get_text, self.allDescriptionWidgets)
        
    def run(self):
        commands = self.getAllCommands()
        autoGenerated = self.getAutoGenerated(commands)
        autoGeneratedInfo = self.parseAutoGenerated(autoGenerated)
        self.isAutoGenerated = len(autoGenerated) > 0
        dialog = self.createDialog(autoGeneratedInfo, commands)
        self.runDialog(dialog, autoGenerated, autoGeneratedInfo)

    def runDialog(self, dialog, autoGenerated, autoGeneratedInfo):
        response = dialog.run()
        if response == gtk.RESPONSE_ACCEPT:
            newNames = self.getNewUsecaseNames()
            if len(autoGenerated) > 0:
                duplicateNames = self.getDuplicateNames(newNames)
                if duplicateNames and not self.acceptDuplicateNames(dialog, duplicateNames):
                    self.runDialog(dialog, autoGenerated, autoGeneratedInfo)
                dialog.destroy()
                self.replaceInFile(self.fileName, self.makeReplacement, zip(autoGenerated, newNames))
                toStore = zip(self.getNewWidgetDescriptions(), autoGeneratedInfo.values(), newNames)
                for widgetDescription, signalInfo, eventName in toStore:
                    self.uiMapFileHandler.storeInfo(widgetDescription, signalInfo[-1], eventName)
                self.uiMapFileHandler.write()
        elif len(autoGenerated) == 0:
            dialog.destroy()
        else:
            # Don't leave a half generated filename behind, if we didn't fill in the dialog properly
            # we should remove it so nobody saves it...
            for shortcutName in self.createdShortcuts:
                os.remove(shortcutName)
            os.remove(self.fileName)
            dialog.destroy()

    def getDuplicateNames(self, allNames):
        duplicates = [] 
        for name in allNames:
            value, args = self.uiMapFileHandler.splitOptionValue(name)
            if name and value and not args:
                duplicates.append(name)
        return duplicates

    def acceptDuplicateNames(self, parent, duplicateNames):                
        message = "You have entered a name that already exists in the UI map.\n" + \
            "You are allowed to do this but please be aware it may cause problems.\n" + \
            "The following duplicate names were found:\n"
        for duplicate in duplicateNames:
            message += duplicate + "\n"
        dialog = self.createMessageDialog(parent, message, gtk.MESSAGE_WARNING, "Warning", gtk.BUTTONS_OK_CANCEL)
        dialog.show_all()
        response = dialog.run()
        dialog.hide()
        return response == gtk.RESPONSE_OK

    def getAutoGenerated(self, commands):
        # Find the auto-generated commands and strip them of their arguments
        autoGenerated = []
        for command in commands:
            if command.startswith("Auto."):
                pos = command.rfind("'")
                commandWithoutArg = command[:pos + 1]
                if not commandWithoutArg in autoGenerated:
                    autoGenerated.append(commandWithoutArg)
        return autoGenerated

    def parseAutoGenerated(self, commands):
        autoGenerated = OrderedDict()
        for command in commands:
            parts = command[5:].split("'")
            initialPart = parts[0][:-1]
            widgetType, signalName = initialPart.split(".", 1)
            widgetDescription = self.uiMapFileHandler.unescape(parts[1])
            autoGenerated[command] = widgetType, widgetDescription, signalName
        return autoGenerated

    def replaceInFile(self, fileName, replaceMethod, *args):
        newFileName = fileName + ".tmp"
        newFile = encodingutils.openEncoded(newFileName, "w")
        for i, line in enumerate(encodingutils.openEncoded(fileName)):
            newLine = replaceMethod(line, i, *args)
            if newLine:
                newFile.write(newLine)
        newFile.close()
        shutil.move(newFileName, fileName)

    def makeReplacement(self, command, position, replacements):
        for origName, newName in replacements:
            if command.startswith(origName):
                if newName:
                    return command.replace(origName, newName)
                else:
                    return
        return command

    def createDialog(self, autoGenerated, commands):
        title = self.enterTitle if len(autoGenerated) > 0 else self.editTitle
        dialog = gtk.Dialog(title, flags=gtk.DIALOG_MODAL)
        dialog.set_name("Name Entry Window")
        height = int(gtk.gdk.screen_height() * 0.6)
        if len(autoGenerated) > 0:
            contents = self.createTable(autoGenerated, dialog)
            dialog.vbox.pack_start(contents, expand=True, fill=True)
            dialog.vbox.pack_start(gtk.HSeparator(), expand=False, fill=False)
            dialog.set_default_size(-1, height)
        else:
            width = min(int(gtk.gdk.screen_width() * 0.2), 500)
            dialog.set_default_size(width, height)
        
        preview = self.createPreview(commands, autoGenerated)
        dialog.vbox.pack_start(preview, expand=True, fill=True)
        yesButton = dialog.add_button(gtk.STOCK_OK, gtk.RESPONSE_ACCEPT)
        self.scriptEngine.monitorSignal("finish name entry editing", "clicked", yesButton)
        self.scriptEngine.monitorSignal("close editor window", "delete-event", dialog)
        if len(autoGenerated) > 0:
            cancelButton = dialog.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
            self.scriptEngine.monitorSignal("cancel name entry editing", "clicked", cancelButton)
        dialog.set_default_response(gtk.RESPONSE_ACCEPT)
        dialog.show_all()
        return dialog

    def addScrollBar(self, widget, viewport=False): 
        window = gtk.ScrolledWindow()
        window.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
        if viewport:
            window.add_with_viewport(widget)
        else:
            window.add(widget)
        return window
        
    def createMarkupLabel(self, markup):
        label = gtk.Label()
        label.set_markup(markup)
        return label

    def activateEntry(self, entry, dialog, *args):
        entryPos = self.allEntries.values().index(entry)
        if entryPos == len(self.allEntries) - 1:
            dialog.response(gtk.RESPONSE_ACCEPT)
        else:
            nextEntry = self.allEntries.values()[entryPos + 1]
            nextEntry.grab_focus()

    def getActionDescription(self, signalName, widgetType):
        try:
            exec "from " + self.interface + "toolkit import ScriptEngine"
        except ImportError:
            # If we haven't even got any such interface, don't worry about this mechanism
            return signalName
        desc = ScriptEngine.getDisplayName(signalName) #@UndefinedVariable
        if desc:
            return desc
        if signalName == "activate":
            if "Entry" in widgetType:
                return "pressed Enter"
            else:
                return "selected"
        elif signalName == "changed":
            if "Entry" in widgetType:
                return "edited text"
            else:
                return "selected item"

        parts = signalName.split(".")
        if len(parts) == 1:
            return signalName.replace("-", " ")

        if parts[0] == "response":
            text = parts[1]
            if "--" in text:
                return text.replace("--", "='") + "'"
            else:
                return text

        columnName = parts[1]
        remaining = parts[0]
        if remaining == "toggled":
            remaining = ".".join([ remaining, parts[-1] ])
        return ScriptEngine.getColumnDisplayName(remaining) + " '" + columnName + "'" #@UndefinedVariable
        
    def splitAutoCommand(self, command, autoGenerated):
        for cmd in autoGenerated.keys():
            if command.startswith(cmd):
                arg = command.replace(cmd, "").strip()
                return cmd, arg
        return None, None

    def addArgumentMarkup(self, arg):
        return "<i>" + escape(arg) + "</i>" if arg else ""

    def updatePreview(self, entry, data):
        model, iter = data
        text = entry.get_text() or "?"
        args = model.get_value(iter, 2)
        arg = " " + args[0] if args else ""
        markupFullText = self.convertToMarkup(text + self.addArgumentMarkup(arg))
        fullText = text + arg
        model.set_value(iter, 0, markupFullText)
        model.set_value(iter, 1, fullText)
        
    def addText(self, model, rootIter, text, originalText, arguments, followIter=None):
        return model.insert_before(rootIter, followIter, [self.convertToMarkup(text), originalText, arguments])

    def addCommandToModel(self, command, model, rootIter=None):
        shortcut, args = self.shortcutManager.findShortcut(command)
        if shortcut:
            self.addShortcutCommandToModel(shortcut, args, model, rootIter)
        else:
            self.addBasicCommandToModel(command, model, rootIter)
            
    def addShortcutCommandToModel(self, shortcut, args, model, rootIter, followIter=None):
        italicArgs = [ "<i>" + escape(arg) + "</i>" for arg in args ]
        text = "<b>" + shortcut.getShortcutNameWithArgs(italicArgs) + "</b>"
        iter = self.addText(model, rootIter, text, shortcut.getShortcutNameWithArgs(args), args, followIter)
        if not followIter:
            for step in shortcut.commands:
                self.addCommandToModel(shortcut.replaceArgs(step, args), model, iter)
        return iter
            
    def extractArgsAddMarkup(self, text, cmd):
        markup = text.replace(cmd, cmd + "<i>", 1) + "</i>"
        arg = text.replace(cmd, "", 1).strip()
        args = [ arg ] if arg else []
        return markup, args

    def getErrorColouredText(self, text):
        return '<span foreground="red">' + text + "</span>"

    def addBasicCommandToModel(self, command, model, rootIter):
        args = []
        if command.startswith(waitCommandName):
            markup, _ = self.extractArgsAddMarkup(escape(command), waitCommandName)
            # Ignore args for wait commands, they don't have anything in common
            text = '<span foreground="#826200">' + markup + "</span>"
            widgetDetails = []
        else:
            widgetDetails = self.uiMapFileHandler.findSectionsAndOptions(command)
            text = escape(command)
            if widgetDetails:
                widgetDesc, signalName = widgetDetails[0]
                cmd = self.uiMapFileHandler.get(widgetDesc, signalName)
                if cmd != text:
                    text, args = self.extractArgsAddMarkup(text, cmd)
            else:
                text = self.getErrorColouredText(text)
        iter = self.addText(model, rootIter, text, command, args)
        for widgetDesc, signalName in widgetDetails:
            msg = self.makeUIMapMessage(signalName, widgetDesc)
            self.addText(model, iter, msg, signalName, [widgetDesc])

    def makeUIMapMessage(self, signalName, widgetDesc):
        return "Perform '" + signalName + "' on widget identified by '" + escape(widgetDesc) + "'"

    def createPreview(self, commands, autoGenerated):
        self.treeModel = gtk.TreeStore(str, str, object)
        view = gtk.TreeView(self.treeModel)
        view.set_headers_visible(False)
        cell = gtk.CellRendererText()
        column = gtk.TreeViewColumn("", cell, markup=0)
        view.append_column(column)
        view.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
        self.popup = self.createPopupMenu(view)
        for command in commands:
            autoCmdName, autoArg = self.splitAutoCommand(command, autoGenerated)
            if autoCmdName:
                args = [ autoArg ] if autoArg else []
                autoArgMarkup = self.addArgumentMarkup(autoArg)
                text = "? " + autoArgMarkup if autoArgMarkup else "?"
                iter = self.addText(self.treeModel, None, text, None, args)
                entry = self.allEntries.get(autoCmdName)
                entry.connect("changed", self.updatePreview, (self.treeModel, iter))
                widgetType, widgetDesc, signalName = autoGenerated.get(autoCmdName)
                msg = "Perform '" + signalName + "' on widget of type '" + widgetType + "' identified by '" + widgetDesc + "'"
                self.addText(self.treeModel, iter, msg, msg, [])
            else:
                self.addCommandToModel(command, self.treeModel)

        view.connect("button-press-event", self.showPopupMenu)
        self.scriptEngine.monitorSignal("expand preview node", "row-expanded", view)
        self.scriptEngine.monitorSignal("select preview node", "changed", view)
        self.scriptEngine.monitorSignal("show preview node options for", "button-press-event", view)
        scrolled = self.addScrollBar(view)
        if len(autoGenerated) > 0:
            frame = gtk.Frame("Current Usecase Preview")
            frame.add(scrolled)
            return frame
        else:
            return scrolled

    def convertToUtf8(self, text):
        return self.convertEncoding(text, 'utf-8', 'replace')
    
    def convertToMarkup(self, text):
        return self.convertEncoding(text, 'ascii', 'xmlcharrefreplace')
    
    def convertEncoding(self, text, targetEncoding, replaceMethod):
        try:
            return text.encode(targetEncoding, replaceMethod)
        except ValueError:
            return text
        
    def findPossibleWidgetDescriptions(self, fullWidgetDesc):
        parts = fullWidgetDesc.split(", ")
        badNames = [ "Label=OK", "Label=Cancel", "Label=Yes", "Label=No" ]
        sections, descs, badDescs = [], [], []
        for i in range(1, len(parts) + 1):
            for sectionNameParts in itertools.combinations(parts, i):
                sectionName = ", ".join(sectionNameParts)
                actualSection = self.uiMapFileHandler.getSection(sectionName)
                if actualSection:
                    sections.append(self.convertToUtf8(escape(actualSection)))
                else:
                    utf8Name = self.convertToUtf8(escape(sectionName))
                    if sectionName in badNames or (i == 1 and sectionName.startswith("Type=")):
                        badDescs.append(self.getErrorColouredText(utf8Name))
                    else:
                        descs.append(utf8Name)
        return sections + descs + badDescs
    
    def getWidgetDescriptionWidget(self, possibleWidgetDescs, signalName, widgetType):
        if len(possibleWidgetDescs) == 1:
            label = gtk.Label()
            label.set_markup(possibleWidgetDescs[0])
            return label
        else: 
            liststore = gtk.ListStore(str)
            for desc in possibleWidgetDescs:
                liststore.append([ desc ])
            
            combobox = gtk.ComboBox(liststore)
            cell = gtk.CellRendererText()
            combobox.pack_start(cell, True)
            combobox.add_attribute(cell, 'markup', 0)
            combobox.set_active(0)
            scriptName = "choose widget description for signal '" + signalName + "' on " + widgetType + " '" + possibleWidgetDescs[0] + "' ="
            self.scriptEngine.monitorSignal(scriptName, "changed", combobox)
            return combobox

    def createTable(self, autoGenerated, dialog):
        table = gtk.Table(rows=len(autoGenerated) + 1, columns=4)
        table.set_col_spacings(20)
        headers = [ "Widget Type", "Identified By", "Action Performed", "Usecase Name" ]
        for col, header in enumerate(headers):
            table.attach(self.createMarkupLabel("<b><u>" + header + "</u></b>"), 
                         col, col + 1, 0, 1, xoptions=gtk.FILL, yoptions=gtk.FILL)
        for rowIndex, (command, (widgetType, fullWidgetDesc, signalName)) in enumerate(autoGenerated.items()):
            table.attach(gtk.Label(widgetType), 0, 1, rowIndex + 1, rowIndex + 2, xoptions=gtk.FILL, yoptions=gtk.FILL)
            actionDesc = self.getActionDescription(signalName, widgetType)
            possibleWidgetDescs = self.findPossibleWidgetDescriptions(fullWidgetDesc)
            widgetDescWidget = self.getWidgetDescriptionWidget(possibleWidgetDescs, signalName, widgetType)
            table.attach(widgetDescWidget, 1, 2, rowIndex + 1, rowIndex + 2, xoptions=gtk.FILL, yoptions=gtk.FILL)
            table.attach(gtk.Label(actionDesc), 2, 3, rowIndex + 1, rowIndex + 2, xoptions=gtk.FILL, yoptions=gtk.FILL)
            entry = gtk.Entry()
            fieldIdentifier = "for signal '" + signalName + "' on " + widgetType + " '" + removeMarkup(unescape(possibleWidgetDescs[0])) + "'"
            scriptName = "enter usecase name " + fieldIdentifier + " ="
            self.scriptEngine.monitorSignal(scriptName, "changed", entry)
            entry.connect("activate", self.activateEntry, dialog)
            self.scriptEngine.monitorSignal("press <enter> in field " + fieldIdentifier, "activate", entry)
            self.allEntries[command] = entry
            self.allDescriptionWidgets.append(widgetDescWidget)
            table.attach(entry, 3, 4, rowIndex + 1, rowIndex + 2, yoptions=gtk.FILL)
        table.show_all()
        frame = gtk.Frame("Previously unseen actions: provide names for the interesting ones")
        frame.add(self.addScrollBar(table, viewport=True))
        return frame

    def createPopupMenu(self, widget):
        menu = gtk.Menu()
        item = gtk.MenuItem("Create shortcut")
        deleteItem = gtk.MenuItem("Delete shortcut")
        updateUIMapItem = gtk.MenuItem("Update UI map file")
        renameItem = gtk.MenuItem("Rename")
        separator = gtk.SeparatorMenuItem()
        menu.append(item)
        menu.append(deleteItem)
        menu.append(renameItem)
        menu.append(separator)
        menu.append(updateUIMapItem)
        item.connect("activate", self.createShortcut, widget)
        deleteItem.connect("activate", self.deleteShortcut, widget)
        renameItem.connect("activate", self.rename, widget)
        updateUIMapItem.connect("activate", self.updateUIMap, widget)
        self.popupSensitivities[item] = self.setCreateShortcutSensitivity
        self.popupSensitivities[deleteItem] = self.setDeleteShortcutSensitivity
        self.popupSensitivities[renameItem] = self.setRenameSensitivity
        self.popupSensitivities[updateUIMapItem] = self.setUpdateUIMapSensitivity
        self.scriptEngine.monitorSignal("create a new shortcut", "activate", item)
        self.scriptEngine.monitorSignal("delete shortcut", "activate", deleteItem)
        self.scriptEngine.monitorSignal("rename a usecase name or shortcut", "activate", renameItem)
        self.scriptEngine.monitorSignal("update ui map file", "activate", updateUIMapItem)
        item.show()
        deleteItem.show()
        renameItem.show()
        separator.show()
        updateUIMapItem.show()
        return menu
    
    def applySensitivities(self, selection):
        for item, method in self.popupSensitivities.items():
            method(item, selection)

    def setCreateShortcutSensitivity(self, item, selection):
        # Check selection has at least 2 elements and is consecutive
        item.set_sensitive(selection.count_selected_rows() > 1 and self.isConsecutive(selection))

    def setDeleteShortcutSensitivity(self, item, selection):
        item.set_sensitive(selection.count_selected_rows() == 1 and self.shortcutsSelected(selection))

    def showPopupMenu(self, treeView, event):
        if event.button == 3:
            time = event.time
            pathInfo = treeView.get_path_at_pos(int(event.x), int(event.y))
            selection = treeView.get_selection()
            selectedRows = selection.get_selected_rows()
            # If they didnt right click on a currently selected
            # row, change the selection
            if pathInfo is not None:
                if pathInfo[0] not in selectedRows[1]:
                    selection.unselect_all()
                    selection.select_path(pathInfo[0])
                treeView.grab_focus()
                self.popup.popup(None, None, None, event.button, time)
                treeView.emit_stop_by_name("button-press-event")
            self.applySensitivities(selection)

    def createShortcut(self, widget, view):
        selection = view.get_selection()
        lines, arguments = self.selectionToModel(selection)
        self.createShortcutFromLines(lines, arguments)
    
    def selectionToModel(self, selection):
        lines = []
        allArguments = []
        def addSelected(treemodel, path, iter, *args):
            line = treemodel.get_value(iter, 1)
            currArgs = treemodel.get_value(iter, 2)
            lines.append(line)
            for arg in currArgs:
                allArguments.append(arg)
            
        selection.selected_foreach(addSelected)
        return lines, allArguments
        
    def createShortcutFromLines(self, lines, arguments):
        dialog = gtk.Dialog("New Shortcut", flags=gtk.DIALOG_MODAL)
        dialog.set_name("New Shortcut Window")
        label = gtk.Label("New name for shortcut:")
        entry = gtk.Entry()
        entry.set_name("New Name")
        if arguments:
            defaultText = "Do something with " + " and ".join(arguments)
            entry.set_text(defaultText)

        dialog.vbox.set_spacing(10)
        dialog.vbox.pack_start(label, expand=False, fill=False)
        dialog.vbox.pack_start(entry, expand=True, fill=True)
        dialog.vbox.pack_start(gtk.HSeparator(), expand=False, fill=False)
        self.scriptEngine.monitorSignal("enter new shortcut name", "changed", entry)
        shortcutView = self.createShortcutPreview(lines, arguments, entry)
        frame = gtk.Frame("")
        frame.get_label_widget().set_use_markup(True)
        self.updateShortcutName(entry, frame, arguments)
        frame.add(shortcutView)
        entry.connect("changed", self.updateShortcutName, frame, arguments)
        dialog.vbox.pack_end(frame, expand=True, fill=True)
        yesButton = dialog.add_button(gtk.STOCK_OK, gtk.RESPONSE_ACCEPT)
        self.scriptEngine.monitorSignal("accept new shortcut name", "clicked", yesButton)
        cancelButton = dialog.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
        self.scriptEngine.monitorSignal("cancel new shortcut name", "clicked", cancelButton)
        dialog.connect("response", self.respond, entry, frame, shortcutView)
        dialog.show_all()
        
    def updateShortcutName(self, textEntry, frame, arguments):
        newName = textEntry.get_text()
        for arg in arguments:
            newName = newName.replace(arg, "$")
        markup = "<b><i>" + newName.lower().replace(" ", "_") + ".shortcut" + "</i></b>"
        frame.get_label_widget().set_label(markup)

    def getShortcutFileName(self, shortcutName):
        return shortcutName.lower().replace(" ", "_") + ".shortcut"

    def copyRow(self, iter, parentIter, followIter=None):
        row = list(self.treeModel.get(iter, 0, 1, 2))
        if followIter is None:
            newIter = self.treeModel.append(parentIter, row)
        else:
            newIter = self.treeModel.insert_before(parentIter, followIter, row)
        subIter = self.treeModel.iter_children(iter)
        if subIter is not None:
            self.copyRow(subIter, newIter)

    def getTopLevelIters(self):
        iters = []
        def addSelected(model, path, iter, *args):
            if len(path) == 1:
                iters.append(iter)
        self.treeModel.foreach(addSelected)
        return iters
    
    def getFirstDifferentIter(self, iters, commands):
        for i, iter in enumerate(iters):
            if commands[i] != self.treeModel.get_value(iter, 1):
                return i, iter
    
    def addShortcutToPreview(self):
        allCommands = self.getAllCommands()
        while True:
            topLevelIters = self.getTopLevelIters()
            topLevelNames = [ self.treeModel.get_value(iter, 1) for iter in topLevelIters ]
            if topLevelNames == allCommands:
                break
            
            iterIx, iter = self.getFirstDifferentIter(topLevelIters, allCommands)
            shortcut, args = self.shortcutManager.findShortcut(allCommands[iterIx])
            if shortcut:
                shortcutIter = self.addShortcutCommandToModel(shortcut, args, self.treeModel, None, iter)
                shortcutLength = len(shortcut.commands)
                currentIters = topLevelIters[iterIx:iterIx + shortcutLength]
                for iter in currentIters:
                    self.copyRow(iter, shortcutIter)
                    self.treeModel.remove(iter)
            else:
                sys.stderr.write("ERROR: mismatch in files, expected shortcut for '" + allCommands[iterIx] + "', but found none.\n")
                break
            
    def findShortcutIters(self, shortcut):
        iters = []
        def addSelected(model, path, iter, *args):
            value = model.get_value(iter, 1)
            args = model.get_value(iter, 2)
            currShortcut = self.getShortcut(value, args)
            if currShortcut is shortcut:
                iters.append(iter)
        self.treeModel.foreach(addSelected)
        return iters
    
    def removeShortcutFromPreview(self, shortcut):
        for iter in self.findShortcutIters(shortcut):
            childIter = self.treeModel.iter_children(iter)
            parentIter = self.treeModel.iter_parent(iter)
            nextIter = self.treeModel.iter_next(iter)
            while childIter is not None:
                self.copyRow(childIter, parentIter, nextIter)
                childIter = self.treeModel.iter_next(childIter)
            self.treeModel.remove(iter)
        
    def removeShortcutFromUsecase(self, shortcut):
        recordScript = RecordScript(self.fileName, [])
        for iter in self.getTopLevelIters():
            value = self.treeModel.get_value(iter, 1)
            args = self.treeModel.get_value(iter, 2)
            if self.getShortcut(value, args) is shortcut:
                childIter = self.treeModel.iter_children(iter)
                while childIter is not None:
                    recordScript.record(self.treeModel.get_value(childIter, 1))
                    childIter = self.treeModel.iter_next(childIter)
            else:
                recordScript.record(value)

    def respond(self, dialog, responseId, entry, frame, shortcutView):
        if responseId == gtk.RESPONSE_ACCEPT:
            if self.checkShortcutName(dialog, entry.get_text().lower()):
                dialog.hide()
                shortcut = self.saveShortcut(frame.get_label(), self.getShortcutLines(shortcutView))
                self.shortcutManager.add(shortcut)
                if self.isAutoGenerated:
                    self.createdShortcuts.append(shortcut.name)
                self.recreateUsecaseFile()
                self.addShortcutToPreview()
        else:
            dialog.hide()
            
    def recreateUsecaseFile(self):
        recordScript = RecordScript(self.fileName, [shortcut for _, shortcut in self.shortcutManager.shortcuts])
        for iter in self.getTopLevelIters():
            value =self.treeModel.get_value(iter, 1)
            if "<b>" in self.treeModel.get_value(iter, 0):
                args = self.treeModel.get_value(iter, 2)
                shortcut = self.getShortcut(value, args)
                self.runShortcutCommands(recordScript, shortcut, args)
            else:
                recordScript.record(value)

    def runShortcutCommands(self, recordScript, shortcut, args):
        shortcutCopy = ReplayScript(shortcut.name, True)
        while not shortcutCopy.hasTerminated():
            command = shortcutCopy.getCommand(args)
            self.recordShortcutCommand(recordScript, command)
    
    def recordShortcutCommand(self, recordScript, command):
        shortcut, args = self.shortcutManager.findShortcut(command)
        if shortcut:
            self.runShortcutCommands(recordScript, shortcut, args)
        else:
            recordScript.record(command)

    def getShortcut(self, shortcutNameWithArgs, args):
        for _, shortcut in self.shortcutManager.shortcuts:
            if shortcut.getShortcutNameWithArgs(args) == shortcutNameWithArgs:
                return shortcut

    def getShortcutLines(self, shortcutView):
        model = shortcutView.get_model()
        lines = []
        def addSelected(model, path, iter, *args):
            lines.append(model.get_value(iter, 0))
        model.foreach(addSelected)
        return lines
    
    def checkShortcutName(self, parent, name):
        if not name:
            self.showErrorDialog(parent, "The shortcut name can't be empty.")
            return False
        elif self.isInUIMap(name):
            self.showErrorDialog(parent, "The shortcut name already exists in the UI map file.")
            return False
        elif self.isInShortcuts(name):
            self.showErrorDialog(parent, "The shortcut name is already being used for another shortcut.")
            return False
        return True
    
    def isInUIMap(self, name):
        return len(self.uiMapFileHandler.findSectionsAndOptions(name)) > 0
    
    def isInShortcuts(self, name):
        return self.shortcutManager.findShortcut(name)[0] is not None
        
    def saveShortcut(self, name, lines):
        storytextDir = os.environ["STORYTEXT_HOME"]
        if not os.path.isdir(storytextDir):
            os.makedirs(storytextDir)
        fileName = os.path.join(storytextDir, name)
        with open(fileName, "w") as f:
            for line in lines:
                f.write(line + "\n")
        print "Shortcut", repr(name), "created."
        return ReplayScript(fileName)
    
    def shortcutsSelected(self, selection):
        shortcuts = []
        def addSelected(treemodel, path, iter, *args):
            shortcuts.append("<b>" in treemodel.get_value(iter, 0))

        selection.selected_foreach(addSelected)
        return any(shortcuts)
    
    def isConsecutive(self, selection):
        paths = []
        def addSelected(treemodel, path, *args):
            paths.append(path)

        selection.selected_foreach(addSelected)
        prevIx = None
        for path in paths:
            if len(path) > 1:
                return False # Can't make shortcuts out of lines further down the hierarchy
            ix = path[0]
            if prevIx is not None and ix - prevIx > 1:
                return False
            prevIx = ix
        return True
            
    def showConfirmationDialog(self, parent, message, *args):
        self.showErrorWarningDialog(parent, message, gtk.MESSAGE_WARNING, "Confirmation", gtk.BUTTONS_OK_CANCEL, *args)
            
    def showErrorDialog(self, parent, message):
        self.showErrorWarningDialog(parent, message, gtk.MESSAGE_ERROR, "Error", gtk.BUTTONS_OK)

    def showErrorWarningDialog(self, parent, message, stockIcon, alarmLevel, buttons, *args):
        dialog = self.createMessageDialog(parent, message, stockIcon, alarmLevel, buttons)
        dialog.connect("response", self.respondErrorWarning, *args)
        dialog.show_all()

    def respondErrorWarning(self, dialog, response, *args):
        dialog.hide()
        if response == gtk.RESPONSE_OK and args:
            args[0](*args[1:])

    def createMessageDialog(self, parent, message, stockIcon, alarmLevel, buttons=gtk.BUTTONS_OK):
        dialogTitle = "StoryText " + alarmLevel
        dialog = gtk.MessageDialog(parent, gtk.DIALOG_MODAL, stockIcon, buttons, None)
        # Would like to use dialog.get_widget_for_response(gtk.RESPONSE_OK), introduced in gtk 2.22 instead
        for button in dialog.action_area.get_children():
            response = dialog.get_response_for_widget(button)
            if response == gtk.RESPONSE_OK:
                self.scriptEngine.monitorSignal("accept message", "clicked", button)
            elif response == gtk.RESPONSE_CANCEL:
                self.scriptEngine.monitorSignal("cancel message", "clicked", button)
        dialog.set_title(dialogTitle)        
        dialog.set_markup(message)
        dialog.set_default_response(gtk.RESPONSE_OK)
        return dialog
    
    def createShortcutPreview(self, commands, arguments, textEntry):
        listModel = gtk.ListStore(str, str, object)
        view = gtk.TreeView(listModel)
        view.set_headers_visible(False)
        cmdRenderer = gtk.CellRendererText()
        cmdColumn = gtk.TreeViewColumn("", cmdRenderer, text=0)
        view.append_column(cmdColumn)
        view.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
        argumentIndex = 0
        for command in commands:
            shortcut, args =self.shortcutManager.findShortcut(command)
            if shortcut:
                text = shortcut.getShortcutName()
            else:
                arg = arguments[argumentIndex] if argumentIndex < len(arguments) else ""
                text, argument = self.replaceArguments(command, arg)
                args = [argument] if argument else []
                if argument:
                    argumentIndex = argumentIndex + 1 % len(arguments)
            iter1 = listModel.append([text, command, args])
            if not shortcut and args:
                textEntry.connect("changed", self.handleArguments, (listModel, iter1))
        self.scriptEngine.monitorSignal("select preview node", "changed", view)
        self.scriptEngine.monitorSignal("show preview node options for", "button-press-event", view)
        return view

    def replaceArguments(self, command, argument):
        if argument and command.endswith(argument):
            return command.replace(argument, "$"), argument
        else:
            return command, ""
            
    def handleArguments(self, widget, data):
        model, iter = data
        newName = widget.get_text()
        originalValue = model.get_value(iter, 1)
        currentValue = model.get_value(iter, 0)
        args = model.get_value(iter, 2)
        if len(args) == 1 and re.search("\\b" + args[0] + "\\b", newName):
            if originalValue == currentValue:
                model.set_value(iter, 0, currentValue.replace(args[0], "$"))
        else:
            model.set_value(iter, 0, originalValue)
            
    def setUpdateUIMapSensitivity(self, item, selection):
        if selection.count_selected_rows() == 1 and self.uiMapSelected(selection):
            item.set_sensitive(True)
        else:
            item.set_sensitive(False)
    
    def uiMapSelected(self, selection):
        uiMaps = []
        def addSelected(treemodel, path, iter, *args):
            uiMaps.append(" on widget identified by " in treemodel.get_value(iter, 0))

        selection.selected_foreach(addSelected)
        return any(uiMaps)

    def updateUIMap(self, widget, view):
        selection = view.get_selection()
        signals, widgetDescriptions = self.selectionToModel(selection)
        self.createUIMapDialog(signals[0], widgetDescriptions[0])
        
    def createUIMapDialog(self, signal, widgetDescription):
        cmd = self.uiMapFileHandler.get(widgetDescription, signal)
        dialog = gtk.Dialog('Update UI map for ' + "'" + cmd + "'", flags=gtk.DIALOG_MODAL)
        dialog.vbox.set_spacing(10)
        label = gtk.Label("Widget description")
        widgetDescEntry = gtk.Entry()
        widgetDescEntry.set_text(widgetDescription)
        hbox = gtk.HBox(False, 10)
        hbox.pack_start(label, expand=False, fill=False)
        hbox.pack_start(widgetDescEntry, expand=True, fill=True)
        dialog.vbox.pack_start(hbox, expand=False, fill=False)

        label2 = gtk.Label("Activity")
        signalEntry = gtk.Entry()
        signalEntry.set_text(signal)
        hbox2 = gtk.HBox(False, 10)
        hbox2.pack_start(label2, expand=False, fill=False)
        hbox2.pack_start(signalEntry, expand=True, fill=True)
        dialog.vbox.pack_start(hbox2, expand=False, fill=False)
        
        updateButton = dialog.add_button('Update', gtk.RESPONSE_ACCEPT)
        cancelButton = dialog.add_button('Cancel', gtk.RESPONSE_CANCEL)
        self.scriptEngine.monitorSignal("enter new widget description", "changed", widgetDescEntry)
        self.scriptEngine.monitorSignal("enter new activity name", "changed", signalEntry)
        self.scriptEngine.monitorSignal("accept update ui map file", "clicked", updateButton)
        self.scriptEngine.monitorSignal("cancel update ui map file", "clicked", cancelButton)
        
        dialog.connect("response", self.respondUpdateUIMap, signalEntry, widgetDescEntry, signal, widgetDescription)
        dialog.show_all()

    def respondUpdateUIMap(self, dialog, responseId, signalEntry, widgetDescEntry, oldSignal, oldWidgetDescription):
        if responseId == gtk.RESPONSE_ACCEPT:
            newSignal = signalEntry.get_text()
            newWidgetDesc = widgetDescEntry.get_text()
            if self.checkUpdateUIMapEntryNames(dialog, newWidgetDesc, newSignal):
                dialog.hide()
                self.uiMapFileHandler.updateSectionAndOptionNames(oldWidgetDescription, newWidgetDesc, oldSignal, newSignal)
                self.updateUIMapPreview(oldWidgetDescription, newWidgetDesc, oldSignal, newSignal)

        else:
            dialog.hide()
    
    def updateUIMapPreview(self, oldWidgetDescription, newWidgetDesc, oldSignal, newSignal):
        leafIters = self.getLeafIters()
        for iter in leafIters:
            descValue = self.treeModel.get_value(iter, 2)
            if descValue:
                if oldWidgetDescription == descValue[0]:
                    signalValue = self.treeModel.get_value(iter, 1)
                    if signalValue != oldSignal:
                        newSignal = oldSignal
                    msg = self.makeUIMapMessage(newSignal, newWidgetDesc)
                    self.treeModel.set(iter, 0, msg, 1, newSignal, 2, [newWidgetDesc])

    def getLeafIters(self):
        iters = []
        def addLeafIters(model, path, iter, *args):
            if not model.iter_has_child(iter):
                iters.append(iter)
        self.treeModel.foreach(addLeafIters)
        return iters
    
    def checkUpdateUIMapEntryNames(self, parent, widgetName, activityName):
        if not widgetName:
            self.showErrorDialog(parent, "The widget name can't be empty.")
            return False
        elif not activityName:
            self.showErrorDialog(parent, "The activity name can't be empty.")
            return False
        return True
    
    def setRenameSensitivity(self, item, selection):
        if selection.count_selected_rows() == 1 and (self.shortcutsSelected(selection) or (self.usecaseSelected(selection) and self.isNewName(selection))):
            item.set_sensitive(True)
        else:
            item.set_sensitive(False)
    
    def isNewName(self, selection):
        newNames = []
        def addNames(treeModel, path, iter, *args):
            command = treeModel.get_value(iter, 1)
            cmd, args = self.uiMapFileHandler.splitOptionValue(command)
            if cmd:
                newNames.append(cmd)
        selection.selected_foreach(addNames)
        return any(newNames)
    
    def usecaseSelected(self, selection):
        return not self.shortcutsSelected(selection) and not self.uiMapSelected(selection) \
            and not self.waitCommandSelected(selection)
    
    def waitCommandSelected(self, selection):
        commands, _ = self.selectionToModel(selection)
        return any(cmd.startswith(waitCommandName) for cmd in commands)

    def rename(self, widget, view):
        selection = view.get_selection()
        commands, _ = self.selectionToModel(selection)
        self.createRenameDialog(commands[0], self.shortcutsSelected(selection))
        
    def deleteShortcut(self, menuItem, view):
        selection = view.get_selection()
        command = self.selectionToModel(selection)[0][0]
        shortcut, args = self.shortcutManager.findShortcut(command)
        confirmationMessage = "You are about to delete the file '" + os.path.basename(shortcut.name) + "'\nand remove all references to it in the current usecase."
        self.showConfirmationDialog(menuItem.get_toplevel(), confirmationMessage, self.performShortcutDeletion, shortcut)
        
    def performShortcutDeletion(self, shortcut):
        print "ShortcutRemove", repr(shortcut.getShortcutRegexp().pattern), "renamed to '" + "\\n".join(shortcut.commands) + "'"
        self.removeShortcutFromUsecase(shortcut)
        self.removeShortcutFromPreview(shortcut)
        self.shortcutManager.remove(shortcut)
            
    def createRenameDialog(self, command, isShortcut=False):
        if isShortcut:
            name = 'shortcut'
            shortcut, args = self.shortcutManager.findShortcut(command)
            cmd = shortcut.getShortcutName()
        else:
            cmd, args = self.uiMapFileHandler.splitOptionValue(command)
            name = "usecase"
        dialog = gtk.Dialog('Rename ' + name, flags=gtk.DIALOG_MODAL)
        dialog.vbox.set_spacing(10)
        label = gtk.Label(name.title())
        nameEntry = gtk.Entry()
        nameEntry.set_text(cmd)
        hbox = gtk.HBox(False, 10)
        hbox.pack_start(label, expand=False, fill=False)
        hbox.pack_start(nameEntry, expand=True, fill=True)
        dialog.vbox.pack_start(hbox, expand=False, fill=False)
        renameButton = dialog.add_button('Rename', gtk.RESPONSE_ACCEPT)
        cancelButton = dialog.add_button('Cancel', gtk.RESPONSE_CANCEL)
        self.scriptEngine.monitorSignal("rename " + name, "changed", nameEntry)
        self.scriptEngine.monitorSignal("accept rename", "clicked", renameButton)
        self.scriptEngine.monitorSignal("cancel rename", "clicked", cancelButton)
        
        dialog.connect("response", self.respondRenameShortcut if isShortcut else self.respondRenameUsecase, nameEntry, cmd)
        dialog.show_all()

    def respondRenameUsecase(self, dialog, responseId, nameEntry, oldValue):
        methodName = "UsecaseRename"
        newValue = nameEntry.get_text()
        if responseId == gtk.RESPONSE_ACCEPT:
            if self.checkUsecaseName(dialog, newValue):
                self.updateUsecaseNameInUIMap(oldValue, newValue)
                self.updateUsecaseNameInShorcuts(oldValue, newValue)
                self.replaceInFile(self.fileName, self.makeReplacement, [(oldValue, newValue)])
                print encodingutils.encodeToLocale(methodName + " '" + oldValue + "' renamed to '" + newValue + "'")
                self.initShortcutManager()
                self.updateNameInPreview(oldValue, newValue)
            dialog.hide()
        else:
            dialog.hide()
            
    def respondRenameShortcut(self, dialog, responseId, nameEntry, oldValue):
        methodName = "ShortcutRename"
        newValue = nameEntry.get_text()
        if responseId == gtk.RESPONSE_ACCEPT:
            if self.checkShortcutName(dialog, newValue) and self.checkShortcutArguments(dialog, oldValue, newValue):
                self.shortcutManager.rename(oldValue, self.getShortcutFileName(newValue))
                oldValueRegexp = ReplayScript.transformToRegexp(oldValue)
                # Update shortcut name in shortcut files
                for _, shortcut in self.shortcutManager.getShortcuts():
                    if shortcut.getShortcutName() != newValue:
                        self.replaceInFile(shortcut.name, self.replaceShortcutName, oldValueRegexp, newValue)
                # Update shortcut name in current usecase file
                self.replaceInFile(self.fileName, self.replaceShortcutName, oldValueRegexp, newValue)
                print methodName, repr(oldValueRegexp), "renamed to", repr(newValue)
                self.initShortcutManager()
                self.updateShortcutNameInPreview(oldValueRegexp, newValue)
                dialog.hide()
            else:
                nameEntry.set_text(oldValue)
        else:
            dialog.hide()
            
    def checkUsecaseName(self, parent, name):
        if not name:
            self.showErrorDialog(parent, "The usecase name can't be empty.")
            return False
        elif self.isInUIMap(name):
            self.showErrorDialog(parent, "The usecase name already exists in the UI map file.")
            return False
        return True
    
    def updateUsecaseNameInUIMap(self, oldCommand, newCommand):
        for section, option in self.uiMapFileHandler.findSectionsAndOptions(oldCommand):
            self.uiMapFileHandler.updateOptionValue(section, option, newCommand)
        
    def updateUsecaseNameInShorcuts(self, oldCommand, newCommand):
        for _, shortcut in self.shortcutManager.getShortcuts():
            self.replaceInFile(shortcut.name, self.makeReplacement, [(oldCommand, newCommand)])
            
    def checkShortcutArguments(self, parentDialog, oldShortcut, newShortcut):
        numArgs = oldShortcut.count("$")
        if numArgs != newShortcut.count("$"):
            self.showErrorDialog(parentDialog, "The number of shortcut arguments('$') must be "+ str(numArgs))
            return False
        return True
            
    def updateNameInPreview(self, oldValue, newValue):
        def updateNode(model, path, iter, *args):
            markup = model.get_value(iter, 0)
            text = model.get_value(iter, 1)
            if text.startswith(oldValue):
                model.set_value(iter, 0, markup.replace(oldValue, newValue))
                model.set_value(iter, 1, text.replace(oldValue, newValue))
        self.treeModel.foreach(updateNode)
    
    def updateShortcutNameInPreview(self, oldNameRegexp, newName):
        def updateNode(model, path, iter, *args):
            markup = model.get_value(iter, 0)
            if markup.startswith("<b>"):
                text = model.get_value(iter, 1)
                newText = self.replaceShortcutName(text, 0, oldNameRegexp , newName)
                newMarkup = self.replaceShortcutName(markup, 0, oldNameRegexp, newName)
                if text != newText:
                    if newMarkup == markup:
                        newMarkup = newMarkup.replace(text, newText)
                    model.set_value(iter, 0, newMarkup)
                    model.set_value(iter, 1, newText)
        self.treeModel.foreach(updateNode)

    def disconnectEntries(self):
        for entry, handler in self.connectedEntries:
            entry.disconnect(handler)
        self.connectedEntries = []

    def regexpReplace(self, regexp, line, newText):
        return re.sub(regexp, newText, line)

    def replaceShortcutName(self, line, position, oldNameRegexp, newName):
        def replaceArgs(matchobj):
            return ReplayScript.getTextWithArgs(newName, [arg for arg in matchobj.groups()])
        return self.regexpReplace(oldNameRegexp, line, replaceArgs)

def main():
    usage = """usage: %prog [options] [FILE] ...

The "StoryText Editor" is a small PyGTK program that allows StoryText users to enter
domain-specific names for the actions they record in their GUI tests. The aim is therefore to
produce a complete usecase script if some actions had no names, and to update the UI map file 
accordingly.

It also acts as a hierchical viewer to be able to easily see shortcuts, UI map references etc"""

    parser = OptionParser(usage, version="%prog " + __version__)
    parser.add_option("-i", "--interface", metavar="INTERFACE",
                      help="type of interface used by application, should be 'gtk' or 'tkinter' ('gtk' is default)", 
                      default="gtk")
    parser.add_option("-l", "--loglevel", default="WARNING", 
                      help="produce logging at level LEVEL, should be a valid Python logging level such as 'info' or 'debug'. Basically useful for debugging and testing", metavar="LEVEL")
    parser.add_option("-m", "--mapfiles", default=os.path.join(gtktoolkit.ScriptEngine.storytextHome, "ui_map.conf"),
                      help="Update the UI map file(s) at FILE1,... If not set StoryText will read and write such a file at the its own default location ($STORYTEXT_HOME/ui_map.conf). If multiple files are provided, the last in the list will be used for writing.", metavar="FILE1,...")
    
    options, args = parser.parse_args()
    if os.path.isfile(args[0]):
        uiMapFiles = options.mapfiles.split(",")
        level = eval("logging." + options.loglevel.upper())
        logging.basicConfig(level=level, stream=sys.stdout, format="%(message)s")
        editor = UseCaseEditor(args[0], options.interface, uiMapFiles)
        editor.run()
