
import storytext.guishared, util, logging, os, time, sys
from storytext.definitions import UseCaseScriptError
from storytext import applicationEvent, applicationEventDelay, applicationEventRemove
from difflib import SequenceMatcher
from org.eclipse import swt, jface
import org.eclipse.swtbot.swt.finder as swtbot
from org.hamcrest.core import IsAnything
from java.lang import IllegalStateException, IndexOutOfBoundsException, RuntimeException, NullPointerException, Exception
from java.text import ParseException
from java.util import ArrayList
from threading import Lock

applicationEventType = 1234 # anything really, just don't conflict with the real SWT events

class PythonResult(swtbot.results.Result):
    def __init__(self, method, args):
        self.method = method
        self.args = args
    
    def run(self):
        return self.method(*self.args)

def runOnUIThread(method, *args):
    try:
        return swtbot.finders.UIThreadRunnable.syncExec(PythonResult(method, args))
    except IllegalStateException:
        raise UseCaseScriptError, "The GUI has already exited"
    except NullPointerException, e:
        # Temporary code to try to find intermittent Windows error
        print "Caught intermittent Windows NullPointerException!"
        e.printStackTrace()
        raise

class WidgetAdapter(storytext.guishared.WidgetAdapter):
    popupMenuContexts = {}
    def getChildWidgets(self):
        return [] # don't use this...
        
    def getWidgetTitle(self):
        return ""
        
    def getLabel(self):
        if isinstance(self.widget, (swtbot.widgets.SWTBotText, swtbot.widgets.SWTBotCombo, swtbot.widgets.SWTBotSpinner, FakeSWTBotCCombo)) or \
               not hasattr(self.widget.widget, "getText"):
            return self.getFromUIThread(util.getTextLabel, self.widget.widget)
        try:
            return util.getItemText(self.widget.getText())
        except:
            return ""

    def getDialogTitle(self):
        def _getDialogTitle():
            if isinstance(self.widget.widget, swt.widgets.Control):
                shell = self.widget.widget.getShell()
                if shell.getParent():
                    id = shell.getData("org.eclipse.swtbot.widget.key")
                    return id or shell.getText()
            return ""
        return self.getFromUIThread(_getDialogTitle)

    def getType(self):
        # SWT name, not the SWTBot name
        return self.widget.widget.__class__.__name__
        
    def isAutoGenerated(self, name):
        return len(name) == 0

    def getTooltip(self):
        try:
            return self.widget.getToolTipText()
        except:
            return ""

    def getName(self):
        return self.widget.getId() or ""
    
    def getActionId(self):
        data = runOnUIThread(self.widget.widget.getData)
        return data.getAction().getId() if hasattr(data, "getAction") else ""
    
    def findPossibleUIMapIdentifiers(self):
        ids = storytext.guishared.WidgetAdapter.findPossibleUIMapIdentifiers(self)
        actionId = self.getActionId()
        if actionId:
            pos = ids.index("Type=" + self.getType())
            ids.insert(pos, "Action=" + actionId)
        return ids
    
    def getNameForAppEvent(self):
        return self.getName() or self.getType().lower()

    def getFromUIThread(self, method, *args):
        try:
            return runOnUIThread(method, *args)
        except:
            return ""
    
    def getContextAncestor(self):
        return runOnUIThread(self.widget.widget.getParent)

    def getContextName(self):
        parent = self.getContextAncestor()
        return self.getContextNameFromAncestor(parent)
    
    def getMenuContextNameFromAncestor(self, parent):
        def getParentText():
            item = parent.getParentItem()
            return util.getItemText(item.getText()) if item else self.getPopupMenuContext(parent)
        return runOnUIThread(getParentText)
    
    def getMenuContextFromWidget(self, widget):
        if widget:
            adapter = WidgetMonitor.makeAdapter(widget)
            if adapter:
                return adapter.findPossibleUIMapIdentifiers()[0].replace("Label=", "").replace("=", ":")
            
        return "Popup Menu"
    
    def getPopupMenuContext(self, menu):
        if menu in self.popupMenuContexts:
            return self.popupMenuContexts.get(menu)
        parentShell = menu.getParent()
        widget = self.findWidgetWithMenu(parentShell, menu)
        context = self.getMenuContextFromWidget(widget)
        self.popupMenuContexts[menu] = context
        return context
    
    def findWidgetWithMenu(self, widget, menu):
        if widget.getMenu() == menu:
            return widget
        
        if hasattr(widget, "getChildren"):
            for child in widget.getChildren():
                fromChild = self.findWidgetWithMenu(child, menu)
                if fromChild:
                    return fromChild
    
    def getContextNameFromAncestor(self, parent):
        if isinstance(parent, swt.widgets.Menu):
            return self.getMenuContextNameFromAncestor(parent)
        elif isinstance(parent, swt.widgets.Table):
            return "TableCell"
        elif isinstance(parent, swt.widgets.Tree):
            return "TreeCell"
        else:
            return ""
        

storytext.guishared.WidgetAdapter.adapterClass = WidgetAdapter    
        
class BasicRecordListener(swt.widgets.Listener):
    def __init__(self, method, event):
        self.method = method
        self.event = event
        
    def handleEvent(self, e): 
        storytext.guishared.catchAll(self.method, e, self.event)

        
class SignalEvent(storytext.guishared.GuiEvent):
    def __init__(self, name, widget, argumentParseData, *args):
        self.generationModifiers = argumentParseData.split(",") if argumentParseData else []
        storytext.guishared.GuiEvent.__init__(self, name, widget, *args)
        
    def connectRecord(self, method):
        eventType = self.getRecordEventType()
        try:
            runOnUIThread(self.addListeners, eventType, BasicRecordListener(method, self))
        except: # Get 'widget is disposed' sometimes, don't know why...
            pass
        
    @classmethod
    def getRecordEventType(cls):
        return getattr(swt.SWT, cls.getAssociatedSignal(None))
        
    def addListeners(self, *args):
        # Three indirections: WidgetAdapter -> SWTBotMenu -> MenuItem
        return self.widget.widget.widget.addListener(*args)

    def generate(self, *args):
        try:
            self._generate(*args)
        except (IllegalStateException, IndexOutOfBoundsException), _:
            pass # get these for actions that close the UI. But only after the action is done :)

    def shouldRecord(self, event, *args):
        return DisplayFilter.instance.getEventFromUser(event, self.isTriggeringEvent)
    
    def isTriggeringEvent(self, *args):
        return False

    def delayLevel(self, event, *args):
        # If there are events for other shells, implies we should delay as we're in a dialog
        return DisplayFilter.instance.otherEventCount(event, self.isTriggeringEvent)

    def widgetDisposed(self):
        return runOnUIThread(self.widget.widget.widget.isDisposed)

    def widgetVisible(self):
        return self.widget.isVisible()
        
    def widgetSensitive(self):
        return self.widget.isEnabled()
        
    def describeWidget(self):
        return "of type " + self.widget.getType()
    
    @classmethod
    def getSignalsToFilter(cls):
        return [ cls.getRecordEventType() ]


class StateChangeEvent(SignalEvent):
    def outputForScript(self, *args):
        return ' '.join([self.name, self.getStateText(*args) ])

    def isStateChange(self, *args):
        return True


class SelectEvent(SignalEvent):    
    def _generate(self, *args):
        self.widget.click()

    @classmethod
    def getAssociatedSignal(cls, widget):
        return "Selection"
    
    def allowsIdenticalCopies(self):
        return self.widget.isInstanceOf(swtbot.widgets.SWTBotMenu)


class LinkSelectEvent(SelectEvent):
    def _generate(self, *args):
        # There is self.widget.click(), but it is very low level, seizes the mouse pointer,
        # and fails utterly under KDE. See https://bugs.eclipse.org/bugs/show_bug.cgi?id=337548
        text = self.widget.getText()
        startPos = text.find(">") + 1
        endPos = text.rfind("<")
        hyperlinkText = text[startPos:endPos]
        self.widget.click(hyperlinkText)
            
        
