diff --git a/direct/src/tkpanels/DirectSessionPanel.py b/direct/src/tkpanels/DirectSessionPanel.py index 82ba7f78d9..08eeb4bb84 100644 --- a/direct/src/tkpanels/DirectSessionPanel.py +++ b/direct/src/tkpanels/DirectSessionPanel.py @@ -989,7 +989,5 @@ class DirectSessionPanel(AppShell): def onDestroy(self, event): # Remove hooks - print 'here' for event, method in self.actionEvents: self.ignore(event) - print 'there' diff --git a/direct/src/tkwidgets/Dial.py b/direct/src/tkwidgets/Dial.py index c62dd43241..93c2f9faae 100644 --- a/direct/src/tkwidgets/Dial.py +++ b/direct/src/tkwidgets/Dial.py @@ -1,13 +1,11 @@ from Tkinter import * -from tkSimpleDialog import askfloat from PandaModules import ClockObject -import Task +from WidgetPropertiesDialog import * import Pmw -import tkMessageBox +import Task import math import string import operator -import types TWO_PI = 2.0 * math.pi ONEPOINTFIVE_PI = 1.5 * math.pi @@ -25,187 +23,6 @@ globalClock = ClockObject.getGlobalClock() from tkSimpleDialog import Dialog -class WidgetPropertiesDialog(Toplevel): - """Class to open dialogs to adjust widget properties.""" - def __init__(self, widget, propertyList, title = None, parent = None): - """Initialize a dialog. - Arguments: - propertyList -- a list of properties to be edited - parent -- a parent window (the application window) - title -- the dialog title - """ - # Record widget and property list - self.widget = widget - self.propertyList = propertyList - # Use default parent if none specified - if not parent: - import Tkinter - parent = Tkinter._default_root - # Create toplevel window - Toplevel.__init__(self, parent) - self.transient(parent) - # Set title - if title: - self.title(title) - # Record parent - self.parent = parent - # Initialize result - self.result = None - # Create body - body = Frame(self) - self.initial_focus = self.body(body) - body.pack(padx=5, pady=5) - # Create OK Cancel button - self.buttonbox() - # Initialize window state - self.grab_set() - self.protocol("WM_DELETE_WINDOW", self.cancel) - self.geometry("+%d+%d" % (parent.winfo_rootx()+50, - parent.winfo_rooty()+50)) - self.initial_focus.focus_set() - self.wait_window(self) - - def destroy(self): - """Destroy the window""" - self.propertyList = [] - self.entryList = [] - self.initial_focus = None - Toplevel.destroy(self) - - # - # construction hooks - def body(self, master): - """create dialog body. - return widget that should have initial focus. - This method should be overridden, and is called - by the __init__ method. - """ - self.labelList = [] - self.entryList = [] - count = 0 - for propertySet in self.propertyList: - # Make singletons into lists - if type(propertySet) is not types.ListType: - propertySet = [propertySet] - # Name of widget property - property = propertySet[0] - initialvalue = self.widget[property] - try: - entryType = propertySet[1] - except IndexError: - entryType = 'float' - try: - fAllowNone = propertySet[2] - except IndexError: - fAllowNone = 0 - # Create label - label = Label(master, text=property, justify=LEFT) - label.grid(row=count, col = 0, padx=5, sticky=W) - self.labelList.append(label) - # Create entry - entry = Entry(master) - entry.grid(row=count, col = 1, padx=5, sticky=W+E) - if initialvalue is None: - entry.insert(0, 'None') - else: - entry.insert(0, initialvalue) - if entryType == 'float': - validateFunc = self.validateFloat - elif entryType == 'int': - validateFunc = self.validateInt - else: - validateFunc = self.validateString - callback = (lambda event, vf = validateFunc, - e=entry,p=property,fn = fAllowNone,: vf(e, p, fn)) - entry.bind('', callback) - self.entryList.append(entry) - count += 1 - # Set initial focus - if len(self.entryList) > 0: - entry = self.entryList[0] - entry.select_range(0, END) - # Set initial focus to first entry in the list - return self.entryList[0] - else: - # Just set initial focus to self - return self - - def buttonbox(self): - """add standard button box buttons. - """ - box = Frame(self) - # Create buttons - w = Button(box, text="OK", width=10, command=self.ok, default=ACTIVE) - w.pack(side=LEFT, padx=5, pady=5) - w = Button(box, text="Cancel", width=10, command=self.cancel) - w.pack(side=LEFT, padx=5, pady=5) - # Bind commands - self.bind("", self.cancel) - # Pack - box.pack() - - # - # standard button semantics - def ok(self, event=None): - self.withdraw() - self.update_idletasks() - self.apply() - self.cancel() - - def cancel(self, event=None): - # put focus back to the parent window - self.parent.focus_set() - self.destroy() - - def validateFloat(self, entry, property, fAllowNone): - value = entry.get() - errormsg = "Please enter a floating point value" - if fAllowNone: - errormsg += "\nor the string 'None'" - try: - value = string.atof(value) - except ValueError: - if fAllowNone and (value == 'None'): - value = None - else: - tkMessageBox.showwarning( - "Illegal value", errormsg, parent = self) - return 0 - self.widget[property] = value - return 1 - - def validateInt(self, entry, property, fAllowNone): - value = entry.get() - errormsg = "Please enter an integer value" - if fAllowNone: - errormsg += "\nor the string 'None'" - try: - value = string.atoi(value) - except ValueError: - if fAllowNone and (value == 'None'): - value = None - else: - tkMessageBox.showwarning( - "Illegal value", errormsg, parent = self) - return 0 - self.widget[property] = value - return 1 - - def validateString(self, entry, property, fAllowNone): - value = entry.get() - if fAllowNone and (value == 'None'): - value = None - self.widget[property] = value - - def apply(self): - """process the data - - This method is called automatically to process the data, *after* - the dialog is destroyed. By default, it does nothing. - """ - pass # override - - class Dial(Pmw.MegaWidget): @@ -254,12 +71,20 @@ class Dial(Pmw.MegaWidget): command = self.setEntry, value = self['value']) + self._dial.propertyDict['numDigits'] = { + 'widget' : self, + 'type' : 'integer', + 'help' : 'Enter number of digits after decimal point.' + } + self._dial.propertyList.append('numDigits') + # The Label self._label = self.createcomponent('label', (), None, Label, (interior,), text = self['text'], font = ('MS Sans Serif',12,'bold'), anchor = CENTER) + self._label.bind('', self._dial.popupDialMenu) # The entry self._entryVal = StringVar() @@ -269,6 +94,7 @@ class Dial(Pmw.MegaWidget): width = 12, textvariable = self._entryVal) self._entry.bind('', self.validateEntryInput) + self._entry.bind('', self._dial.popupDialMenu) self._entryBackground = self._entry.cget('background') if self['style'] == DIAL_FULL: @@ -374,6 +200,7 @@ 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 @@ -396,7 +223,7 @@ class DialWidget(Pmw.MegaWidget): ('min', None, None), ('max', None, None), ('resolution', None, None), - ('numDigits', 2, None), + ('numDigits', 2, self.setNumDigits), # Value dial jumps to on reset ('resetValue', 0.0, None), ## Behavior @@ -435,6 +262,31 @@ class DialWidget(Pmw.MegaWidget): # Radius of the inner knob inner_radius = max(3,radius * INNER_SF) + # A Dictionary of dictionaries + 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', 'numSegments', 'resetValue'] + # The canvas self._canvas = self.createcomponent('canvas', (), None, Canvas, (interior,), @@ -625,8 +477,11 @@ class DialWidget(Pmw.MegaWidget): def computeKnobSF(self, event): x = self._canvas.canvasx(event.x) y = self._canvas.canvasy(event.y) - minExp = math.floor(-self['numDigits']/math.log10(DialWidget.sfBase)) - sf = math.pow(DialWidget.sfBase, minExp + (abs(x) / DialWidget.sfDist)) + offset = max(0, abs(x) - DialWidget.deadband) + if offset == 0: + return 0 + sf = math.pow(DialWidget.sfBase, + self.minExp + offset/DialWidget.sfDist) if x > 0: return sf else: @@ -674,6 +529,11 @@ class DialWidget(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(DialWidget.sfBase)) + # The following methods are used to handle the popup menu def popupDialMenu(self,event): self._popupMenu.post(event.widget.winfo_pointerx(), @@ -690,56 +550,12 @@ class DialWidget(Pmw.MegaWidget): # This handles the popup dial min dialog def getProperties(self): # Popup dialog to adjust widget properties - WidgetPropertiesDialog(self, [ - ['min', 'float', 1], - ['min', 'float', 1], - ['base', 'float', 1], - ['delta', 'float', 0], - ['resetValue', 'float', 0]]) + WidgetPropertiesDialog( + self.propertyDict, + propertyList = self.propertyList, + title = 'Dial Widget Properties', + parent = self._canvas) - def getMin(self): - newMin = askfloat('Dial Min', 'Min:', - initialvalue = `self['min']`, - parent = self.interior()) - if newMin is not None: - self['min'] = newMin - self.updateIndicator(self.value) - - # This handles the popup dial base value dialog - def getBase(self): - newBase = askfloat('Dial Base Value', 'Base:', - initialvalue = `self['base']`, - parent = self.interior()) - if newBase is not None: - self['base'] = newBase - self.updateIndicator(self.value) - - # This handles the popup dial delta dialog - def getDelta(self): - newDelta = askfloat('Delta Per Revolution', 'Delta:', - initialvalue = `self['delta']`, - parent = self.interior()) - if newDelta is not None: - self['delta'] = newDelta - self.updateIndicator(self.value) - - # This handles the popup dial max dialog - def getMax(self): - newMax = askfloat('Dial Max', 'Max:', - initialvalue = `self['max']`, - parent = self.interior()) - if newMax is not None: - self['max'] = newMax - self.updateIndicator(self.value) - - # This handles the popup dial resetValue dialog - def getResetValue(self): - newResetValue = askfloat('Dial ResetValue', 'ResetValue:', - initialvalue = `self['resetValue']`, - parent = self.interior()) - if newResetValue is not None: - self['resetValue'] = newResetValue - # User callbacks def _onButtonPress(self, *args): """ User redefinable callback executed on button press """ diff --git a/direct/src/tkwidgets/WidgetPropertiesDialog.py b/direct/src/tkwidgets/WidgetPropertiesDialog.py new file mode 100644 index 0000000000..9b1413f50c --- /dev/null +++ b/direct/src/tkwidgets/WidgetPropertiesDialog.py @@ -0,0 +1,221 @@ +from Tkinter import * +import Pmw +import types +import string + +""" +TODO: + Checkboxes for None? + Floaters to adjust float values + OK and Cancel to allow changes to be delayed + Something other than Return to accept a new value +""" + +class WidgetPropertiesDialog(Toplevel): + """Class to open dialogs to adjust widget properties.""" + def __init__(self, propertyDict, propertyList = None, parent = None, + title = 'Widget Properties'): + """Initialize a dialog. + Arguments: + propertyDict -- a dictionary of properties to be edited + parent -- a parent window (the application window) + title -- the dialog title + """ + # Record property list + self.propertyDict = propertyDict + self.propertyList = propertyList + if self.propertyList is None: + self.propertyList = self.propertyDict.keys() + self.propertyList.sort() + # Use default parent if none specified + if not parent: + import Tkinter + parent = Tkinter._default_root + # Create toplevel window + Toplevel.__init__(self, parent) + self.transient(parent) + # Set title + if title: + self.title(title) + # Record parent + self.parent = parent + # Initialize modifications + self.modifiedDict = {} + # Create body + body = Frame(self) + self.initial_focus = self.body(body) + body.pack(padx=5, pady=5) + # Create OK Cancel button + self.buttonbox() + # Initialize window state + self.grab_set() + self.protocol("WM_DELETE_WINDOW", self.cancel) + self.geometry("+%d+%d" % (parent.winfo_rootx()+50, + parent.winfo_rooty()+50)) + self.initial_focus.focus_set() + self.wait_window(self) + + def destroy(self): + """Destroy the window""" + self.propertyDict = {} + self.initial_focus = None + # Clean up balloons! + for balloon in self.balloonList: + balloon.withdraw() + Toplevel.destroy(self) + + # + # construction hooks + def body(self, master): + """create dialog body. + return entry that should have initial focus. + This method should be overridden, and is called + by the __init__ method. + """ + count = 0 + entryList = [] + self.balloonList = [] + for property in self.propertyList: + propertySet = self.propertyDict[property] + # Widget + widget = propertySet.get('widget', None) + # Get initial value + initialvalue = widget[property] + # Type of entry + entryType = propertySet.get('type', 'real') + # Is None an allowable value? + fAllowNone = propertySet.get('fNone', 0) + # Help string specified? + helpString = propertySet.get('help', None) + # Create label + label = Label(master, text=property, justify=LEFT) + label.grid(row=count, col = 0, padx=5, sticky=W) + + # Create entry + entry = Pmw.EntryField(master, entry_justify = 'right') + entry.grid(row=count, col = 1, padx=5, sticky=W+E) + if initialvalue is None: + entry.insert(0, 'None') + else: + entry.insert(0, initialvalue) + + # Create balloon for help + balloon = Pmw.Balloon(state = 'balloon') + self.balloonList.append(balloon) + # extra info if None is allowed value + if helpString is None: + if fAllowNone: + extra = ' or None' + else: + extra = '' + # Set up help string and validator based upon type + if entryType == 'real': + entry['validate'] = { 'validator' : self.realOrNone } + if helpString is None: + helpString = 'Enter a floating point number' + extra + '.' + elif entryType == 'integer': + entry['validate'] = { 'validator' : self.intOrNone } + if helpString is None: + helpString = 'Enter an integer' + extra + '.' + else: + entry['validate'] = { 'validator' : 'alphanumeric' } + if helpString is None: + helpString = 'Enter a string' + extra + '.' + # Bind balloon with help string to entry + balloon.bind(entry, helpString) + # Create callback to execute whenever a value is changed + modifiedCallback = (lambda f=self.modified, w=widget, e=entry, + p=property, t=entryType,fn=fAllowNone: + f(w,e,p,t, fn)) + entry['modifiedcommand'] = modifiedCallback + # Keep track of the entrys + entryList.append(entry) + count += 1 + # Set initial focus + if len(entryList) > 0: + entry = entryList[0] + entry.select_range(0, END) + # Set initial focus to first entry in the list + return entryList[0] + else: + # Just set initial focus to self + return self + + def modified(self, widget, entry, property, type, fNone): + self.modifiedDict[property] = (widget,entry,type,fNone) + + def buttonbox(self): + """add standard button box buttons. + """ + box = Frame(self) + # Create buttons + w = Button(box, text="OK", width=10, command=self.ok) + w.pack(side=LEFT, padx=5, pady=5) + # Create buttons + w = Button(box, text="Cancel", width=10, command=self.cancel) + w.pack(side=LEFT, padx=5, pady=5) + # Bind commands + self.bind("", self.ok) + self.bind("", self.cancel) + # Pack + box.pack() + + def realOrNone(self, val): + val = string.lower(val) + if string.find('none', val) != -1: + if val == 'none': + return Pmw.OK + else: + return Pmw.PARTIAL + return Pmw.realvalidator(val) + + def intOrNone(self, val): + val = string.lower(val) + if string.find('none', val) != -1: + if val == 'none': + return Pmw.OK + else: + return Pmw.PARTIAL + return Pmw.integervalidator(val) + + # + # standard button semantics + def ok(self, event=None): + self.withdraw() + self.update_idletasks() + self.validateChanges() + self.apply() + self.cancel() + + def cancel(self, event=None): + # put focus back to the parent window + self.parent.focus_set() + self.destroy() + + def validateChanges(self): + for property in self.modifiedDict.keys(): + tuple = self.modifiedDict[property] + widget = tuple[0] + entry = tuple[1] + type = tuple[2] + fNone = tuple[3] + value = entry.get() + lValue = string.lower(value) + if (string.find('none', lValue) != -1): + if fNone and (lValue == 'none'): + widget[property] = None + else: + if type == 'real': + value = string.atof(value) + elif type == 'integer': + value = string.atoi(value) + widget[property] = value + + def apply(self): + """process the data + + This method is called automatically to process the data, *after* + the dialog is destroyed. By default, it does nothing. + """ + pass # override +