"""
Floater Class: Velocity style controller for floating point values with
                a label, entry (validated), and scale
"""
from PandaObject import *
from Tkinter import *
import Pmw
import string

class Floater(Pmw.MegaWidget):
    "Velocity style floating point controller"
 
    def __init__(self, parent = None, **kw):

        # Define the megawidget options.
        optiondefs = (
            ('initialValue',        0.0,        Pmw.INITOPT),
            ('resolution',          None,       None),
            ('command',             None,       None),
            ('commandData',         [],         None),
            ('callbackData',        [],         None),
            ('maxVelocity',         10.0,       None),
            ('min',                 None,       self._updateValidate),
            ('max',                 None,       self._updateValidate),
            ('text',                'Floater',  self._updateLabelText),
            ('significantDigits',   2,          self._setSigDigits),
            )
        self.defineoptions(kw, optiondefs)
 
        # Initialise superclass
        Pmw.MegaWidget.__init__(self, parent)

        # Initialize some class variables
        self.value = self['initialValue']
        self.velocity = 0.0
        self.entryFormat = '%.2f'

        # Create the components.

        # Setup up container
        interior = self.interior()
        interior.configure(relief = GROOVE, borderwidth = 2)

        # Create a label and an entry
        self.labelFrame = self.createcomponent('frame', (), None,
                                               Frame, interior)
        # Create an entry field to display and validate the floater's value
        self.entryValue = StringVar()
        self.entryValue.set(self['initialValue'])
        self.entry = self.createcomponent('entryField',
                                          # Access floaters entry using "entry"
                                          (('entry', 'entryField_entry'),),
                                          None,
                                          Pmw.EntryField, self.labelFrame,
                                          entry_width = 10,
                                          validate = { 'validator' : 'real',
                                                       'min' : self['min'],
                                                       'max' : self['max'],
                                                       'minstrict' : 0,
                                                       'maxstrict' : 0},
                                          entry_justify = 'right',
                                          entry_textvar = self.entryValue,
                                          command = self._entryCommand)
        self.entry.pack(side='left',padx = 4)
                                          
        # Create the Floater's label
        self.label = self.createcomponent('label', (), None,
                                          Label, self.labelFrame,
                                          text = self['text'],
                                          width = 12,
                                          anchor = 'center',
                                          font = "Arial 10 bold")
        self.label.pack(side='left', expand = 1, fill = 'x')

        # Now pack the frame
        self.labelFrame.pack(expand = 1, fill = 'both')

        # Create the scale component.
        self.scale = self.createcomponent('scale', (), None,
                                          Scale, interior,
                                          command = self._scaleToVelocity,
                                          orient = 'horizontal',
                                          length = 150,
                                          from_ = -1.0,
                                          to = 1.0,
                                          resolution = 0.001,
                                          showvalue = 0)
        self.scale.pack(expand = 1, fill = 'x')
        # Set scale to the middle of its range
        self.scale.set(0.0)
        
        # Add scale bindings: When interacting with mouse:
        self.scale.bind('<Button-1>', self._startFloaterTask)
        self.scale.bind('<ButtonRelease-1>', self._floaterReset)
        # In case you wish to interact using keys
        self.scale.bind('<KeyPress-Right>', self._floaterKeyCommand)
        self.scale.bind('<KeyRelease-Right>', self._floaterReset)
        self.scale.bind('<KeyPress-Left>', self._floaterKeyCommand)
        self.scale.bind('<KeyRelease-Left>', self._floaterReset)
 
        # Check keywords and initialise options based on input values.
        self.initialiseoptions(Floater)

    def label(self):
        return self.label
    def scale(self):
        return self.scale
    def entry(self):
        return self.entry
    
    def _updateLabelText(self):
        self.label['text'] = self['text']

    def _updateValidate(self):
        self.configure(entryField_validate = {
            'validator' : 'real',
            'min' : self['min'],
            'max' : self['max'],
            'minstrict' : 0,
            'maxstrict' : 0})

    def _scaleToVelocity(self, strVal):
        # convert scale val to float
        val = string.atof(strVal)
        # Square val, but retain sign of velocity by only calling abs once
        self.velocity = self['maxVelocity'] * val * abs(val)

    def _startFloaterTask(self,event):
        self._fFloaterTask = 1
        apply(self.onPress,self['callbackData'])
        taskMgr.spawnMethodNamed(self._floaterTask, 'floaterTask')

    def _floaterTask(self, state):
        if self.velocity != 0.0:
            self.set( self.value + self.velocity )
        return Task.cont

    def _floaterReset(self, event):
        taskMgr.removeTasksNamed('floaterTask')
        self.velocity = 0.0
        self.scale.set(0.0)
        apply(self.onRelease, self['callbackData'])

    def _floaterKeyCommand(self, event):
        if self.velocity != 0.0:
            self.set( self.value + self.velocity )

    def _entryCommand(self, event = None):
        try:
            val = string.atof( self.entryValue.get() )
            apply(self.onReturn,self['callbackData'])
            self.set( val )
            apply(self.onReturnRelease,self['callbackData'])
        except ValueError:
            pass

    def _setSigDigits(self):
        sd = self['significantDigits']
        self.entryFormat = '%.' + '%d' % sd + 'f'
        # And reset value to reflect change
        self.entryValue.set( self.entryFormat % self.value )

    def get(self):
        return self.value
    
    def set(self, newVal, fCommand = 1):
        # Clamp value
        if self['min'] is not None:
            if newVal < self['min']:
                newVal = self['min']
        if self['max'] is not None:
            if newVal > self['max']:
                newVal = self['max']
        # Round by resolution
        if self['resolution'] is not None:
            newVal = round(newVal / self['resolution']) * self['resolution']
        
        # Update floater's value
        self.value = newVal
        # Update entry to reflect formatted value
        self.entryValue.set( self.entryFormat % self.value )
        self.entry.checkentry()
        
        # execute command
        if fCommand and (self['command'] is not None):
            apply(self['command'], [newVal] + self['commandData'])

    def reset(self):
        self.set(self['initialValue'])

    def disable(self):
        self.scale['state'] = 'disabled'
        self.label['state'] = 'disabled'
        self.component('entry')['state'] = 'disabled'

    def enable(self):
        self.scale['state'] = 'normal'
        self.label['state'] = 'normal'
        self.component('entry')['state'] = 'normal'

    def onReturn(self, *args):
        """ User redefinable callback executed on <Return> in entry """
        pass

    def onReturnRelease(self, *args):
        """ User redefinable callback executed on <Return> release in entry """
        pass

    def onPress(self, *args):
        """ User redefinable callback executed on button press """
        pass

    def onRelease(self, *args):
        """ User redefinable callback executed on button release """
        pass