class RadioSelectEvent(SelectEvent):
    def shouldRecord(self, event, *args):
        return SignalEvent.shouldRecord(self, event, *args) and event.widget.getSelection()

    def getSelectedButton(self):
        method = swtbot.widgets.SWTBotRadio.getDeclaredMethod("otherSelectedButton", None)
        method.setAccessible(True)
        return method.invoke(self.widget.widget, None)
        
    def _generate(self, *args):
        if self.widget.isInstanceOf(swtbot.widgets.SWTBotRadio):
            # Workaround for bug in SWTBot which doesn't handle Eclipse radio buttons properly
            # See https://bugs.eclipse.org/bugs/show_bug.cgi?id=344484 for details
            selectedButton = self.getSelectedButton()
            runOnUIThread(selectedButton.widget.setSelection, False)
        SelectEvent._generate(self)
        
# just so we can distinguish later on...
class SWTBotRadioMenu(swtbot.widgets.SWTBotMenu):
    def click(self):
        # This case also isn't handled correctly by SWTBot!
        # See https://bugs.eclipse.org/bugs/show_bug.cgi?id=397649    
        selectedMenuItem = runOnUIThread(self.getSelectedMenuItem)
        if selectedMenuItem:
            swtbot.widgets.SWTBotMenu(selectedMenuItem).click() # Should have same effect, i.e. disable it
        swtbot.widgets.SWTBotMenu.click(self)

    def getSelectedMenuItem(self):
        menu = self.widget.getParent()
        for item in menu.getItems():
            if item.getStyle() & swt.SWT.RADIO and item.getSelection():
                return item
            
class DropDownButtonEvent(SelectEvent):
    def shouldRecord(self, event, *args):
        return SignalEvent.shouldRecord(self, event, *args) and event.detail & swt.SWT.ARROW == 0
    
class DropDownGenerateFilter(swt.widgets.Listener):
    def __init__(self, argumentString):
        self.done = False
        self.argumentString = argumentString
                
    def handleEvent(self, e):
        if isinstance(e.widget, swt.widgets.Menu) and not self.done:
            if e.widget.getItemCount():
                self.describeMenu(e.widget)
            for item in e.widget.getItems():
                if util.getItemText(item.getText()) == self.argumentString:
                    swtbot.widgets.SWTBotMenu(item).click()
                    self.done = True
                    
    def describeMenu(self, menu):
        from describer import Describer
        desc = Describer()
        desc.logger.info("\nChoosing from Drop Down Menu:")
        desc.logger.info(desc.getMenuDescription(menu))
        
    
class DropDownSelectionEvent(SelectEvent):
    @classmethod
    def getAssociatedSignal(cls, widget):
        return "DropDownSelection"
    
    @classmethod
    def getRecordEventType(cls):
        return swt.SWT.Selection
    
    def parseArguments(self, argumentString):
        return argumentString
    
    def _generate(self, argumentString):
        genFilter = DropDownGenerateFilter(argumentString)
        runOnUIThread(self.addFilter, swt.SWT.Show, genFilter)
        try:
            item = self.widget.menuItem(argumentString)
            if not genFilter.done:
                item.click()
            # We caused the menu to be visible
            # Need to hide it again to avoid unwanted effects
            runOnUIThread(self.hideRootMenu, item)
        except swtbot.exceptions.WidgetNotFoundException:
            # thrown if there are no items
            if not genFilter.done:
                print "Got menu with no items"
        runOnUIThread(self.removeFilter, swt.SWT.Show, genFilter)
            
    def connectRecord(self, method):
        class RecordFilter(swt.widgets.Listener):
            def handleEvent(listenerSelf, e): #@NoSelf
                if isinstance(e.widget, swt.widgets.MenuItem):
                    storytext.guishared.catchAll(method, e, self)
                    for listener in e.widget.getListeners(swt.SWT.Selection):
                        if isinstance(listener, BasicRecordListener):
                            e.widget.removeListener(swt.SWT.Selection, listener)
                            break
                    self.removeFilter(swt.SWT.Selection, listenerSelf)
        def arrowClicked(event, *args):
            # The MenuItem object is very transient, must use Filter to find it
            if event.detail & swt.SWT.ARROW != 0:
                self.addFilter(swt.SWT.Selection, RecordFilter())
                
        SelectEvent.connectRecord(self, arrowClicked)
        
    def addFilter(self, *args):
        self.widget.widget.widget.getDisplay().addFilter(*args)
    
    def removeFilter(self, *args):
        self.widget.widget.widget.getDisplay().removeFilter(*args)
        
    def shouldRecord(self, *args):
        return True # Never going to get these programmatically, don't bother...
    
    def outputForScript(self, event, *args):
        return self.name + " " + util.getItemText(event.widget.getText())
                
    def hideRootMenu(self, item):
        menu = util.getRootMenu(item.widget)
        menu.setVisible(False)
    
class TabEvent(SelectEvent):
    def findTabWithText(self, text):
        for item in self.widget.widget.widget.getItems():
            if item.getText() == text:
                return item
        
    def parseArguments(self, text):
        # Seems we can only get tab item text in the UI thread (?)
        item = runOnUIThread(self.findTabWithText, text)
        if item:
            return item
        else:
            raise UseCaseScriptError, "Could not find tab labelled '" + text + "' in TabFolder."
    
    def getItemText(self, item):
        return item.getText()
    
    def outputForScript(self, event, *args):
        # Text may have changed since the application listeners have been applied
        return ' '.join([self.name, self.getItemText(event.item)])

    
class TabSelectEvent(TabEvent):
    swtbotItemClass = swtbot.widgets.SWTBotTabItem
    def _generate(self, tab):
        self.swtbotItemClass(tab).activate()
            

class CTabSelectEvent(TabSelectEvent):
    swtbotItemClass = swtbot.widgets.SWTBotCTabItem
    def isStateChange(self):
        return True

    def implies(self, *args):
        # State change because it can be implied by CTabCloseEvents
        # But don't amalgamate them together, allow several tabs to be selected in sequence
        return False


class CTabCloseEvent(TabEvent):
    def connectRecord(self, method):
        class RecordListener(swt.custom.CTabFolder2Adapter):
            def close(listenerSelf, e): #@NoSelf
                storytext.guishared.catchAll(method, e, self)

        runOnUIThread(self.widget.widget.widget.addCTabFolder2Listener, RecordListener())
        
    @classmethod
    def getRecordEventType(cls):
        return getattr(swt.SWT, cls.getAssociatedSignal(None))
        
    def addListeners(self, *args):
        # Three indirections: WidgetAdapter -> SWTBotMenu -> MenuItem
        return self.widget.widget.widget.addListener(*args)
    
    def _generate(self, tab):
        swtbot.widgets.SWTBotCTabItem(tab).close()

    def shouldRecord(self, *args):
        return DisplayFilter.instance.hasEventOfType([ swt.SWT.MouseUp ], self.widget.widget.widget)

    def isTriggeringEvent(self, e):
        return e.type == swt.SWT.MouseUp

    @classmethod
    def getSignalsToFilter(cls):
        return [ swt.SWT.MouseUp, swt.SWT.Dispose ]

    @classmethod
    def getAssociatedSignal(cls, widget):
        return "CloseTab"


class ShellCloseEvent(SignalEvent):    
    def _generate(self, *args):
        # SWTBotShell.close appears to close things twice, just use the ordinary one for now...
        class CloseRunnable(swtbot.results.VoidResult):
            def run(resultSelf): #@NoSelf
                self.widget.widget.widget.close()
                
        swtbot.finders.UIThreadRunnable.asyncExec(CloseRunnable())
        
    @classmethod
    def getAssociatedSignal(cls, widget):
        return "Close"
    

class ResizeEvent(StateChangeEvent):
    @classmethod
    def getAssociatedSignal(cls, widget):
        return "Resize"
    
    def parseArguments(self, argumentString):
        words = argumentString.split()
        width = int(words[1])
        height = int(words[-1])
        return width, height

    def _generate(self, arguments):
        width, height = arguments
        runOnUIThread(self.widget.widget.widget.setSize, width, height)

    def dimensionText(self, dimension):
        return str((dimension / 10) * 10)
    
    def getSize(self):
        size = self.widget.widget.widget.getSize()
        return size.x, size.y 
        
    def getStateText(self, *args):
        width, height = self.getSize()
        return "width " + self.dimensionText(width) + " and height " + self.dimensionText(height)

