Browse Source

*** empty log message ***

Mark Mine 24 years ago
parent
commit
3f4ad61a6d

+ 146 - 320
direct/src/tkwidgets/Dial.py

@@ -1,17 +1,20 @@
+"""
+Dial Class: Velocity style controller for floating point values with
+             a label, entry (validated), and scale
+"""
 from Tkinter import *
-from PandaModules import ClockObject
-import WidgetPropertiesDialog
+from Valuator import *
 import Pmw
 import Task
 import math
 import string
 import operator
+from PandaModules import ClockObject
 
 TWO_PI = 2.0 * math.pi
 ONEPOINTFIVE_PI = 1.5 * math.pi
 POINTFIVE_PI = 0.5 * math.pi
 INNER_SF = 0.2
-MAX_EXP = 5
 
 DIAL_FULL = 'full'
 DIAL_MINI = 'mini'
@@ -21,178 +24,113 @@ DIAL_MINI_SIZE = 30
 
 globalClock = ClockObject.getGlobalClock()
 
-from tkSimpleDialog import Dialog
-
-
 
-class Dial(Pmw.MegaWidget):
+class Dial(Valuator):
+    """
+    Valuator widget which includes an angle dial and an entry for setting
+    floating point values
+    """
     def __init__(self, parent = None, **kw):
-        #define the megawidget options
         INITOPT = Pmw.INITOPT
         optiondefs = (
             ('style',             DIAL_FULL,      INITOPT),
-            # Widget relief
-            ('relief',            GROOVE,         None),
-            # Widget borderwidth
-            ('borderwidth',       2,              None),
-            ('value',             0.0,            INITOPT),
-            ('resetValue',        0.0,            self.setResetValue),
-            ('text',              'Dial Widget',  self.setLabel),
-            ('numDigits',         2,              self.setEntryFormat),
-            ('command',           None,           None),
-            ('commandData',       [],             None),
-            ('min',               None,           self.setMin),
-            ('max',               None,           self.setMax),
             ('base',              0.0,            self.setBase),
             ('delta',             1.0,            self.setDelta),
-            # Callbacks to execute when updating widget's value
-            ('preCallback',       None,           self.setPreCallbackCmd),
-            ('postCallback',      None,           self.setPostCallbackCmd),
-            # Extra data to be passed to callback function, needs to be a list
-            ('callbackData',      [],             self.setCallbackData),
+            ('fSnap',             0,              self.setSnap),
+            ('fRollover',         1,              self.setRollover),
             )
         self.defineoptions(kw, optiondefs)
-        
-        # Initialize the superclass
-        Pmw.MegaWidget.__init__(self, parent)
-
-        # Override size if style specified by size is not
-        if not kw.has_key('dial_size'):
-            if self['style'] == DIAL_FULL:
-                dialSize = DIAL_FULL_SIZE
-            else:
-                dialSize = DIAL_MINI_SIZE
-        else:
-            dialSize = DIAL_FULL_SIZE
-
-        # Create the components
-        interior = self.interior()
-        interior.configure(relief = self['relief'], bd = self['borderwidth'])
-        
-        # The Dial 
-        self._dial = self.createcomponent('dial', (), None,
-                                          DialWidget, (interior,),
-                                          size = dialSize,
-                                          command = self.setEntry,
-                                          value = self['value'])
-
-        self._dial.addPropertyToDialog(
-            'text',
-            {'widget' : self,
-             'type' : 'string',
-             'help' : 'Enter label text for Dial.'
-             }
-            )
-        self._dial.addPropertyToDialog(
-            'numDigits',
-            {'widget' : self,
-             'type' : 'integer',
-             'help' : 'Enter number of digits after decimal point.'
-             }
-            )
-
-        # The Label
-        self._label = self.createcomponent('label', (), None,
-                                           Label, (interior,),
-                                           text = self['text'],
-                                           font = ('MS Sans Serif',12,'bold'),
-                                           anchor = CENTER)
-        self._label.bind('<ButtonPress-3>', self._dial.popupDialMenu)
-
-        # The entry
-        self._entryVal = StringVar()
-        self._entry = self.createcomponent('entry', (), None,
-                                           Entry, (interior,),
-                                           justify = RIGHT,
-                                           width = 12,
-                                           textvariable = self._entryVal)
-        self._entry.bind('<Return>', self.validateEntryInput)
-        self._entry.bind('<ButtonPress-3>', self._dial.popupDialMenu)
-        self._entryBackground = self._entry.cget('background')
+        Valuator.__init__(self, parent)
+        self.initialiseoptions(Dial)
 
+    def createValuator(self):
+        self._valuator = self.createcomponent(
+            'valuator',
+            (('dial', 'valuator'),),
+            None,
+            DialWidget,
+            (self.interior(),),
+            style = self['style'],
+            command = self.setEntry,
+            value = self['value'])
+        self._valuator._canvas.bind('<Double-ButtonPress-1>', self.mouseReset)
+
+    def packValuator(self):
         if self['style'] == DIAL_FULL:
-            # Attach dial to entry
-            self._dial.grid(rowspan = 2, columnspan = 2, padx = 2, pady = 2)
-            self._label.grid(row = 0, col = 2, sticky = EW)
+            self._valuator.grid(rowspan = 2, columnspan = 2,
+                                padx = 2, pady = 2)
+            if self._label:
+                self._label.grid(row = 0, col = 2, sticky = EW)
             self._entry.grid(row = 1, col = 2, sticky = EW)
-            interior.columnconfigure(2, weight = 1)
+            self.interior().columnconfigure(2, weight = 1)
         else:
-            self._label.grid(row=0,col=0, sticky = EW)
+            if self._label:
+                self._label.grid(row=0,col=0, sticky = EW)
             self._entry.grid(row=0,col=1, sticky = EW)
-            self._dial.grid(row=0,col=2, padx = 2, pady = 2)
-            interior.columnconfigure(0, weight = 1)
-
-        # Make sure input variables processed 
-        self.initialiseoptions(Dial)
-
-    def set(self, value, fCommand = 1):
-        # Pass fCommand to dial which will return it to self.setEntry
-        self._dial['commandData'] = [fCommand]
-        self._dial.set(value)
-        
-    def get(self):
-        return self._dial.get()
-
-    def setEntry(self, value, fCommand = 1):
-        self._entryVal.set(self.entryFormat % value)
-        # Execute command
-        if fCommand and (self['command'] != None):
-            apply(self['command'], [value] + self['commandData'])
-
-    def setEntryFormat(self):
-        self.entryFormat = "%." + "%df" % self['numDigits']
-        self.setEntry(self.get())
-        self._dial['numDigits'] = self['numDigits']
-
-    def validateEntryInput(self, event):
-        input = self._entryVal.get()
-        try:
-            self._onReturnPress()
-            self._entry.configure(background = self._entryBackground)
-            newValue = string.atof(input)
-            self.set(newValue)
-            self._onReturnRelease()
-        except ValueError:
-            self._entry.configure(background = 'Pink')
-
-    def _onReturnPress(self, *args):
-        """ User redefinable callback executed on <Return> in entry """
-        if self['preCallback']:
-            apply(self['preCallback'], self['callbackData'])
-
-    def _onReturnRelease(self, *args):
-        """ User redefinable callback executed on <Return> release in entry """
-        if self['postCallback']:
-            apply(self['postCallback'], self['callbackData'])
-
-    # Pass settings down to dial
-    def setCallbackData(self):
-        # Pass callback data down to dial
-        self._dial['callbackData'] = self['callbackData']
-
-    def setResetValue(self):
-        self._dial['resetValue'] = self['resetValue']
-
-    def setMin(self):
-        self._dial['min'] = self['min']
-
-    def setMax(self):
-        self._dial['max'] = self['max']
-
+            self._valuator.grid(row=0,col=2, padx = 2, pady = 2)
+            self.interior().columnconfigure(0, weight = 1)
+
+    def addValuatorPropertiesToDialog(self):
+        self.addPropertyToDialog(
+            'base',
+            { 'widget' : self._valuator,
+              'type' : 'real',
+              'help' : 'Dial value = base + delta * numRevs'})
+        self.addPropertyToDialog(
+            'delta',
+            { 'widget' : self._valuator,
+              'type' : 'real',
+              'help' : 'Dial value = base + delta * numRevs'})
+        self.addPropertyToDialog(
+            'numSegments',
+            { 'widget' : self._valuator,
+              'type' : 'integer',
+              'help' : 'Number of segments to divide dial into.'})
+            
+    def addValuatorMenuEntries(self):
+        # The popup menu
+        self._fSnap = IntVar()
+        self._fSnap.set(self['fSnap'])
+        self._popupMenu.add_checkbutton(label = 'Snap',
+                                        variable = self._fSnap,
+                                        command = self._setSnap)
+        self._fRollover = IntVar()
+        self._fRollover.set(self['fRollover'])
+        if self['fAdjustable']:
+            self._popupMenu.add_checkbutton(label = 'Rollover',
+                                            variable = self._fRollover,
+                                            command = self._setRollover)
+            
     def setBase(self):
