"""
Slider Class: Velocity style controller for floating point values with
               a label, entry (validated), and min/max slider
"""

__all__ = ['Slider', 'SliderWidget', 'rgbPanel']

from direct.showbase.TkGlobal import *
from Tkinter import *
from Valuator import Valuator, rgbPanel, VALUATOR_MINI, VALUATOR_FULL
from direct.task import Task
import math, sys, string
import operator, Pmw
from pandac.PandaModules import ClockObject

class Slider(Valuator):
    """
    Valuator widget which includes an min/max slider and an entry for setting
    floating point values in a range
    """
    def __init__(self, parent = None, **kw):
        INITOPT = Pmw.INITOPT
        optiondefs = (
            ('min',        0.0,           self.setMin),
            ('max',        100.0,         self.setMax),
            ('style',      VALUATOR_MINI,   INITOPT),
            )
        self.defineoptions(kw, optiondefs)
        Valuator.__init__(self, parent)
        # Can not enter None for min or max, update propertyDict to reflect
        self.propertyDict['min']['fNone'] = 0
        self.propertyDict['min']['help'] = 'Minimum allowable value.'
        self.propertyDict['max']['fNone'] = 0
        self.propertyDict['max']['help'] = 'Maximum allowable value.'
        self.initialiseoptions(Slider)

    def createValuator(self):
        self._valuator = self.createcomponent(
            'valuator',
            (('slider', 'valuator'),),
            None,
            SliderWidget,
            (self.interior(),),
            style = self['style'],
            command = self.setEntry,
            value = self['value'])
        #self._valuator._widget.bind('<Double-ButtonPress-1>', self.mouseReset)

        # Add popup bindings to slider widget
        try:
            self._valuator._arrowBtn.bind(
                '<ButtonPress-3>', self._popupValuatorMenu)
        except AttributeError:
            pass
        self._valuator._minLabel.bind(
            '<ButtonPress-3>', self._popupValuatorMenu)
        self._valuator._maxLabel.bind(
            '<ButtonPress-3>', self._popupValuatorMenu)

    def packValuator(self):
        if self['style'] == VALUATOR_FULL:
            if self._label:
                self._label.grid(row = 0, column = 0, sticky = EW)
            self._entry.grid(row = 0, column = 1, sticky = EW)
            self._valuator.grid(row = 1, columnspan = 2,
                                padx = 2, pady = 2, sticky = 'ew')
            self.interior().columnconfigure(0, weight = 1)
        else:
            if self._label:
                self._label.grid(row=0, column=0, sticky = EW)
            self._entry.grid(row=0, column=1, sticky = EW)
            self._valuator.grid(row=0, column=2, padx = 2, pady = 2)
            self.interior().columnconfigure(0, weight = 1)

    def setMin(self):
        if self['min'] is not None:
            self._valuator['min'] = self['min']

    def setMax(self):
        if self['max'] is not None:
            self._valuator['max'] = self['max']