class FreeTextEvent(SignalEvent):
    def connectRecord(self, method):
        pass # Intended for events in filechoosers etc, which we cannot record anyway
    
    @classmethod
    def getSignalsToFilter(cls):
        return []
    
    @classmethod
    def getAssociatedSignal(cls, widget):
        return "TypeText"
    
    def shouldRecord(self, *args):
        return False
    
    def parseArguments(self, text):
        return text
    
    def generate(self, argumentString):
        keyboard = util.callPrivateMethod(self.widget.widget, "keyboard")
        try:
            keyStroke = jface.bindings.keys.KeyStroke.getInstance(argumentString)
            key = keyStroke.getNaturalKey()
            keyboard.pressShortcut(keyStroke.getModifierKeys(), key, chr(key))
        except Exception:
            keyboard.typeText(argumentString + "\n", swtbot.utils.SWTBotPreferences.TYPE_INTERVAL)


class SpinnerSelectEvent(StateChangeEvent):
    @classmethod
    def getAssociatedSignal(cls, widget):
        return "Selection"
    
    def getStateText(self, *args):
        return self.widget.getText()
    
    def parseArguments(self, argumentString):
        if argumentString.isdigit():
            return int(argumentString)
        else:
            raise UseCaseScriptError, "Cannot set Spinners to non-numeric values!"
    
    def _generate(self, argument):
        self.widget.setSelection(argument)

    
class TextEvent(StateChangeEvent):
    physicalEventWidget = None
    def __init__(self, *args):
        StateChangeEvent.__init__(self, *args)
        self.stateText = self.getStateText()
    
    @classmethod
    def getAssociatedSignal(cls, widget):
        return "Modify"
    
    def connectRecord(self, method):
        StateChangeEvent.connectRecord(self, method)
        if self.isTyped():
            class PhysicalEventListener(swt.widgets.Listener):
                def handleEvent(listenerSelf, e): #@NoSelf
                    TextEvent.physicalEventWidget = e.widget

            runOnUIThread(self.addListeners, swt.SWT.KeyDown, PhysicalEventListener())
            runOnUIThread(self.addListeners, swt.SWT.MouseDown, PhysicalEventListener())
        
    def parseArguments(self, text):
        return text
        
    def selectAll(self):
        self.widget.selectAll()
        
    def isTriggeringEvent(self, e):
        # Don't include the Enter presses from TextActivateEvent below...
        return e.type == swt.SWT.KeyDown and e.character != swt.SWT.CR

    def isTyped(self):
        return "typed" in self.generationModifiers

    def _generate(self, argumentString):
        self.widget.setFocus()
        if self.isTyped() and argumentString:
            self.selectAll()
            self.widget.typeText(argumentString)
        else:
            self.widget.setText(argumentString)

    def getStateText(self, *args):
        return self.widget.getText()
    
    def textChanged(self, newStateText):
        return self.stateText is None or self.stateText != newStateText
    
    def shouldRecord(self, event, *args):
        if not self.isEditable():
            return False

        newStateText = self.getStateText()
        if not self.textChanged(newStateText):
            return False
        
        self.stateText = newStateText
        if newStateText and self.isTyped() and self.physicalEventWidget is not self.widget.widget.widget:
            return False
        
        return not self.widget.widget.widget in CComboSelectEvent.internalWidgets and StateChangeEvent.shouldRecord(self, event, *args)
        
    def isEditable(self):
        return not (self.widget.widget.widget.getStyle() & swt.SWT.READ_ONLY) 
    
    def implies(self, stateChangeOutput, stateChangeEvent, *args):
        if isinstance(stateChangeEvent, TextEvent) and \
            stateChangeEvent.widget.widget.widget == self.widget.widget.widget and self.isTyped() != stateChangeEvent.isTyped():
            return True
            
        if self.isTyped():
            currOutput = self.outputForScript(stateChangeEvent, *args)
            return StateChangeEvent.implies(self, stateChangeOutput, stateChangeEvent, *args) and \
               self.hasGainedOrLostCharacters(currOutput, stateChangeOutput)
        else:
            return StateChangeEvent.implies(self, stateChangeOutput, stateChangeEvent, *args)
            
    @classmethod
    def hasGainedOrLostCharacters(cls, text1, text2):
        matcher = SequenceMatcher(None, text1, text2)
        blocks = matcher.get_matching_blocks()
        totalMatchLength = sum((block[2] for block in blocks))
        return totalMatchLength == min(len(text1), len(text2)) and cls.mismatchingSectionCount(blocks) <= 1
    
    @staticmethod
    def mismatchingSectionCount(blocks):
        # We want to make sure text has only been inserted or deleted in a block, as a user would do
        startMismatch = blocks[0][0] > 0 or blocks[0][1] > 0
        middleMismatches = max(len(blocks) - 2, 0)
        endMismatch = blocks[-2][0] + blocks[-2][2] != blocks[-1][0] or \
                      blocks[-2][1] + blocks[-2][2] != blocks[-1][1]
        return int(startMismatch) + middleMismatches + int(endMismatch)
        
            
class TextActivateEvent(SignalEvent):
    @classmethod
    def getAssociatedSignal(cls, widget):
        return "Activate"
    
    @classmethod
    def getRecordEventType(cls):
        return swt.SWT.Traverse
    
    def isTraverseReturn(self, event):
        return event.type == swt.SWT.Traverse and event.detail == swt.SWT.TRAVERSE_RETURN

    def isTriggeringEvent(self, event):
        return event.widget in CComboChangeEvent.internalWidgets and self.isTraverseReturn(event)
    
    def shouldRecord(self, event, *args):
        return not self.widget.widget.widget in CComboChangeEvent.internalWidgets and self.isTraverseReturn(event) and \
            (not self.widget.isInstanceOf(FakeSWTBotCCombo) or \
             DisplayFilter.instance.hasEventOfType([ swt.SWT.Traverse ], util.getPrivateField(self.widget.widget.widget, "text")))
    
    def _generate(self, argumentString):
        self.widget.setFocus()
        self.widget.typeText("\n")

class TextContentAssistEvent(SignalEvent):
    @classmethod
    def getAssociatedSignal(cls, widget):
        return "ContentAssist"
    
    @classmethod
    def getRecordEventType(cls):
        return swt.SWT.KeyDown
    
    def shouldRecord(self, event, *args):
        return event.type == swt.SWT.KeyDown and event.character == " " and (event.stateMask & swt.SWT.CTRL) != 0
    
    def _generate(self, argumentString):
        self.widget.pressShortcut([ swtbot.keyboard.Keystrokes.CTRL, swtbot.keyboard.Keystrokes.SPACE ])


class ComboSelectEvent(StateChangeEvent):
    def __init__(self, *args):
        StateChangeEvent.__init__(self, *args)
        self.stateText = None

    def getStateText(self, *args):
        widget = self.widget.widget.widget
        return DisplayFilter.instance.itemTextCache.get(widget, widget.getText())
    
    def parseArguments(self, argumentString):
        index = runOnUIThread(self.widget.widget.widget.indexOf, argumentString)
        if index == -1:
            raise UseCaseScriptError, "Could not find item labelled '" + argumentString + "' in Combo box."
        return index
    
    def _generate(self, index):
        self.widget.setSelection(index)
        
    @classmethod
    def getAssociatedSignal(cls, widget):
        return "Selection"

    def shouldRecord(self, event, *args):
        newStateText = self.getStateText()
        if self.stateText is not None and self.stateText == newStateText:
            return False
        self.stateText = newStateText
        return StateChangeEvent.shouldRecord(self, event, *args)
    
    def implies(self, stateChangeOutput, stateChangeEvent, *args):
        currOutput = self.outputForScript(*args)
        _, currSelection = currOutput.split(self.name, 1)
        _, oldSelection = stateChangeOutput.split(stateChangeEvent.name, 1)
        return (self.isImpliedEventType(stateChangeEvent) and currSelection == oldSelection) or StateChangeEvent.implies(self, stateChangeOutput, *args)

    def isImpliedEventType(self, event):
        return isinstance(event, ComboSelectEvent)
    
