
# Experimental and rather basic support for Tkinter

import guishared
import os, time, Tkinter, logging, re
from definitions import UseCaseScriptError
from gridformatter import GridFormatter

try:
    from collections import OrderedDict
except ImportError:
    from ordereddict import OrderedDict

def getWidgetOption(widget, optionName):
    try:
        return widget.cget(optionName)
    except:
        return ""

def getMenuParentOption(widget):
    parentMenuPath = widget.winfo_parent()
    parent = widget.nametowidget(parentMenuPath)
    if parent and isinstance(parent, Tkinter.Menu):
        endIndex = parent.index(Tkinter.END)
        for i in range(endIndex + 1):
            if parent.type(i) == "cascade":
                submenuName = parent.entrycget(i, "menu")
                if submenuName.endswith(widget.winfo_name()):
                    return parent, i
    return parent, -1

def getMenuParentLabel(widget, defaultLabel=""):
    parent, index = getMenuParentOption(widget)
    if index >= 0:
        return parent.entrycget(index, "label")
    elif isinstance(parent, Tkinter.Menubutton):
        return getWidgetOption(parent, "text")
    else:
        return defaultLabel


class WidgetAdapter(guishared.WidgetAdapter):
    def getChildWidgets(self):
        return filter(lambda c: not isinstance(c, Tkinter.Toplevel), self.widget.winfo_children())
        
    def getWidgetTitle(self):
        return self.widget.title()
        
    def getLabel(self):
        text = getWidgetOption(self.widget, "text")
        if text:
            return text
        elif isinstance(self.widget, Tkinter.Menu):
            return getMenuParentLabel(self.widget)
        return ""

    def isAutoGenerated(self, name):
        return re.match("[0-9]*L?$", name) or name == "tk"

    def getName(self):
        return self.widget.winfo_name()

guishared.WidgetAdapter.adapterClass = WidgetAdapter

origTk = Tkinter.Tk
origToplevel = Tkinter.Toplevel

class WindowIdleManager:
    idle_methods = []
    timeout_methods = []
    handlers = [] 
    def __init__(self):
        self.protocols = {}

    def protocol(self, protocolName, method):
        self.protocols[protocolName] = method

    def setUpHandlers(self):
        self.removeHandlers()
        self.addIdleMethods()

    def removeHandlers(self):
        for handler in self.handlers:
            self.after_cancel(handler)
        WindowIdleManager.handlers = []

    def addIdleMethods(self):
        if not self.winfo_ismapped():
            self.wait_visibility()
        for idle_method in self.idle_methods: 
            self.handlers.append(self.after(0, idle_method))
        for args in self.timeout_methods:
            self.handlers.append(self.after(*args))

    def destroy(self):
        self.event_generate("<Destroy>") # Make sure we can record whatever it was caused this to be called



class Tk(WindowIdleManager, origTk):
    orig_protocol = origTk.protocol
    def __init__(self, *args, **kw):
        WindowIdleManager.__init__(self)
        origTk.__init__(self, *args, **kw)
        # Set up the handlers from the mainloop call, don't want things to happen before we're ready as seems quite possible
        origMainLoop = self.tk.mainloop
        def mainloop(n=0):
            self.setUpHandlers()
            self.tk.mainloop(n)
        def mainloopMethod(w, n=0):
            mainloop(n)
        Tkinter.Misc.mainloop = mainloopMethod
        Tkinter.mainloop = mainloop
                
class Toplevel(WindowIdleManager, origToplevel):
    instances = []
    orig_protocol = origToplevel.protocol
    def __init__(self, *args, **kw):
        WindowIdleManager.__init__(self)
        origToplevel.__init__(self, *args, **kw)
        self.setUpHandlers()
        Toplevel.instances.append(self)

    def wait_window(self, *args, **kw):
        # Not at all clear what this method is for but it causes a hang if we manage to delete the window before it's called
        # Do nothing for now...
        pass

Tkinter.Tk = Tk
Tkinter.Toplevel = Toplevel

origMenu = Tkinter.Menu

