import wx
from wx.lib import masked        

"""Module for controls that bind with a variable that is controlled.
"""

def identityFunction(x):
    return x

def createBindRadioSetter(parent, obj, varName, labels, useIndex = True, 
                          func = identityFunction, title = "", tooltip = ""):
        """Create a row radio box bound to variable via a specified
        function of the radio box value.
        
        parent:    parent window (not Null)
        
        obj:       the object with the attribute to be set
        
        varName:   the name of obj's attribute that is to be set
        
        labels:    list of button labels
        
        useIndex:  if true, in setting the value, start from the index of the
                   newly selected button as opposed to the label on the button.
        
        func:      final value will be func(index) or func(labels[index]), 
                   depending on useIndex, where index is the offset from 0 of 
                   the newly selected button.
                   
        title:     title on radio box
        
        tooltip:   tooltip text for radio button
        
        The initial setting of the radio box is chosen to fit with the 
        value of the specified variable.  An exception is thrown if the initial
        value of the variable does not correspond to a possible radio button.
        """
        
        rb = RadioSetter(parent, -1, title, wx.DefaultPosition, wx.DefaultSize,
                         labels, 1, wx.RA_SPECIFY_ROWS | wx.NO_BORDER)
        if tooltip: 
            rb.SetToolTip(wx.ToolTip(tooltip))

        rb.bindVar(obj, varName, useIndex, func)
        
        return rb
    
        
class RadioSetter(wx.RadioBox):
    """A RadioBox is bound to a variable either in a two step process, 
       constructing a RadioSetter using any desired RadioBox parameters, 
       and then calling self.bindVar to finish
          or
       use the single module convenience method createBindRadioSetter, which
       returns the RadioBox.
    """
    
    __init__ = wx.RadioBox.__init__

    def bindVar(self, obj, varName, useIndex = True, func = identityFunction):

        """Bind a specified variable via a specified
        function of the radio box value.
        
        obj:       the object with the attribute to be set
        
        varName:   the name of obj's attribute that is to be set
        
        useIndex:  if true, in setting the value, start from the index of the
                   newly selected button as opposed to the label on the button.
        
        func:      final value will be func(index) or func(labels[index]), 
                   depending on useIndex, where index is the offset from 0 of 
                   the newly selected button.
                   
        The initial setting of the radio box is the first to fit with the value
        of the specified variable.  Raise an exception if no fit is found.
        """
        
        # added attributes to the radio button
        self.obj = obj
        self.varName = varName
        self.func = func
        self.fullFunc = func

        if not useIndex:
            def labelFunc(index, rb = self):
                return rb.func(rb.GetItemLabel(index))    
                
            self.fullFunc = labelFunc

        self.matchSelection()

        self.Bind(wx.EVT_RADIOBOX, self.EvtRadioBox, self)



    def matchSelection(self):
        """Set the selection of the radio box so it is consistent with the
        current value of variable specified to match it.  Throw an exception
        if there is no way to make a match.
        """
        
        val = getattr(self.obj, self.varName)
        for index in range(self.GetCount()):
            if val == self.fullFunc(index):
                self.SetSelection(index)
                return
        
        raise Exception, \
              "Desired bound value does not fit with any radio box setting."
                   
    def EvtRadioBox(self, event):
        """Set specified variable as specified function of the newly
        selected radio box's index.
        """
        
        setattr(self.obj, self.varName, self.fullFunc(event.GetInt()))



#---------------------------------------------------------------------------

minf = min  # keep wx param names min, max, which cover the global functions
maxf = max

def createBindIntSetter(parent, obj, varName, min = None, max = None, 
                        integerWidth = 10, tooltip = ""):
        """Create a integer NumCtrl bound to variable when the focus shifts out.
        
        parent:       parent window (not Null)
        
        obj:          the object with the attribute to be set
        
        varName:      the name of obj's attribute that is to be set
        
        min:          minimum value
        
        max:          maximum value
        
        integerWidth: max characters (or max char in specified min and max) 
        
        tooltip:      tooltip text; substring "min-max" replaced by actual bound
        
        The initial setting of the NumCtrl is set to the value of the specified 
        variable.  An exception is thrown if the initial value of the variable 
        is not legal, given min, max, or size.
        """
        
        if min != None and max != None:
           integerWidth =    minf(integerWidth, 
                                 maxf(len(str(min)), len(str(max))) ) 
        nc = IntSetter(parent, integerWidth = integerWidth, 
                       min = min, max = max) #, limited = True) # fails? 