class CComboSelectEvent(ComboSelectEvent):
    internalWidgets = []
    def __init__(self, *args):
        ComboSelectEvent.__init__(self, *args)
        self.addWidgets()

    def addWidgets(self):
        list_ = util.getPrivateField(self.widget.widget.widget, "list")
        text_ = util.getPrivateField(self.widget.widget.widget, "text")
        self.internalWidgets.append(list_)
        self.internalWidgets.append(text_)

    def isTriggeringEvent(self, e):
        return e.widget in self.internalWidgets or StateChangeEvent.isTriggeringEvent(self, e)
    
    def isImpliedEventType(self, event):
        return isinstance(event, CComboChangeEvent)

class ComboTextEvent(TextEvent):
    def parseArguments(self, *args):
        if not runOnUIThread(self.isEditable):
            raise UseCaseScriptError, "Cannot edit text in this Combo Box as it is readonly"
        return TextEvent.parseArguments(self, *args)
    
    def selectAll(self):
        # Strangely, there is no selectAll method...
        selectionPoint = swt.graphics.Point(0, len(self.widget.getText()))
        runOnUIThread(self.widget.widget.widget.setSelection, selectionPoint)
    
    def isTriggeringEvent(self, e):
        # Combo editing sometimes generates two modify events, one "inside" the other
        # Without this we risk the inner one being rejected because the outer one hasn't run,
        # and the outer being rejected because the inner one has updated the text already
        return e.type == swt.SWT.Modify and e.widget is self.widget.widget.widget

class CComboChangeEvent(CComboSelectEvent):
    def parseArguments(self, text):
        if runOnUIThread(self.widget.widget.widget.getStyle) & swt.SWT.READ_ONLY:
            raise UseCaseScriptError, "Cannot edit text in this Combo Box as it is readonly"
        return text
    
    def _generate(self, argumentString):
        try:
            self.widget.setFocus()
            self.widget.setText(argumentString)
        except RuntimeException, e:
            raise UseCaseScriptError, e.getMessage()

    @classmethod
    def getAssociatedSignal(cls, widget):
        return "Modify"
    
    def implies(self, stateChangeOutput, stateChangeEvent, *args):
        return StateChangeEvent.implies(self, stateChangeOutput, stateChangeEvent, *args)

    def getStateText(self, *args):
        return self.widget.getText()
    

class StoryTextSwtBotTable(swtbot.widgets.SWTBotTable):    
    def select(self, indices):
        # When clicking in a cell, SWTBot likes to select the entire row first, generating mouse events and all
        # This can cause trouble, e.g. the cells can translate it into clicking the first column
        runOnUIThread(self.widget.deselectAll)
        for i in indices:
            runOnUIThread(self.widget.select, i)


class TableSelectEvent(StateChangeEvent):
    def __init__(self, *args):
        StateChangeEvent.__init__(self, *args)
        TableIndexer.getIndexer(self.widget.widget.widget)

    @classmethod
    def getAssociatedSignal(cls, widget):
        return "MouseDown"
    
    @classmethod
    def getAssociatedSignatures(cls, widget):
        return [ "CellSelection" ]
    
    def parseArguments(self, argumentString):
        indexer = TableIndexer.getIndexer(self.widget.widget.widget)
        row, col = indexer.getViewCellIndices(argumentString)
        return row, col
            
    def _generate(self, cell):
        self.widget.click(*cell)
        
    def getStateText(self, event, *args):
        row, col = self.findCell(event)
        indexer = TableIndexer.getIndexer(self.widget.widget.widget)
        return indexer.getCellDescription(row, col)
    
    def clickCount(self):
        return 1
    
    def shouldRecord(self, event, *args):
        if event.count != self.clickCount():
            return False
        row, _ = self.findCell(event)
        return row is not None and StateChangeEvent.shouldRecord(self, event, *args)
    
    def findCell(self, event):
        pt = swt.graphics.Point(event.x, event.y)
        table = event.widget
        firstRow = table.getTopIndex()
        columnCount = table.getColumnCount()
        for rowIndex in range(firstRow, firstRow + table.getItemCount()):
            item = table.getItem(rowIndex)
            if columnCount:
                for col in range(columnCount):
                    rect = item.getBounds(col)
                    if rect.contains(pt):
                        return rowIndex, col
            else:
                rect = item.getBounds()
                if rect.contains(pt):
                    return rowIndex, None
        return None, None
    
    def implies(self, stateChangeOutput, stateChangeEvent, *args):
        currOutput = self.outputForScript(*args)
        return currOutput == stateChangeOutput
    
class TableDoubleClickEvent(TableSelectEvent):
    @classmethod
    def getAssociatedSignal(cls, widget):
        return "MouseDoubleClick"
    
    @classmethod
    def getAssociatedSignatures(cls, widget):
        return [ "CellDoubleClick" ]
    
    def clickCount(self):
        return 2
    
    def _generate(self, cell):
        self.widget.doubleClick(*cell)
    
    def implies(self, stateChangeOutput, stateChangeEvent, *args):
        return isinstance(stateChangeEvent, TableSelectEvent) or SelectEvent.implies(self, stateChangeOutput, stateChangeEvent, *args)
    
    
class TableIndexer(storytext.guishared.TableIndexer):
    def __init__(self, table):
        self.cachedRowCount = 0
        storytext.guishared.TableIndexer.__init__(self, table)
        
    def getRowCount(self):
        return runOnUIThread(self.widget.getItemCount)

    def getCellValue(self, row, col):
        return self.widget.getItem(row).getText(col)
    
    def getColumnText(self, col):
        return self.widget.getColumn(col).getText()
    
    def findColumnIndex(self, columnName):
        return runOnUIThread(storytext.guishared.TableIndexer.findColumnIndex, self, columnName)
    
    def findRowNames(self):
        column, rowNames = runOnUIThread(storytext.guishared.TableIndexer.findRowNames, self)
        self.cachedRowCount = len(rowNames)
        return column, rowNames
    
    def checkNameCache(self):
        if self.getRowCount() != self.cachedRowCount or all((r.startswith("<unnamed>") for r in self.rowNames)):
            self.primaryKeyColumn, self.rowNames = self.findRowNames()
            self.logger.debug("Rebuilt indexer cache, primary key " + str(self.primaryKeyColumn) +
                              ", row names now " + repr(self.rowNames))
        
    def getCellDescription(self, *args, **kw):
        self.checkNameCache()
        return storytext.guishared.TableIndexer.getCellDescription(self, *args, **kw)

    def getViewCellIndices(self, *args, **kw):
        self.checkNameCache()
        return storytext.guishared.TableIndexer.getViewCellIndices(self, *args, **kw)
    
    
class TableColumnHeaderEvent(SignalEvent):
    def __init__(self, *args):
        SignalEvent.__init__(self, *args)
        self.columnsFound = set()
    
    @classmethod
    def getAssociatedSignal(cls, widget):
        return "Selection"
    
    @classmethod
    def getAssociatedSignatures(cls, widget):
        return [ "ColumnSelection" ]
    
    def addColumnListeners(self, *args):
        for column in self.widget.widget.widget.getColumns():
            if column not in self.columnsFound:
                self.columnsFound.add(column)
                column.addListener(*args)

    def addListeners(self, *args):
        self.addColumnListeners(*args)            
        class PaintListener(swt.widgets.Listener):
            def handleEvent(lself, e): #@NoSelf
                storytext.guishared.catchAll(self.addColumnListeners, *args)
        self.widget.widget.widget.addListener(swt.SWT.Paint, PaintListener())
        
    def outputForScript(self, event, *args):
        return " ".join([ self.name, event.widget.getText() ])
    
    def parseArguments(self, argumentString):
        try:
            return self.widget.header(argumentString)
        except swtbot.exceptions.WidgetNotFoundException:
            raise UseCaseScriptError, "Could not find column labelled '" + argumentString + "' in table."
        
    def _generate(self, column):
        column.click()