class Menu(origMenu):
    replayActive = False
    def __init__(self, *args, **kw):
        origMenu.__init__(self, *args, **kw)
        self.commands = {}

    def add_command(self, command=None, **kw):
        origMenu.add_command(self, command=command, **kw)
        self.commands[self.index(Tkinter.END)] = command

    def post(self, *args, **kw):
        if not self.replayActive or os.name != "nt":
            # On Windows this causes a hang that can only be relieved by clicking the mouse
            # somewhere. Seems to be a bug but requests on Tkinter list didn't get anywhere.
            # For now just don't post menus for real when replaying on Windows
            origMenu.post(self, *args, **kw)
        describer = Describer()
        describer.describePopup(self)

Tkinter.Menu = Menu

origCheckbutton = Tkinter.Checkbutton

class Checkbutton(origCheckbutton):
    def __init__(self, *args, **kw):
        origCheckbutton.__init__(self, *args, **kw)
        self.variable = kw.get("variable")
        self.command = kw.get("command")

    def configure(self, *args, **kw):
        origCheckbutton.configure(self, *args, **kw)
        self.variable = kw.get("variable") or self.variable
        self.command = kw.get("command") or self.command

    def __setitem__(self, key, value):
        origCheckbutton.__setitem__(self, key, value)
        if key == "command":
            self.command = value
        elif key == "variable":
            self.variable = value

    config = configure
    internal_configure = origCheckbutton.configure

Tkinter.Checkbutton = Checkbutton

origButton = Tkinter.Button

class Button(origButton):
    def __init__(self, *args, **kw):
        origButton.__init__(self, *args, **kw)
        self.command = kw.get("command")

    def configure(self, *args, **kw):
        origButton.configure(self, *args, **kw)
        self.command = kw.get("command") or self.command

    def __setitem__(self, key, value):
        origButton.__setitem__(self, key, value)
        if key == "command":
            self.command = value

    config = configure
    internal_configure = origButton.configure

Tkinter.Button = Button


class SignalEvent(guishared.GuiEvent):
    class RecordHandler:
        def __init__(self, index, event, method):
            self.index = index
            self.event = event
            self.method = method

        def __call__(self, tkEvent, *args):
            self.event.logger.debug("Got event number " + repr(self.index))
            if self.event.recordIndex == self.index:
                self.event.handleNext(tkEvent, self.method)
                
    def __init__(self, eventName, eventDescriptor, widget, *args):
        guishared.GuiEvent.__init__(self, eventName, widget)
        self.eventDescriptors = eventDescriptor.split(",")
        self.logger = logging.getLogger("gui log")
        self.recordIndex = 0

    def connectRecord(self, method):
        for i, eventDescriptor in enumerate(self.eventDescriptors):
            handler = self.RecordHandler(i, self, method)
            self.widget.bind(eventDescriptor, handler, "+")
    
    def getChangeMethod(self):
        return self.widget.event_generate

    def handleNext(self, tkEvent, method):
        self.recordIndex += 1
        self.logger.debug("Got event number " + repr(self.recordIndex) + " for '" + self.name + "'")
        if self.recordIndex >= len(self.eventDescriptors):
            method(tkEvent, self)
            self.recordIndex = 0
    
    def generate(self, *args, **kw):
        for eventDescriptor in self.eventDescriptors:
            self.logger.debug("Generating event '" + eventDescriptor + "' on widget of type '" + self.widget.__class__.__name__ + "'")
            self.changeMethod(eventDescriptor, x=0, y=0, **kw) 

    @classmethod
    def getAssociatedSignatures(cls, widget):
        # Assume anything else just gets clicked on
        return [ "<Button-1>", "<Button-2>", "<Button-3>" ]

    
class CanvasEvent(SignalEvent):
    def outputForScript(self, tkEvent, *args):
        items = self.widget.find_closest(self.widget.canvasx(tkEvent.x), self.widget.canvasy(tkEvent.y))
        tags = self.widget.gettags(items)
        if len(tags) == 0 or (len(tags) == 1 and tags[0] == "current"):
            itemName = str(items[0])
        else:
            itemName = tags[0]
        return self.name + " " + itemName

    def generate(self, tagOrId):
        item = self.findItem(tagOrId)
        x1, y1, x2, y2 = self.widget.bbox(item)
        x = x1 + x2 / 2
        y = y1 + y2 / 2
        self.changeMethod(self.eventDescriptors[0], x=x, y=y)

    def findItem(self, tagOrId):
        # Seems obvious to use find_withtag, but that fails totally when
        # the tag looks like an integer. So we make our own...
        for item in self.widget.find_all():
            if str(item) == tagOrId or tagOrId in self.widget.gettags(item):
                return item
        raise UseCaseScriptError, "Could not find canvas item '" + tagOrId + "'"


