
from wxPython.wx import *
from wxPython.lib.anchors import LayoutAnchors

import utils

[ SC_PANEL, SC_SPINBUT, SC_MODBUT ] = map(lambda __init__: wxNewId(), range(3))

class SpinCombo(wxComboBox):
    """ extended spin combo composite control for strings
         - interface is somewhat similar to EditableListBox (Get/SetStrings())
         - different sort/list order/checker via subclassing
    """
    SPINBUTW = 15
    SPINBUTH = 24
    SPINBUTSZ = (SPINBUTW, SPINBUTH)
    MODBUTW = 22
    MODBUTH = 22
    MODBUTSZ = (MODBUTW, MODBUTH)
    CBOXH = 22

    NOPMODE = 0
    DELMODE = 1
    SAVMODE = 2

    def __init__(self, choices = [], id = -1, name = None, parent = -1,
                        pos = (-1, -1), size = (100, 25), style = 0,
                        validator = wxDefaultValidator, value = ''):
        self.__chkfunc = None
        self.__cmpfunc = None
        self.choices = []
        self.style = style
        if style & wxCB_READONLY:
            self.mode = self.NOPMODE
        else:
            self.mode = self.DELMODE
        #self.value = value
        self.value = None
        self.focus = None
        self.count = len(choices)
        self.index = 0
        if not self.count:
            self.index = -1
        # bitmaps
        try:
            import images
            self.DELBM = images.getdelBitmap()
            self.SAVBM = images.getokBitmap()
        except:
            self.DELBM = wxBitmap('images/del.png', wxBITMAP_TYPE_PNG)
            self.SAVBM = wxBitmap('images/yes.png', wxBITMAP_TYPE_PNG)
        # panel
        self.panel = wxPanel(id = SC_PANEL, parent = parent, pos = pos, size = size,
                             style = wxTAB_TRAVERSAL)

        # add/remove correction factor to force spinbut
        # to fit exactly (on wxGTK at least)
        sbpos = (0, 0)
        cbpos = self.SPINBUTW - 2, 0
        cbsiz = size[0] - self.SPINBUTW + 2, self.CBOXH
        if not style & wxCB_READONLY:
            cbsiz = cbsiz[0] - self.MODBUTW, self.CBOXH
            dbpos = size[0] - self.MODBUTW, 0
        # spin button
        self.spinB = wxSpinButton(id = SC_SPINBUT, parent = self.panel, pos = sbpos,
                                size = self.SPINBUTSZ, style = wxSP_VERTICAL)
        EVT_SPIN(self.spinB, SC_SPINBUT, self.OnSpin)
        # combo box
        wxComboBox.__init__(self, id = id, name = name, parent = self.panel,
                            pos = cbpos, size = cbsiz, style = style,
                            validator = validator, value = value)
        EVT_COMBOBOX(self, id, self.OnCombo)
        EVT_SIZE(self, self.OnSize)
        if self.mode:
            EVT_IDLE(self, self.OnIdle)
        # delete button
        if not (style & wxCB_READONLY):
            self.modB = wxBitmapButton(bitmap = self.DELBM,
                            id = SC_MODBUT, parent = self.panel, pos = dbpos,
                            size = self.MODBUTSZ, style = wxBU_AUTODRAW)
            EVT_BUTTON(self.modB, SC_MODBUT, self.OnButton)
        # populate the combo
        self.SetStrings(choices)
        # set value
        if (style & wxCB_READONLY) and value not in choices:
            self.SetItem(self.index)
        else:
            self.SetValue(value)

    def Append(self, v):
        """ append value, and opt. sort items """
        if v not in self.choices:
            self.choices.append(v)
            f = self.GetCmpfunc()
            if f:
                self.SetStrings(self.choices)
            else:
                self.SetSpinRange()
        self.SetItemByValue(v)

    def Delete(self, i):
        """ delete item from combo """
        if i >= 0 and i < self.count:
            del self.choices[i]
            wxComboBox.Delete(self, i)
            self.count -= 1
            self.SetItem(i)

    def SetValue(self, v = ''):
        """ clear or set value and item accordingly """
        self.value = v
        wxComboBox.SetValue(self, v)
        if v:
            self.SetItemByValue(v)

    def SetItemByValue(self, v):
        """ set item by value """
        try:
            self.SetItem(self.choices.index(v))
        except:
            pass

    def SetItem(self, i):
        """ set item and implicit value by index """
        # boundary check
        if i >= self.count:
            i = self.count - 1
        # index is -1 if no items
        self.index = i
        if i >= 0:
            # set value and selection
            self.SetSelection(i)
            # adjust spin item
            self.SetSpinItem(i)
            # adjust current value
            self.value = self.choices[i]
        else:
            # clear current value
            self.SetValue()

    def SetStrings(self, sl = []):
        """ populate combo and opt. sort items """
        self.choices = sl
        self.Clear()
        f = self.GetCmpfunc()
        if f:
            self.choices.sort(f)
        for i in sl:
            wxComboBox.Append(self, i)
        self.SetSpinRange()

    def GetStrings(self):
        """ return copy of combo item list """
        return self.choices[:]

    def GetIndex(self):
        """ get current selected index """
        return self.index

    def GetChkfunc(self):
        """ overload value checker """
        return self.__chkfunc

    def GetCmpfunc(self):
        """ overload compare function """
        return self.__cmpfunc

    def SetSpinRange(self):
        """ adjust skin to proper range """
        self.count = len(self.choices)
        self.spinB.SetRange(0, self.count)

    def GetSpinItem(self):
        """ overload to adjust list/spin order """
        return self.count - self.spinB.GetValue()

    def SetSpinItem(self, i):
        """ overload to adjust list/spin order """
        self.spinB.SetValue(self.count - i)

    def addCheckedValue(self, v):
        """ call validator and add or reset value """
        cv = v
        f = self.GetChkfunc()
        if f:
            cv = f(v)
        if v == cv:
            # check ok
            self.Append(v)
        else:
            # invalid, force a reedit
            self.SetValue(cv)
            self.focus = true

    def SetConstraints(self, constr):
        """ multiplex layout constraints """
        wxComboBox.SetConstraints(self, constr)
        # XXX: how retrieve the constraints dynamically?
        self.panel.SetConstraints(LayoutAnchors(self.panel, true, true, true, false))
        #self.panel.SetConstraints(LayoutAnchors(self.panel, constr.left, constr.top, constr.right, constr.bottom))

    def OnSize(self, event):
        """ adjust ctrl positions """
        pos = self.GetPosition()
        self.spinB.SetPosition((pos[0] - self.SPINBUTW + 2, pos[1]))
        if not self.style & wxCB_READONLY:
            self.modB.SetPosition((pos[0] + self.GetSize()[0], pos[1]))
        event.Skip()

    def OnIdle(self, event):
        """ display buttons according to state in editable mode """
        if not (self.style & wxCB_READONLY):
            # 'empty' default state
            m = self.NOPMODE
            bm = self.DELBM
            v = self.GetValue()
            # value exist?
            if v in self.choices:
                # delete button
                m = self.DELMODE
            # not empty?
            elif v:
                # save button
                m = self.SAVMODE
                bm = self.SAVBM

            # state changed?
            if m != self.mode:
                self.mode = m
                self.modB.SetBitmapLabel(bm)
                self.modB.SetBitmapSelected(bm)
                self.modB.SetBitmapFocus(bm)
                if m == self.NOPMODE:
                    self.modB.Enable(false)
                else:
                    self.modB.Enable(true)
            # catch focus?
            if self.focus:
                self.focus = false
                self.SetFocus()
        event.Skip()

    def OnCombo(self, event):
        """ add possibly changed value """
        self.addCheckedValue(self.GetValue())
        event.Skip()

    def OnSpin(self, event):
        """ spin through items """
        self.SetItem(self.GetSpinItem())
        event.Skip()

    def OnButton(self, event = None):
        """ do button action, according to state """
        v = self.GetValue()
        if self.mode == self.SAVMODE:
            self.addCheckedValue(v)
        elif self.mode == self.DELMODE:
            self.Delete(self.index)
        if event:
            event.Skip()


