"""
Edit Boxes, Combobox, etc
"""
#  Copyright (C) 2004  Henning Jacobs <henning@srcco.de>
#
#  This program is free software; you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation; either version 2 of the License, or
#  (at your option) any later version.
#
#  $Id: InputWidgets.py 92 2004-11-28 15:34:44Z henning $

import sys, os
from Tkinter import *
import Pmw
import debug
import IconImages
import ToolTip

class ArrowButton(Canvas):
    def __init__(self, master, direction='down', command=None, **kws):
        kws.setdefault('width', 16)
        kws.setdefault('height', 16)
        kws.setdefault('borderwidth', 2)
        Canvas.__init__(self, master,
            relief=RAISED,
            highlightthickness=0,
            **kws)
        self._disabled = False
        self.direction = direction
        self.command = command
        self.bind('<ButtonPress-1>', self._btnPress)
        self.bind('<ButtonRelease-1>', self._btnRelease)
        self._drawArrow()

    _arrowRelief = RAISED
    def _drawArrow(self, sunken=0):
        if sunken:
            self._arrowRelief = self.cget('relief')
            self.configure(relief = SUNKEN)
        else:
            self.configure(relief = self._arrowRelief)

        if self._disabled:
            color = '#aaaaaa'
        else:
            color = '#000000'
        Pmw.drawarrow(self, color, self.direction, 'arrow')

    def _btnPress(self, event):
        if not self._disabled:
            self._drawArrow(sunken=1)
        
    def _btnRelease(self, event):
        if not self._disabled:
            if self.command: self.command()
            self._drawArrow()

    def setdirection(self, dir):
        self.direction = dir
        self._drawArrow()

    def configure(self, **kws):
        if kws.has_key('state'):
            self._disabled = kws['state'] == 'disabled'
            self._drawArrow()
        Canvas.configure(self, **kws)

class AbstractSingleVarEdit:
    def __init__(self):
        self._state = NORMAL
        self._var = None
        self._save_hooks = []
    # Descendants should implement the following:    
    #def get(self):
    #    pass
    #def clear(self):
    #    pass
    #def set(self):
    #    pass
    def save(self, event=None):
        if self._state != DISABLED and self._var is not None:
            if self._var.get() != self.get():
                self._var.set(self.get())
                for hook in self._save_hooks:
                    hook()
    def add_save_hook(self, save_hook):
        self._save_hooks.append(save_hook)
    def bindto(self, var):
        self.save()
        self._var = None
        self.clear()
        if var is not None:
            self._var = var
            self.set(var.get())

class LabelPseudoEdit(AbstractSingleVarEdit, Label):
    "Fake Edit Widget, polymorphic to TextEdit and Pmw.EntryField"
    def __init__(self, master, **kws):
        AbstractSingleVarEdit.__init__(self)
        kws.setdefault('width', 16)
        Label.__init__(self, master, **kws)
    def set(self, val):
        self.configure(text=val)
    setentry = set
    def get(self):
        return self.cget('text')
    getvalue = get
    def clear(self):
        self.set('')
    def component(self, name):
        return self
    def cget(self, opt):
        return Label.cget(self, opt.split('_')[-1])
        
class TextEdit(AbstractSingleVarEdit, Pmw.EntryField):
    def __init__(self, master, **kws):
        AbstractSingleVarEdit.__init__(self)
        kws.setdefault('entry_width', 16)
        kws.setdefault('modifiedcommand', self.save)
        Pmw.EntryField.__init__(self, master, 
            **kws)
    def toolTipMaster(self):
        "Returns real Tk widget for use by ToolTip"
        return self.component('entry')
    def clear(self):
        self.delete(0, END)
    def set(self, text):
        self.delete(0, END)
        self.insert(END, text)
        self.save()
    def disable(self):
        self._state = DISABLED
        self.clear()
        self.configure(entry_state = DISABLED)
    def enable(self):
        self._state = NORMAL
        self.configure(entry_state = NORMAL)
    def focus_set(self):
        self.component('entry').focus_set()
        