class TreeIndexer(storytext.guishared.TreeIndexer):
    def populate(self):
        self.allItems = {}
        self.allDescriptions = {}
        for item in runOnUIThread(self.widget.getItems):
            runOnUIThread(self.storeSubItems, item)

    def storeSubItems(self, item):
        self.storeItem(item)
        if hasattr(item, "getItems"):
            for subItem in item.getItems():
                self.storeSubItems(subItem)
    
    def getDescriptionToStore(self, item):
        return DisplayFilter.instance.itemTextCache.pop(item, item.getText())        

class TreeEvent(SignalEvent):
    indexerClass = TreeIndexer
    def parseArguments(self, argumentString):
        if argumentString:
            indexer = self.indexerClass.getIndexer(self.widget.widget.widget)
            item = indexer.getItem(argumentString)
            if not item or item.isDisposed():
                indexer.populate()
                item = indexer.getItem(argumentString)
            if item:
                return self.getItemClass()(item)
            else:
                raise UseCaseScriptError, "Could not find item labelled '" + argumentString + "' in " + self.getClassDesc() + "."
        else:
            return ""
    
    def getClassDesc(self):
        return self.widget.widget.widget.__class__.__name__.lower()

    def getTextToRecord(self, item):
        indexer = self.indexerClass.getIndexer(self.widget.widget.widget)
        desc = indexer.getItemDescription(item)
        if not desc:
            indexer.populate()
            desc = indexer.getItemDescription(item)
        return desc
        
    def outputForScript(self, event, *args):
        if event.item is not None:
            # Text may have changed since the application listeners have been applied
            text = self.getTextToRecord(event.item)
            return ' '.join([self.name, text])
        else:
            return self.name
        
    def getItemClass(self):
        if isinstance(self.widget.widget.widget, swt.widgets.ExpandBar):
            return swtbot.widgets.SWTBotExpandItem
        return swtbot.widgets.SWTBotTreeItem


class ExpandEvent(TreeEvent):
    @classmethod
    def getAssociatedSignal(cls, widget):
        return "Expand"

    def parseArguments(self, argumentString):
        item = TreeEvent.parseArguments(self, argumentString)
        if hasattr(item, "rowCount") and item.rowCount() == 0:
            raise UseCaseScriptError, "Item labelled '" + argumentString + "' in " + self.getClassDesc() + " is not expandable."
        return item
    
    def _generate(self, item):
        item.expand()

class CollapseEvent(TreeEvent):
    @classmethod
    def getAssociatedSignal(cls, widget):
        return "Collapse"

    def _generate(self, item):
        item.collapse()

class TreeClickEvent(TreeEvent):
    @classmethod
    def getAssociatedSignal(cls, widget):
        return "Selection"
    
    @staticmethod
    def getPairings(argumentString):
        pairings = []
        arg1SoFar, arg2 = "", argumentString
        for _ in range(argumentString.count(",")):
            arg1, arg2 = arg2.split(",", 1)
            if arg1SoFar:
                arg1SoFar += ","
            arg1SoFar += arg1
            pairings.append((arg1SoFar, arg2))
        return pairings
            
    def parseArguments(self, argumentString):
        if argumentString:
            try:
                return [ TreeEvent.parseArguments(self, argumentString) ]
            except UseCaseScriptError:
                if "," in argumentString:
                    for arg1, arg2 in self.getPairings(argumentString):
                        try:
                            return self.parseArguments(arg1) + self.parseArguments(arg2)
                        except UseCaseScriptError:
                            pass
                raise                    
        else:
            return []

    def shouldRecord(self, event, *args):
        # Seem to get selection events even when nothing has been selected...
        # Record if there is no item, or if we've pressed control (deselecting) or
        # if whatever we selected is still in the selection.
        return TreeEvent.shouldRecord(self, event, *args) and \
            (event.item is None or event.stateMask == swt.SWT.CTRL or event.item in event.widget.getSelection())

    def _generate(self, items):
        if items:
            self.selectItems(items)
        else:
            self.widget.unselect()
            
    def selectItems(self, items):
        # Swtbot select and click methods doesn't seem to generate all events that a mouse click does.
        # In particular the select methods generate events without the item field set which can cause great confusion.
        runOnUIThread(self.widget.widget.widget.setSelection, [ item.widget for item in items ])
        items[0].click()
        for item in items[1:]:
            self.postControlEvent(swt.SWT.KeyDown)
            item.click()
            self.postControlEvent(swt.SWT.KeyUp)
                        
    def postControlEvent(self, eventType):
        event = swt.widgets.Event()
        event.type = eventType
        event.keyCode = swt.SWT.CTRL
        event.character = '\0'
        runOnUIThread(self.widget.display.post, event)
        
    def isStateChange(self):
        return True
    
    def outputForScript(self, event, *args):
        items = event.widget.getSelection()
        if not items:
            return self.name

        args = map(self.getTextToRecord, event.widget.getSelection())
        return ' '.join([self.name, ",".join(args)])
        
    def implies(self, stateChangeOutput, stateChangeEvent, *args):
        currOutput = self.outputForScript(*args)
        return currOutput.startswith(stateChangeOutput)


class TreeDoubleClickEvent(TreeEvent):
    @classmethod
    def getAssociatedSignal(cls, widget):
        return "DefaultSelection"

    def _generate(self, item):
        item.doubleClick()

    def implies(self, stateChangeLine, stateChangeEvent, swtEvent, *args):
        return isinstance(stateChangeEvent, TreeClickEvent) and \
               stateChangeLine == stateChangeEvent.name + " " + stateChangeEvent.getTextToRecord(swtEvent.item)

class ListClickEvent(StateChangeEvent):
    @classmethod
    def getAssociatedSignal(cls, widget):
        return "Selection"
    
    def implies(self, stateChangeOutput, stateChangeEvent, *args):
        currOutput = self.outputForScript(*args)
        return currOutput.startswith(stateChangeOutput)

    def getStateText(self, *args):
        return ",".join(self.widget.selection())

    def _generate(self, indices):
        if len(indices) == 0:
            self.widget.unselect()
        else:
            self.widget.select(indices)
            
    def parseArguments(self, argumentString):
        return self.getIndices(argumentString) if argumentString else []
            
    def shouldRecord(self, *args):
        return not self.widget.widget.widget in CComboSelectEvent.internalWidgets and StateChangeEvent.shouldRecord(self, *args)

    def getIndices(self, argumentString):
        indices = []
        for itemText in argumentString.split(","):
            index = self.widget.indexOf(itemText)
            if index >= 0:
                indices.append(index)
            else:
                raise UseCaseScriptError, "Could not find item labelled '" + itemText + "' in list."
        return indices

class DateTimeEvent(StateChangeEvent):
    def __init__(self, *args, **kw):
        StateChangeEvent.__init__(self, *args, **kw)
        self.dateFormat = self.getDateFormat()

    def getDateFormat(self):
        if runOnUIThread(self.widget.widget.widget.getStyle) & swt.SWT.TIME:
            return util.getDateFormat(swt.SWT.TIME)
        else:
            return util.getDateFormat(swt.SWT.DATE)
    @classmethod
    def getAssociatedSignal(cls, widget):
        return "Selection"
    
    def getStateText(self, *args):
        return self.dateFormat.format(self.widget.getDate())
    
    def parseArguments(self, argumentString):
        try:
            return self.dateFormat.parse(argumentString)
        except ParseException:
            raise UseCaseScriptError, "Could not parse date/time argument '" + argumentString + \
                  "', not of format '" + self.dateFormat.toPattern() + "'."

    def _generate(self, currDate):
        self.widget.setDate(currDate)
        

class EventFinishedListener(swt.widgets.Listener):
    def __init__(self, event, method):
        self.event = event
        self.method = method
        
    def handleEvent(self, e2):
        if e2 is self.event:
            storytext.guishared.catchAll(self.method, self.event)
            if not self.event.widget.isDisposed():
                self.event.widget.removeListener(self.event.type, self)