-        self._dial['base'] = self['base']
+        """ Set Dial base value: value = base + delta * numRevs """
+        self._valuator['base'] = self['base']
         
     def setDelta(self):
-        self._dial['delta'] = self['delta']
+        """ Set Dial delta value: value = base + delta * numRevs """
+        self._valuator['delta'] = self['delta']
         
-    def setLabel(self):
-        self._label['text'] = self['text']
+    def _setSnap(self):
+        """ Menu command to turn Dial angle snap on/off """
+        self._valuator['fSnap'] = self._fSnap.get()
+
+    def setSnap(self):
+        """ Turn Dial angle snap on/off """
+        self._fSnap.set(self['fSnap'])
+        # Call menu command to send down to valuator
+        self._setSnap()
 
-    def setPreCallbackCmd(self):
-        self._dial['preCallback'] = self['preCallback']
+    def _setRollover(self):
+        """
+        Menu command to turn Dial rollover on/off (i.e. does value accumulate
+        every time you complete a revolution of the dial?)
+        """
+        self._valuator['fRollover'] = self._fRollover.get()
 
-    def setPostCallbackCmd(self):
-        self._dial['postCallback'] = self['postCallback']
+    def setRollover(self):
+        """ Turn Dial rollover (accumulation of a sum) on/off """
+        self._fRollover.set(self['fRollover'])
+        # Call menu command to send down to valuator
+        self._setRollover()
 
 
 class AngleDial(Dial):
@@ -200,7 +138,7 @@ class AngleDial(Dial):
         # Set the typical defaults for a 360 degree angle dial
         optiondefs = (
             ('delta',             360.0,          None),
-            ('dial_fRollover',    0,              None),
+            ('fRollover',         0,              None),
             ('dial_numSegments',  12,             None),
             )
         self.defineoptions(kw, optiondefs)
@@ -212,37 +150,25 @@ class AngleDial(Dial):
 
 
 class DialWidget(Pmw.MegaWidget):
