open_toontown_panda3d/direct/src/tkwidgets/Dial.py

538 lines
21 KiB
Python

from Tkinter import *
from tkSimpleDialog import askfloat
import Pmw
import math
import string
import operator
# TODO:
# More standardized use of 'max' and 'min'
# Better floater style action
# New option? 'delta'? 'repeatVal'? 'modulus'
TWO_PI = 2.0 * math.pi
ONEPOINTFIVE_PI = 1.5 * math.pi
POINTFIVE_PI = 0.5 * math.pi
INNER_SF = 0.175
MAX_EXP = 5
class Dial(Pmw.MegaWidget):
def __init__(self, parent = None, **kw):
#define the megawidget options
INITOPT = Pmw.INITOPT
optiondefs = (
# Widget relief
('relief', GROOVE, INITOPT),
# Widget borderwidth
('borderwidth', 2, INITOPT),
# Relief of dial inset
('canvas_relief', GROOVE, INITOPT),
# Borderwidth of dial inset
('canvas_bd', 2, INITOPT),
# Size of edge of dial inset
('edgeLength', 50, INITOPT),
('initialValue', 0.0, INITOPT),
# Snap to angle on/off
('fSnap', 0, None),
# Do values rollover (i.e. accumulate) with multiple revolutions
('fRollover', 1, None),
('command', None, None),
('commandData', [], None),
('callbackData', [], None),
('text', 'Dial Widget', self.updateLabel),
('numTicks', 10, self.createTicks),
('numDigits', 2, self.updateEntryFormat),
('min', 0.0, self.setScaleFactor),
('max', 1.0, self.setScaleFactor),
)
self.defineoptions(kw, optiondefs)
# Initialize the superclass
Pmw.MegaWidget.__init__(self, parent)
# Set up some local and instance variables
dim = self['edgeLength']
self.sfGridDelta = dim / 10
half = self.half = int(dim/2.0)
radius = self.radius = half - 4
# Running total which increments/decrements every time around dial
self.baseVal = 0.0
# Determines value of one dial revolution
self.scaleFactor = 1.0
self.dialAngle = None
# Current value
self.value = self['initialValue']
# Create the components
interior = self.interior()
interior.configure(relief = self['relief'], bd = self['borderwidth'])
# The canvas
self._canvas = self.createcomponent('canvas', (), None,
Canvas, (interior,),
width = dim + 12, height = dim,
scrollregion = ((- half),(- half),
half, half))
self._canvas.grid(rowspan = 2, columnspan = 2)
# The shuttle ring
self._canvas.create_oval(-half, -half, half, half,
fill = 'white', tags = ('ring',))
# The dial face
self._canvas.create_oval(-radius, -radius, radius, radius,
fill = 'white', tags = ('dial',))
self.createTicks()
# The velocity knob
self._canvas.create_oval(-radius * INNER_SF, -radius * INNER_SF,
radius * INNER_SF, radius * INNER_SF,
fill = '#909090', tags = ('velocityKnob',))
# The indicator
self._canvas.create_line(0, 0, 0, (- radius), width = 2,
tags = ('indicator', 'dial'))
# The Scale Factor marker
self._canvas.create_polygon( half + 4, - 4, half + 12, 0,
half + 4, + 4, fill = '#A0A0A0',
tags = ('sfMarker',))
self.sfy = 0
# The Dial's label
self._label = self.createcomponent('label', (), None,
Label, (interior,),
text = self['text'],
font = ('MS Sans Serif', 12, 'bold'),
anchor = CENTER)
self._label.grid(row = 0, col = 2, sticky = EW)
# The entry
self._entryVal = StringVar()
self._entry = self.createcomponent('entry', (), None,
Entry, (interior,),
justify = RIGHT,
textvariable = self._entryVal)
self._entry.grid(row = 1, col = 2, sticky = EW)
self._entry.bind('<Return>', self.validateEntryInput)
self._entryBackground = self._entry.cget('background')
interior.columnconfigure(2, weight = 1)
# The popup menu
self._popupMenu = Menu(interior, tearoff = 0)
self._fAngleSnap = IntVar()
self._fAngleSnap.set(self['fSnap'])
self._popupMenu.add_checkbutton(label = 'Angle snap',
variable = self._fAngleSnap,
command = self.setAngleSnap)
self._fRollover = IntVar()
self._fRollover.set(self['fRollover'])
self._popupMenu.add_checkbutton(label = 'Rollover',
variable = self._fRollover,
command = self.setRollover)
sfMenu = Menu(interior, tearoff = 1)
self.expVar = DoubleVar()
self.expVar.set(0)
for exp in range (MAX_EXP, -(MAX_EXP + 1), -1):
sf = "%g" % math.pow(10, exp)
sfMenu.add_radiobutton(label = sf, value = exp,
variable = self.expVar,
command = self.setScaleFactor)
sfMenu.add_command(label = 'Scale Factor...',
command = self.getScaleFactor)
self._popupMenu.add_cascade(label = 'Scale Factor',
menu = sfMenu)
self._popupMenu.add_command(label = 'Reset Dial',
command = self.reset)
# Add event bindings
self._canvas.tag_bind('ring', '<Enter>', self.highlightRing)
self._canvas.tag_bind('ring', '<Leave>', self.restoreRing)
self._canvas.tag_bind('ring', '<ButtonPress-1>', self.ringMouseDown)
self._canvas.tag_bind('ring', '<B1-Motion>', self.ringMouseMotion)
self._canvas.tag_bind('ring', '<ButtonRelease-1>', self.ringMouseUp)
self._canvas.tag_bind('dial', '<ButtonPress-1>', self.mouseDown)
self._canvas.tag_bind('dial', '<B1-Motion>', self.mouseMotion)
self._canvas.tag_bind('dial', '<Shift-B1-Motion>', self.shiftMouseMotion)
self._canvas.tag_bind('dial', '<ButtonRelease-1>', self.mouseUp)
self._canvas.tag_bind('sfMarker', '<Enter>', self.highlightSFMarker)
self._canvas.tag_bind('sfMarker', '<Leave>', self.restoreSFMarker)
self._canvas.tag_bind('velocityKnob', '<Enter>', self.highlightKnob)
self._canvas.tag_bind('velocityKnob', '<Leave>', self.restoreKnob)
self._canvas.tag_bind('sfMarker', '<ButtonPress-1>', self.sfMouseDown)
self._canvas.tag_bind('sfMarker', '<B1-Motion>', self.sfMouseMotion)
self._canvas.tag_bind('sfMarker', '<ButtonRelease-1>', self.sfMouseUp)
self._canvas.tag_bind('velocityKnob', '<ButtonPress-1>', self.knobMouseDown)
self._canvas.tag_bind('velocityKnob', '<B1-Motion>', self.knobMouseMotion)
self._canvas.tag_bind('velocityKnob', '<ButtonRelease-1>', self.knobMouseUp)
self._canvas.bind('<ButtonPress-3>', self.popupDialMenu)
self._canvas.bind('<Double-ButtonPress-1>', self.mouseReset)
self._canvas.bind('<Up>', self.expUp)
self._canvas.bind('<Down>', self.expDown)
# Make sure input variables processed
self.initialiseoptions(Dial)
def updateLabel(self):
self._label['text'] = self['text']
def createTicks(self):
self._canvas.delete('ticks')
# Based upon input snap angle, how many ticks
numTicks = self['numTicks']
# Compute snapAngle (radians)
self.snapAngle = snapAngle = TWO_PI / numTicks
# Create the ticks at the snap angles
for ticknum in range(numTicks):
angle = snapAngle * ticknum
# convert to canvas coords
angle = angle - POINTFIVE_PI
# Compute tick endpoints
startx = math.cos(angle) * self.radius
starty = math.sin(angle) * self.radius
# Elongate ticks at 90 degree points
if (angle % POINTFIVE_PI) == 0.0:
sf = 0.6
else:
sf = 0.8
endx = startx * sf
endy = starty * sf
self._canvas.create_line(startx, starty, endx, endy,
tags = ('ticks','dial'))
def mouseDown(self,event):
apply(self.onPress, self['callbackData'])
self.lastAngle = dialAngle = self.computeDialAngle(event)
self.computeValueFromAngle(dialAngle)
def mouseUp(self,event):
apply(self.onRelease, self['callbackData'])
def shiftMouseMotion(self,event):
self.mouseMotion(event, 1)
def mouseMotion(self, event, fShift = 0):
dialAngle = self.computeDialAngle(event, fShift)
self.computeValueFromAngle(dialAngle)
def computeDialAngle(self,event, fShift = 0):
x = self._canvas.canvasx(event.x)
y = self._canvas.canvasy(event.y)
rawAngle = math.atan2(y,x)
# Snap to grid
# Convert to dial coords to do snapping
dialAngle = rawAngle + POINTFIVE_PI
if operator.xor(self['fSnap'], fShift):
dialAngle = round(dialAngle / self.snapAngle) * self.snapAngle
return dialAngle
def computeValueFromAngle(self, dialAngle):
delta = self.delta
dialAngle = dialAngle % TWO_PI
# Check for rollover, if necessary
if (self.lastAngle > ONEPOINTFIVE_PI) and (dialAngle < POINTFIVE_PI):
self.baseVal = self.baseVal + delta
elif (self.lastAngle < POINTFIVE_PI) and (dialAngle > ONEPOINTFIVE_PI):
self.baseVal = self.baseVal - delta
self.lastAngle = dialAngle
# Update value and entry
newValue = self['min'] + self.baseVal + delta * (dialAngle / TWO_PI)
self.dialAngle = dialAngle
self.set(newValue)
def get(self):
return self.value
def set(self, value, fCommand = 1):
if not self['fRollover']:
if value > self['max']:
self.baseVal = 0.0
value = ((value - self['min']) %
(self['max'] - self['min'])) + self['min']
self.updateEntry(value)
if self.dialAngle:
self.updateIndicatorRadians(self.dialAngle)
self.dialAngle = None
else:
self.updateIndicator(value)
if fCommand and (self['command'] != None):
apply(self['command'], [value] + self['commandData'])
def updateIndicator(self, value):
# compute new indicator angle
delta = self.delta
factors = divmod(value - self['min'], delta)
self.baseVal = factors[0] * delta
self.updateIndicatorRadians( (factors[1]/delta) * TWO_PI )
def updateIndicatorDegrees(self, degAngle):
self.updateIndicatorRadians(degAngle * (math.pi/180.0))
def updateIndicatorRadians(self,dialAngle):
rawAngle = dialAngle - POINTFIVE_PI
# Compute end points
endx = math.cos(rawAngle) * self.radius
endy = math.sin(rawAngle) * self.radius
# Draw new indicator
self._canvas.coords('indicator', endx * INNER_SF, endy * INNER_SF,
endx, endy)
def updateEntry(self, value):
self._entryVal.set(self.entryFormat % value)
def updateEntryFormat(self):
self.entryFormat = "%." + "%df" % self['numDigits']
self.updateEntry(self.value)
def validateEntryInput(self, event):
input = self._entryVal.get()
try:
newValue = string.atof(input)
apply(self.onReturn, self['callbackData'])
self.set(newValue)
self._entry.configure(background = self._entryBackground)
apply(self.onReturnRelease, self['callbackData'])
except ValueError:
self._entry.configure(background = 'Pink')
def sfMouseDown(self, event):
# Record marker starting position
self.starty = self.sfy
# Record mouse starting position (convert to canvas coords)
self.lasty = self._canvas.canvasy(event.y)
def sfMouseMotion(self, event):
# How far did the mouse move?
dy = self._canvas.canvasy(event.y) - self.lasty
# Apply this delta to the marker
newy = self.starty + dy
# Compute new exponent based upon current position
exp = self.sfComputeExp(newy)
# Set resulting scale factor
self.setScaleFactorExp(exp)
def sfMouseUp(self, event):
self._canvas.delete('sfText')
# Compute exponent based on current marker position
def sfComputeExp(self, y, fSnap = 1):
# Force marker to stay visible
newy = max( -self.half, min( self.half, y ) )
# Snap it
gridDelta = self.sfGridDelta
if fSnap:
newy = round( newy / gridDelta ) * gridDelta
# Compute resulting exponent
return (-(newy / gridDelta))
def setScaleFactorExp(self, exp, showText = 1, fUpdateIndicator = 1):
self.exp = exp
# Update popup scale factor menu to nearest exponent
self.expVar.set(int(round(exp)))
# Compute new scale factor
self.scaleFactor = math.pow(10, exp)
# Compute resulting delta
self.delta = self.scaleFactor * (self['max'] - self['min'])
# Update indicator to reflect new scale factor
if fUpdateIndicator:
self.updateIndicator(self.value)
# Move marker to correct position
self.updateScaleFactorMarker(-exp*self.sfGridDelta, showText)
def expUp(self,event):
self.setScaleFactorExp(min(MAX_EXP, self.exp + 1), 0)
def expDown(self,event):
self.setScaleFactorExp(max(-MAX_EXP, self.exp - 1), 0)
def ringMouseDown(self,event):
apply(self.onPress, self['callbackData'])
self.startRingAngle = self.computeRingAngle(event)
self.deltaRingAngle = 0.0
self.velocityTask = self.after(100, self.ringComputeVelocity)
def ringMouseMotion(self, event):
# What is the current ring angle
ringAngle = self.computeRingAngle(event)
self.deltaRingAngle = ringAngle - self.startRingAngle
def ringComputeVelocity(self):
# Compute new exponent based upon current ring position
exp = self.deltaRingAngle/POINTFIVE_PI
# Set resulting scale factor
self.setScaleFactorExp(exp, fUpdateIndicator = 1)
# Update value and entry
delta = self.delta
self.value = newValue = self.value + delta * self.deltaRingAngle
self.set(newValue)
self.updateIndicator(self.value)
self.velocityTask = self.after(100, self.ringComputeVelocity)
def computeRingAngle(self, event):
x = self._canvas.canvasx(event.x)
y = self._canvas.canvasy(event.y)
rawAngle = math.atan2(y,x)
# Convert to dial coords
dialAngle = rawAngle + TWO_PI
return dialAngle
def ringMouseUp(self, event):
self.after_cancel(self.velocityTask)
# reset indicator
self.updateIndicator(self.value)
apply(self.onRelease, self['callbackData'])
def knobMouseDown(self,event):
apply(self.onPress, self['callbackData'])
self.lasty = self._canvas.canvasy(event.y)
self.updateIndicatorRadians(0.0)
self.velocityTask = self.after(100, self.computeVelocity)
def knobMouseMotion(self, event):
# How far is the mouse from the origin?
dx = self._canvas.canvasx(event.x)
self.lasty = self._canvas.canvasy(event.y)
exp = -5 + dx/20.0
exp = max( -5, min( 5, exp ) )
# Set resulting scale factor
self.setScaleFactorExp(exp, 0, fUpdateIndicator = 0)
def knobMouseUp(self, event):
self.after_cancel(self.velocityTask)
# reset indicator
self.updateIndicator(self.value)
apply(self.onRelease, self['callbackData'])
def computeVelocity(self):
if self.lasty < 0:
sign = -1.0
else:
sign = 1.0
lasty = abs(self.lasty)
if lasty > 5:
lasty = lasty - 5
sf = min(100, lasty)/100.0
sf = pow(sf, 3.0)
newVal = self.value - sign * sf * self.delta
self.dialAngle = - sign * sf * POINTFIVE_PI
self.set(newVal)
self.velocityTask = self.after(100, self.computeVelocity)
def updateScaleFactorMarker(self, newy, showText = 1):
# Move marker
self._canvas.move('sfMarker', 0, newy - self.sfy)
self.sfy = newy
# Show current scaling factor
if showText:
sfText = '%g' % (self.delta / 10.0,)
self._canvas.delete('sfText')
self._canvas.create_rectangle( self.half - 40, newy - 6,
self.half, newy + 7,
fill = 'white',
tags = ('sfText',))
self._canvas.create_text( self.half, newy,
justify = RIGHT,
anchor = E,
text = sfText,
fill = 'Red',
tags = ('sfText',))
# The following routines are used to handle the popup menu
def popupDialMenu(self,event):
self._popupMenu.post(event.widget.winfo_pointerx(),
event.widget.winfo_pointery())
# This is called by the scale factor popup menu and when the user
# changes the dial 'delta' value
def setScaleFactor(self):
exp = self.expVar.get()
self.setScaleFactorExp(exp, showText = 0)
# This handles the popup scale factor dialog
def getScaleFactor(self):
sf = askfloat('Dial Scale Factor', 'Scale Factor:',
parent = self.interior())
if sf:
self.setScaleFactorExp(math.log10(sf), showText = 0)
# Turn angle snap on/off
def setAngleSnap(self):
self['fSnap'] = self._fAngleSnap.get()
# Turn rollover (accumulation of a sum) on/off
def setRollover(self):
self['fRollover'] = self._fRollover.get()
def highlightSFMarker(self, event):
self._canvas.itemconfigure('sfMarker', fill = '#252525')
def restoreSFMarker(self, event):
self._canvas.itemconfigure('sfMarker', fill = '#A0A0A0')
def highlightRing(self, event):
self._canvas.itemconfigure('ring', fill = '#A0A0A0')
def restoreRing(self, event):
self._canvas.itemconfigure('ring', fill = 'white')
def highlightKnob(self, event):
self._canvas.itemconfigure('velocityKnob', fill = '#252525')
def restoreKnob(self, event):
self._canvas.itemconfigure('velocityKnob', fill = '#A0A0A0')
# Reset dial to zero
def mouseReset(self,event):
if not self._canvas.find_withtag(CURRENT):
self.reset()
def reset(self):
self.set(self['initialValue'])
# Should we do this?
self.setScaleFactorExp(0, showText = 0)
def onReturn(self, *args):
""" User redefinable callback executed on <Return> in entry """
pass
def onReturnRelease(self, *args):
""" User redefinable callback executed on <Return> release in entry """
pass
def onPress(self, *args):
""" User redefinable callback executed on button press """
pass
def onRelease(self, *args):
""" User redefinable callback executed on button release """
pass
class AngleDial(Dial):
def __init__(self, parent = None, **kw):
# Set the typical defaults for a 360 degree angle dial
optiondefs = (
('fRollover', 0, None),
('numTicks', 12, None),
('max', 360.0, None),
)
self.defineoptions(kw, optiondefs)
# Initialize the superclass
Dial.__init__(self, parent)
# Needed because this method checks if self.__class__ is myClass
# where myClass is the argument passed into inialiseoptions
self.initialiseoptions(AngleDial)
if __name__ == '__main__':
tl = Toplevel()
d = Dial(tl)
d2 = Dial(tl, numTicks = 12, max = 360, fRollover = 0, initialValue = 180)
d3 = Dial(tl, numTicks = 12, max = 90, min = -90, fRollover = 0)
d4 = Dial(tl, numTicks = 16, max = 256, 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)