class WindowManagerDeleteEvent(guishared.GuiEvent):
    def __init__(self, eventName, eventDescriptor, widget, *args):
        guishared.GuiEvent.__init__(self, eventName, widget)
        self.recordMethod = None
        
    @classmethod
    def getAssociatedSignal(cls, widget):
        return "WM_DELETE_WINDOW"
    
    def connectRecord(self, method):
        self.recordMethod = method
        self.widget.orig_protocol(self.getAssociatedSignal(self.widget), self.deleteWindow)

    def deleteWindow(self):
        self.recordMethod(self)
        protocolMethod = self.widget.protocols.get(self.getAssociatedSignal(self.widget))
        protocolMethod()
        
    def generate(self, *args, **kw):
        self.deleteWindow()

class ButtonEvent(SignalEvent):
    @classmethod
    def getAssociatedSignatures(cls, widget):
        return [ "<Enter>,<Button-1>,<ButtonRelease-1>" ]

    def getChangeMethod(self):
        return self.widget.invoke

    def generate(self, argumentString):
        self.changeMethod()

    def connectRecord(self, method):
        def handler():
            if self.widget.command:
                self.widget.command()
            return method(self)

        self.widget.internal_configure(command=handler)

class ToggleHandler:
    def __init__(self, widget, method, event):
        self.widget = widget
        self.method = method
        self.events = [ event ]

    def __call__(self):
        if self.widget.command:
            self.widget.command()
        for event in self.events:
            event.logger.debug("Try to record event '" + event.name + "'" + event.__class__.__name__)
            if event.shouldRecord():
                return self.method(event)
    

class ToggleEvent(ButtonEvent):
    def isStateChange(self):
        return True

    def connectRecord(self, method):
        if hasattr(self.widget, "toggleHandler"):
            self.widget.toggleHandler.events.append(self)
        else:
            self.widget.toggleHandler = ToggleHandler(self.widget, method, self)
            self.widget.internal_configure(command=self.widget.toggleHandler)

class CheckEvent(ToggleEvent):
    @classmethod
    def getAssociatedSignatures(cls, widget):
        return [ "checked" ]
    
    def shouldRecord(self, *args):
        return self.widget.variable.get()

class UncheckEvent(ToggleEvent):
    @classmethod
    def getAssociatedSignatures(cls, widget):
        return [ "unchecked" ]
        
    def shouldRecord(self, *args):
        return not self.widget.variable.get()


class EntryEvent(SignalEvent):
    @classmethod
    def getAssociatedSignatures(cls, widget):
        return [ "<KeyPress>,<KeyRelease>" ]
    
    def isStateChange(self):
        return True

    def generate(self, argumentString):
        self.widget.focus_force()
        self.widget.delete(0, Tkinter.END)
        self.widget.insert(Tkinter.END, argumentString)
        # Generate a keypress, just to trigger recording
        SignalEvent.generate(self, keysym="Right")

    def outputForScript(self, *args):
        return self.name + " " + self.widget.get()


class MenuEvent(guishared.GuiEvent):
    class CommandWithRecord:
        def __init__(self, index, event, command, method):
            self.index = index
            self.event = event
            self.command = command
            self.method = method

        def __call__(self):
            self.method(self.index, self.event)
            self.command()
                
    def __init__(self, eventName, eventDescriptor, widget, *args):
        guishared.GuiEvent.__init__(self, eventName, widget)

    @classmethod
    def getAssociatedSignatures(cls, widget):
        return [ "select item" ]

    def connectRecord(self, method):
        endIndex = self.widget.index(Tkinter.END)
        for i in range(endIndex + 1):
            if self.widget.type(i) == "command":
                command = self.widget.commands[i]
                self.widget.entryconfigure(i, command=self.CommandWithRecord(i, self, command, method))

    def getItemText(self, index):
        return self.widget.encodeToLocale(self.widget.entrycget(index, "label"))

    def outputForScript(self, index, *args):
        return self.name + " " + self.getItemText(index)

    def findIndex(self, label):
        endIndex = self.widget.index(Tkinter.END)
        for i in range(endIndex + 1):
            if self.widget.type(i) == "command" and self.getItemText(i) == label:
                return i
        raise UseCaseScriptError, "Could not find item '" + label + "' in menu."

    def getChangeMethod(self):
        return self.widget.invoke

    def generate(self, argumentString):
        index = self.findIndex(argumentString)
        self.changeMethod(index)
        try:
            self.widget.unpost()
        except: # pragma: no cover - seems to happen under rather unpredictable circumstances
            # Yes it's ugly, the menu might not have been posted in the first place
            # That seems to throw some unprintable exception: trying to examine it
            # causes the program to exit with error code
            pass