class DisplayFilter:
    instance = None
    def otherEventCount(self, event, isTriggeringEvent):
        relevantEvents = [ e for e in self.eventsFromUser if e is not event and not isTriggeringEvent(e) ]
        return len(relevantEvents)
        
    def getEventFromUser(self, event, isTriggeringEvent):
        if event in self.eventsFromUser:
            return not self.hasPreviousEventOnShell(event, isTriggeringEvent)
        else:
            if len(self.eventsFromUser) == 0:
                self.logger.debug("Rejecting event, it has not yet been seen in the display filter")
            else:
                self.logger.debug("Received event " + event.toString())
                self.logger.debug("Rejecting event, not yet processed " + repr([ e.toString() for e in self.eventsFromUser ]))
            return False
        
    def hasEvents(self):
        return len(self.eventsFromUser) > 0

    def __init__(self, widgetEventTypes):
        self.widgetEventTypes = widgetEventTypes
        self.eventsFromUser = []
        self.delayedAppEvents = []
        self.itemTextCache = {}
        self.logger = logging.getLogger("storytext record")
        DisplayFilter.instance = self
        
    def getShell(self, widget):
        # Note : widget might be an Item rather than a widget!
        if widget is not None and not widget.isDisposed():
            if hasattr(widget, "getShell"):
                return widget.getShell()
            elif hasattr(widget, "getParent"):
                return self.getShell(widget.getParent())

    def hasPreviousEventOnShell(self, event, isTriggeringEvent):
        widget = event.widget
        currShell = self.getShell(widget)
        if not currShell:
            return False

        for e in self.eventsFromUser:
            if e is event:
                return False
            elif not isTriggeringEvent(e) and self.getShell(e.widget) is currShell:
                self.logger.debug("Previous event on shell found: " + repr(e))
                return True
        return False
    
    def hasEventOfType(self, eventTypes, widget):
        return any((event.type in eventTypes and event.widget is widget for event in self.eventsFromUser))
        
    def addFilters(self, display):
        class DisplayListener(swt.widgets.Listener):
            def handleEvent(listenerSelf, e): #@NoSelf
                storytext.guishared.catchAll(self.handleFilterEvent, e)

        for eventType in self.getAllEventTypes():
            self.logger.debug("Adding filter for events of type " + str(eventType))
            runOnUIThread(display.addFilter, eventType, DisplayListener())
            
        self.addApplicationEventFilter(display)

    def handleEventFinished(self, e):        
        # Any application events that were delayed should be no longer, if they haven't been handled yet
        for appEvent in self.delayedAppEvents:
            applicationEventDelay(appEvent, fromLevel=len(self.eventsFromUser), increase=False)
        self.delayedAppEvents = []
        self.logger.debug("Filter removed for event " + e.toString())
        self.eventsFromUser.remove(e)

    def cacheItemText(self, e):
        if e.item and not e.item.isDisposed():
            self.itemTextCache[e.item] = e.item.getText()
        elif hasattr(e.widget, "getSelectionIndex") and hasattr(e.widget, "getText") and e.type == swt.SWT.Selection:
            self.itemTextCache[e.widget] = e.widget.getText()
            
    def handleFilterEvent(self, e):
        if self.shouldCheckWidget(e.widget, e.type):
            self.logger.debug("Filter for event " + e.toString())
            self.eventsFromUser.append(e)
            runOnUIThread(e.widget.addListener, e.type, EventFinishedListener(e, self.handleEventFinished))
            # Safe guard against the application changing the text before we can record
            self.cacheItemText(e)
        else:
            self.logger.debug("Filter ignored event " + e.toString())

    def addApplicationEventFilter(self, display):
        class ApplicationEventListener(swt.widgets.Listener):
            def handleEvent(listenerSelf, e): #@NoSelf
                if e.text:
                    storytext.guishared.catchAll(self.registerApplicationEvent, e.text, e.data or "system")
        runOnUIThread(display.addFilter, applicationEventType, ApplicationEventListener())
 
    @classmethod       
    def registerApplicationEvent(cls, name, category, **kw):
        delayLevel = len(cls.instance.eventsFromUser) if cls.instance else 0
        if delayLevel:
            cls.instance.delayedAppEvents.append(name)
        applicationEvent(name, category, delayLevel=delayLevel, **kw)

    def shouldCheckWidget(self, widget, eventType):
        if not util.isVisible(widget):
            return False
        for cls, types in self.widgetEventTypes:
            if isinstance(widget, cls) and eventType in types and not self.hasComplexAncestors(widget):
                return True
        return False

    def hasComplexAncestors(self, widget):
        return isinstance(widget.getParent(), swt.widgets.DateTime)

    def getAllEventTypes(self):
        eventTypeSet = set()
        for _, eventTypes in self.widgetEventTypes:
            eventTypeSet.update(eventTypes)
        return eventTypeSet
    
    @classmethod
    def removeApplicationEvent(cls, *args, **kw):
        applicationEventRemove(*args, **kw)
    
# There is no SWTBot class for these things, so we make our own. We aren't actually going to use it anyway...    
class FakeSWTBotTabFolder(swtbot.widgets.AbstractSWTBot):
    pass

class FakeSWTBotCTabFolder(swtbot.widgets.AbstractSWTBot):
    pass

# There is no way to type text in SWTBotCCombo, so make our own.
class FakeSWTBotCCombo(swtbot.widgets.SWTBotCCombo):
    def typeText(self, text):
        self.setFocus()
        self.keyboard().typeText(text, swtbot.utils.SWTBotPreferences.TYPE_INTERVAL)

class BrowserUpdateMonitor(swt.browser.ProgressListener):
    def __init__(self, widget):
        self.widget = widget
    
    def changed(self, e):
        pass
    
    def completed(self, e):
        storytext.guishared.catchAll(self.onCompleted, e)
        
    def onCompleted(self, e):
        self.sendApplicationEvent(self.widget.getNameForAppEvent() + " to finish loading", "browser")

    def sendApplicationEvent(self, *args):
        applicationEvent(*args)

class EventPoster:
    def __init__(self, display):
        self.display = display

    def moveMouseAndWait(self, x, y):
        runOnUIThread(storytext.guishared.catchAll, self.postMouseMove, x, y)
        runOnUIThread(storytext.guishared.catchAll, self.waitForCursor, x, y)
       
    def postMouseMove(self, x ,y):
        event = swt.widgets.Event()
        event.type = swt.SWT.MouseMove
        event.x = x
        event.y = y
        self.display.post(event)

    def clickMouse(self):        
        runOnUIThread(storytext.guishared.catchAll, self.postMouseDown)
        runOnUIThread(storytext.guishared.catchAll, self.postMouseUp)

    def postMouseDown(self, button=1):
        event = swt.widgets.Event()
        event.type = swt.SWT.MouseDown
        event.button = button
        self.display.post(event)

    def postMouseUp(self, button=1):
        event = swt.widgets.Event()
        event.type = swt.SWT.MouseUp
        event.button = button
        self.display.post(event)
        
    def checkAndPostKeyPressed(self, keyModifiers):
        if keyModifiers & swt.SWT.CTRL != 0:
            runOnUIThread(storytext.guishared.catchAll, self.postKeyPressed, swt.SWT.CTRL, '\0')
            
    def checkAndPostKeyReleased(self, keyModifiers):
        if keyModifiers & swt.SWT.CTRL != 0:
            runOnUIThread(storytext.guishared.catchAll, self.postKeyReleased, swt.SWT.CTRL, '\0')
            
    def postKeyPressed(self, code, character):
        event = swt.widgets.Event()
        event.type = swt.SWT.KeyDown
        event.keyCode = code
        event.character = character
        self.display.post(event)
        
    def postKeyReleased(self, code, character):
        event = swt.widgets.Event()
        event.type = swt.SWT.KeyUp
        event.keyCode = code
        event.character = character
        self.display.post(event)
   
    def waitForCursor(self, x, y):
        while self.display.getCursorLocation().x != x and self.display.getCursorLocation().y != y:
            time.sleep(0.1)

    def moveClickAndReturn(self, x, y, keyModifiers=0):
        self.performAndReturn(self.moveAndClick, x, y, keyModifiers)
        
    def moveAndClick(self, x, y, keyModifiers):
        self.moveMouseAndWait(x, y)
        self.checkAndPostKeyPressed(keyModifiers)
        self.clickMouse()
        self.checkAndPostKeyReleased(keyModifiers)
        
    def performAndReturn(self, method, *args):
        currPos = runOnUIThread(self.display.getCursorLocation)
        method(*args)
        time.sleep(0.1)
        self.moveMouseAndWait(currPos.x, currPos.y)


