historical/toontown-classic.git/panda/direct/tkwidgets/Valuator.py

762 lines
28 KiB
Python
Raw Normal View History

2024-01-16 11:20:27 -06:00
"""Undocumented Module"""
__all__ = ['Valuator', 'ValuatorGroup', 'ValuatorGroupPanel']
from direct.showbase.DirectObject import *
from direct.showbase.TkGlobal import *
from . import WidgetPropertiesDialog
import Pmw
from direct.directtools.DirectUtil import getTkColorString
from panda3d.core import Vec4
if sys.version_info >= (3, 0):
from tkinter.colorchooser import askcolor
else:
from tkColorChooser import askcolor
VALUATOR_MINI = 'mini'
VALUATOR_FULL = 'full'
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 = self['value']
# 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 'resetValue' not in kw:
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._widget.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):
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 = float(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']:
self['preCallback'](*self['callbackData'])
def _postCallback(self):
# Exectute post callback if one defined
if self['postCallback']:
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._widget['state'] = NORMAL
elif self['state'] == DISABLED:
self._entry['background'] = 'grey75'
self._entry['state'] = DISABLED
self._valuator._widget['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._widget.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'
ANGLEDIAL = 'angledial'
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 = ['v[%d]' % x for x in 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),
('min', None, INITOPT),
('max', None, INITOPT),
('resolution', None, 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),
# 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 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:
from . import Dial
valuatorType = Dial.Dial
elif self['type'] == ANGLEDIAL:
from . import Dial
valuatorType = Dial.AngleDial
elif self['type'] == SLIDER:
from . import Slider
valuatorType = Slider.Slider
else:
from . import Floater
valuatorType = Floater.Floater
f = self.createcomponent(
'valuator%d' % index, (), 'valuator', valuatorType,
(interior,), value = self._value[index],
min = self['min'], max = self['max'],
resolution = self['resolution'],
text = self['labels'][index],
command = lambda val, i = index: self._valuatorSetAt(i, val),
preCallback = self._preCallback,
postCallback = self._postCallback,
callbackData = [self],
)
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 _preCallback(self, valGroup):
# Execute pre callback
if self['preCallback']:
self['preCallback'](*valGroup.get())
def _postCallback(self, valGroup):
# Execute post callback
if self['postCallback']:
self['postCallback'](*valGroup.get())
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 = ['v[%d]' % x for x in 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),
('min', None, INITOPT),
('max', None, INITOPT),
('resolution', None, 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),
# Callbacks to execute when updating widget's value
('preCallback', None, self._setPreCallback),
('postCallback', None, self._setPostCallback),
# Extra data to be passed to callback function, needs to be a list
('callbackData', [], self._setCallbackData),
# Destroy or withdraw
('fDestroy', 0, INITOPT)
)
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())
if self['fDestroy']:
dismissCommand = self.destroy
else:
dismissCommand = self.withdraw
menubar.addmenuitem(
'Valuator Group', 'command', 'Dismiss Valuator Group panel',
label = 'Dismiss', command = dismissCommand)
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'],
min = self['min'],
max = self['max'],
resolution = self['resolution'],
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 _setPreCallback(self):
self.valuatorGroup['preCallback'] = self['preCallback']
def _setPostCallback(self):
self.valuatorGroup['postCallback'] = self['postCallback']
def _setCallbackData(self):
self.valuatorGroup['callbackData'] = self['callbackData']
def reset(self):
self.set(self['value'])
Pmw.forwardmethods(ValuatorGroupPanel, ValuatorGroup, 'valuatorGroup')
def rgbPanel(nodePath, callback = None, style = 'mini'):
def onRelease(r, g, b, a, nodePath = nodePath):
messenger.send('RGBPanel_setColor', [nodePath, r, g, b, a])
def popupColorPicker():
# Can pass in current color with: color = (255, 0, 0)
color = askcolor(
parent = vgp.interior(),
# Initialize it to current color
initialcolor = tuple(vgp.get()[:3]))[0]
if color:
vgp.set((color[0], color[1], color[2], vgp.getAt(3)))
def printToLog():
c=nodePath.getColor()
print("Vec4(%.3f, %.3f, %.3f, %.3f)"%(c[0], c[1], c[2], c[3]))
# Check init color
if nodePath.hasColor():
initColor = nodePath.getColor() * 255.0
else:
initColor = Vec4(255)
# Create entry scale group
vgp = ValuatorGroupPanel(title = 'RGBA Panel: ' + nodePath.getName(),
dim = 4,
labels = ['R','G','B','A'],
value = [int(initColor[0]),
int(initColor[1]),
int(initColor[2]),
int(initColor[3])],
type = 'slider',
valuator_style = style,
valuator_min = 0,
valuator_max = 255,
valuator_resolution = 1,
# Destroy not withdraw panel on dismiss
fDestroy = 1)
# Update menu button
vgp.component('menubar').component('Valuator Group-button')['text'] = (
'RGBA Panel')
# Set callback
vgp['postCallback'] = onRelease
# Add a print button which will also serve as a color tile
pButton = Button(vgp.interior(), text = 'Print to Log',
bg = getTkColorString(initColor),
command = printToLog)
pButton.pack(expand = 1, fill = BOTH)
# Update menu
menubar = vgp.component('menubar')
menubar.deletemenuitems('Valuator Group', 1, 1)
# Some helper functions
# Clear color
menubar.addmenuitem(
'Valuator Group', 'command',
label='Clear Color', command=lambda: nodePath.clearColor())
# Set Clear Transparency
menubar.addmenuitem(
'Valuator Group', 'command',
label='Set Transparency', command=lambda: nodePath.setTransparency(1))
menubar.addmenuitem(
'Valuator Group', 'command',
label='Clear Transparency', command=lambda: nodePath.clearTransparency())
# System color picker
menubar.addmenuitem(
'Valuator Group', 'command',
label='Popup Color Picker', command=popupColorPicker)
menubar.addmenuitem(
'Valuator Group', 'command',
label='Print to log', command=printToLog)
menubar.addmenuitem(
'Valuator Group', 'command', 'Dismiss Valuator Group panel',
label='Dismiss', command=vgp.destroy)
def setNodePathColor(color):
nodePath.setColor(color[0]/255.0, color[1]/255.0,
color[2]/255.0, color[3]/255.0)
# Update color chip button
pButton['bg'] = getTkColorString(color)
# Execute callback to pass along color info
if callback:
callback(color)
vgp['command'] = setNodePathColor
return vgp
def lightRGBPanel(light, style = 'mini'):
# Color picker for lights
def popupColorPicker():
# Can pass in current color with: color = (255, 0, 0)
color = askcolor(
parent = vgp.interior(),
# Initialize it to current color
initialcolor = tuple(vgp.get()[:3]))[0]
if color:
vgp.set((color[0], color[1], color[2], vgp.getAt(3)))
def printToLog():
n = light.getName()
c=light.getColor()
print(n + (".setColor(Vec4(%.3f, %.3f, %.3f, %.3f))" %
(c[0], c[1], c[2], c[3])))
# Check init color
initColor = light.getColor() * 255.0
# Create entry scale group
vgp = ValuatorGroupPanel(title = 'RGBA Panel: ' + light.getName(),
dim = 4,
labels = ['R','G','B','A'],
value = [int(initColor[0]),
int(initColor[1]),
int(initColor[2]),
int(initColor[3])],
type = 'slider',
valuator_style = style,
valuator_min = 0,
valuator_max = 255,
valuator_resolution = 1,
# Destroy not withdraw panel on dismiss
fDestroy = 1)
# Update menu button
vgp.component('menubar').component('Valuator Group-button')['text'] = (
'Light Control Panel')
# Add a print button which will also serve as a color tile
pButton = Button(vgp.interior(), text = 'Print to Log',
bg = getTkColorString(initColor),
command = printToLog)
pButton.pack(expand = 1, fill = BOTH)
# Update menu
menubar = vgp.component('menubar')
# System color picker
menubar.addmenuitem(
'Valuator Group', 'command',
label='Popup Color Picker', command=popupColorPicker)
menubar.addmenuitem(
'Valuator Group', 'command',
label='Print to log', command=printToLog)
def setLightColor(color):
light.setColor(Vec4(color[0]/255.0, color[1]/255.0,
color[2]/255.0, color[3]/255.0))
# Update color chip button
pButton['bg'] = getTkColorString(color)
vgp['command'] = setLightColor
return vgp