class FloaterGroup(Pmw.MegaToplevel):
    def __init__(self, parent = None, **kw):

        # Default group size
        DEFAULT_DIM = 1
        # Default value depends on *actual* group size, test for user input
        DEFAULT_VALUE = [0.0] * kw.get('dim', DEFAULT_DIM)
        DEFAULT_LABELS = map(lambda x: 'v[%d]' % x,
                             range(kw.get('dim', DEFAULT_DIM)))

        #define the megawidget options
        INITOPT = Pmw.INITOPT
        optiondefs = (
            ('dim',             DEFAULT_DIM,            INITOPT),
            ('side',            TOP,                    INITOPT),
            ('title',           'Floater Group',        None),
            # A tuple of initial values, one for each floater
            ('initialValue',    DEFAULT_VALUE,          INITOPT),
            # The command to be executed any time one of the floaters is updated
            ('command',         None,                   None),
            # A tuple of labels, one for each floater
            ('labels',          DEFAULT_LABELS,         self._updateLabels),
            )
        self.defineoptions(kw, optiondefs)

        # Initialize the toplevel widget
        Pmw.MegaToplevel.__init__(self, parent)
        
        # Create the components
        interior = self.interior()
        # Get a copy of the initial value (making sure its a list)
        self._value = list(self['initialValue'])

        # The Menu Bar
        self.balloon = Pmw.Balloon()
        menubar = self.createcomponent('menubar',(), None,
                                       Pmw.MenuBar, (interior,),
                                       balloon = self.balloon)
        menubar.pack(fill=X)
        
        # FloaterGroup Menu
        menubar.addmenu('Floater Group', 'Floater Group Operations')
        menubar.addmenuitem(
            'Floater Group', 'command', 'Reset the Floater Group panel',
            label = 'Reset',
            command = lambda s = self: s.reset())
        menubar.addmenuitem(
            'Floater Group', 'command', 'Dismiss Floater Group panel',
            label = 'Dismiss', command = self.withdraw)
        
        menubar.addmenu('Help', 'Floater Group Help Operations')
        self.toggleBalloonVar = IntVar()
        self.toggleBalloonVar.set(0)
        menubar.addmenuitem('Help', 'checkbutton',
                            'Toggle balloon help',
                            label = 'Balloon Help',
                            variable = self.toggleBalloonVar,
                            command = self.toggleBalloon)

        self.floaterList = []
        for index in range(self['dim']):
            # Add a group alias so you can configure the floaters via:
            #   fg.configure(Valuator_XXX = YYY)
            f = self.createcomponent(
                'floater%d' % index, (), 'Valuator', Floater,
                (interior,), initialValue = self._value[index],
                text = self['labels'][index])
            # Do this separately so command doesn't get executed during construction
            f['command'] = lambda val, s=self, i=index: s._floaterSetAt(i, val)
            f.pack(side = self['side'], expand = 1, fill = X)
            self.floaterList.append(f)

        # Make sure floaters are initialized
        self.set(self['initialValue'])
        
        # Make sure input variables processed 
        self.initialiseoptions(FloaterGroup)

    def _updateLabels(self):
        if self['labels']:
            for index in range(self['dim']):
                self.floaterList[index]['text'] = self['labels'][index]

    def toggleBalloon(self):
        if self.toggleBalloonVar.get():
            self.balloon.configure(state = 'balloon')
        else:
            self.balloon.configure(state = 'none')

    def get(self):
        return self._value

    def getAt(self,index):
        return self._value[index]

    # This is the command is used to set the groups value
    def set(self, value, fCommand = 1):
        for i in range(self['dim']):
            self._value[i] = value[i]
            # Update floater, but don't execute its command
            self.floaterList[i].set(value[i], 0)
        if fCommand and (self['command'] is not None):
            self['command'](self._value)

    def setAt(self, index, value):
        # Update floater and execute its command
        self.floaterList[index].set(value)

    # This is the command used by the floater
    def _floaterSetAt(self, index, value):
        self._value[index] = value
        if self['command']:
            self['command'](self._value)

    def reset(self):
        self.set(self['initialValue'])