class WidgetMonitor:
    swtbotMap = { swt.widgets.Button   : (swtbot.widgets.SWTBotButton,
                                         [ (swt.SWT.RADIO, swtbot.widgets.SWTBotRadio),
                                           (swt.SWT.CHECK, swtbot.widgets.SWTBotCheckBox) ]),
                  swt.widgets.MenuItem : (swtbot.widgets.SWTBotMenu, 
                                          [ (swt.SWT.RADIO, SWTBotRadioMenu) ]),
                  swt.widgets.Shell    : (swtbot.widgets.SWTBotShell, []),
                  swt.widgets.ToolItem : ( swtbot.widgets.SWTBotToolbarPushButton,
                                         [ (swt.SWT.DROP_DOWN, swtbot.widgets.SWTBotToolbarDropDownButton),
                                           (swt.SWT.RADIO    , swtbot.widgets.SWTBotToolbarRadioButton),
                                           (swt.SWT.SEPARATOR, swtbot.widgets.SWTBotToolbarSeparatorButton),
                                           (swt.SWT.TOGGLE   , swtbot.widgets.SWTBotToolbarToggleButton) ]),
                  swt.widgets.Spinner  : (swtbot.widgets.SWTBotSpinner, []),
                  swt.widgets.Text     : (swtbot.widgets.SWTBotText, []),
                  swt.widgets.Link     : (swtbot.widgets.SWTBotLink, []),
                  swt.widgets.List     : (swtbot.widgets.SWTBotList, []),
                  swt.widgets.Combo    : (swtbot.widgets.SWTBotCombo, []),
                  swt.custom.CCombo    : (FakeSWTBotCCombo, []),
                  swt.widgets.Table    : (StoryTextSwtBotTable, []),
                  swt.widgets.TableColumn : (swtbot.widgets.SWTBotTableColumn, []),
                  swt.widgets.Tree     : (swtbot.widgets.SWTBotTree, []),
                  swt.widgets.ExpandBar: (swtbot.widgets.SWTBotExpandBar, []),
                  swt.widgets.DateTime : (swtbot.widgets.SWTBotDateTime, []),
                  swt.widgets.TabFolder: (FakeSWTBotTabFolder, []),
                  swt.custom.CTabFolder: (FakeSWTBotCTabFolder, []),
                  swt.browser.Browser  : (swtbot.widgets.SWTBotBrowser, [])
                  }
    def __init__(self, uiMap):
        self.bot = self.createSwtBot()
        self.widgetsMonitored = set()
        self.allMenus = set()
        self.uiMap = uiMap
        # Do this here, when things will be loaded with the right classloader
        # Might affect which event types are used. Has to be set up like this so RCP works.
        self.uiMap.scriptEngine.importCustomEventTypesFromSimulator(eventTypes)
        self.displayFilter = self.getDisplayFilterClass()(self.getWidgetEventTypes())
        self.widgetMonitorLock = Lock()

    def handleReplayFailure(self, errorText, events):
        if "Could not find row identified by" in errorText:
            # Problems with row identification in Table Cells
            # A common cause is that someone has edited the row identifier and not committed the change
            # We press Enter and hope for the best...
            if any((runOnUIThread(self.hasTableWithEditor, e) for e in events)):
                keyboard = swtbot.keyboard.KeyboardFactory.getSWTKeyboard()
                keyboard.pressShortcut([ swtbot.keyboard.Keystrokes.CR ])
        elif "MenuItem has already been disposed" in errorText or "no widget found" in errorText:
            runOnUIThread(self.recheckPopupMenus)
            
    def hasTableWithEditor(self, event):
        widget = event.widget.widget.widget
        if not widget.isDisposed() and isinstance(widget, swt.widgets.Table) and len(widget.getChildren()) > 0:
            return any((not child.isDisposed() and child.isVisible() for child in widget.getChildren()))
        else:
            return False

    def recheckPopupMenus(self):
        for menu in self.allMenus:
            self.uiMap.logger.debug("Rechecking popup menu " + str(id(menu)))
            if not menu.isDisposed():
                self.monitorNewWidgets(menu)

    def getDisplayFilterClass(self):
        return DisplayFilter

    def createSwtBot(self):
        return swtbot.SWTBot()
        
    def getWidgetEventTypes(self):
        return self.getWidgetEventInfo(lambda eventClass: eventClass.getSignalsToFilter())

    @classmethod
    def getWidgetEventTypeNames(cls):
        return cls.getWidgetEventInfo(lambda eventClass: [ eventClass.getAssociatedSignal(None) ])

    @classmethod
    def getWidgetEventInfo(cls, method):
        allEventTypes = []
        eventTypeDict = dict(eventTypes)
        for widgetClass, (defaultSwtbotClass, styleSwtbotInfo) in cls.swtbotMap.items():
            currEventTypes = set()
            for swtBotClass in [ defaultSwtbotClass] + [ cls for _, cls in styleSwtbotInfo ]:
                for eventClass in eventTypeDict.get(swtBotClass, []):
                    currEventTypes.update(method(eventClass))
            if currEventTypes:
                allEventTypes.append((widgetClass, currEventTypes))
        return allEventTypes
    
    def setUp(self):
        self.forceShellActive()
        self.setUpDisplayFilter()
        allWidgets = self.findAllWidgets()
        if len(allWidgets) > 0:
            self.uiMap.logger.debug("Monitoring all widgets in active shell...")
            self.monitorAllWidgets(list(allWidgets))
            self.uiMap.logger.debug("Done Monitoring all widgets in active shell.")
            return True
        else:
            return False
        
    def forceShellActive(self):
        if os.pathsep == ":": # os.name == "java", so can't find out that way if we're on UNIX
            # Need to do this for running under Xvfb on UNIX
            # Seems to throw exceptions occasionally on Windows, so don't bother
            runOnUIThread(self.bot.getFinder().getShells()[0].forceActive)

    def getDisplay(self):
        return self.bot.getDisplay()

    def setUpDisplayFilter(self):
        display = self.getDisplay()
        self.addMonitorFilter(display)
        self.displayFilter.addFilters(display)

    def addMonitorFilter(self, display):
        class MonitorListener(swt.widgets.Listener):
            def handleEvent(listenerSelf, e): #@NoSelf
                storytext.guishared.catchAll(self.widgetShown, e)

        monitorListener = MonitorListener()
        runOnUIThread(display.addFilter, swt.SWT.Show, monitorListener)
        runOnUIThread(display.addFilter, swt.SWT.Paint, monitorListener)
        runOnUIThread(display.addFilter, swt.SWT.Selection, monitorListener)
        
    def widgetShown(self, e):
        if self.shouldMonitor(e.widget):
            if isinstance(e.widget, swt.widgets.Menu):
                e.widget.addListener(e.type, EventFinishedListener(e, self.monitorWidgetsFromEvent))
            else:
                self.monitorNewWidgets(e.widget, e.type == swt.SWT.Show)
            
    def shouldMonitor(self, widget):
        # Don't try to monitor widgets before the shells they appear in!
        return widget not in self.widgetsMonitored and \
            (isinstance(widget, swt.widgets.Shell) or not isinstance(widget, swt.widgets.Control) or widget.getShell() in self.widgetsMonitored)
            
    def monitorWidgetsFromEvent(self, e):
        if not e.widget.isDisposed():
            self.monitorNewWidgets(e.widget, e.type == swt.SWT.Show)
            
    def monitorNewWidgets(self, parent, findInvisible=True):
        if findInvisible:
            self.bot.getFinder().setShouldFindInvisibleControls(True)

        widgets = self.findDescendants(parent)
        if findInvisible:
            self.bot.getFinder().setShouldFindInvisibleControls(False)

        self.uiMap.logger.debug("Showing/painting widget of type " +
                                parent.__class__.__name__ + " " + str(id(parent)) + ", monitoring found widgets")
        if isinstance(parent, swt.widgets.Menu):
            self.allMenus.add(parent)
        self.monitorAllWidgets(widgets)
        self.uiMap.logger.debug("Done Monitoring all widgets after showing/painting " + 
                                parent.__class__.__name__ + " " + str(id(parent)) + ".")
        
    def findDescendants(self, widget):
        if isinstance(widget, swt.widgets.Menu):
            return ArrayList(self.getMenuItems(widget))
        else:
            matcher = IsAnything()
            return self.bot.widgets(matcher, widget)

    def getMenuItems(self, menu):
        items = []
        for item in menu.getItems():
            submenu = item.getMenu()
            if submenu:
                items += self.getMenuItems(submenu)
            else:
                items.append(item)
        return items

    def monitorAllWidgets(self, widgets):
        # Called both on the entire initial widget set and whenever a widgets is shown -> different threads
        # Use lock to avoid racing
        popupMenus = self.getPopupMenus(widgets)
        self.allMenus.update(popupMenus)
        widgets += popupMenus
        self.widgetMonitorLock.acquire()
        try:
            newWidgets = [ w for w in widgets if w not in self.widgetsMonitored and not w.isDisposed() ]
            self.uiMap.logger.debug(str(len(widgets)) + " widgets found, " + str(len(newWidgets)) + " new.")  
            self.widgetsMonitored.update(newWidgets)
        finally:
            self.widgetMonitorLock.release()
        for widget in self.makeAdapters(newWidgets):
            self.uiMap.monitorWidget(widget)
            self.monitorAsynchronousUpdates(widget)

    def monitorAsynchronousUpdates(self, widget):
        # Browsers load their stuff in the background, must wait for them to finish
        if widget.isInstanceOf(swtbot.widgets.SWTBotBrowser):
            monitor = self.getBrowserUpdateMonitorClass()(widget)
            runOnUIThread(widget.widget.widget.addProgressListener, monitor)
            
    def getBrowserUpdateMonitorClass(self):
        return BrowserUpdateMonitor
    
    def addToolbarEmbeddedWidgets(self, widgets, matcher):
        # Should not be necessary, but SWTBot doesn't find these, as of 2.0.5
        # See https://bugs.eclipse.org/bugs/show_bug.cgi?id=356883
        # We work around and find them ourselves
        extraControls = []
        for widget in widgets:
            if isinstance(widget, swt.widgets.ToolItem):
                control = runOnUIThread(widget.getControl)
                if control is not None:
                    extraControls.append(control)
        for control in extraControls:
            widgets.addAll(self.bot.widgets(matcher, control))

    def findAllWidgets(self):
        matcher = IsAnything()
        shell = runOnUIThread(self.getActiveShell)
        try:
            widgets = self.bot.widgets(matcher, shell)
            self.addToolbarEmbeddedWidgets(widgets, matcher)
            menus = self.bot.getFinder().findMenus(matcher)
            widgets.addAll(menus)
            return widgets
        except TypeError:
            sys.stderr.write("ERROR: classloader clash when trying to use SWTBot.\n" +
                             "Possible causes: has Mockito been exported with the product (suggest to make dependency optional if so)?\n"
                             "Are you using Jython 2.5.2 instead of Jython 2.5.1?\n")
            swtbot.widgets.SWTBotShell(shell).close()
            return []

    def getPopupMenus(self, widgets):
        menus = []
        for widget in widgets:
            if isinstance(widget, swt.widgets.Control):
                menuFinder = swtbot.finders.ContextMenuFinder(widget)
                menus += filter(lambda m: m not in self.widgetsMonitored, menuFinder.findMenus(IsAnything()))
        return menus

    @classmethod
    def findSwtbotClass(cls, widget, widgetClass):
        defaultClass, styleClasses = cls.swtbotMap.get(widgetClass)
        for currStyle, styleClass in styleClasses:
            if runOnUIThread(widget.getStyle) & currStyle:
                return styleClass
        return defaultClass

    def makeAdapters(self, widgets):
        adapters = []
        for widget in widgets:
            adapter = self.makeAdapter(widget)
            if adapter:
                adapters.append(adapter)
        return adapters

    @classmethod
    def makeAdapter(cls, widget):
        for widgetClass in cls.swtbotMap.keys():
            if isinstance(widget, widgetClass):
                swtbotClass = cls.findSwtbotClass(widget, widgetClass)
                try:
                    return WidgetAdapter.adapt(swtbotClass(widget))
                except RuntimeException:
                    # Sometimes widgets are already disposed
                    pass
        
    def getActiveShell(self):
        finder = self.bot.getFinder()
        activeShell = finder.activeShell()
        if activeShell is not None:
            return activeShell
        shells = filter(lambda s: s.getText() and s.isVisible(), finder.getShells())
        if shells:
            return shells[-1]
        
    def removeMousePointerIfNeeded(self):
        # If the mouse pointer is inside the window, this might cause accidental mouseovers and indeterminism. Relocate it to 0,0 if so
        display = self.getDisplay()
        def pointerInWindow():
            return self.getActiveShell().getClientArea().contains(display.getCursorLocation())
            
        if runOnUIThread(pointerInWindow):
            self.uiMap.logger.debug("Removing mouse pointer from window, to avoid accidental mouseovers")
            poster = EventPoster(display)
            poster.moveMouseAndWait(0, 0)
            self.uiMap.logger.debug("Mouse pointer now at 0,0")


        