class CheckboxEdit(AbstractSingleVarEdit, Pmw.RadioSelect):
    def __init__(self, master, **kws):
        AbstractSingleVarEdit.__init__(self)
        kws.setdefault('buttontype', 'checkbutton')
        kws.setdefault('command', self.saveCallback)
        Pmw.RadioSelect.__init__(self, master, 
            **kws)
        self.add('yes')
    def saveCallback(self, nothing=None, nothing2=None): 
        self.save()
    def toolTipMaster(self):
        "Returns real Tk widget for use by ToolTip"
        return self.component('yes')
    def clear(self):
        self.setvalue([])
    def get(self):
        if 'yes' in self.getvalue(): return 'yes'
        return 'no'
    def set(self, text):
        if text == 'yes':
            self.setvalue(['yes'])
        else:
            self.setvalue([])
        self.save()
    def disable(self):
        self._state = DISABLED
        self.clear()
    def enable(self):
        self._state = NORMAL
    def focus_set(self):
        self.component('yes').focus_set()

class TextComboEdit(AbstractSingleVarEdit, Pmw.ComboBox): 
    def __init__(self, master, **kws):
        AbstractSingleVarEdit.__init__(self)
        if sys.platform != 'win32':
            # Thin Scrollbars:
            kws['scrolledlist_horizscrollbar_borderwidth'] = 1
            kws['scrolledlist_horizscrollbar_width'] = 10
            kws['scrolledlist_vertscrollbar_borderwidth'] = 1
            kws['scrolledlist_vertscrollbar_width'] = 10
        self.nomanualedit = False    
        if kws.has_key('nomanualedit'):
            self.nomanualedit = kws['nomanualedit']
            del kws['nomanualedit']    
        modifiedcommand = self.save
        if kws.has_key('modifiedcommand'):
            modifiedcommand = kws['modifiedcommand']
            del kws['modifiedcommand']
        if self.nomanualedit:
            kws['entryfield_pyclass'] = LabelPseudoEdit
            kws['entryfield_relief'] = SUNKEN
            kws['entryfield_borderwidth'] = 1
            kws['selectioncommand'] = modifiedcommand
        else:
            kws['entryfield_modifiedcommand'] = modifiedcommand
        Pmw.ComboBox.__init__(self, master, 
            scrolledlist_scrollmargin=0,
            history=0,
            unique=0,
            buttonaspect=0.75,
            hull_borderwidth=0, 
            arrowbutton_highlightthickness=0,
            **kws)
    def toolTipMaster(self):
        "Returns real Tk widget for use by ToolTip"
        return self.component('entry')
    def clear(self):
        self.component("entryfield").clear()
    def get(self):
        return self.component("entryfield").getvalue() 
    def set(self, val):
        self.setentry(val)
    def setlist(self, items):
        self.component("scrolledlist").setlist(items)
    def getlist(self):
        return self.component("scrolledlist").get()

class FilenameEdit(AbstractSingleVarEdit, Frame):
    def __init__(self, master, **kws):
        AbstractSingleVarEdit.__init__(self)
        Frame.__init__(self, master, borderwidth=0)
        self.type = kws.get('type', 'open')
        self.filetypes = kws.get('filetypes', [("All Files", "*")])
        self.showbasenameonly = kws.get('showbasenameonly', False)
        self.columnconfigure(0, weight=1)
        if self.showbasenameonly:
            self.edtFilename = LabelPseudoEdit(self, borderwidth=1,
                relief=SUNKEN)
            self._fullpath = ''    
        else:    
            self.edtFilename = TextEdit(self)
        self.edtFilename.grid(sticky=W+E)
        self.btnBrowse = Button(self, text='...', command=self._browse,
            padx=0, pady=0)
        ToolTip.ToolTip(self.btnBrowse, 'browse files')    
        self.btnBrowse.grid(row=0, column=1)
    _browsedlg = None   
    def _browse(self):
        if not self._browsedlg:
            dir, fname = os.path.split(self.get())
            import tkFileDialog
            if self.type == 'saveas':
                self._browsedlg = tkFileDialog.SaveAs(filetypes = self.filetypes,
                    initialfile=fname, initialdir=dir)
            else:
                self._browsedlg = tkFileDialog.Open(filetypes = self.filetypes,
                    initialfile=fname, initialdir=dir)
        ret = self._browsedlg.show()
        if ret: self.set(ret)
    def toolTipMaster(self):
        "Returns real Tk widget for use by ToolTip"
        return self.edtFilename.toolTipMaster()
    def clear(self):
        self.edtFilename.clear()
        self._fullpath = ''
    def bindto(self, var):
        if self.showbasenameonly:
            AbstractSingleVarEdit.bindto(self, var)
        else:
            self.edtFilename.bindto(var)
    def set(self, val):
        if self.showbasenameonly:
            self._fullpath = val
            self.edtFilename.set(os.path.basename(val))
            self.save()
        else:    
            self.edtFilename.set(val)
    def get(self):
        if self.showbasenameonly:
            return self._fullpath
        else:    
            return self.edtFilename.get()
            