##        print nc.IsLimited()
        nc.SetGroupDigits(False);
        nc.SetLimited(True)  # ?? no effect so have kludge in event handler
        if tooltip:
            rangeStr = ''
            if min != None:
                rangeStr += "min: %d" % min
                if max != None:
                    rangeStr += "; "
            if max != None:
                rangeStr += "max: %d" % max            
            tooltip = tooltip.replace("min-max", rangeStr) 
            nc.SetToolTip(wx.ToolTip(tooltip))

        nc.bindVar(obj, varName)
        
        return nc
    
class IntSetter(masked.NumCtrl):
    """A NumCtrl is bound to a variable either in a two step process, 
       constructing an IntSetter using any desired NumCtrl parameters, 
       and then calling self.bindVar to finish
          or
       use the single module convenience method createBindIntSetter, which
       returns the IntSetter.
    """
    
    __init__ = masked.NumCtrl.__init__
    
    

    def bindVar(self, obj, varName):

        """Bind a specified variable via a specified
        function of the radio box value.
        
        obj:       the object with the attribute to be set
        
        varName:   the name of obj's attribute that is to be set
                           
        The initial setting of the radio box is the value of the specified 
        variable.  
        """
        
        # added attributes to the control
        self.obj = obj
        self.varName = varName
        
        self.SetValue(getattr(self.obj, self.varName))

##        self.Bind(masked.EVT_NUM, self.EvtCtrl, self)
        self.Bind(wx.EVT_KILL_FOCUS, self.EvtCtrl, self)


    def EvtCtrl(self, event):
        """Set specified variable to match NumCtrl when focus leaves.
        Inbounds test is a kludge since SetLimits does not appear to work
        in wx version 2.6 
        """

        if self.IsInBounds():
            setattr(self.obj, self.varName, self.GetValue()) #?!val not in event
        else:
            self.SetValue(getattr(self.obj, self.varName))

#---------------------------------------------------------------------------

class IntSetterTester(wx.Panel):
    def __init__(self, parent):
        wx.Panel.__init__(self, parent)
        
        class Tester:
            def __init__(self, x):
                self.v = x
    
        self.t = Tester(200)
        self.u = Tester(2)

        nc = createBindIntSetter(self, self.t, "v", tooltip = "shows in stdout")

        nc2 = createBindIntSetter(self, self.u, "v", min = -100, max = 50, 
                                    tooltip = "v val min-max")
                                   
        btn = wx.Button(self, -1, "see latest val")
        self.Bind(wx.EVT_BUTTON, self.OnButton, btn)

        # Use a sizer to layout the controls, stacked vertically and with
        # a 10 pixel border around each
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(nc, 0, wx.ALL, 10)
        sizer.Add(nc2, 0, wx.ALL, 10)
        sizer.Add(btn, 0, wx.ALL, 10)
        self.SetSizer(sizer)

    def OnButton(self, evt):
        """Event handler for the button click displays linked variables."""
        print self.t.v
        print self.u.v

#------------------------------------------------------------------
class RadioSetterTester(wx.Panel):
    """Panel to test RadioSetter."""
    
    def __init__(self, parent):
        wx.Panel.__init__(self, parent)
        
        class Tester:
            def __init__(self, x):
                self.v = x
    
        self.t = Tester(2)
        self.u = Tester(True)

        rb = createBindRadioSetter(self, self.t, "v", ["0", "1", "2", "3"], 
                                   title = "testing", 
                                   tooltip = "shows in stdout")

        rb2 = createBindRadioSetter(self, self.u, "v", ["No", "Yes"], 
                                    func = bool,
                                    tooltip = "conversion to bool")
                                   
        btn = wx.Button(self, -1, "see latest val")
        self.Bind(wx.EVT_BUTTON, self.OnButton, btn)

        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(rb, 0, wx.ALL, 0)
        sizer.Add(rb2, 0, wx.ALL, 0)
        sizer.Add(btn, 0, wx.ALL, 5)
        self.SetSizer(sizer)
##        panel.Layout()

    def OnButton(self, evt):
        """Event handler for the button click displays linked variables."""
        print self.t.v
        print self.u.v


if __name__ == "__main__":
    import paneltester
    paneltester.main(RadioSetterTester)
    paneltester.main(IntSetterTester) #, redirect = False)