## SAMPLE CODE
if __name__ == '__main__':
    # Initialise Tkinter and Pmw.
    root = Toplevel()
    root.title('Pmw Floater demonstration')

    # Dummy command
    def printVal(val):
        print val
    
    # Create and pack a Floater megawidget.
    mega1 = Floater(root, command = printVal)
    mega1.pack(side = 'left', expand = 1, fill = 'x')

    """
    # These are things you can set/configure
    # Starting value for floater    
    mega1['initialValue'] = 123.456
    mega1['text'] = 'Drive delta X'
    mega1['min'] = 0.0
    mega1['max'] = 1000.0
    mega1['resolution'] = 1.0
    # To change the color of the label:
    mega1.label['foreground'] = 'Red'
    # Max change/update, default is 100
    # To have really fine control, for example
    # mega1['maxVelocity'] = 0.1
    # Number of digits to the right of the decimal point, default = 2
    # mega1['significantDigits'] = 5
    """

    # To create a floater group to set an RGBA value:
    group1 = FloaterGroup(root, dim = 4,
                          title = 'Simple RGBA Panel',
                          labels = ('R', 'G', 'B', 'A'),
                          Valuator_min = 0.0,
                          Valuator_max = 255.0,
                          Valuator_resolution = 1.0,
                          command = printVal)
    
    # Uncomment this if you aren't running in IDLE
    #root.mainloop()