class FontEdit(AbstractSingleVarEdit, Frame):
    def __init__(self, master, **kws):
        AbstractSingleVarEdit.__init__(self)
        Frame.__init__(self, master, borderwidth=0)
        self.columnconfigure(0, weight=1)
        self.edtFontFamily = TextComboEdit(self, nomanualedit=True,
            modifiedcommand=self.save)
        import tkFont
        fontfamilies = list(tkFont.families())
        fontfamilies.sort()
        self.edtFontFamily.setlist(fontfamilies)
        self.edtFontFamily.grid(rowspan=2, sticky=W+E)
        self.edtFontSize = Pmw.EntryField(self,
            validate={'validator':'integer', 'min':1, 'max':72},
            entry_width=3,
            entry_justify=RIGHT,
            modifiedcommand=self.save)
        self.edtFontSize.grid(row=0, rowspan=2, column=1)
        btn = ArrowButton(self, direction='up', borderwidth=1, width=8, height=8,
            command=self._sizeIncr)
        btn.grid(row=0, column=2)
        btn = ArrowButton(self, direction='down', borderwidth=1, width=8, height=8,
            command=self._sizeDecr)
        btn.grid(row=1, column=2)
        self.edtFontSizeUnit = TextComboEdit(self, nomanualedit=True,
            modifiedcommand=self.save, entryfield_width=2)
        self.edtFontSizeUnit.setlist(['pt','px'])
        self.edtFontSizeUnit.grid(row=0, rowspan=2, column=3)
        self.edtFontModifiers = TextComboEdit(self, nomanualedit=True,
            modifiedcommand=self.save, entryfield_width=8)
        self.edtFontModifiers.setlist(['normal', 'bold', 'italic', 'bold italic'])    
        self.edtFontModifiers.grid(row=0, rowspan=2, column=4)
    def _sizeIncr(self):
        text = self.edtFontSize.getvalue()
        if text != '': 
            size = int(text); size += 1
        else: size = 10    
        self.edtFontSize.setvalue(size)
    def _sizeDecr(self):
        text = self.edtFontSize.getvalue()
        if text != '': 
            size = int(text); size += -1
        else: size = 10    
        self.edtFontSize.setvalue(size)
    def clear(self):
        pass
    def set(self, val):
        try: 
            family, size, mod = val
        except:
            debug.echo('FontEdit.set(): Illegal font specification.')
            family, size, mod = ('', '', '')
        self.edtFontFamily.set(family)
        if size[:1] == '-':
            size = size[1:]
            self.edtFontSizeUnit.set('px')
        else:    
            self.edtFontSizeUnit.set('pt')
        self.edtFontSize.setvalue(size)
        if mod == '': mod = 'normal'
        self.edtFontModifiers.set(mod)
    def get(self):
        size = self.edtFontSize.getvalue()
        if self.edtFontSizeUnit.get() == 'px':
            size = '-'+size
        mod = self.edtFontModifiers.get()
        if mod == 'normal': mod = ''
        return (self.edtFontFamily.get(), size, mod)