eventTypes =  [ (swtbot.widgets.SWTBotButton            , [ SelectEvent ]),
                (SWTBotRadioMenu                        , [ RadioSelectEvent ]),
                (swtbot.widgets.SWTBotMenu              , [ SelectEvent ]),
                (swtbot.widgets.SWTBotToolbarPushButton , [ SelectEvent ]),
                (swtbot.widgets.SWTBotToolbarDropDownButton , [ DropDownButtonEvent, DropDownSelectionEvent ]),
                (swtbot.widgets.SWTBotToolbarRadioButton, [ RadioSelectEvent ]),
                (swtbot.widgets.SWTBotLink              , [ LinkSelectEvent ]),
                (swtbot.widgets.SWTBotRadio             , [ RadioSelectEvent ]),
                (swtbot.widgets.SWTBotSpinner           , [ SpinnerSelectEvent ]),
                (swtbot.widgets.SWTBotText              , [ TextEvent, TextActivateEvent, TextContentAssistEvent ]),
                (swtbot.widgets.SWTBotShell             , [ ShellCloseEvent, ResizeEvent, FreeTextEvent ]),
                (StoryTextSwtBotTable                   , [ TableColumnHeaderEvent, TableSelectEvent, TableDoubleClickEvent ]),
                (swtbot.widgets.SWTBotTableColumn       , [ TableColumnHeaderEvent ]),
                (swtbot.widgets.SWTBotTree              , [ ExpandEvent, CollapseEvent,
                                                            TreeClickEvent, TreeDoubleClickEvent ]),
                (swtbot.widgets.SWTBotExpandBar         , [ ExpandEvent, CollapseEvent ]),
                (swtbot.widgets.SWTBotList              , [ ListClickEvent ]),
                (swtbot.widgets.SWTBotCombo             , [ ComboSelectEvent,  ComboTextEvent, TextActivateEvent ]),
                (FakeSWTBotCCombo                       , [ CComboSelectEvent, CComboChangeEvent, TextActivateEvent ]),
                (FakeSWTBotTabFolder                    , [ TabSelectEvent ]),
                (FakeSWTBotCTabFolder                   , [ CTabSelectEvent, CTabCloseEvent ]),
                (swtbot.widgets.SWTBotDateTime          , [ DateTimeEvent ]),
                (swtbot.widgets.SWTBotCheckBox          , [ SelectEvent ]) ]