class ListboxEvent(SignalEvent):
    @classmethod
    def getAssociatedSignatures(cls, widget):
        return [ "<<ListboxSelect>>" ]

    def outputForScript(self, *args):
        indices = self.widget.curselection()
        texts = map(self.widget.get, indices)
        return self.name + " " + ",".join(texts)

    def generate(self, argumentString):
        self.widget.selection_clear(0, Tkinter.END)
        index = self.findIndex(argumentString)
        self.widget.selection_set(index)
        SignalEvent.generate(self, argumentString)

    def findIndex(self, label):
        for i in range(self.widget.size()):
            if self.widget.get(i) == label:
                return i
        raise UseCaseScriptError, "Could not find item '" + label + "' in Listbox."
        

class ScriptEngine(guishared.ScriptEngine):
    eventTypes = [
        (Tkinter.Button      , [ ButtonEvent ]),
        (Tkinter.Checkbutton , [ CheckEvent, UncheckEvent ]),
        (Tkinter.Label       , [ SignalEvent ]),
        (Tkinter.Canvas      , [ CanvasEvent ]),
        (Tkinter.Toplevel    , [ WindowManagerDeleteEvent ]),
        (Tkinter.Tk          , [ WindowManagerDeleteEvent ]),
        (Tkinter.Entry       , [ EntryEvent ]),
        (Tkinter.Listbox     , [ ListboxEvent ]),
        (Tkinter.Menu        , [ MenuEvent ])
        ]
    signalDescs = {
        "<Enter>,<Button-1>,<ButtonRelease-1>": "clicked",
        "<KeyPress>,<KeyRelease>": "edited text",
        "<Button-1>": "left-clicked",
        "<Button-2>": "middle-clicked",
        "<Button-3>": "right-clicked",
        "<<ListboxSelect>>": "select item",
        "WM_DELETE_WINDOW": "closed"
        }
    columnSignalDescs = {}
    def __init__(self, *args, **kw):
        guishared.ScriptEngine.__init__(self, *args, **kw)
        Menu.replayActive = self.replayerActive()
         
    def createReplayer(self, universalLogging=False, **kw):
        return UseCaseReplayer(self.uiMap, universalLogging, self.recorder, **kw)

    def _createSignalEvent(self, eventName, eventDescriptor, widget, argumentParseData):
        for eventClass in self.findEventClassesFor(widget):
            if eventClass is not SignalEvent and eventDescriptor in eventClass.getAssociatedSignatures(widget):
                return eventClass(eventName, eventDescriptor, widget, argumentParseData)
        return SignalEvent(eventName, eventDescriptor, widget)

    def getDescriptionInfo(self):
        return "Tkinter", "Tkinter", "actions", "http://infohost.nmt.edu/tcc/help/pubs/tkinter/"

    def addSignals(self, classes, widgetClass, currEventClasses, module):
        widget = widgetClass()
        signalNames = set()
        for eventClass in currEventClasses:
            signatures = eventClass.getAssociatedSignatures(widget)
            descs = set([ self.signalDescs.get(s, s) for s in signatures ])
            signalNames.update(descs)
        className = self.getClassName(widgetClass, module)
        classes[className] = sorted(signalNames)

    def getSupportedLogWidgets(self):
        return Describer.statelessWidgets + Describer.stateWidgets