class NSpinCombo(SpinCombo):
    """ extended numeric spin combo """
    def __init__(self, *args, **kwargs):
        self.min = None
        self.max = None
        apply(SpinCombo.__init__, (self,) + args, kwargs)

    def SetRange(self, min, max):
        self.min = min
        self.max = max

    def GetCmpfunc(self):
        return self.__Cmp

    def __Cmp(self, v1, v2):
        try:
            v1, v2 = long(v1), long(v2)
        except:
            pass
        return cmp(v1, v2)

    def GetChkfunc(self):
        return self.__Check

    def __Check(self, v):
        try:
            i = long(v)
        except:
            v = ''
        else:
            if self.min != None and i < self.min:
                i = self.min
            if self.max != None and i > self.max:
                i = self.max
            v = str(i)
        return v

    def SetSpinRange(self):
        self.count = len(self.choices)
        self.spinB.SetRange(-self.count, 0)

    def GetSpinItem(self):
        return self.spinB.GetValue() + self.count

    def SetSpinItem(self, i):
        self.spinB.SetValue(i - self.count)


class ACSpinCombo(SpinCombo):
    """ auto completion spin combo """
    def __init__(self, *args, **kwargs):
        self.expanded = None
        self.marks = None
        self.bs = false     # backspace
        apply(SpinCombo.__init__, (self,) + args, kwargs)
        EVT_TEXT(self, self.GetId(), self.OnText)
        EVT_CHAR(self, self.OnChar)
        EVT_IDLE(self, self.OnIdle)

    def OnIdle(self, event):
        if self.marks:
            self.SetMark(self.marks[0], self.marks[1])
            self.SetInsertionPoint(self.marks[0])
            self.marks = None
        SpinCombo.OnIdle(self, event)

    def OnChar(self, event):
        key = event.GetKeyCode()
        if key == WXK_BACK:
            self.bs = true
        else:
            self.bs = false
        event.Skip()

    def OnText(self, event = None):
        v = self.GetValue()
        if not v:
            self.expanded = None
        else:
            if self.expanded and v not in self.expanded:
                self.expanded = None
            if not self.expanded and not self.bs:
                sl = self.GetStrings()
                sl.sort()
                for i in sl:
                    if utils.strcmp(i, v):
                        if i != v:
                            self.marks = len(v), len(i)
                            self.expanded = v, i
                            self.SetValue(i)
                        break
        if event:
            event.Skip()