class MemoEdit(AbstractSingleVarEdit, Pmw.ScrolledText):
    def __init__(self, master, **kws):
        AbstractSingleVarEdit.__init__(self)
        kws.setdefault('text_width', 20)
        kws.setdefault('text_height', 4)
        kws.setdefault('text_wrap', WORD)
        # File extension for temp-File to open in 
        # external editor:
        kws.setdefault('file_extension', '.txt')
        self._file_extension = kws['file_extension']
        del kws['file_extension']    
        if sys.platform != 'win32':
            # Thin Scrollbars:
            kws['horizscrollbar_borderwidth'] = 1
            kws['horizscrollbar_width'] = 10
            kws['vertscrollbar_borderwidth'] = 1
            kws['vertscrollbar_width'] = 10
        Pmw.ScrolledText.__init__(self, master,
            scrollmargin=0,
            **kws)
        self.component('text').bind("<FocusOut>", self.save)
        self.component('text').bind("<Return>", self.save)
        self.component('text').bind('<Control-KeyPress-e>', self.editInExternalEditor)
    def toolTipMaster(self):
        "Returns real Tk widget for use by ToolTip"
        return self.component('text')
    def get(self):
        return Pmw.ScrolledText.get(self, "1.0", END)[:-1]
    def clear(self):
        self.delete("1.0", END)
    def set(self, text):
        self.clear()
        self.insert(END, text, 'default')
    def editInExternalEditor(self, event=None):
        import tempfile
        fhandle, fname = tempfile.mkstemp(self._file_extension)
        os.write(fhandle, self.get().encode('utf-8', 'replace'))
        os.close(fhandle)
        try:
            # open external editor
            os.spawnlp(os.P_WAIT, 'gvim', 'gvim', '-f', fname)
            # read modified temporary file
            fileobj = open(fname, 'rb')
            self.set(unicode(fileobj.read(), 'utf-8', 'replace'))
            fileobj.close()
        finally:
            # finally delete the temporary file
            os.unlink(fname)

class DateEdit(AbstractSingleVarEdit, Pmw.Counter): 
    def __init__(self, master, **kws):
        AbstractSingleVarEdit.__init__(self)
        if kws.has_key('label_text'): kws['frame_borderwidth']=0
        Pmw.Counter.__init__(self, master, 
            entry_width = 10,
            entry_justify = LEFT,
            buttonaspect=0.5,
            datatype={'counter':'date',
                'separator':'-',
                'format':'ymd',
                'yyyy':1},
            entryfield_value = "0000-00-00",
            entryfield_validate = {'validator' : 'date',
                'min' : '1700-01-01', 'max' : '2100-12-31',
                'minstrict' : 0, 'maxstrict' : 0,
                'format' : 'ymd', 'separator' : '-'},
            entryfield_modifiedcommand=self.save,
            hull_borderwidth=0, 
            #downarrow_borderwidth=1,
            downarrow_highlightthickness=0,
            #uparrow_borderwidth=1,
            uparrow_highlightthickness=0,
            **kws)
    def toolTipMaster(self):
        "Returns real Tk widget for use by ToolTip"
        return self.component('entry')
    def get(self):
        return self.getvalue() # inherited from Pmw.Counter / Pmw.EntryField
    def set(self, val):
        self.setvalue(val)
        
class TimeEdit(AbstractSingleVarEdit, Pmw.Counter): 
    def __init__(self, master, **kws):
        AbstractSingleVarEdit.__init__(self)
        if kws.has_key('label_text'): kws['frame_borderwidth']=0
        Pmw.Counter.__init__(self, master, 
            entry_width=8,
            entry_justify = LEFT,
            buttonaspect=0.5,
            datatype={'counter':'time',
                'separator':':',
                'time24':1},
            entryfield_value = "00:00:00",
            entryfield_validate = {'validator' : 'time',
                'separator' : ':'},
            entryfield_modifiedcommand=self.save,
            hull_borderwidth=0, 
            #downarrow_borderwidth=1,
            downarrow_highlightthickness=0,
            #uparrow_borderwidth=1,
            uparrow_highlightthickness=0,
            **kws)
    def toolTipMaster(self):
        "Returns real Tk widget for use by ToolTip"
        return self.component('entry')
    def get(self):
        return self.getvalue() # inherited from Pmw.Counter / Pmw.EntryField 
    def set(self, val):
        self.setvalue(val)