class UseCaseReplayer(guishared.IdleHandlerUseCaseReplayer):
    def __init__(self, *args, **kw):
        guishared.IdleHandlerUseCaseReplayer.__init__(self, *args, **kw)
        self.describer = Describer()

    def makeIdleHandler(self, method):
        if Tkinter._default_root:
            return Tkinter._default_root.after(0, method)
        else:
            Tk.idle_methods.append(method)
            return True # anything to show we've got something

    def findWindowsForMonitoring(self):
        return [ Tkinter._default_root ] + Toplevel.instances

    def handleNewWindows(self):
        self.describer.describeUpdates()
        guishared.IdleHandlerUseCaseReplayer.handleNewWindows(self)

    def describeNewWindow(self, window):
        self.describer.describe(window)

    def removeHandler(self, handler):
        # Need to do this for real handlers, don't need it yet
        #Tkinter._default_root.after_cancel(handler)
        Tk.idle_methods = []

    def makeTimeoutReplayHandler(self, method, milliseconds): 
        if Tkinter._default_root:
            return Tkinter._default_root.after(milliseconds, method)
        else:
            Tk.timeout_methods.append((milliseconds, method))
            return True # anything to show we've got something

    def describeAndRun(self):
        try:
            Tkinter._default_root.update_idletasks()
        except Tkinter.TclError: # pragma: no cover - pathological and hard to reproduce
            # Seems to occasionally get called by Tkinter even after application is terminated.
            # That causes a TclError here. We should ignore it and just terminate
            return
        guishared.IdleHandlerUseCaseReplayer.describeAndRun(self)
        