# Based on Pmw ComboBox code.
class SliderWidget(Pmw.MegaWidget):
    def __init__(self, parent = None, **kw):

        # Define the megawidget options.
        INITOPT = Pmw.INITOPT
        optiondefs = (
            # Appearance
            ('style',           VALUATOR_MINI,      INITOPT),
            ('relief',          RAISED,             self.setRelief),
            ('borderwidth',     2,                  self.setBorderwidth),
            ('background',      'grey75',           self.setBackground),
            ('fliparrow',       0,                  INITOPT),
            # Behavior
            # Bounds
            ('min',             0.0,            self.setMin),
            ('max',             100.0,          self.setMax),
            # Initial value of slider, use self.set to change value
            ('value',           0.0,            INITOPT),
            ('numDigits',       2,              self.setNumDigits),
            # Command to execute on slider updates
            ('command',         None,           None),
            # Extra data to be passed to command function
            ('commandData',     [],             None),
            # Callback's to execute during mouse interaction
            ('preCallback',     None,           None),
            ('postCallback',    None,           None),
            # Extra data to be passed to callback function, needs to be a list
            ('callbackData',    [],             None),
        )
        self.defineoptions(kw, optiondefs)

        # Initialise the base class (after defining the options).
        Pmw.MegaWidget.__init__(self, parent)

        # Create the components.
        interior = self.interior()

        # Current value
        self.value = self['value']
        self.formatString = '%2f'
        self.increment = 0.01

        # Interaction flags
        self._isPosted = 0
        self._fUnpost = 0
        self._fUpdate = 0
        self._firstPress = 1
        self._fPressInsde = 0

        # Slider dimensions
        width = 100
        self.xPad = xPad = 10
        sliderWidth = width + 2 * xPad
        height = 20
        self.left = left = -(width/2.0)
        self.right = right = (width/2.0)
        top = -5
        bottom = top + height

        def createSlider(parent):
            # Create the slider inside the dropdown window.
            # Min label
            self._minLabel = Label(parent, text = self['min'], width = 8,
                                   anchor = W)
            self._minLabel.pack(side = LEFT)
            # Slider widget
            if self['style'] == VALUATOR_FULL:
                # Use a scale slider
                self._widgetVar = DoubleVar()
                self._widgetVar.set(self['value'])
                self._widget = self.createcomponent(
                    'slider', (), None,
                    Scale, (interior,),
                    variable = self._widgetVar,
                    from_ = self['min'], to = self['max'],
                    resolution = 0.0,
                    width = 10,
                    orient = 'horizontal',
                    showvalue = 0,
                    length = sliderWidth,
                    relief = FLAT, bd = 2,
                    highlightthickness = 0)
            else:
                # Use a canvas slider
                self._widget = self.createcomponent(
                    'slider', (), None,
                    Canvas, (parent,),
                    width = sliderWidth,
                    height = height,
                    bd = 2,
                    highlightthickness = 0,
                    scrollregion = (left - xPad, top, right + xPad, bottom))
                # Interaction marker
                xShift = 1
                # Shadow arrow
                self._marker = self._widget.create_polygon(-7 + xShift, 12,
                                                           7 + xShift, 12,
                                                           xShift, 0,
                                                           fill = 'black',
                                                           tags = ('marker',))
                # Arrow
                self._widget.create_polygon(-6.0, 10,
                                            6.0, 10,
                                            0, 0,
                                            fill = 'grey85',
                                            outline = 'black',
                                            tags = ('marker',))
                # The indicator
                self._widget.create_line(left, 0,
                                         right, 0,
                                         width = 2,
                                         tags = ('line',))

            self._widget.pack(side = LEFT, expand=1, fill=X)

            # Max label
            self._maxLabel = Label(parent, text = self['max'], width = 8,
                                   anchor = W)
            self._maxLabel.pack(side = LEFT)

        # Create slider
        if self['style'] == VALUATOR_MINI:

            # Create the arrow button to invoke slider
            self._arrowBtn = self.createcomponent(
                'arrowbutton',
                (), None,
                Canvas, (interior,), borderwidth = 0,
                relief = FLAT, width = 14, height = 14,
                scrollregion = (-7, -7, 7, 7))
            self._arrowBtn.pack(expand = 1, fill = BOTH)
            self._arrowBtn.create_polygon(-5, -5, 5, -5, 0, 5,
                                          fill = 'grey50',
                                          tags = 'arrow')
            self._arrowBtn.create_line(-5, 5, 5, 5,
                                       fill = 'grey50',
                                       tags = 'arrow')
            # Create the dropdown window.
            self._popup = self.createcomponent(
                'popup',
                (), None,
                Toplevel, (interior,),
                relief = RAISED, borderwidth = 2)
            self._popup.withdraw()
            self._popup.overrideredirect(1)

            # Create popup slider
            createSlider(self._popup)

            # Bind events to the arrow button.
            self._arrowBtn.bind('<1>', self._postSlider)
            self._arrowBtn.bind('<Enter>', self.highlightWidget)
            self._arrowBtn.bind('<Leave>', self.restoreWidget)
            # Need to unpost the popup if the arrow Button is unmapped (eg:
            # its toplevel window is withdrawn) while the popup slider is
            # displayed.
            self._arrowBtn.bind('<Unmap>', self._unpostSlider)

            # Bind events to the dropdown window.
            self._popup.bind('<Escape>', self._unpostSlider)
            self._popup.bind('<ButtonRelease-1>', self._widgetBtnRelease)
            self._popup.bind('<ButtonPress-1>', self._widgetBtnPress)
            self._popup.bind('<Motion>', self._widgetMove)

            self._widget.bind('<Left>', self._decrementValue)
            self._widget.bind('<Right>', self._incrementValue)
            self._widget.bind('<Shift-Left>', self._bigDecrementValue)
            self._widget.bind('<Shift-Right>', self._bigIncrementValue)
            self._widget.bind('<Home>', self._goToMin)
            self._widget.bind('<End>', self._goToMax)
        else:
            createSlider(interior)
            self._widget['command'] = self._firstScaleCommand
            self._widget.bind('<ButtonRelease-1>', self._scaleBtnRelease)
            self._widget.bind('<ButtonPress-1>', self._scaleBtnPress)

        # Check keywords and initialise options.
        self.initialiseoptions(SliderWidget)

        # Adjust relief
        if 'relief' not in kw:
            if self['style'] == VALUATOR_FULL:
                self['relief'] = FLAT

        self.updateIndicator(self['value'])

    def destroy(self):
        if (self['style'] == VALUATOR_MINI) and self._isPosted:
            Pmw.popgrab(self._popup)
        Pmw.MegaWidget.destroy(self)

    #======================================================================

    # Public methods

    def set(self, value, fCommand = 1):
        """
        self.set(value, fCommand = 1)
        Set slider to new value, execute command if fCommand == 1
        """
        # Send command if any
        if fCommand and (self['command'] != None):
            apply(self['command'], [value] + self['commandData'])
        # Record value
        self.value = value

    def get(self):
        """
        self.get()
        Get current slider value
        """
        return self.value

    def updateIndicator(self, value):
        if self['style'] == VALUATOR_MINI:
            # Get current marker position
            percentX = (value - self['min'])/float(self['max'] - self['min'])
            newX = percentX * (self.right - self.left) + self.left
            markerX = self._getMarkerX()
            dx = newX - markerX
            self._widget.move('marker', dx, 0)
        else:
            # Update scale's variable, which update scale without
            # Calling scale's command
            self._widgetVar.set(value)

    #======================================================================

    # Private methods for slider.

    def _postSlider(self, event = None):
        self._isPosted = 1
        self._fUpdate = 0

        # Make sure that the arrow is displayed sunken.
        self.interior()['relief'] = SUNKEN
        self.update_idletasks()
        # Position popup so that marker is immediately below center of
        # Arrow button
        # Find screen space position of bottom/center of arrow button
        x = (self._arrowBtn.winfo_rootx() + self._arrowBtn.winfo_width()/2.0 -
             self.interior()['bd'])