class MultiSelectButtons(Frame):
    from Set import Set
    def __init__(self, master, buttondefs, icons, iconsgrey):
        self.__state = NORMAL
        self.__var = None
        self._save_hooks = []
        self.__set = self.Set()
        self.__buttondefs = buttondefs
        self.__icons = icons
        self.__iconsgrey = iconsgrey
        self.__buttons = {}
        Frame.__init__(self, master)
        self.__createWidgets()
    def __createWidgets(self):
        import ToolTip
        for name, descr in self.__buttondefs:
            def command(self=self, name=name):
                if name in self.__set: 
                    self.__set.remove(name)
                    self.save()
                else:
                    self.__set.add(name)
                    self.save()
                self.__update() 
            self.__buttons[name] = btn = Button(self,
                image=self.__iconsgrey[name],
                command=command,
                relief=FLAT)
            btn.pack(side=LEFT, fill=X, expand=TRUE)
            tooltip = ToolTip.ToolTip(btn, descr)
            def MouseEnter(event, self=self, name=name):
                if not name in self.__set:
                    self.__buttons[name]["image"] = self.__icons[name]
            def MouseLeave(event, self=self, name=name):
                if not name in self.__set:
                    self.__buttons[name]["image"] = self.__iconsgrey[name]
            btn.bind("<Enter>", MouseEnter, add="+")
            btn.bind("<Leave>", MouseLeave, add="+")
    def __update(self):
        for name, btn in zip(self.__buttons.keys(), self.__buttons.values()):
            if name in self.__set:
                btn["relief"] = SUNKEN
                btn["image"] = self.__icons[name]
            else:
                btn["relief"] = FLAT
                btn["image"] = self.__iconsgrey[name]
    def save(self, event=None):
        if self.__state != DISABLED and self.__var is not None:
            self.__var.assign(self.__set)
            for hook in self._save_hooks:
                hook()
    def add_save_hook(self, save_hook):
        self._save_hooks.append(save_hook)
    def bindto(self, var):
        if var:
            self.__var = var
            self.__set.assign(var)
        else:
            self.__set.assign(self.Set())
            self.__var = None
        self.__update()
    def disable(self):
        self.__state = DISABLED
        for btn in self.__buttons.values():
            btn["state"] = DISABLED
        self.__set.assign(self.Set())
        self.__update()
    def enable(self):
        self.__state = NORMAL
        for btn in self.__buttons.values():
            btn["state"] = NORMAL
                