class Describer(guishared.Describer):
    ignoreWidgets = [ Tkinter.Scrollbar ]
    statelessWidgets = [ Tkinter.Button, Tkinter.Menubutton, Tkinter.Frame,
                         Tkinter.LabelFrame, Tkinter.Label, Tkinter.Menu ]
    stateWidgets = [  Tkinter.Checkbutton, Tkinter.Entry, Tkinter.Text, Tkinter.Canvas,
                      Tkinter.Listbox, Tkinter.Toplevel, Tkinter.Tk ]
    visibleMethodName = "not_used"
    childrenMethodName = "winfo_children"
    def __init__(self):
        guishared.Describer.__init__(self)
        self.canvasWindows = set()
        self.defaultLabelBackground = None

    def getTkState(self, window):
        return window.title()

    def getToplevelState(self, window):
        return window.title()

    def getPackSlavesDescription(self, widget, slaves):
        sideGroups = {}
        for slave in widget.pack_slaves():
            try:
                info = slave.pack_info()
                slaves.add(slave)
                sideGroups.setdefault(self.getSide(slave, info), []).append(self.getDescription(slave))
            except Tkinter.TclError: 
                # Weirdly, sometimes get things in here that then deny they know anything about packing...
                pass
        if len(sideGroups) == 0:
            return ""
        
        menuDesc = "\n\n".join(sideGroups.get("Menus", []))
        topDesc = "\n".join(sideGroups.get(Tkinter.TOP, []))
        bottomDesc = "\n".join(list(reversed(sideGroups.get(Tkinter.BOTTOM, []))))
        horizDesc = " , ".join(sideGroups.get(Tkinter.LEFT, []) + list(reversed(sideGroups.get(Tkinter.RIGHT, []))))
        desc = self.addToDescription(topDesc, menuDesc)
        desc = self.addToDescription(desc, horizDesc)
        return self.addToDescription(desc, bottomDesc)

    def getSide(self, slave, info):
        # Always show menu buttons vertically as their menus invariably take vertical space
        if isinstance(slave, Tkinter.Menubutton):
            return "Menus"
        else:
            return info.get("side")

    def getGridSlavesDescription(self, widget, slaves, children):
        row_count = widget.grid_size()[-1]
        grid = []
        if row_count == 0:
            return ""
        for x in range(row_count):
            rowSlaves = filter(lambda w: w in children and w not in slaves, widget.grid_slaves(row=x))
            rowSlaves.reverse()
            slaves.update(rowSlaves)
            allDescs = [ self.getDescription(w) for w in rowSlaves ] or [ "" ]
            grid.append(allDescs)
        column_count = max((len(row) for row in grid))
        # More conventional str() fails on unicode!
        return GridFormatter(grid, column_count).__str__()

    def isPopupMenu(self, child, parent):
        return isinstance(child, Tkinter.Menu) and not isinstance(parent, (Tkinter.Menu, Tkinter.Menubutton))

    def _getChildrenDescription(self, widget):
        slaves = set()
        children = widget.winfo_children()
        desc = ""
        menuChildName = getWidgetOption(widget, "menu")
        if menuChildName:
            menuChild = widget.nametowidget(menuChildName)
            slaves.add(menuChild)
            desc = self.addToDescription(desc, self.getDescription(menuChild))
        gridDesc = self.getGridSlavesDescription(widget, slaves, children)
        packDesc = self.getPackSlavesDescription(widget, slaves)
        childDesc = ""
        for child in children:
            if child not in slaves and not isinstance(child, Tkinter.Toplevel) and \
                   not self.isPopupMenu(child, widget) and not child in self.canvasWindows:
                childDesc = self.addToDescription(childDesc, self.getDescription(child))
        
        desc = self.addToDescription(desc, packDesc)
        desc = self.addToDescription(desc, gridDesc)
        desc = self.addToDescription(desc, childDesc)
        return desc.rstrip()

    def getDefaultLabelBackground(self, widget):
        if self.defaultLabelBackground is None:
            self.defaultLabelBackground = Tkinter.Label(widget.master).cget("bg")
        return self.defaultLabelBackground

    def getWindowClasses(self):
        return Tkinter.Tk, Tkinter.Toplevel

    def getTextEntryClass(self):
        return Tkinter.Entry

    def getUpdatePrefix(self, widget, oldState, state):
        if isinstance(widget, Tkinter.Checkbutton):
            if "(checked)" in oldState == "(checked)" in state:
                return "Changed state of "
            else:
                return "Toggled "
        elif isinstance(widget, Tkinter.Button):
            return "Changed state of "
        else:
            return guishared.Describer.getUpdatePrefix(self, widget, oldState, state)
    
    def getState(self, widget):
        state = self.getSpecificState(widget)
        sensitivity = self.getSensitivityState(widget)
        if sensitivity:
            state = state.rstrip() + " " + sensitivity
        return state.strip()

    def getSensitivityState(self, widget):
        state = getWidgetOption(widget, "state")
        if state == Tkinter.DISABLED:
            return "(greyed out)"
        else:
            return ""
        
    def getFrameDescription(self, widget):
        if getWidgetOption(widget, "bd"):
            return ".................."
        else:
            return ""

    def getMenubuttonDescription(self, widget):
        if len(widget.winfo_children()) == 0:
            return getWidgetOption(widget, "text") + " menu (empty)"
        else:
            return ""

    def getLabelFrameDescription(self, widget):
        return "....." + getWidgetOption(widget, "text") + "......"

    def getButtonDescription(self, widget):
        text = "Button"
        labelText = getWidgetOption(widget, "text")
        if labelText:
            text += " '" + labelText + "'"
        state = self.getState(widget)
        self.widgetsWithState[widget] = state
        if state:
            text += " " + state
        return text

    def getCheckbuttonState(self, widget):
        if widget.variable.get():
            return "(checked)"
        else:
            return ""

    def getCheckbuttonDescription(self, widget):
        return "Check " + self.getButtonDescription(widget)

    def getLabelDescription(self, widget):
        text = "'" + getWidgetOption(widget, "text") + "'"
        bg = getWidgetOption(widget, "bg")
        if bg and bg != self.getDefaultLabelBackground(widget):
            text += " (" + bg + ")"
        return text

    def getEntryDescription(self, widget):
        text = "Text entry"
        state = self.getState(widget)
        self.widgetsWithState[widget] = state
        if state:
            text += " (set to '" + state + "')"
        return text

    def getEntryState(self, widget):
        text = widget.get()
        showChar = getWidgetOption(widget, "show")
        if showChar:
            return showChar * len(text)
        else:
            return text

    def getMenuDescription(self, widget, rootDesc="Root"):
        endIndex = widget.index(Tkinter.END)
        text = getMenuParentLabel(widget, rootDesc)
        text += " menu:\n"
        for i in range(endIndex + 1):
            text += "  " + self.getMenuItemDescription(widget, i) + "\n"
        return text

    def describePopup(self, menu):
        desc = ""
        desc = self.addToDescription(desc, self.getMenuDescription(menu, rootDesc="Posting popup"))
        desc = self.addToDescription(desc, self.getChildrenDescription(menu)) # submenus
        self.logger.info(desc.rstrip())

    def getListboxState(self, widget):
        text = ""
        if getWidgetOption(widget, "bd"):
            text += ".................\n"
        for i in range(widget.size()):
            text += "-> " + widget.get(i) + "\n"
        if getWidgetOption(widget, "bd"):
            text += ".................\n"
        return text

    def getListboxDescription(self, widget):
        state = self.getState(widget)
        self.widgetsWithState[widget] = state
        return state

    def getTextDescription(self, widget):
        state = self.getState(widget)
        self.widgetsWithState[widget] = state
        return self.headerAndFooter(state, "Text")

    def getTextState(self, widget):
        return widget.get("1.0", Tkinter.END).rstrip()

    def headerAndFooter(self, text, title):
        header = "=" * 10 + " " + title + " " + "=" * 10
        return header + "\n" + text.rstrip() + "\n" + "=" * len(header)

    def getMenuItemDescription(self, widget, index):
        typeName = widget.type(index)
        if typeName in [ "cascade", "command" ]:
            text = widget.entrycget(index, "label")
            if typeName == "cascade":
                text += " (+)"
            return text
        elif typeName == "separator":
            return "---"
        else:
            return ">>>"

    def getCanvasDescription(self, widget):
        self.widgetsWithState[widget] = self.getCanvasState(widget, includeWindows=False)
        desc = self.getCanvasState(widget, includeWindows=True)
        return self.headerAndFooter(desc, "Canvas")

    def getCanvasState(self, widget, includeWindows=False):
        items = set()
        allDescs = {}
        for item in widget.find_all():
            if item not in items:
                desc = self.getCanvasItemDescription(widget, item, includeWindows)
                allDescs.setdefault(self.getRow(widget, item, allDescs.keys()), []).append(desc)
                for enclosedItem in self.findEnclosedItems(widget, item):
                    items.add(enclosedItem)
                    desc = "  " + self.getCanvasItemDescription(widget, enclosedItem, includeWindows)
                    allDescs.setdefault(self.getRow(widget, enclosedItem, allDescs.keys()), []).append(desc)
        return self.arrange(allDescs)

    def getRow(self, widget, item, existingRows):
        x1, y1, x2, y2 = widget.bbox(item)
        for attempt in [ y1, y1 - 1, y1 + 1 ]:
            if attempt in existingRows:
                return attempt
        return y1

    def getCanvasItemDescription(self, widget, item, includeWindows):
        itemType = widget.type(item)
        if itemType in ("rectangle", "oval", "polygon"):
            return itemType.capitalize() + " (" + widget.itemcget(item, "fill") + ")"
        elif itemType == "text":
            return "'" + widget.itemcget(item, "text") + "'"
        elif itemType == "window":
            if includeWindows:
                windowWidgetName = widget.itemcget(item, "window")
                windowWidget = widget.nametowidget(windowWidgetName)
                self.canvasWindows.add(windowWidget) # Stop it being described by other means
                return self.getDescription(windowWidget)
            else:
                return ""
        else: # pragma: no cover - not really supposed to happen
            return "A Canvas Item of type '" + itemType + "'"

    def findEnclosedItems(self, widget, item):
        bbox = widget.bbox(item)
        allItems = list(widget.find_enclosed(*bbox))
        if item in allItems:
            allItems.remove(item)
        return allItems

    def padColumns(self, allDescs):
        widths = self.getColumnWidths(allDescs)
        for descList in allDescs.values():
            for col, desc in enumerate(descList):
                if len(desc) < widths[col]:
                    descList[col] = desc.ljust(widths[col])

    def getColumnWidths(self, allDescs):
        widths = []
        for descList in allDescs.values():
            for col, desc in enumerate(descList):
                if col >= len(widths):
                    widths.append(len(desc))
                elif len(desc) >= widths[col]:
                    widths[col] = len(desc)
        return widths

    def arrange(self, allDescs):
        self.padColumns(allDescs)
        text = ""
        for row in sorted(allDescs.keys()):
            text += " , ".join(allDescs[row]) + "\n"
        return text.strip()
            