#             string.atoi(self.interior()['bd']))
        y = self._arrowBtn.winfo_rooty() + self._arrowBtn.winfo_height()
        # Popup border width
        bd = self._popup['bd']
#        bd = string.atoi(self._popup['bd'])
        # Get width of label
        minW = self._minLabel.winfo_width()
        # Width of canvas to adjust for
        cw = (self._getMarkerX() - self.left) + self.xPad
        popupOffset = bd + minW + cw
        ch =  self._widget.winfo_height()
        sh = self.winfo_screenheight()

        # Compensate if too close to edge of screen
        if y + ch > sh and y > sh / 2:
            y = self._arrowBtn.winfo_rooty() - ch
        # Popup window
        Pmw.setgeometryanddeiconify(self._popup, '+%d+%d' % (x-popupOffset, y))

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

        # Ignore the first release of the mouse button after posting the
        # dropdown slider, unless the mouse enters the dropdown slider.
        self._fUpdate = 0
        self._fUnpost = 0
        self._firstPress = 1
        self._fPressInsde = 0

    def _updateValue(self, event):
        mouseX = self._widget.canvasx(
            event.x_root - self._widget.winfo_rootx())
        if mouseX < self.left:
            mouseX = self.left
        if mouseX > self.right:
            mouseX = self.right
        # Update value
        sf = (mouseX - self.left)/(self.right - self.left)
        newVal = sf * (self['max'] - self['min']) + self['min']
        self.set(newVal)

    def _widgetBtnPress(self, event):
        # Check behavior for this button press
        widget = self._popup
        xPos = event.x_root - widget.winfo_rootx()
        yPos = event.y_root - widget.winfo_rooty()
        fInside = ((xPos > 0) and (xPos < widget.winfo_width()) and
                   (yPos > 0) and (yPos < widget.winfo_height()))
        # Set flags based upon result
        if fInside:
            self._fPressInside = 1
            self._fUpdate = 1
            if self['preCallback']:
                apply(self['preCallback'], self['callbackData'])
            self._updateValue(event)
        else:
            self._fPressInside = 0
            self._fUpdate = 0

    def _widgetMove(self, event):
        if self._firstPress and not self._fUpdate:
            canvasY = self._widget.canvasy(
                event.y_root - self._widget.winfo_rooty())
            if canvasY > 0:
                self._fUpdate = 1
                if self['preCallback']:
                    apply(self['preCallback'], self['callbackData'])
                self._unpostOnNextRelease()
        elif self._fUpdate:
            self._updateValue(event)

    def _scaleBtnPress(self, event):
        if self['preCallback']:
            apply(self['preCallback'], self['callbackData'])

    def _scaleBtnRelease(self, event):
        # Do post callback if any
        if self['postCallback']:
            apply(self['postCallback'], self['callbackData'])

    def _widgetBtnRelease(self, event):
        # Do post callback if any
        if self._fUpdate and self['postCallback']:
            apply(self['postCallback'], self['callbackData'])
        if (self._fUnpost or
            (not (self._firstPress or self._fPressInside))):
            self._unpostSlider()
        # Otherwise, continue
        self._fUpdate = 0
        self._firstPress = 0
        self._fPressInside = 0

    def _unpostOnNextRelease(self, event = None):
        self._fUnpost = 1

    def _unpostSlider(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

        # Raise up arrow button
        self.interior()['relief'] = RAISED

    def _incrementValue(self, event):
        self.set(self.value + self.increment)
    def _bigIncrementValue(self, event):
        self.set(self.value + self.increment * 10.0)
    def _decrementValue(self, event):
        self.set(self.value - self.increment)
    def _bigDecrementValue(self, event):
        self.set(self.value - self.increment * 10.0)
    def _goToMin(self, event):
        self.set(self['min'])
    def _goToMax(self, event):
        self.set(self['max'])

    def _firstScaleCommand(self, val):
        """ Hack to avoid calling command on instantiation of Scale """
        self._widget['command'] = self._scaleCommand

    def _scaleCommand(self, val):
        self.set(string.atof(val))

    # Methods to modify floater characteristics
    def setMin(self):
        self._minLabel['text'] = self.formatString % self['min']
        if self['style'] == VALUATOR_FULL:
            self._widget['from_'] = self['min']
        self.updateIndicator(self.value)

    def setMax(self):
        self._maxLabel['text'] = self.formatString % self['max']
        if self['style'] == VALUATOR_FULL:
            self._widget['to'] = self['max']
        self.updateIndicator(self.value)

    def setNumDigits(self):
        self.formatString = '%0.' + ('%d' % self['numDigits']) + 'f'
        self._minLabel['text'] = self.formatString % self['min']
        self._maxLabel['text'] = self.formatString % self['max']
        self.updateIndicator(self.value)
        self.increment = pow(10, -self['numDigits'])

    def _getMarkerX(self):
        # Get marker triangle coordinates
        c = self._widget.coords(self._marker)
        # Marker postion defined as X position of third vertex
        return c[4]

    def setRelief(self):
        self.interior()['relief'] = self['relief']

    def setBorderwidth(self):
        self.interior()['borderwidth'] = self['borderwidth']

    def setBackground(self):
        self._widget['background'] = self['background']

    def highlightWidget(self, event):
        self._arrowBtn.itemconfigure('arrow', fill = 'black')

    def restoreWidget(self, event):
        self._arrowBtn.itemconfigure('arrow', fill = 'grey50')