class MultiRecordEdit(Pmw.Group):
    "Edit Control e.g. for vCard.adr/tel/email"
    def __init__(self, master, recclass, title, rectitle=None):
        self.state = NORMAL
        self.__records = []
        self.__recidx = 0
        self.__recclass = recclass
        if not rectitle: rectitle = title
        self.__rectitle = rectitle
        Pmw.Group.__init__(self, master, tag_text = title)
        self.__createWidgets()
    def __createWidgets(self):
        master = self.interior()

        topbar = Frame(master)
        topbar.pack(side=TOP, fill=X)

        self.body = Frame(master)
        self.body.pack(side=BOTTOM, fill=BOTH, expand=1)

        topbar.columnconfigure(0, weight=1)
        
        self.__selRecord = Pmw.OptionMenu(topbar, items=[],
            command=self.__SelectRecordCallback, menubutton_padx=2, menubutton_pady=1)
        self.__selRecord.grid(row=0, column=0, sticky=W+E)

        self.__lblRecordCount = Label(topbar, text="(0)")
        self.__lblRecordCount.grid(row=0, column=1)

        self.__btnAddRecord = Button(topbar, image=IconImages.IconImages["new"], command=self._AddRecord)
        self.__btnAddRecord.grid(row=0, column=2)
        
        self.__btnDelRecord = Button(topbar, image=IconImages.IconImages["trash"], command=self._DelRecord)
        self.__btnDelRecord.grid(row=0, column=3)

        self.createBody()
        self.disableAll()
    def createBody(self):
        """Create Edit Widgets in self.body for selected Record
        To Be overwritten by SubClass"""
        raise NotImplementedError

    def bodyChildren(self):         
        """return list of widgets created in createBody
        To Be overwritten by SubClass"""
        return []

    def disableAll(self):
        for x in self.bodyChildren():
            try:
                x.disable()
            except:
                pass
        self.__btnDelRecord["state"] = DISABLED
        self.state = DISABLED
    def enableAll(self):    
        for x in self.bodyChildren():
            try:
                x.enable()
            except:
                pass
        self.__btnDelRecord["state"] = NORMAL
        self.state = NORMAL
    def __updateMenu(self):
        items = []
        for x in range(1, len(self.__records)+1):
            items.append(self.__rectitle + " %d" % x)
        self.__selRecord.setitems(items)
        self.__lblRecordCount["text"] = "(%d)" % len(items)
    def __SelectRecordCallback(self, str):
        self.__recidx = int(str.split()[1])-1
        self.bindtorec(self.__records[self.__recidx])
    def __Select(self, idx):
        self.__selRecord.invoke(idx)
    onRecordAdd = None  
    def _AddRecord(self):
        self.__records.append(self.__recclass())
        self.__updateMenu()
        self.__Select(len(self.__records)-1)
        self.enableAll()
        # Callback can be set by SubClass:
        if self.onRecordAdd:
            self.onRecordAdd(self.__records[-1])
    onRecordDel = None      
    def _DelRecord(self):
        del self.__records[self.__recidx]
        self.__updateMenu()
        if self.__records == []:
            self.disableAll()
        else:
            self.__Select(0)
        # Callback can be set by SubClass:
        if self.onRecordDel:
            self.onRecordDel()
    def bindto(self, records):
        if self.__records:
            # Save Old Binding (Rebind old):
            self.bindtorec(self.__records[self.__recidx])
        if records is None:
            self.__records = []
            self.__updateMenu()
            self.disableAll()
            self.bindtorec(None)
        else:
            self.__records = records
            self.__updateMenu()
            if len(self.__records)>0:
                self.enableAll()
                prefidx = 0
                # Find the preferred record:
                for i in range(len(self.__records)):
                    if self.__records[i].is_pref():
                        prefidx = i
                        break
                self.__Select(prefidx)          
            else:
                self.disableAll()
                self.bindtorec(None)
    def bindtorec(self, rec):
        "To be overridden by SubClass"
        pass

class UnicodeVar(Variable):
    """Value holder for unicode string variables."""
    _default = ""
    def __init__(self, master=None):
        """Construct a string variable.

        MASTER can be given as master widget."""
        Variable.__init__(self, master)

    def get(self):
        """Return value of variable as unicode string."""
        # Tk uses UTF8 => convert to Python Unicode String
        #return unicode(self._tk.globalgetvar(self._name), 'utf8')
        # At least in Python 2.3 this conversion is not needed:
        return self._tk.globalgetvar(self._name)

