EntryScale.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546
  1. """
  2. EntryScale Class: Scale with a label, and a linked and validated entry
  3. """
  4. from PandaObject import *
  5. from Tkinter import *
  6. import Pmw
  7. import string
  8. import tkColorChooser
  9. from tkSimpleDialog import *
  10. """
  11. Change Min/Max buttons to labels, add highlight binding
  12. """
  13. class EntryScale(Pmw.MegaWidget):
  14. "Scale with linked and validated entry"
  15. def __init__(self, parent = None, **kw):
  16. # Define the megawidget options.
  17. optiondefs = (
  18. ('initialValue', 0.0, Pmw.INITOPT),
  19. ('resolution', 0.001, None),
  20. ('command', None, None),
  21. ('callbackData', [], None),
  22. ('min', 0.0, self._updateValidate),
  23. ('max', 100.0, self._updateValidate),
  24. ('text', 'EntryScale', self._updateLabelText),
  25. ('significantDigits', 2, self._setSigDigits),
  26. )
  27. self.defineoptions(kw, optiondefs)
  28. # Initialise superclass
  29. Pmw.MegaWidget.__init__(self, parent)
  30. # Initialize some class variables
  31. self.value = self['initialValue']
  32. self.entryFormat = '%.2f'
  33. self.fScaleCommand = 0
  34. # Create the components.
  35. # Setup up container
  36. interior = self.interior()
  37. interior.configure(relief = GROOVE, borderwidth = 2)
  38. # Create a label and an entry
  39. self.labelFrame = self.createcomponent('frame', (), None,
  40. Frame, interior)
  41. # Create an entry field to display and validate the entryScale's value
  42. self.entryValue = StringVar()
  43. self.entryValue.set(self['initialValue'])
  44. self.entry = self.createcomponent('entryField',
  45. # Access widget's entry using "entry"
  46. (('entry', 'entryField_entry'),),
  47. None,
  48. Pmw.EntryField, self.labelFrame,
  49. entry_width = 10,
  50. validate = { 'validator' : 'real',
  51. 'min' : self['min'],
  52. 'max' : self['max'],
  53. 'minstrict' : 0,
  54. 'maxstrict' : 0},
  55. entry_justify = 'right',
  56. entry_textvar = self.entryValue,
  57. command = self._entryCommand)
  58. self.entry.pack(side='left',padx = 4)
  59. # Create the EntryScale's label
  60. self.label = self.createcomponent('label', (), None,
  61. Label, self.labelFrame,
  62. text = self['text'],
  63. width = 12,
  64. anchor = 'center',
  65. font = "Arial 12 bold")
  66. self.label.pack(side='left', expand = 1, fill = 'x')
  67. self.label.bind('<Button-3>', self.askForLabel)
  68. # Now pack the frame
  69. self.labelFrame.pack(expand = 1, fill = 'both')
  70. # Create a label and an entry
  71. self.minMaxFrame = self.createcomponent('frame', (), None,
  72. Frame, interior)
  73. # Create the EntryScale's min max labels
  74. self.minLabel = self.createcomponent('minLabel', (), None,
  75. Label, self.minMaxFrame,
  76. text = `self['min']`,
  77. relief = FLAT,
  78. width = 5,
  79. anchor = W,
  80. font = "Arial 8")
  81. self.minLabel.pack(side='left', fill = 'x')
  82. self.minLabel.bind('<Button-3>', self.askForMin)
  83. # Create the scale component.
  84. self.scale = self.createcomponent('scale', (), None,
  85. Scale, self.minMaxFrame,
  86. command = self._scaleCommand,
  87. orient = 'horizontal',
  88. length = 150,
  89. from_ = self['min'],
  90. to = self['max'],
  91. resolution = self['resolution'],
  92. showvalue = 0)
  93. self.scale.pack(side = 'left', expand = 1, fill = 'x')
  94. # Set scale to the middle of its range
  95. self.scale.set(self['initialValue'])
  96. self.scale.bind('<Button-1>', self.__onPress)
  97. self.scale.bind('<ButtonRelease-1>', self.__onRelease)
  98. self.scale.bind('<Button-3>', self.askForResolution)
  99. self.maxLabel = self.createcomponent('maxLabel', (), None,
  100. Label, self.minMaxFrame,
  101. text = `self['max']`,
  102. relief = FLAT,
  103. width = 5,
  104. anchor = E,
  105. font = "Arial 8")
  106. self.maxLabel.bind('<Button-3>', self.askForMax)
  107. self.maxLabel.pack(side='left', fill = 'x')
  108. self.minMaxFrame.pack(expand = 1, fill = 'both')
  109. # Check keywords and initialise options based on input values.
  110. self.initialiseoptions(EntryScale)
  111. def label(self):
  112. return self.label
  113. def scale(self):
  114. return self.scale
  115. def entry(self):
  116. return self.entry
  117. def askForLabel(self, event = None):
  118. newLabel = askstring(title = self['text'],
  119. prompt = 'New label:',
  120. initialvalue = `self['text']`,
  121. parent = self.interior())
  122. if newLabel:
  123. self['text'] = newLabel
  124. def askForMin(self, event = None):
  125. newMin = askfloat(title = self['text'],
  126. prompt = 'New min val:',
  127. initialvalue = `self['min']`,
  128. parent = self.interior())
  129. if newMin:
  130. self.setMin(newMin)
  131. def setMin(self, newMin):
  132. self['min'] = newMin
  133. self.scale['from_'] = newMin
  134. self.minLabel['text'] = newMin
  135. self.entry.checkentry()
  136. def askForMax(self, event = None):
  137. newMax = askfloat(title = self['text'],
  138. parent = self.interior(),
  139. initialvalue = self['max'],
  140. prompt = 'New max val:')
  141. if newMax:
  142. self.setMax(newMax)
  143. def setMax(self, newMax):
  144. self['max'] = newMax
  145. self.scale['to'] = newMax
  146. self.maxLabel['text'] = newMax
  147. self.entry.checkentry()
  148. def askForResolution(self, event = None):
  149. newResolution = askfloat(title = self['text'],
  150. parent = self.interior(),
  151. initialvalue = self['resolution'],
  152. prompt = 'New resolution:')
  153. if newResolution:
  154. self.setResolution(newResolution)
  155. def setResolution(self, newResolution):
  156. self['resolution'] = newResolution
  157. self.scale['resolution'] = newResolution
  158. self.entry.checkentry()
  159. def _updateLabelText(self):
  160. self.label['text'] = self['text']
  161. def _updateValidate(self):
  162. self.configure(entryField_validate = {
  163. 'validator' : 'real',
  164. 'min' : self['min'],
  165. 'max' : self['max'],
  166. 'minstrict' : 0,
  167. 'maxstrict' : 0})
  168. self.minLabel['text'] = self['min']
  169. self.scale['from_'] = self['min']
  170. self.scale['to'] = self['max']
  171. self.maxLabel['text'] = self['max']
  172. def _scaleCommand(self, strVal):
  173. if not self.fScaleCommand:
  174. return
  175. # convert scale val to float
  176. self.set(string.atof(strVal))
  177. """
  178. # Update entry to reflect formatted value
  179. self.entryValue.set( self.entryFormat % self.value )
  180. self.entry.checkentry()
  181. if self['command']:
  182. self['command'](self.value)
  183. """
  184. def _entryCommand(self, event = None):
  185. try:
  186. val = string.atof( self.entryValue.get() )
  187. apply(self.onReturn,self['callbackData'])
  188. self.set( val )
  189. apply(self.onReturnRelease,self['callbackData'])
  190. except ValueError:
  191. pass
  192. def _setSigDigits(self):
  193. sd = self['significantDigits']
  194. self.entryFormat = '%.' + '%d' % sd + 'f'
  195. # And reset value to reflect change
  196. self.entryValue.set( self.entryFormat % self.value )
  197. def get(self):
  198. return self.value
  199. def set(self, newVal, fCommand = 1):
  200. # Clamp value
  201. if self['min'] is not None:
  202. if newVal < self['min']:
  203. newVal = self['min']
  204. if self['max'] is not None:
  205. if newVal > self['max']:
  206. newVal = self['max']
  207. # Round by resolution
  208. if self['resolution'] is not None:
  209. newVal = round(newVal / self['resolution']) * self['resolution']
  210. # Record updated value
  211. self.value = newVal
  212. # Update scale's position
  213. self.scale.set(newVal)
  214. # Update entry to reflect formatted value
  215. self.entryValue.set( self.entryFormat % self.value )
  216. self.entry.checkentry()
  217. # execute command
  218. if fCommand and (self['command'] is not None):
  219. self['command']( newVal )
  220. def onReturn(self, *args):
  221. """ User redefinable callback executed on <Return> in entry """
  222. pass
  223. def onReturnRelease(self, *args):
  224. """ User redefinable callback executed on <Return> release in entry """
  225. pass
  226. def __onPress(self, event):
  227. # First execute onpress callback
  228. apply(self.onPress, self['callbackData'])
  229. # Now enable slider command
  230. self.fScaleCommand = 1
  231. def onPress(self, *args):
  232. """ User redefinable callback executed on button press """
  233. pass
  234. def __onRelease(self, event):
  235. # Now disable slider command
  236. self.fScaleCommand = 0
  237. # First execute onpress callback
  238. apply(self.onRelease, self['callbackData'])
  239. def onRelease(self, *args):
  240. """ User redefinable callback executed on button release """
  241. pass
  242. class EntryScaleGroup(Pmw.MegaToplevel):
  243. def __init__(self, parent = None, **kw):
  244. # Default group size
  245. DEFAULT_DIM = 1
  246. # Default value depends on *actual* group size, test for user input
  247. DEFAULT_VALUE = [0.0] * kw.get('dim', DEFAULT_DIM)
  248. DEFAULT_LABELS = map(lambda x: 'v[%d]' % x,
  249. range(kw.get('dim', DEFAULT_DIM)))
  250. #define the megawidget options
  251. INITOPT = Pmw.INITOPT
  252. optiondefs = (
  253. ('dim', DEFAULT_DIM, INITOPT),
  254. ('side', TOP, INITOPT),
  255. ('title', 'EntryScale Group', None),
  256. # A tuple of initial values, one for each entryScale
  257. ('initialValue', DEFAULT_VALUE, INITOPT),
  258. # The command to be executed any time one of the entryScales is updated
  259. ('command', None, None),
  260. # A tuple of labels, one for each entryScale
  261. ('labels', DEFAULT_LABELS, self._updateLabels),
  262. # Destroy or withdraw
  263. ('fDestroy', 0, INITOPT)
  264. )
  265. self.defineoptions(kw, optiondefs)
  266. # Initialize the toplevel widget
  267. Pmw.MegaToplevel.__init__(self, parent)
  268. # Create the components
  269. interior = self.interior()
  270. # Get a copy of the initial value (making sure its a list)
  271. self._value = list(self['initialValue'])
  272. # The Menu Bar
  273. self.balloon = Pmw.Balloon()
  274. menubar = self.createcomponent('menubar',(), None,
  275. Pmw.MenuBar, (interior,),
  276. balloon = self.balloon)
  277. menubar.pack(fill=X)
  278. # EntryScaleGroup Menu
  279. menubar.addmenu('EntryScale Group', 'EntryScale Group Operations')
  280. menubar.addmenuitem(
  281. 'EntryScale Group', 'command', 'Reset the EntryScale Group panel',
  282. label = 'Reset',
  283. command = lambda s = self: s.reset())
  284. if self['fDestroy']:
  285. dismissCommand = self.destroy
  286. else:
  287. dismissCommand = self.withdraw
  288. menubar.addmenuitem(
  289. 'EntryScale Group', 'command', 'Dismiss EntryScale Group panel',
  290. label = 'Dismiss', command = dismissCommand)
  291. menubar.addmenu('Help', 'EntryScale Group Help Operations')
  292. self.toggleBalloonVar = IntVar()
  293. self.toggleBalloonVar.set(0)
  294. menubar.addmenuitem('Help', 'checkbutton',
  295. 'Toggle balloon help',
  296. label = 'Balloon Help',
  297. variable = self.toggleBalloonVar,
  298. command = self.toggleBalloon)
  299. self.entryScaleList = []
  300. for index in range(self['dim']):
  301. # Add a group alias so you can configure the entryScales via:
  302. # fg.configure(Valuator_XXX = YYY)
  303. f = self.createcomponent(
  304. 'entryScale%d' % index, (), 'Valuator', EntryScale,
  305. (interior,), initialValue = self._value[index],
  306. text = self['labels'][index])
  307. # Do this separately so command doesn't get executed during construction
  308. f['command'] = lambda val, s=self, i=index: s._entryScaleSetAt(i, val)
  309. f['callbackData'] = [self]
  310. # Callbacks
  311. f.onReturn = self.__onReturn
  312. f.onReturnRelease = self.__onReturnRelease
  313. f.onPress = self.__onPress
  314. f.onRelease = self.__onRelease
  315. f.pack(side = self['side'], expand = 1, fill = X)
  316. self.entryScaleList.append(f)
  317. # Make sure entryScales are initialized
  318. self.set(self['initialValue'])
  319. # Make sure input variables processed
  320. self.initialiseoptions(EntryScaleGroup)
  321. def _updateLabels(self):
  322. if self['labels']:
  323. for index in range(self['dim']):
  324. self.entryScaleList[index]['text'] = self['labels'][index]
  325. def toggleBalloon(self):
  326. if self.toggleBalloonVar.get():
  327. self.balloon.configure(state = 'balloon')
  328. else:
  329. self.balloon.configure(state = 'none')
  330. def get(self):
  331. return self._value
  332. def getAt(self,index):
  333. return self._value[index]
  334. # This is the command is used to set the groups value
  335. def set(self, value, fCommand = 1):
  336. for i in range(self['dim']):
  337. self._value[i] = value[i]
  338. # Update entryScale, but don't execute its command
  339. self.entryScaleList[i].set(value[i], 0)
  340. if fCommand and (self['command'] is not None):
  341. self['command'](self._value)
  342. def setAt(self, index, value):
  343. # Update entryScale and execute its command
  344. self.entryScaleList[index].set(value)
  345. # This is the command used by the entryScale
  346. def _entryScaleSetAt(self, index, value):
  347. self._value[index] = value
  348. if self['command']:
  349. self['command'](self._value)
  350. def reset(self):
  351. self.set(self['initialValue'])
  352. def __onReturn(self, esg):
  353. # Execute onReturn callback
  354. apply(self.onReturn, esg.get())
  355. def onReturn(self, *args):
  356. """ User redefinable callback executed on button press """
  357. pass
  358. def __onReturnRelease(self, esg):
  359. # Execute onReturnRelease callback
  360. apply(self.onReturnRelease, esg.get())
  361. def onReturnRelease(self, *args):
  362. """ User redefinable callback executed on button press """
  363. pass
  364. def __onPress(self, esg):
  365. # Execute onPress callback
  366. apply(self.onPress, esg.get())
  367. def onPress(self, *args):
  368. """ User redefinable callback executed on button press """
  369. pass
  370. def __onRelease(self, esg):
  371. # Execute onRelease callback
  372. apply(self.onRelease, esg.get())
  373. def onRelease(self, *args):
  374. """ User redefinable callback executed on button release """
  375. pass
  376. def rgbPanel(nodePath, callback = None):
  377. def setNodePathColor(color, np = nodePath, cb = callback):
  378. np.setColor(color[0]/255.0, color[1]/255.0,
  379. color[2]/255.0, color[3]/255.0)
  380. # Execute callback to pass along color info
  381. if cb:
  382. cb(color)
  383. # Check init color
  384. if nodePath.hasColor():
  385. initColor = nodePath.getColor() * 255.0
  386. else:
  387. initColor = Vec4(255)
  388. # Create entry scale group
  389. esg = EntryScaleGroup(title = 'RGBA Panel: ' + nodePath.getName(),
  390. dim = 4,
  391. labels = ['R','G','B','A'],
  392. initialValue = [int(initColor[0]),
  393. int(initColor[1]),
  394. int(initColor[2]),
  395. int(initColor[3])],
  396. Valuator_max = 255,
  397. Valuator_resolution = 1,
  398. # Destroy not withdraw panel on dismiss
  399. fDestroy = 1,
  400. command = setNodePathColor)
  401. # Update menu button
  402. esg.component('menubar').component('EntryScale Group-button')['text'] = (
  403. 'RGBA Panel')
  404. # Update menu
  405. menu = esg.component('menubar').component('EntryScale Group-menu')
  406. # Some helper functions
  407. # Clear color
  408. menu.insert_command(index = 1, label = 'Clear Color',
  409. command = lambda np = nodePath: np.clearColor())
  410. # Set Clear Transparency
  411. menu.insert_command(index = 2, label = 'Set Transparency',
  412. command = lambda np = nodePath: np.setTransparency(1))
  413. menu.insert_command(
  414. index = 3, label = 'Clear Transparency',
  415. command = lambda np = nodePath: np.clearTransparency())
  416. # System color picker
  417. def popupColorPicker(esg = esg):
  418. # Can pass in current color with: color = (255, 0, 0)
  419. color = tkColorChooser.askcolor(
  420. parent = esg.interior(),
  421. # Initialize it to current color
  422. initialcolor = tuple(esg.get()[:3]))[0]
  423. if color:
  424. esg.set((color[0], color[1], color[2], esg.getAt(3)))
  425. menu.insert_command(index = 4, label = 'Popup Color Picker',
  426. command = popupColorPicker)
  427. def printToLog(nodePath=nodePath):
  428. c=nodePath.getColor()
  429. print "Vec4(%.3f, %.3f, %.3f, %.3f)"%(c[0], c[1], c[2], c[3])
  430. menu.insert_command(index = 5, label = 'Print to log',
  431. command = printToLog)
  432. # Set callback
  433. def onRelease(r,g,b,a, nodePath = nodePath):
  434. messenger.send('RGBPanel_setColor', [nodePath, r,g,b,a])
  435. esg.onRelease = onRelease
  436. return esg
  437. ## SAMPLE CODE
  438. if __name__ == '__main__':
  439. # Initialise Tkinter and Pmw.
  440. root = Toplevel()
  441. root.title('Pmw EntryScale demonstration')
  442. # Dummy command
  443. def printVal(val):
  444. print val
  445. # Create and pack a EntryScale megawidget.
  446. mega1 = EntryScale(root, command = printVal)
  447. mega1.pack(side = 'left', expand = 1, fill = 'x')
  448. """
  449. # These are things you can set/configure
  450. # Starting value for entryScale
  451. mega1['initialValue'] = 123.456
  452. mega1['text'] = 'Drive delta X'
  453. mega1['min'] = 0.0
  454. mega1['max'] = 1000.0
  455. mega1['resolution'] = 1.0
  456. # To change the color of the label:
  457. mega1.label['foreground'] = 'Red'
  458. # Max change/update, default is 100
  459. # To have really fine control, for example
  460. # mega1['maxVelocity'] = 0.1
  461. # Number of digits to the right of the decimal point, default = 2
  462. # mega1['significantDigits'] = 5
  463. """
  464. # To create a entryScale group to set an RGBA value:
  465. group1 = EntryScaleGroup(root, dim = 4,
  466. title = 'Simple RGBA Panel',
  467. labels = ('R', 'G', 'B', 'A'),
  468. EntryScale_min = 0.0,
  469. EntryScale_max = 255.0,
  470. EntryScale_resolution = 1.0,
  471. command = printVal)
  472. # Uncomment this if you aren't running in IDLE
  473. #root.mainloop()