-    sfBase = 3.0
-    sfDist = 15
-    deadband = 10
     def __init__(self, parent = None, **kw):
         #define the megawidget options
         INITOPT = Pmw.INITOPT
         optiondefs = (
-            ## Appearance
-            # Edge size of the dial
-            ('size',            DIAL_FULL_SIZE, INITOPT),
-            # Widget relief
+            # Appearance
+            ('style',           DIAL_FULL,      INITOPT),
+            ('size',            None,           INITOPT),
             ('relief',          SUNKEN,         self.setRelief),
-            # Widget borderwidth
             ('borderwidth',     2,              self.setBorderwidth),
-            ('background',      'white',        INITOPT),
+            ('background',      'white',        self.setBackground),
             # Number of segments the dial is divided into
             ('numSegments',     10,             self.setNumSegments),
-            ## Values
+            # Behavior
             # Initial value of dial, use self.set to change value
             ('value',           0.0,            INITOPT),
+            ('numDigits',       2,              self.setNumDigits),
+            # Dial specific options
             ('base',            0.0,            None),
             ('delta',           1.0,            None),
-            ('min',             None,           None),
-            ('max',             None,           None),
-            ('resolution',      None,           None),
-            ('numDigits',       2,              self.setNumDigits),
-            # Value dial jumps to on reset
-            ('resetValue',      0.0,            None),
-            ## Behavior
-            # Able to adjust max/min
-            ('fAdjustable',     1,              None),
             # Snap to angle on/off
             ('fSnap',           0,              None),
             # Do values rollover (i.e. accumulate) with multiple revolutions
@@ -263,24 +189,33 @@ class DialWidget(Pmw.MegaWidget):
         Pmw.MegaWidget.__init__(self, parent)
 
         # Set up some local and instance variables        
+        # Create the components
+        interior = self.interior()
 
-        # Running total which increments/decrements every time around dial
-        self.rollCount = 0
         # Current value
         self.value = self['value']
 
-        # Create the components
-        interior = self.interior()
-        dim = self['size']
+        # Running total which increments/decrements every time around dial
+        self.rollCount = 0
+
+        # Base dial size on style, if size not specified, 
+        if not self['size']:
+            if self['style'] == DIAL_FULL:
+                size = DIAL_FULL_SIZE
+            else:
+                size = DIAL_MINI_SIZE
+        else:
+            size = self['size']
+        
         # Radius of the dial
-        radius = self.radius = int(dim/2.0)
+        radius = self.radius = int(size/2.0)
         # Radius of the inner knob
         inner_radius = max(3,radius * INNER_SF)
 
         # The canvas 
         self._canvas = self.createcomponent('canvas', (), None,
                                             Canvas, (interior,),
-                                            width = dim, height = dim,
+                                            width = size, height = size,
                                             background = self['background'],
                                             highlightthickness = 0,
                                             scrollregion = (-radius,-radius,
@@ -302,53 +237,6 @@ class DialWidget(Pmw.MegaWidget):
                                  fill = '#A0A0A0',
                                  tags = ('knob',))
 
-        # A Dictionary of dictionaries used for the popup property dialog
-        self.propertyDict = {
-            'min' : { 'widget' : self,
-                      'type' : 'real',
-                      'fNone' : 1,
-                      'help' : 'Minimum allowable dial value, Enter None for no minimum'},
-            'max' : { 'widget' : self,
-                      'type' : 'real',
-                      'fNone' : 1,
-                      'help' : 'Maximum allowable dial value, Enter None for no maximum'},
-            'base' : { 'widget' : self,
-                       'type' : 'real',
-                       'help' : 'Dial value = base + delta * numRevs'},
-            'delta' : { 'widget' : self,
-                        'type' : 'real',
-                        'help' : 'Dial value = base + delta * numRevs'},
-            'numSegments' : { 'widget' : self,
-                              'type' : 'integer',
-                              'help' : 'Number of segments to divide dial into'},
-            'resetValue' : { 'widget' : self,
-                             'type' : 'real',
-                             'help' : 'Enter value to set dial to on reset.'}
-            }
-        self.propertyList = ['min', 'max', 'base', 'delta',
-                             'resetValue', 'numSegments']
-
-        # The popup menu
-        self._popupMenu = Menu(interior, tearoff = 0)
-        self._fSnap = IntVar()
-        self._fSnap.set(self['fSnap'])
-        self._popupMenu.add_checkbutton(label = 'Snap',
-                                        variable = self._fSnap,
-                                        command = self.setSnap)
-        self._fRollover = IntVar()
-        self._fRollover.set(self['fRollover'])
-        if self['fAdjustable']:
-            self._popupMenu.add_checkbutton(label = 'Rollover',
-                                            variable = self._fRollover,
-                                            command = self.setRollover)
-            self._popupMenu.add_command(
-                label = 'Properties...',
-                command = self.popupPropertiesDialog)
-        self._popupMenu.add_command(label = 'Zero Dial',
-                                    command = self.zero)
-        self._popupMenu.add_command(label = 'Reset Dial',
-                                    command = self.reset)
-
         # Add event bindings
         self._canvas.tag_bind('dial', '<ButtonPress-1>', self.mouseDown)
         self._canvas.tag_bind('dial', '<B1-Motion>', self.mouseMotion)
@@ -356,12 +244,10 @@ class DialWidget(Pmw.MegaWidget):
                               self.shiftMouseMotion)
         self._canvas.tag_bind('dial', '<ButtonRelease-1>', self.mouseUp)
         self._canvas.tag_bind('knob', '<ButtonPress-1>', self.knobMouseDown)
-        self._canvas.tag_bind('knob', '<B1-Motion>', self.knobMouseMotion)
+        self._canvas.tag_bind('knob', '<B1-Motion>', self.updateDialSF)
         self._canvas.tag_bind('knob', '<ButtonRelease-1>', self.knobMouseUp)
         self._canvas.tag_bind('knob', '<Enter>', self.highlightKnob)
         self._canvas.tag_bind('knob', '<Leave>', self.restoreKnob)
-        self._canvas.bind('<Double-ButtonPress-1>', self.mouseReset)
-        self._canvas.bind('<ButtonPress-3>', self.popupDialMenu)
 
         # Make sure input variables processed 
         self.initialiseoptions(DialWidget)
@@ -371,49 +257,16 @@ class DialWidget(Pmw.MegaWidget):
         self.set(value, fCommand = 1)
         Set dial to new value, execute command if fCommand == 1
         """
-        # Clamp value
-        if self['min'] is not None:
-            if value < self['min']:
-                value = self['min']
-        if self['max'] is not None:
-            if value > self['max']:
-                value = self['max']
-        # Round by resolution
-        if self['resolution'] is not None:
-            value = round(value / self['resolution']) * self['resolution']
         # Adjust for rollover
         if not self['fRollover']:
             if value > self['delta']:
                 self.rollCount = 0
             value = self['base'] + ((value - self['base']) % self['delta'])
-        # Update indicator to reflect adjusted value
-        self.updateIndicator(value)
         # Send command if any
         if fCommand and (self['command'] != None):
             apply(self['command'], [value] + self['commandData'])
         # Record value
         self.value = value
-    
-    # Set floater to zero
-    def zero(self):
-        """
-        self.reset()
-        Set dial to zero
-        """
-        self.set(0.0)
-
-    # Reset dial to reset value
-    def reset(self):
-        """
-        self.reset()
-        Reset dial to reset value
-        """
-        self.set(self['resetValue'])
-
-    def mouseReset(self,event):
-        # If not over any canvas item
-        if not self._canvas.find_withtag(CURRENT):
-            self.reset()
         
     def get(self):
         """
@@ -486,10 +339,10 @@ class DialWidget(Pmw.MegaWidget):
     def knobMouseDown(self,event):
         self._onButtonPress()
         self.knobSF = 0.0
-        t = taskMgr.add(self.knobComputeVelocity, 'cv')
-        t.lastTime = globalClock.getFrameTime()
+        self.updateTask = taskMgr.add(self.updateDialTask, 'updateDial')
+        self.updateTask.lastTime = globalClock.getFrameTime()
 
-    def knobComputeVelocity(self, state):
+    def updateDialTask(self, state):
         # Update value
         currT = globalClock.getFrameTime()
         dt = currT - state.lastTime
@@ -497,35 +350,39 @@ class DialWidget(Pmw.MegaWidget):
         state.lastTime = currT
         return Task.cont
 
-    def knobMouseMotion(self, event):
-        # What is the current knob angle
-        self.knobSF = self.computeKnobSF(event)
-
-    def computeKnobSF(self, event):
+    def updateDialSF(self, event):
         x = self._canvas.canvasx(event.x)
         y = self._canvas.canvasy(event.y)
-        offset = max(0, abs(x) - DialWidget.deadband)
+        offset = max(0, abs(x) - Valuator.deadband)
         if offset == 0:
             return 0
-        sf = math.pow(DialWidget.sfBase,
-                      self.minExp + offset/DialWidget.sfDist)
+        sf = math.pow(Valuator.sfBase,
+                      self.minExp + offset/Valuator.sfDist)
         if x > 0:
-            return sf
+            self.knobSF = sf
         else:
-            return -sf
+            self.knobSF = -sf
 
     def knobMouseUp(self, event):
-        taskMgr.remove('cv')
+        taskMgr.remove(self.updateTask)
         self.knobSF = 0.0
         self._onButtonRelease()
 
-    def highlightKnob(self, event):
-        self._canvas.itemconfigure('knob', fill = 'black')
-
-    def restoreKnob(self, event):
-        self._canvas.itemconfigure('knob', fill = '#A0A0A0')
+    def setNumDigits(self):
+        # Set minimum exponent to use in velocity task
+        self.minExp = math.floor(-self['numDigits']/
+                                 math.log10(Valuator.sfBase))        
 
     # Methods to modify dial characteristics    
+    def setRelief(self):
+        self.interior()['relief'] = self['relief']
+
+    def setBorderwidth(self):
+        self.interior()['borderwidth'] = self['borderwidth']
+
+    def setBackground(self):
+        self._canvas['background'] = self['background']
+
     def setNumSegments(self):
         self._canvas.delete('ticks')
         # Based upon input snap angle, how many ticks
@@ -550,44 +407,13 @@ class DialWidget(Pmw.MegaWidget):
             self._canvas.create_line(startx, starty, endx, endy,
                                      tags = ('ticks','dial'))
 
-    def setRelief(self):
-        self.interior()['relief'] = self['relief']
-
-    def setBorderwidth(self):
-        self.interior()['borderwidth'] = self['borderwidth']
-
-    def setNumDigits(self):
-        # Set minimum exponent to use in velocity task
-        self.minExp = math.floor(-self['numDigits']/
-                                 math.log10(DialWidget.sfBase))        
-
-    # The following methods are used to handle the popup menu
-    def popupDialMenu(self,event):
-        self._popupMenu.post(event.widget.winfo_pointerx(),
-                             event.widget.winfo_pointery())
+    def highlightKnob(self, event):
+        self._canvas.itemconfigure('knob', fill = 'black')
 
-    # Turn angle snap on/off
-    def setSnap(self):
-        self['fSnap'] = self._fSnap.get()
+    def restoreKnob(self, event):
+        self._canvas.itemconfigure('knob', fill = '#A0A0A0')
 
-    # Turn rollover (accumulation of a sum) on/off
-    def setRollover(self):
-        self['fRollover'] = self._fRollover.get()
-
-    # This handles the popup dial min dialog
-    def popupPropertiesDialog(self):
-        # Popup dialog to adjust widget properties
-        WidgetPropertiesDialog.WidgetPropertiesDialog(
-            self.propertyDict,
-            propertyList = self.propertyList,
-            title = 'Dial Widget Properties',
-            parent = self._canvas)
-
-    def addPropertyToDialog(self, property, pDict):
-        self.propertyDict[property] = pDict
-        self.propertyList.append(property)
-            
-    # User callbacks
+    # To call user callbacks
     def _onButtonPress(self, *args):
         """ User redefinable callback executed on button press """
         if self['preCallback']:

+ 1 - 0
direct/src/tkwidgets/EntryScale.py

@@ -19,6 +19,7 @@ class EntryScale(Pmw.MegaWidget):
 
         # Define the megawidget options.
         optiondefs = (
+            ('state',        None,          None),
             ('value',        0.0,           Pmw.INITOPT),
             ('resolution',          0.001,         None),
             ('command',             None,          None),

+ 85 - 312
direct/src/tkwidgets/Floater.py

@@ -4,80 +4,88 @@ Floater Class: Velocity style controller for floating point values with
 """
 from PandaObject import *
 from Tkinter import *
+from Valuator import *
 import Pmw
-import WidgetPropertiesDialog
+import Task
+import math
 import string
 
 globalClock = ClockObject.getGlobalClock()
 
+FLOATER_WIDTH = 22
+FLOATER_HEIGHT = 18
 
-FLOATER_FULL = 'full'
-FLOATER_MINI = 'mini'
+class Floater(Valuator):
+    def __init__(self, parent = None, **kw):
+        optiondefs = ()
+        self.defineoptions(kw, optiondefs)
+        # Initialize the superclass
+        Valuator.__init__(self, parent)
+        self.initialiseoptions(Floater)
+        
+    def createValuator(self):
+        self._valuator = self.createcomponent('valuator',
+                                              (('floater', 'valuator'),),
+                                              None,
+                                              FloaterWidget,
+                                              (self.interior(),),
+                                              command = self.setEntry,
+                                              value = self['value'])
+        self._valuator._canvas.bind('<Double-ButtonPress-1>', self.mouseReset)
+
+    def packValuator(self):
+        # Position components
+        if self._label:
+            self._label.grid(row=0,col=0, sticky = EW)
+        self._entry.grid(row=0,col=1, sticky = EW)
+        self._valuator.grid(row=0,col=2, padx = 2, pady = 2)
+        self.interior().columnconfigure(0, weight = 1)
 
-FLOATER_WIDTH = 25
-FLOATER_HEIGHT = 20
 
 class FloaterWidget(Pmw.MegaWidget):
-    sfBase = 3.0
-    sfDist = 15
-    deadband = 10
     def __init__(self, parent = None, **kw):
         #define the megawidget options
         INITOPT = Pmw.INITOPT
         optiondefs = (
-            ## Appearance
-            # Edge size of the floater
+            # Appearance
             ('width',           FLOATER_WIDTH,  INITOPT),
             ('height',          FLOATER_HEIGHT, INITOPT),
-            # Color
-            ('background',      'white',        INITOPT),
-            # Widget relief
             ('relief',          SUNKEN,         self.setRelief),
-            # Widget borderwidth
             ('borderwidth',     2,              self.setBorderwidth),
-            ## Values
+            ('background',      'white',        self.setBackground),
+            # Behavior
             # Initial value of floater, use self.set to change value
             ('value',           0.0,            INITOPT),
-            ('min',             None,           None),
-            ('max',             None,           None),
-            ('resolution',      None,           None),
             ('numDigits',       2,              self.setNumDigits),
-            # Value floater jumps to on reset
-            ('resetValue',      0.0,            None),
-            ## Behavior
-            # Able to adjust max/min
-            ('fAdjustable',     1,              None),
             # Command to execute on floater 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),
+            ('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)
 
-        #print 'FLOATER WIDGET', self['resetValue']
-        
         # Initialize the superclass
         Pmw.MegaWidget.__init__(self, parent)
 
         # Set up some local and instance variables        
-        # Current value
-        self.value = self['value']
-
         # Create the components
         interior = self.interior()
 
+        # Current value
+        self.value = self['value']
+
         # The canvas
         width = self['width']
         height = self['height']
         self._canvas = self.createcomponent('canvas', (), None,
                                             Canvas, (interior,),
-                                            width = self['width'],
-                                            height = self['height'],
+                                            width = width,
+                                            height = height,
                                             background = self['background'],
                                             highlightthickness = 0,
                                             scrollregion = (-width/2.0,
@@ -96,46 +104,12 @@ class FloaterWidget(Pmw.MegaWidget):
                                     fill = '#A0A0A0',
                                     tags = ('floater',))
 
-        # A Dictionary of dictionaries for the popup property dialog
-        self.propertyDict = {
-            'min' : { 'widget' : self,
-                      'type' : 'real',
-                      'fNone' : 1,
-                      'help' : 'Minimum allowable floater value, Enter None for no minimum'},
-            'max' : { 'widget' : self,
-                      'type' : 'real',
-                      'fNone' : 1,
-                      'help' : 'Maximum allowable floater value, Enter None for no maximum'},
-            'resetValue' : { 'widget' : self,
-                             'type' : 'real',
-                             'help' : 'Enter value to set floater to on reset.'}
-            }
-        self.propertyList = ['min', 'max', 'resetValue']
-
-        # The popup menu
-        self._popupMenu = Menu(interior, tearoff = 0)
-
-        if self['fAdjustable']:
-            self._popupMenu.add_command(
-                label = 'Properties...',
-                command = self.popupPropertiesDialog)
-        self._popupMenu.add_command(label = 'Zero Floater',
-                                    command = self.zero)
-        self._popupMenu.add_command(label = 'Reset Floater',
-                                    command = self.reset)
-
         # Add event bindings
         self._canvas.bind('<ButtonPress-1>', self.mouseDown)
-        self._canvas.bind('<B1-Motion>', self.mouseMotion)
+        self._canvas.bind('<B1-Motion>', self.updateFloaterSF)
         self._canvas.bind('<ButtonRelease-1>', self.mouseUp)
-        self._canvas.bind('<ButtonPress-3>', self.popupFloaterMenu)
-        self._canvas.bind('<Double-ButtonPress-1>', self.mouseReset)
-        self._canvas.bind('<ButtonPress-3>', self.popupFloaterMenu)
-        self._canvas.bind('<Enter>', self.highlightIcon)
-        self._canvas.bind('<Leave>', self.restoreIcon)
-        self._canvas.tag_bind('floater', '<ButtonPress-1>', self.mouseDown)
-        self._canvas.tag_bind('floater', '<B1-Motion>', self.mouseMotion)
-        self._canvas.tag_bind('floater', '<ButtonRelease-1>', self.mouseUp)
+        self._canvas.bind('<Enter>', self.highlightWidget)
+        self._canvas.bind('<Leave>', self.restoreWidget)
 
         # Make sure input variables processed 
         self.initialiseoptions(FloaterWidget)
@@ -145,44 +119,16 @@ class FloaterWidget(Pmw.MegaWidget):
         self.set(value, fCommand = 1)
         Set floater to new value, execute command if fCommand == 1
         """
-        # Clamp value
-        if self['min'] is not None:
-            if value < self['min']:
-                value = self['min']
-        if self['max'] is not None:
-            if value > self['max']:
-                value = self['max']
-        # Round by resolution
-        if self['resolution'] is not None:
-            value = round(value / self['resolution']) * self['resolution']
-
         # Send command if any
         if fCommand and (self['command'] != None):
             apply(self['command'], [value] + self['commandData'])
         # Record value
         self.value = value
-    
-    # Set floater to zero
-    def zero(self):
-        """
-        self.reset()
-        Set floater to zero
-        """
-        self.set(0.0)
-
-    # Reset floater to reset value
-    def reset(self):
-        """
-        self.reset()
-        Reset floater to reset value
-        """
-        self.set(self['resetValue'])
 
-    def mouseReset(self,event):
-        # If not over any canvas item
-        #if not self._canvas.find_withtag(CURRENT):
-        self.reset()
-        
+    def updateIndicator(self, value):
+        # Nothing visible to update on this type of widget
+        pass
+    
     def get(self):
         """
         self.get()
@@ -193,46 +139,56 @@ class FloaterWidget(Pmw.MegaWidget):
     ## Canvas callback functions
     # Floater velocity controller
     def mouseDown(self,event):
-        self._onButtonPress()
+        """ Begin mouse interaction """
+        # Exectute user redefinable callback function (if any)
+        if self['preCallback']:
+            apply(self['preCallback'], self['callbackData'])
         self.velocitySF = 0.0
-        t = taskMgr.add(self.computeVelocity, 'cv')
-        t.lastTime = globalClock.getFrameTime()
+        self.updateTask = taskMgr.add(self.updateFloaterTask,
+                                        'updateFloater')
+        self.updateTask.lastTime = globalClock.getFrameTime()
 
-    def computeVelocity(self, state):
-        # Update value
+    def updateFloaterTask(self, state):
+        """
+        Update floaterWidget value based on current scaleFactor
+        Adjust for time to compensate for fluctuating frame rates
+        """
         currT = globalClock.getFrameTime()
         dt = currT - state.lastTime
         self.set(self.value + self.velocitySF * dt)
         state.lastTime = currT
         return Task.cont
 
-    def mouseMotion(self, event):
-        # What is the current knob angle
-        self.velocitySF = self.computeVelocitySF(event)
-
-    def computeVelocitySF(self, event):
+    def updateFloaterSF(self, event):
+        """
+        Update velocity scale factor based of mouse distance from origin
+        """
         x = self._canvas.canvasx(event.x)
         y = self._canvas.canvasy(event.y)
-        offset = max(0, abs(x) - FloaterWidget.deadband)
+        offset = max(0, abs(x) - Valuator.deadband)
         if offset == 0:
             return 0
-        sf = math.pow(FloaterWidget.sfBase,
-                      self.minExp + offset/FloaterWidget.sfDist)
+        sf = math.pow(Valuator.sfBase,
+                      self.minExp + offset/Valuator.sfDist)
         if x > 0:
-            return sf
+            self.velocitySF = sf
         else:
-            return -sf
+            self.velocitySF = -sf
 
     def mouseUp(self, event):
-        taskMgr.remove('cv')
+        taskMgr.remove(self.updateTask)
         self.velocitySF = 0.0
-        self._onButtonRelease()
-
-    def highlightIcon(self, event):
-        self._canvas.itemconfigure('floater', fill = 'black')
+        # Execute user redefinable callback function (if any)
+        if self['postCallback']:
+            apply(self['postCallback'], self['callbackData'])
 
-    def restoreIcon(self, event):
-        self._canvas.itemconfigure('floater', fill = '#A0A0A0')
+    def setNumDigits(self):
+        """
+        Adjust minimum exponent to use in velocity task based
+        upon the number of digits to be displayed in the result
+        """
+        self.minExp = math.floor(-self['numDigits']/
+                                 math.log10(Valuator.sfBase))        
 
     # Methods to modify floater characteristics    
     def setRelief(self):
@@ -241,194 +197,14 @@ class FloaterWidget(Pmw.MegaWidget):
     def setBorderwidth(self):
         self.interior()['borderwidth'] = self['borderwidth']
 
-    def setNumDigits(self):
-        # Set minimum exponent to use in velocity task
-        self.minExp = math.floor(-self['numDigits']/
-                                 math.log10(FloaterWidget.sfBase))        
-
-    # The following methods are used to handle the popup menu
-    def popupFloaterMenu(self,event):
-        self._popupMenu.post(event.widget.winfo_pointerx(),
-                             event.widget.winfo_pointery())
-
-    # Popup dialog to adjust widget properties
-    def popupPropertiesDialog(self):
-        WidgetPropertiesDialog.WidgetPropertiesDialog(
-            self.propertyDict,
-            propertyList = self.propertyList,
-            title = 'Floater Widget Properties',
-            parent = self._canvas)
-
-    def addPropertyToDialog(self, property, pDict):
-        self.propertyDict[property] = pDict
-        self.propertyList.append(property)
-            
-    # User callbacks
-    def _onButtonPress(self, *args):
-        """ User redefinable callback executed on button press """
-        if self['preCallback']:
-            apply(self['preCallback'], self['callbackData'])
+    def setBackground(self):
+        self._canvas['background'] = self['background']
 
-    def _onButtonRelease(self, *args):
-        """ User redefinable callback executed on button release """
-        if self['postCallback']:
-            apply(self['postCallback'], self['callbackData'])
-
-
-class Floater(Pmw.MegaWidget):
-    def __init__(self, parent = None, **kw):
-        #define the megawidget options
-        INITOPT = Pmw.INITOPT
-        optiondefs = (
-            # Widget relief
-            ('relief',            GROOVE,         None),
-            # Widget borderwidth
-            ('borderwidth',       2,              None),
-            ('value',             0.0,            INITOPT),
-            ('resetValue',        0.0,            self.setResetValue),
-            ('text',              'Floater',      self.setLabel),
-            ('numDigits',         2,              self.setEntryFormat),
-            ('command',           None,           None),
-            ('commandData',       [],             None),
-            ('min',               None,           self.setMin),
-            ('max',               None,           self.setMax),
-            # Callbacks to execute when updating widget's value
-            ('preCallback',     None,           self.setButtonPressCmd),
-            ('postCallback',   None,           self.setButtonReleaseCmd),
-            # Extra data to be passed to callback function, needs to be a list
-            ('callbackData',      [],             self.setCallbackData),
-            )
-        self.defineoptions(kw, optiondefs)
-        
-        # Initialize the superclass
-        Pmw.MegaWidget.__init__(self, parent)
-
-        # Create the components
-        interior = self.interior()
-        interior.configure(relief = self['relief'], bd = self['borderwidth'])
-        
-        # The Floater
-        #print self['text'], self['value'], self['resetValue']
-        self._floater = self.createcomponent('floater', (), None,
-                                             FloaterWidget, (interior,),
-                                             command = self.setEntry,
-                                             resetValue = self['value'],
-                                             value = self['value'])
-
-        if not kw.has_key('resetValue'):
-            self['resetValue'] = self['value']
-        self._floater.addPropertyToDialog(
-            'text',
-            {'widget' : self,
-             'type' : 'string',
-             'help' : 'Enter label text for Floater.'
-             }
-            )
-        self._floater.addPropertyToDialog(
-            'numDigits',
-            {'widget' : self,
-             'type' : 'integer',
-             'help' : 'Enter number of digits after decimal point.'
-             }
-            )
-
-        # The Label
-        self._label = self.createcomponent('label', (), None,
-                                           Label, (interior,),
-                                           text = self['text'],
-                                           font = ('MS Sans Serif',12,'bold'),
-                                           anchor = CENTER)
-        self._label.bind('<ButtonPress-3>', self._floater.popupFloaterMenu)
-
-        # The entry
-        self._entryVal = StringVar()
-        self._entry = self.createcomponent('entry', (), None,
-                                           Entry, (interior,),
-                                           justify = RIGHT,
-                                           width = 12,
-                                           textvariable = self._entryVal)
-        self._entry.bind('<Return>', self.validateEntryInput)
-        self._entry.bind('<ButtonPress-3>', self._floater.popupFloaterMenu)
-        self._entryBackground = self._entry.cget('background')
-
-        # Position components
-        self._label.grid(row=0,col=0, sticky = EW)
-        self._entry.grid(row=0,col=1, sticky = EW)
-        self._floater.grid(row=0,col=2, padx = 2, pady = 2)
-        interior.columnconfigure(0, weight = 1)
-
-        # Make sure input variables processed
-        self.fInit = 0
-        self.initialiseoptions(Floater)
-        self.fInit = 1
-
-    def set(self, value, fCommand = 1):
-        # Pass fCommand to user specified data (to control if command
-        # is executed or not) to floater which will return it to self.setEntry
-        self._floater['commandData'] = [fCommand]
-        self._floater.set(value)
-        # Restore commandData to 1 so that interaction via floater widget
-        # will result in command being executed, otherwise a set with
-        # commandData == 0 will stick and commands will not be executed
-        self._floater['commandData'] = [1]
-        
-    def get(self):
-        return self._floater.get()
-
-    def setEntry(self, value, fCommand = 1):
-        self._entryVal.set(self.entryFormat % value)
-        # Execute command
-        if self.fInit and fCommand and (self['command'] != None):
-            apply(self['command'], [value] + self['commandData'])
-
-    def setEntryFormat(self):
-        self.entryFormat = "%." + "%df" % self['numDigits']
-        self.setEntry(self.get())
-        self._floater['numDigits'] = self['numDigits']
-
-    def validateEntryInput(self, event):
-        input = self._entryVal.get()
-        try:
-            self._onReturnPress()
-            self._entry.configure(background = self._entryBackground)
-            newValue = string.atof(input)
-            self.set(newValue)
-            self._onReturnRelease()
-        except ValueError:
-            self._entry.configure(background = 'Pink')
-
-    def _onReturnPress(self, *args):
-        """ User redefinable callback executed on <Return> in entry """
-        if self['preCallback']:
-            apply(self['preCallback'], self['callbackData'])
-
-    def _onReturnRelease(self, *args):
-        """ User redefinable callback executed on <Return> release in entry """
-        if self['postCallback']:
-            apply(self['postCallback'], self['callbackData'])
-
-    # Pass settings down to floater
-    def setCallbackData(self):
-        # Pass callback data down to floater
-        self._floater['callbackData'] = self['callbackData']
-
-    def setResetValue(self):
-        self._floater['resetValue'] = self['resetValue']
-
-    def setMin(self):
-        self._floater['min'] = self['min']
-
-    def setMax(self):
-        self._floater['max'] = self['max']
-
-    def setLabel(self):
-        self._label['text'] = self['text']
-
-    def setButtonPressCmd(self):
-        self._floater['preCallback'] = self['preCallback']
+    def highlightWidget(self, event):
+        self._canvas.itemconfigure('floater', fill = 'black')
 
-    def setButtonReleaseCmd(self):
-        self._floater['postCallback'] = self['postCallback']
+    def restoreWidget(self, event):
+        self._canvas.itemconfigure('floater', fill = '#A0A0A0')
 
 
 class FloaterGroup(Pmw.MegaToplevel):
@@ -568,9 +344,6 @@ if __name__ == '__main__':
     # Starting value for floater    
     mega1['value'] = 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

+ 270 - 0
direct/src/tkwidgets/Slider.py

@@ -0,0 +1,270 @@
+"""
+Slider Class: Velocity style controller for floating point values with
+               a label, entry (validated), and min/max slider
+"""
+from Tkinter import *
+from Valuator import *
+import Pmw
+import Task
+import math
+import string
+import operator
+from PandaModules import ClockObject
+
+SLIDER_FULL = 'full'
+SLIDER_MINI = 'mini'
+
+SLIDER_FULL_WIDTH = 50
+SLIDER_FULL_HEIGHT = 25
+
+SLIDER_MINI_WIDTH = 22
+SLIDER_MINI_HEIGHT = 18
+
+globalClock = ClockObject.getGlobalClock()
+
+
+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 = (
+            ('style',             SLIDER_FULL,    INITOPT),
+            )
+        self.defineoptions(kw, optiondefs)
+        Valuator.__init__(self, parent)
+        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._canvas.bind('<Double-ButtonPress-1>', self.mouseReset)
+
+    def packValuator(self):
+        if self['style'] == SLIDER_FULL:
+            if self._label:
+                self._label.grid(row = 0, col = 0, sticky = EW)
+            self._entry.grid(row = 0, col = 1, sticky = EW)
+            self._valuator.grid(row = 1, columnspan = 2,
+                                padx = 2, pady = 2)
+            self.interior().columnconfigure(0, weight = 1)
+        else:
+            if self._label:
+                self._label.grid(row=0,col=0, sticky = EW)
+            self._entry.grid(row=0,col=1, sticky = EW)
+            self._valuator.grid(row=0,col=2, padx = 2, pady = 2)
+            self.interior().columnconfigure(0, weight = 1)
+
+
+class SliderWidget(Pmw.MegaWidget):
+    def __init__(self, parent = None, **kw):
+        #define the megawidget options
+        INITOPT = Pmw.INITOPT
+        optiondefs = (
+            # Appearance
+            ('style',           SLIDER_MINI,    INITOPT),
+            ('width',           SLIDER_MINI_WIDTH,   INITOPT),
+            ('height',          SLIDER_MINI_HEIGHT,  INITOPT),
+            ('relief',          SUNKEN,         self.setRelief),
+            ('borderwidth',     2,              self.setBorderwidth),
+            ('background',      'white',        self.setBackground),
+            # Behavior
+            # 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)
+        
+        # Initialize the superclass
+        Pmw.MegaWidget.__init__(self, parent)
+
+        # Set up some local and instance variables        
+        # Create the components
+        interior = self.interior()
+
+        # Current value
+        self.value = self['value']
+
+        # Base slider size on style, if size not specified, 
+        if not self['width']:
+            if self['style'] == SLIDER_FULL_SIZE:
+                width = SLIDER_FULL_WIDTH
+            else:
+                width = SLIDER_MINI_WIDTH
+        else:
+            width = self['width']
+
+        if not self['height']:
+            if self['style'] == SLIDER_FULL_SIZE:
+                height = SLIDER_FULL_HEIGHT
+            else:
+                height = SLIDER_MINI_HEIGHT
+        else:
+            height = self['height']
+
+        halfWidth = width/2.0
+        left = -(halfWidth - 2)
+        right = halfWidth - 2
+        halfHeight = height/2.0
+        top = -(halfHeight - 2)
+        bottom = halfHeight - 2
+
+        print left, right,bottom,top
+        
+        # The canvas 
+        self._canvas = self.createcomponent('canvas', (), None,
+                                            Canvas, (interior,),
+                                            width = width,
+                                            height = height,
+                                            background = self['background'],
+                                            highlightthickness = 0,
+                                            scrollregion = (-halfWidth,
+                                                            -halfHeight,
+                                                            halfWidth,
+                                                            halfHeight))
+        self._canvas.pack(expand = 1, fill = BOTH)
+
+        self._canvas.create_polygon(left,top,
+                                    0, bottom,
+                                    right, top,
+                                    fill = '#A0A0A0',
+                                    tags = ('slider',))
+
+        # The indicator
+        self._canvas.create_line(left, bottom,
+                                 right, bottom,
+                                 width = 2)
+
+        # Add event bindings
+        self._canvas.bind('<ButtonPress-1>', self.mouseDown)
+        self._canvas.bind('<B1-Motion>', self.updateSliderSF)
+        self._canvas.bind('<ButtonRelease-1>', self.mouseUp)
+        self._canvas.bind('<Enter>', self.highlightWidget)
+        self._canvas.bind('<Leave>', self.restoreWidget)
+
+        # Make sure input variables processed 
+        self.initialiseoptions(SliderWidget)
+
+    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 updateIndicator(self, value):
+        # Nothing visible to update on this type of widget
+        pass
+    
+    def get(self):
+        """
+        self.get()
+        Get current slider value
+        """
+        return self.value
+
+    ## Canvas callback functions
+    # Slider velocity controller
+    def mouseDown(self,event):
+        """ Begin mouse interaction """
+        # Exectute user redefinable callback function (if any)
+        if self['preCallback']:
+            apply(self['preCallback'], self['callbackData'])
+        self.velocitySF = 0.0
+        self.updateTask = taskMgr.add(self.updateSliderTask,
+                                        'updateSlider')
+        self.updateTask.lastTime = globalClock.getFrameTime()
+
+    def updateSliderTask(self, state):
+        """
+        Update sliderWidget value based on current scaleFactor
+        Adjust for time to compensate for fluctuating frame rates
+        """
+        currT = globalClock.getFrameTime()
+        dt = currT - state.lastTime
+        self.set(self.value + self.velocitySF * dt)
+        state.lastTime = currT
+        return Task.cont
+
+    def updateSliderSF(self, event):
+        """
+        Update velocity scale factor based of mouse distance from origin
+        """
+        x = self._canvas.canvasx(event.x)
+        y = self._canvas.canvasy(event.y)
+        offset = max(0, abs(x) - Valuator.deadband)
+        if offset == 0:
+            return 0
+        sf = math.pow(Valuator.sfBase,
+                      self.minExp + offset/Valuator.sfDist)
+        if x > 0:
+            self.velocitySF = sf
+        else:
+            self.velocitySF = -sf
+
+    def mouseUp(self, event):
+        taskMgr.remove(self.updateTask)
+        self.velocitySF = 0.0
+        # Execute user redefinable callback function (if any)
+        if self['postCallback']:
+            apply(self['postCallback'], self['callbackData'])
+
+    def setNumDigits(self):
+        """
+        Adjust minimum exponent to use in velocity task based
+        upon the number of digits to be displayed in the result
+        """
+        self.minExp = math.floor(-self['numDigits']/
+                                 math.log10(Valuator.sfBase))        
+
+    # Methods to modify slider characteristics    
+    def setRelief(self):
+        self.interior()['relief'] = self['relief']
+
+    def setBorderwidth(self):
+        self.interior()['borderwidth'] = self['borderwidth']
+
+    def setBackground(self):
+        self._canvas['background'] = self['background']
+
+    def highlightWidget(self, event):
+        self._canvas.itemconfigure('slider', fill = 'black')
+
+    def restoreWidget(self, event):
+        self._canvas.itemconfigure('slider', fill = '#A0A0A0')
+  
+if __name__ == '__main__':
+    tl = Toplevel()
+    d = Slider(tl)
+    d2 = Slider(tl, slider_numSegments = 12, max = 360,
+              slider_fRollover = 0, value = 180)
+    d3 = Slider(tl, slider_numSegments = 12, max = 90, min = -90,
+              slider_fRollover = 0)
+    d4 = Slider(tl, slider_numSegments = 16, max = 256,
+              slider_fRollover = 0)
+    d.pack(expand = 1, fill = X)
+    d2.pack(expand = 1, fill = X)
+    d3.pack(expand = 1, fill = X)
+    d4.pack(expand = 1, fill = X)

+ 536 - 0
direct/src/tkwidgets/Valuator.py

@@ -0,0 +1,536 @@
+from PandaObject import *
+from Tkinter import *
+import Pmw
+import WidgetPropertiesDialog
+import string
+
+class Valuator(Pmw.MegaWidget):
+    sfBase = 3.0
+    sfDist = 7
+    deadband = 5
+    """ Base class for widgets used to interactively adjust numeric values """
+    def __init__(self, parent = None, **kw):
+        #define the megawidget options
+        INITOPT = Pmw.INITOPT
+        optiondefs = (
+            ('state',             NORMAL,         self.setState),
+            # Widget appearance
+            ('relief',            GROOVE,         None),
+            ('borderwidth',       2,              None),
+            ('text',              'Valuator',     self.setLabel),
+            # Initial and reset values
+            ('value',             0.0,            INITOPT),
+            ('resetValue',        0.0,            None),
+            # Behavior
+            ('min',               None,           None),
+            ('max',               None,           None),
+            ('resolution',        None,           None),
+            ('numDigits',         2,              self.setEntryFormat),
+            # Enable/disable popup menu
+            ('fAdjustable',       1,              None),
+            # Actions
+            ('command',           None,           None),
+            ('commandData',       [],             None),
+            ('fCommandOnInit',    0,              INITOPT),
+            # Callbacks to execute when updating widget's value
+            ('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)
+        
+        # Initialize the superclass
+        Pmw.MegaWidget.__init__(self, parent)
+
+        # Current adjusted (for min/max/resolution) value
+        self.adjustedValue = 0.0
+
+        # Create the components
+        interior = self.interior()
+        interior.configure(relief = self['relief'], bd = self['borderwidth'])
+        
+        # The Valuator
+        self.createValuator()
+        # Set valuator callbacks for mouse start/stop
+        self._valuator['preCallback'] = self._mouseDown
+        self._valuator['postCallback'] = self._mouseUp
+
+        # The Label
+        if self['text'] is not None:
+            self._label = self.createcomponent('label', (), None,
+                                               Label, (interior,),
+                                               text = self['text'],
+                                               font = ('MS Sans Serif',12),
+                                               anchor = CENTER)
+        else:
+            self._label = None
+
+        # The entry
+        self._entryVal = StringVar()
+        self._entry = self.createcomponent('entry', (), None,
+                                           Entry, (interior,),
+                                           justify = RIGHT,
+                                           width = 12,
+                                           textvariable = self._entryVal)
+        self._entry.bind('<Return>', self.validateEntryInput)
+        self._entryBackground = self._entry.cget('background')
+
+        # Pack Valuator Widget
+        self.packValuator()
+
+        # Set reset value if none specified
+        if not kw.has_key('resetValue'):
+            self['resetValue'] = self['value']
+
+        if self['fAdjustable']:
+            # The popup menu
+            self._popupMenu = Menu(interior, tearoff = 0)
+            self.addValuatorMenuEntries()
+            self._popupMenu.add_command(label = 'Reset',
+                                        command = self.reset)
+            self._popupMenu.add_command(label = 'Set to Zero',
+                                        command = self.zero)
+            self._popupMenu.add_command(
+                label = 'Properties...',
+                command = self._popupPropertiesDialog)
+            # Add key bindings
+            if self._label:
+                self._label.bind(
+                    '<ButtonPress-3>', self._popupValuatorMenu)
+            self._entry.bind(
+                '<ButtonPress-3>', self._popupValuatorMenu)
+            self._valuator._canvas.bind(
+                '<ButtonPress-3>', self._popupValuatorMenu)
+
+            # A Dictionary of dictionaries for the popup property dialog
+            self.propertyDict = {
+                'state' :
+                {'widget': self,
+                 'type': 'string',
+                 'help': 'Enter state: normal or disabled.'
+                 },
+                
+                'text' :
+                {'widget': self,
+                 'type': 'string',
+                 'help': 'Enter label text.'
+                 },
+                
+                'min' :
+                { 'widget' : self,
+                  'type': 'real',
+                  'fNone': 1,
+                  'help': 'Minimum allowable value. Enter None for no minimum.'},
+                'max' :
+                { 'widget': self,
+                  'type': 'real',
+                  'fNone': 1,
+                  'help': 'Maximum allowable value. Enter None for no maximum.'},
+                'numDigits' :
+                {'widget': self,
+                 'type': 'integer',
+                 'help': 'Number of digits after decimal point.'
+                 },
+                
+                'resolution' :
+                {'widget': self,
+                 'type': 'real',
+                 'fNone': 1,
+                 'help':'Widget resolution. Enter None for no resolution .'
+                 },
+                
+                'resetValue' :
+                { 'widget': self,
+                  'type': 'real',
+                  'help': 'Enter value to set widget to on reset.'}
+                }
+            # Property list defines the display order of the properties
+            self.propertyList = [
+                'state', 'text', 'min', 'max', 'numDigits',
+                'resolution', 'resetValue']
+            # Add any valuator specific properties
+            self.addValuatorPropertiesToDialog()
+            
+        # Make sure input variables processed
+        self.fInit = self['fCommandOnInit']
+        self.initialiseoptions(Valuator)
+
+    def set(self, value, fCommand = 1):
+        """
+        Update widget's value by setting valuator, which will in
+        turn update the entry.  fCommand flag (which is passed to the
+        valuator as commandData, which is then passed in turn to
+        self.setEntry) controls command execution.
+        """
+        self._valuator['commandData'] = [fCommand]
+        self._valuator.set(value)
+        # Restore commandData to 1 so that interaction via valuator widget
+        # will result in command being executed, otherwise a set with
+        # commandData == 0 will stick and commands will not be executed
+        self._valuator['commandData'] = [1]
+        
+    def get(self):
+        """ Return current widget value """
+        return self.adjustedValue
+
+    def setEntry(self, value, fCommand = 1):
+        """
+        Update value displayed in entry, fCommand flag controls
+        command execution
+        """
+        # Clamp value
+        if self['min'] is not None:
+            if value < self['min']:
+                value = self['min']
+        if self['max'] is not None:
+            if value > self['max']:
+                value = self['max']
+        # Round by resolution
+        if self['resolution'] is not None:
+            value = round(value / self['resolution']) * self['resolution']
+        # Format value and use it to set entry
+        self._entryVal.set(self.entryFormat % value)
+        # Update indicator (if any) to reflect new adjusted value
+        self._valuator.updateIndicator(value)
+        # Execute command if required
+        if fCommand and self.fInit and (self['command'] is not None):
+            apply(self['command'], [value] + self['commandData'])
+        # Record adjusted value
+        self.adjustedValue = value
+        # Once initialization is finished, allow commands to execute
+        self.fInit = 1
+
+    def setEntryFormat(self):
+        """
+        Change the number of significant digits in entry
+        """
+        # Create new format string
+        self.entryFormat = "%." + "%df" % self['numDigits']
+        # Update entry to reflect new format
+        self.setEntry(self.get())
+        # Pass info down to valuator to adjust valuator sensitivity
+        self._valuator['numDigits'] = self['numDigits']
+
+    def validateEntryInput(self, event):
+        """ Check validity of entry and if valid pass along to valuator """
+        input = self._entryVal.get()
+        try:
+            # Reset background
+            self._entry.configure(background = self._entryBackground)
+            # Get new value and check validity
+            newValue = string.atof(input)
+            # If OK, execute preCallback if one defined
+            self._preCallback()
+            # Call set to update valuator
+            self.set(newValue)
+            # Execute callback
+            self._postCallback()
+            # Update valuator to reflect adjusted value
+            # Don't execute command
+            self._valuator.set(self.adjustedValue, 0)
+        except ValueError:
+            # Invalid entry, flash background
+            self._entry.configure(background = 'Pink')
+
+    # Callbacks executed on mouse down/up
+    def _mouseDown(self):
+        """ Function to execute at start of mouse interaction """
+        # Execute pre interaction callback
+        self._preCallback()
+        
+    def _mouseUp(self):
+        """ Function to execute at end of mouse interaction """
+        # Execute post interaction callback
+        self._postCallback()
+        # Update valuator to reflect adjusted value
+        # Don't execute command
+        self._valuator.set(self.adjustedValue, 0)
+
+    # Callback functions
+    def _preCallback(self):
+        if self['preCallback']:
+            apply(self['preCallback'], self['callbackData'])
+
+    def _postCallback(self):
+        # Exectute post callback if one defined
+        if self['postCallback']:
+            apply(self['postCallback'], self['callbackData'])
+
+    def setState(self):
+        """ Enable/disable widget """
+        if self['state'] == NORMAL:
+            self._entry['state'] = NORMAL
+            self._entry['background'] = self._entryBackground
+            self._valuator._canvas['state'] = NORMAL
+        elif self['state'] == DISABLED:
+            self._entry['background'] = 'grey75'
+            self._entry['state'] = DISABLED
+            self._valuator._canvas['state'] = DISABLED
+
+    def setLabel(self):
+        """ Update label's text """
+        if self._label:
+            self._label['text'] = self['text']
+
+    def zero(self):
+        """
+        self.zero()
+        Set valuator to zero
+        """
+        self.set(0.0)
+
+    def reset(self):
+        """
+        self.reset()
+        Reset valuator to reset value
+        """
+        self.set(self['resetValue'])
+
+    def mouseReset(self,event):
+        """
+        Reset valuator to resetValue
+        """
+        # If not over any canvas item
+        #if not self._canvas.find_withtag(CURRENT):
+        self.reset()
+        
+    # Popup dialog to adjust widget properties
+    def _popupValuatorMenu(self,event):
+        self._popupMenu.post(event.widget.winfo_pointerx(),
+                             event.widget.winfo_pointery())
+
+
+    def _popupPropertiesDialog(self):
+        WidgetPropertiesDialog.WidgetPropertiesDialog(
+            self.propertyDict,
+            propertyList = self.propertyList,
+            title = 'Widget Properties',
+            parent = self.interior())
+
+    def addPropertyToDialog(self, property, pDict):
+        self.propertyDict[property] = pDict
+        self.propertyList.append(property)
+            
+    # Virtual functions to be redefined by subclass
+    def createValuator(self):
+        """ Function used by subclass to create valuator geometry """
+        pass
+
+    def packValuator(self):
+        """ Function used by subclass to pack widget """
+        pass
+
+    def addValuatorMenuEntries(self):
+        """ Function used by subclass to add menu entries to popup menu """
+        pass
+
+    def addValuatorPropertiesToDialog(self):
+        """ Function used by subclass to add properties to property dialog """
+        pass
+
+
+FLOATER = 'floater'
+DIAL = 'dial'
+SLIDER = 'slider'
+
+class ValuatorGroup(Pmw.MegaWidget):
+    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 = (
+            ('type',            FLOATER,                INITOPT),
+            ('dim',             DEFAULT_DIM,            INITOPT),
+            ('side',            TOP,                    INITOPT),
+            # A list of initial values, one for each valuator
+            ('value',           DEFAULT_VALUE,          INITOPT),
+            ('numDigits',       2,                      self._setNumDigits),
+            # A tuple of labels, one for each valuator
+            ('labels',          DEFAULT_LABELS,         self._updateLabels),
+            # The command to be executed when one of the valuators is updated
+            ('command',         None,                   None),
+            )
+        self.defineoptions(kw, optiondefs)
+
+        # Initialize the toplevel widget
+        Pmw.MegaWidget.__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['value'])
+
+        # Create the valuators
+        self._valuatorList = []
+        for index in range(self['dim']):
+            # Add a group alias so you can configure the valuators via:
+            #   fg.configure(Valuator_XXX = YYY)
+            if self['type'] == DIAL:
+                import Dial
+                valuatorType = Dial.Dial
+            else:
+                import Floater
+                valuatorType = Floater.Floater
+            f = self.createcomponent(
+                'valuator%d' % index, (), 'valuator', valuatorType,
+                (interior,), value = self._value[index],
+                text = self['labels'][index],
+                command = lambda val, i = index: self._valuatorSetAt(i, val)
+                )
+            f.pack(side = self['side'], expand = 1, fill = X)
+            self._valuatorList.append(f)
+
+        # Make sure valuators are initialized
+        self.set(self['value'], fCommand = 0)
+        
+        # Make sure input variables processed 
+        self.initialiseoptions(ValuatorGroup)
+
+    # 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 valuator, but don't execute its command
+            self._valuatorList[i].set(value[i], 0)
+        if fCommand and (self['command'] is not None):
+            self['command'](self._value)
+
+    def setAt(self, index, value):
+        # Update valuator and execute its command
+        self._valuatorList[index].set(value)
+
+    # This is the command used by the valuator
+    def _valuatorSetAt(self, index, value):
+        self._value[index] = value
+        if self['command']:
+            self['command'](self._value)
+
+    def get(self):
+        return self._value
+
+    def getAt(self,index):
+        return self._value[index]
+
+    def _setNumDigits(self):
+        self['valuator_numDigits'] = self['numDigits']
+        self.formatString = '%0.' + '%df' % self['numDigits']
+
+    def _updateLabels(self):
+        if self['labels']:
+            for index in range(self['dim']):
+                self._valuatorList[index]['text'] = self['labels'][index]
+
+    def __len__(self):
+        return self['dim']
+    
+    def __repr__(self):
+        str = '[' + self.formatString % self._value[0]
+        for val in self._value[1:]:
+            str += ', ' + self.formatString % val
+        str += ']'
+        return str
+                
+
+
+class ValuatorGroupPanel(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 = (
+            ('type',            FLOATER,                INITOPT),
+            ('dim',             DEFAULT_DIM,            INITOPT),
+            ('side',            TOP,                    INITOPT),
+            ('title',           'Valuator Group',       None),
+            # A list of initial values, one for each floater
+            ('value',           DEFAULT_VALUE,          INITOPT),
+            # A tuple of labels, one for each floater
+            ('labels',          DEFAULT_LABELS,         self._updateLabels),
+            ('numDigits',       2,                      self._setNumDigits),
+            # The command to be executed when one of the floaters is updated
+            ('command',         None,                   self._setCommand),
+            )
+        self.defineoptions(kw, optiondefs)
+
+        # Initialize the toplevel widget
+        Pmw.MegaToplevel.__init__(self, parent)
+        
+        # Create the components
+        interior = self.interior()
+
+        # The Menu Bar
+        self.balloon = Pmw.Balloon()
+        menubar = self.createcomponent('menubar',(), None,
+                                       Pmw.MenuBar, (interior,),
+                                       balloon = self.balloon)
+        menubar.pack(fill=X)
+        
+        # ValuatorGroup Menu
+        menubar.addmenu('Valuator Group', 'Valuator Group Operations')
+        menubar.addmenuitem(
+            'Valuator Group', 'command', 'Reset the Valuator Group panel',
+            label = 'Reset',
+            command = lambda s = self: s.reset())
+        menubar.addmenuitem(
+            'Valuator Group', 'command', 'Dismiss Valuator Group panel',
+            label = 'Dismiss', command = self.withdraw)
+        
+        menubar.addmenu('Help', 'Valuator 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)
+
+        # Create the valuator group
+        self.valuatorGroup = self.createcomponent(
+            'valuatorGroup',
+            (('valuator', 'valuatorGroup_valuator'),),
+            None, ValuatorGroup,
+            (interior,),
+            type = self['type'],
+            dim = self['dim'],
+            value = self['value'],
+            labels = self['labels'],
+            command = self['command'])
+        self.valuatorGroup.pack(expand = 1, fill = X)
+        
+        # Make sure input variables processed 
+        self.initialiseoptions(ValuatorGroupPanel)
+
+    def toggleBalloon(self):
+        if self.toggleBalloonVar.get():
+            self.balloon.configure(state = 'balloon')
+        else:
+            self.balloon.configure(state = 'none')
+
+    def _updateLabels(self):
+        self.valuatorGroup['labels'] = self['labels']
+
+    def _setNumDigits(self):
+        self.valuatorGroup['numDigits'] = self['numDigits']
+        
+    def _setCommand(self):
+        self.valuatorGroup['command'] = self['command']
+
+    def reset(self):
+        self.set(self['value'])
+
+Pmw.forwardmethods(ValuatorGroupPanel, ValuatorGroup, 'valuatorGroup')

+ 3 - 6
direct/src/tkwidgets/VectorWidgets.py

@@ -1,5 +1,6 @@
 from Tkinter import *
 import Pmw
+import Valuator
 import Floater
 import EntryScale
 import string
@@ -232,10 +233,8 @@ class VectorEntry(Pmw.MegaWidget):
             self.configure(Entry_entry_state = 'disabled')
             self.configure(Entry_entry_background = '#C0C0C0')
             # Disable floater Group scale
-            """
             self.component('fGroup').configure(
-                Valuator_scale_state = 'disabled')
-            """
+                Valuator_state = 'disabled')
             # Disable floater group entry
             self.component('fGroup').configure(
                 Valuator_entry_state = 'disabled')
@@ -246,10 +245,8 @@ class VectorEntry(Pmw.MegaWidget):
             self.configure(Entry_entry_state = 'normal')
             self.configure(Entry_entry_background = self.entryBackground)
             # Disable floater Group scale
-            """
             self.component('fGroup').configure(
-                Valuator_scale_state = 'normal')
-            """
+                Valuator_state = 'normal')
             # Disable floater group entry
             self.component('fGroup').configure(
                 Valuator_entry_state = 'normal')