class SearchableCombobox(Frame):
    def __init__(self, master, command, label_text=""):
        self.do_not_execute = 0
        self._isPosted = 0
        self.buttonaspect = 0.75
        # list of (dispname, value, idx) tuples:
        self.list = []
        # current index of self.list:
        self.curidx = 0
        # list of (dispname, value, idx) tuples:
        self.__droplist = []
        self.command = command
        Frame.__init__(self, master)
        self.columnconfigure(1, weight=1)
        
        self.label = label = Label(self, text=label_text)
        label.grid()
        
        self.entryvar = UnicodeVar()
        self.entryvar.trace("w", self.__entrychanged)
        self.entry = entry = Entry(self, textvariable=self.entryvar)
        self.normalBackground = self.entry['background']
        entry.grid(column=1, row=0, sticky=W+E)
        
        self.button = button = Canvas(self, borderwidth=2,
                        relief=RAISED, width=16, height=16,
                        highlightthickness=0)
        button.grid(column=2, row=0)
        self._arrowRelief = self.button.cget('relief')
                        
        self.popup = popup = Toplevel(self, borderwidth=1)
        popup.withdraw()
        popup.overrideredirect(1)

        kws = {}
        if sys.platform != 'win32':
            # Thin Scrollbars:
            kws['horizscrollbar_borderwidth'] = 1
            kws['horizscrollbar_width'] = 10
            kws['vertscrollbar_borderwidth'] = 1
            kws['vertscrollbar_width'] = 10
        self.scrolledlist = scrolledlist = Pmw.ScrolledListBox(popup,
            scrollmargin=0,
            hull_height=240, usehullsize=1,
            listbox_highlightthickness=0,
            **kws)

        scrolledlist.pack(fill=BOTH, expand=TRUE)
        self.listbox = listbox = scrolledlist.component("listbox")

        # Bind events to the arrow button.
        button.bind('<1>', self.invoke)
        button.bind('<Configure>', self._drawArrow)
        button.bind('<3>', self._next)
        button.bind('<Shift-3>', self._previous)
        button.bind('<Down>', self._next)
        button.bind('<Up>', self._previous)
        button.bind('<Shift-Down>', self.invoke)
        button.bind('<Shift-Up>', self.invoke)
        button.bind('<F34>', self.invoke)
        button.bind('<F28>', self.invoke)
        button.bind('<space>', self.invoke)

         # Bind events to the dropdown window.
        popup.bind('<Escape>', self._unpostList)
        popup.bind('<space>', self._selectUnpost)
        popup.bind('<Return>', self._selectUnpost)
        popup.bind('<ButtonRelease-1>', self._dropdownBtnRelease)
        popup.bind('<ButtonPress-1>', self._unpostOnNextRelease)

        # Bind events to the Tk listbox.
        listbox.bind('<Enter>', self._unpostOnNextRelease)

        # Bind events to the Tk entry widget.
        entry.bind('<1>', self._unpostList)
        entry.bind('<FocusIn>', self._entryfocusin)
        entry.bind('<Configure>', self._resizeArrow)
        entry.bind('<Shift-Down>', self.invoke)
        entry.bind('<Shift-Up>', self.invoke)
        entry.bind('<F34>', self.invoke)
        entry.bind('<F28>', self.invoke)
        entry.bind('<Down>', self._next)
        entry.bind('<Up>', self._previous)
        
        # Need to unpost the popup if the entryfield is unmapped (eg: 
        # its toplevel window is withdrawn) while the popup list is
        # displayed.
        entry.bind('<Unmap>', self._unpostList)
    def _next(self, event):
        if self._isPosted:
            self.listbox.focus_set()
            # Select (blue bg) first item:
            self.listbox.select_set(0)
            # Activate (underline) first item:
            self.listbox.activate(0)
        else:
            self.__select(self.curidx +1 )
            self.entry.select_range(0, END)

    def _previous(self, event):
        self.__select(self.curidx -1 )
        self.entry.select_range(0, END)

    def _entryfocusin(self, event):
        self.entry.select_range(0, END)
    
    def _drawArrow(self, event=None, sunken=0):
        arrow = self.button
        if sunken:
            self._arrowRelief = arrow.cget('relief')
            arrow.configure(relief = 'sunken')
        else:
            arrow.configure(relief = self._arrowRelief)

        direction = 'down'
        Pmw.drawarrow(arrow, self.entry['foreground'], direction, 'arrow')

    def invoke(self, event=None):
        self.__setdroplist()
        self._postList()
    
    def _postList(self, event = None):
        self._isPosted = 1
        self._drawArrow(sunken=1)

        # Make sure that the arrow is displayed sunken.
        self.update_idletasks()

        x = self.entry.winfo_rootx()
        y = self.entry.winfo_rooty() + \
            self.entry.winfo_height()
        w = self.entry.winfo_width() + self.button.winfo_width()
        h =  self.listbox.winfo_height()
        sh = self.winfo_screenheight()

        if y + h > sh and y > sh / 2:
            y = self.entry.winfo_rooty() - h

        self.scrolledlist.configure(hull_width=w)

        Pmw.setgeometryanddeiconify(self.popup, '+%d+%d' % (x, y))

        # Grab the popup, so that all events are delivered to it, and
        # set focus to the listbox, to make keyboard navigation
        # easier.
        #Pmw.pushgrab(self.popup, 1, self._unpostList)
        #self.listbox.focus_set()

        self._drawArrow()

        # Ignore the first release of the mouse button after posting the
        # dropdown list, unless the mouse enters the dropdown list.
        self._ignoreRelease = 1
    
    def _dropdownBtnRelease(self, event):
        if (event.widget == self.scrolledlist.component('vertscrollbar') or
                event.widget == self.scrolledlist.component('horizscrollbar')):
            return

        if self._ignoreRelease:
            self._unpostOnNextRelease()
            return

        self._unpostList()

        if (event.x >= 0 and event.x < self.listbox.winfo_width() and
                event.y >= 0 and event.y < self.listbox.winfo_height()):
            self.__listboxselect()
    
    def _unpostOnNextRelease(self, event = None):
        self._ignoreRelease = 0
    
    def _resizeArrow(self, event):
        bw = (int(self.button['borderwidth']) + 
                int(self.button['highlightthickness']))
        newHeight = self.entry.winfo_reqheight() - 2 * bw
        newWidth = int(newHeight * self.buttonaspect)
        self.button.configure(width=newWidth, height=newHeight)
        self._drawArrow()
    
    def _unpostList(self, event=None):
        if not self._isPosted:
            # It is possible to get events on an unposted popup.  For
            # example, by repeatedly pressing the space key to post
            # and unpost the popup.  The <space> event may be
            # delivered to the popup window even though
            # Pmw.popgrab() has set the focus away from the
            # popup window.  (Bug in Tk?)
            return

        # Restore the focus before withdrawing the window, since
        # otherwise the window manager may take the focus away so we
        # can't redirect it.  Also, return the grab to the next active
        # window in the stack, if any.
        #Pmw.popgrab(self.popup)
        self.popup.withdraw()

        self._isPosted = 0
        self._drawArrow()
    
    def _selectUnpost(self, event):
        self._unpostList()
        self.__listboxselect()
    def focus_set(self):
        self.entry.focus_set()
    def __find(self, searchstr):
        def matches(item, searchstr=searchstr.lower()):
            return item[0].lower()[:len(searchstr)] == searchstr
        # Return list of (dispname, value, listidx) tuples:
        ret = filter(matches, self.list)
        return ret
    def __setdroplist(self, list=None):
        if list is not None:
            # Droplist must be (dispname, value, listidx) tuples!
            self.__droplist = list
        else:
            # Default Droplist: All Items
            self.__droplist = self.list
        self.scrolledlist.setlist([name for name, value, idx in self.__droplist])
    def __entrychanged(self, x, y, z):
        text = self.entryvar.get()
        if not text:
            return
        res = self.__find(text)
        if len(res) == 1 or (len(res) > 1 and res[0][0] == text):
            self.entry['background'] = self.normalBackground
            self.update_idletasks()
            dispname, value, idx = res[0]
            # Only one match found --> execute command:
            if not self.do_not_execute:
                    self.__select(idx)      
            self._unpostList()
        elif res:
            # More than one match
            self.entry['background'] = 'pink'
            self.update_idletasks()
            dispname, value, idx = res[0]
            self.curidx = idx
            # Display popup listbox:
            self.__setdroplist(res)
            self._postList()
        else:
            # No match
            self.entry['background'] = 'pink'
            self._unpostList()
    def __select(self, listidx):
        if listidx >= len(self.list):
            listidx = 0
        elif listidx < 0:
            listidx = len(self.list)-1
        dispname, value, idx = self.list[listidx]
        if self.command:
            self.command(value)
        self.do_not_execute = 1
        self.entryvar.set(dispname)
        self.do_not_execute = 0
        self.curidx = listidx
    def __listboxselect(self):
        sels = self.listbox.curselection()
        if len(sels) > 0:
            dispname, value, listidx = self.__droplist[int(sels[0])]
            self.__select(listidx)
    def set(self, text, do_not_execute=None):
        if do_not_execute:
            self.do_not_execute = 1
        self.entryvar.set(text)
        self.do_not_execute = 0
    def setlist(self, list):
        # list must be a list of (dispname, value, idx) tuples:
        self.list = zip([name for name, value in list], [value for name, value in list], xrange(len(list)))
        self.entryvar.set("")
        self.__setdroplist()
