Slider.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490
  1. """
  2. Slider Class: Velocity style controller for floating point values with
  3. a label, entry (validated), and min/max slider
  4. """
  5. from TkGlobal import *
  6. from Valuator import *
  7. import Task
  8. import math
  9. import string
  10. import operator
  11. from PandaModules import ClockObject
  12. class Slider(Valuator):
  13. """
  14. Valuator widget which includes an min/max slider and an entry for setting
  15. floating point values in a range
  16. """
  17. def __init__(self, parent = None, **kw):
  18. INITOPT = Pmw.INITOPT
  19. optiondefs = (
  20. ('min', 0.0, self.setMin),
  21. ('max', 100.0, self.setMax),
  22. ('style', VALUATOR_MINI, INITOPT),
  23. )
  24. self.defineoptions(kw, optiondefs)
  25. Valuator.__init__(self, parent)
  26. # Can not enter None for min or max, update propertyDict to reflect
  27. self.propertyDict['min']['fNone'] = 0
  28. self.propertyDict['min']['help'] = 'Minimum allowable value.'
  29. self.propertyDict['max']['fNone'] = 0
  30. self.propertyDict['max']['help'] = 'Maximum allowable value.'
  31. self.initialiseoptions(Slider)
  32. def createValuator(self):
  33. self._valuator = self.createcomponent(
  34. 'valuator',
  35. (('slider', 'valuator'),),
  36. None,
  37. SliderWidget,
  38. (self.interior(),),
  39. style = self['style'],
  40. command = self.setEntry,
  41. value = self['value'])
  42. #self._valuator._widget.bind('<Double-ButtonPress-1>', self.mouseReset)
  43. # Add popup bindings to slider widget
  44. try:
  45. self._valuator._arrowBtn.bind(
  46. '<ButtonPress-3>', self._popupValuatorMenu)
  47. except AttributeError:
  48. pass
  49. self._valuator._minLabel.bind(
  50. '<ButtonPress-3>', self._popupValuatorMenu)
  51. self._valuator._maxLabel.bind(
  52. '<ButtonPress-3>', self._popupValuatorMenu)
  53. def packValuator(self):
  54. if self['style'] == VALUATOR_FULL:
  55. if self._label:
  56. self._label.grid(row = 0, column = 0, sticky = EW)
  57. self._entry.grid(row = 0, column = 1, sticky = EW)
  58. self._valuator.grid(row = 1, columnspan = 2,
  59. padx = 2, pady = 2, sticky = 'ew')
  60. self.interior().columnconfigure(0, weight = 1)
  61. else:
  62. if self._label:
  63. self._label.grid(row=0,column=0, sticky = EW)
  64. self._entry.grid(row=0,column=1, sticky = EW)
  65. self._valuator.grid(row=0,column=2, padx = 2, pady = 2)
  66. self.interior().columnconfigure(0, weight = 1)
  67. def setMin(self):
  68. if self['min'] is not None:
  69. self._valuator['min'] = self['min']
  70. def setMax(self):
  71. if self['max'] is not None:
  72. self._valuator['max'] = self['max']
  73. # Based on Pmw ComboBox code.
  74. class SliderWidget(Pmw.MegaWidget):
  75. def __init__(self, parent = None, **kw):
  76. # Define the megawidget options.
  77. INITOPT = Pmw.INITOPT
  78. optiondefs = (
  79. # Appearance
  80. ('style', VALUATOR_MINI, INITOPT),
  81. ('relief', RAISED, self.setRelief),
  82. ('borderwidth', 2, self.setBorderwidth),
  83. ('background', 'SystemButtonFace', self.setBackground),
  84. ('fliparrow', 0, INITOPT),
  85. # Behavior
  86. # Bounds
  87. ('min', 0.0, self.setMin),
  88. ('max', 100.0, self.setMax),
  89. # Initial value of slider, use self.set to change value
  90. ('value', 0.0, INITOPT),
  91. ('numDigits', 2, self.setNumDigits),
  92. # Command to execute on slider updates
  93. ('command', None, None),
  94. # Extra data to be passed to command function
  95. ('commandData', [], None),
  96. # Callback's to execute during mouse interaction
  97. ('preCallback', None, None),
  98. ('postCallback', None, None),
  99. # Extra data to be passed to callback function, needs to be a list
  100. ('callbackData', [], None),
  101. )
  102. self.defineoptions(kw, optiondefs)
  103. # Initialise the base class (after defining the options).
  104. Pmw.MegaWidget.__init__(self, parent)
  105. # Create the components.
  106. interior = self.interior()
  107. # Current value
  108. self.value = self['value']
  109. self.formatString = '%2f'
  110. self.increment = 0.01
  111. # Interaction flags
  112. self._isPosted = 0
  113. self._fUnpost = 0
  114. self._fUpdate = 0
  115. self._firstPress = 1
  116. self._fPressInsde = 0
  117. # Slider dimensions
  118. width = 100
  119. self.xPad = xPad = 10
  120. sliderWidth = width + 2 * xPad
  121. height = 20
  122. self.left = left = -(width/2.0)
  123. self.right = right = (width/2.0)
  124. top = -5
  125. bottom = top + height
  126. def createSlider(parent):
  127. # Create the slider inside the dropdown window.
  128. # Min label
  129. self._minLabel = Label(parent, text = self['min'], width = 8,
  130. anchor = W)
  131. self._minLabel.pack(side = LEFT)
  132. # Slider widget
  133. if self['style'] == VALUATOR_FULL:
  134. # Use a scale slider
  135. self._widgetVar = DoubleVar()
  136. self._widgetVar.set(self['value'])
  137. self._widget = self.createcomponent(
  138. 'slider', (), None,
  139. Scale, (interior,),
  140. variable = self._widgetVar,
  141. from_ = self['min'], to = self['max'],
  142. width = 10,
  143. orient = 'horizontal',
  144. showvalue = 0,
  145. length = sliderWidth,
  146. relief = FLAT, bd = 2,
  147. highlightthickness = 0)
  148. else:
  149. # Use a canvas slider
  150. self._widget = self.createcomponent(
  151. 'slider', (), None,
  152. Canvas, (parent,),
  153. width = sliderWidth,
  154. height = height,
  155. bd = 2,
  156. highlightthickness = 0,
  157. scrollregion = (left - xPad, top, right + xPad, bottom))
  158. # Interaction marker
  159. xShift = 1
  160. # Shadow arrow
  161. self._marker = self._widget.create_polygon(-7 + xShift, 12,
  162. 7 + xShift, 12,
  163. xShift, 0,
  164. fill = 'black',
  165. tags = ('marker',))
  166. # Arrow
  167. self._widget.create_polygon(-6.0, 10,
  168. 6.0, 10,
  169. 0, 0,
  170. fill = 'grey85',
  171. outline = 'black',
  172. tags = ('marker',))
  173. # The indicator
  174. self._widget.create_line(left, 0,
  175. right, 0,
  176. width = 2,
  177. tags = ('line',))
  178. self._widget.pack(side = LEFT, expand=1, fill=X)
  179. # Max label
  180. self._maxLabel = Label(parent, text = self['max'], width = 8,
  181. anchor = W)
  182. self._maxLabel.pack(side = LEFT)
  183. # Create slider
  184. if self['style'] == VALUATOR_MINI:
  185. # Create the arrow button to invoke slider
  186. self._arrowBtn = self.createcomponent(
  187. 'arrowbutton',
  188. (), None,
  189. Canvas, (interior,), borderwidth = 0,
  190. relief = FLAT, width = 14, height = 14,
  191. scrollregion = (-7,-7,7,7))
  192. self._arrowBtn.pack(expand = 1, fill = BOTH)
  193. self._arrowBtn.create_polygon(-5, -5, 5, -5, 0, 5,
  194. fill = 'grey50',
  195. tags = 'arrow')
  196. self._arrowBtn.create_line(-5, 5, 5, 5,
  197. fill = 'grey50',
  198. tags = 'arrow')
  199. # Create the dropdown window.
  200. self._popup = self.createcomponent(
  201. 'popup',
  202. (), None,
  203. Toplevel, (interior,),
  204. relief = RAISED, borderwidth = 2)
  205. self._popup.withdraw()
  206. self._popup.overrideredirect(1)
  207. # Create popup slider
  208. createSlider(self._popup)
  209. # Bind events to the arrow button.
  210. self._arrowBtn.bind('<1>', self._postSlider)
  211. self._arrowBtn.bind('<Enter>', self.highlightWidget)
  212. self._arrowBtn.bind('<Leave>', self.restoreWidget)
  213. # Need to unpost the popup if the arrow Button is unmapped (eg:
  214. # its toplevel window is withdrawn) while the popup slider is
  215. # displayed.
  216. self._arrowBtn.bind('<Unmap>', self._unpostSlider)
  217. # Bind events to the dropdown window.
  218. self._popup.bind('<Escape>', self._unpostSlider)
  219. self._popup.bind('<ButtonRelease-1>', self._widgetBtnRelease)
  220. self._popup.bind('<ButtonPress-1>', self._widgetBtnPress)
  221. self._popup.bind('<Motion>', self._widgetMove)
  222. self._widget.bind('<Left>', self._decrementValue)
  223. self._widget.bind('<Right>', self._incrementValue)
  224. self._widget.bind('<Shift-Left>', self._bigDecrementValue)
  225. self._widget.bind('<Shift-Right>', self._bigIncrementValue)
  226. self._widget.bind('<Home>', self._goToMin)
  227. self._widget.bind('<End>', self._goToMax)
  228. else:
  229. createSlider(interior)
  230. self._widget['command'] = self._firstScaleCommand
  231. # Check keywords and initialise options.
  232. self.initialiseoptions(SliderWidget)
  233. # Adjust relief
  234. if not kw.has_key('relief'):
  235. if self['style'] == VALUATOR_FULL:
  236. self['relief'] = FLAT
  237. self.updateIndicator(self['value'])
  238. def destroy(self):
  239. if (self['style'] == VALUATOR_MINI) and self._isPosted:
  240. Pmw.popgrab(self._popup)
  241. Pmw.MegaWidget.destroy(self)
  242. #======================================================================
  243. # Public methods
  244. def set(self, value, fCommand = 1):
  245. """
  246. self.set(value, fCommand = 1)
  247. Set slider to new value, execute command if fCommand == 1
  248. """
  249. # Send command if any
  250. if fCommand and (self['command'] != None):
  251. apply(self['command'], [value] + self['commandData'])
  252. # Record value
  253. self.value = value
  254. def get(self):
  255. """
  256. self.get()
  257. Get current slider value
  258. """
  259. return self.value
  260. def updateIndicator(self, value):
  261. if self['style'] == VALUATOR_MINI:
  262. # Get current marker position
  263. percentX = (value - self['min'])/float(self['max'] - self['min'])
  264. newX = percentX * (self.right - self.left) + self.left
  265. markerX = self._getMarkerX()
  266. dx = newX - markerX
  267. self._widget.move('marker', dx, 0)
  268. else:
  269. # Update scale's variable, which update scale without
  270. # Calling scale's command
  271. self._widgetVar.set(value)
  272. #======================================================================
  273. # Private methods for slider.
  274. def _postSlider(self, event = None):
  275. self._isPosted = 1
  276. self._fUpdate = 0
  277. # Make sure that the arrow is displayed sunken.
  278. self.interior()['relief'] = SUNKEN
  279. self.update_idletasks()
  280. # Position popup so that marker is immediately below center of
  281. # Arrow button
  282. # Find screen space position of bottom/center of arrow button
  283. x = (self._arrowBtn.winfo_rootx() + self._arrowBtn.winfo_width()/2.0 -
  284. string.atoi(self.interior()['bd']))
  285. y = self._arrowBtn.winfo_rooty() + self._arrowBtn.winfo_height()
  286. # Popup border width
  287. bd = string.atoi(self._popup['bd'])
  288. # Get width of label
  289. minW = self._minLabel.winfo_width()
  290. # Width of canvas to adjust for
  291. cw = (self._getMarkerX() - self.left ) + self.xPad
  292. popupOffset = bd + minW + cw
  293. ch = self._widget.winfo_height()
  294. sh = self.winfo_screenheight()
  295. # Compensate if too close to edge of screen
  296. if y + ch > sh and y > sh / 2:
  297. y = self._arrowBtn.winfo_rooty() - ch
  298. # Popup window
  299. Pmw.setgeometryanddeiconify(self._popup, '+%d+%d' % (x-popupOffset, y))
  300. # Grab the popup, so that all events are delivered to it, and
  301. # set focus to the slider, to make keyboard navigation
  302. # easier.
  303. Pmw.pushgrab(self._popup, 1, self._unpostSlider)
  304. self._widget.focus_set()
  305. # Ignore the first release of the mouse button after posting the
  306. # dropdown slider, unless the mouse enters the dropdown slider.
  307. self._fUpdate = 0
  308. self._fUnpost = 0
  309. self._firstPress = 1
  310. self._fPressInsde = 0
  311. def _updateValue(self,event):
  312. mouseX = self._widget.canvasx(
  313. event.x_root - self._widget.winfo_rootx())
  314. if mouseX < self.left:
  315. mouseX = self.left
  316. if mouseX > self.right:
  317. mouseX = self.right
  318. # Update value
  319. sf = (mouseX - self.left)/(self.right - self.left)
  320. newVal = sf * (self['max'] - self['min']) + self['min']
  321. self.set(newVal)
  322. def _widgetBtnPress(self, event):
  323. # Check behavior for this button press
  324. widget = self._popup
  325. xPos = event.x_root - widget.winfo_rootx()
  326. yPos = event.y_root - widget.winfo_rooty()
  327. fInside = ((xPos > 0) and (xPos < widget.winfo_width()) and
  328. (yPos > 0) and (yPos < widget.winfo_height()))
  329. # Set flags based upon result
  330. if fInside:
  331. self._fPressInside = 1
  332. self._fUpdate = 1
  333. if self['preCallback']:
  334. apply(self['preCallback'], self['callbackData'])
  335. self._updateValue(event)
  336. else:
  337. self._fPressInside = 0
  338. self._fUpdate = 0
  339. def _widgetMove(self, event):
  340. if self._firstPress and not self._fUpdate:
  341. canvasY = self._widget.canvasy(
  342. event.y_root - self._widget.winfo_rooty())
  343. if canvasY > 0:
  344. self._fUpdate = 1
  345. if self['preCallback']:
  346. apply(self['preCallback'], self['callbackData'])
  347. self._unpostOnNextRelease()
  348. elif self._fUpdate:
  349. self._updateValue(event)
  350. def _widgetBtnRelease(self, event):
  351. # Do post callback if any
  352. if self._fUpdate and self['postCallback']:
  353. apply(self['postCallback'], self['callbackData'])
  354. if (self._fUnpost or
  355. (not (self._firstPress or self._fPressInside))):
  356. self._unpostSlider()
  357. # Otherwise, continue
  358. self._fUpdate = 0
  359. self._firstPress = 0
  360. self._fPressInside = 0
  361. def _unpostOnNextRelease(self, event = None):
  362. self._fUnpost = 1
  363. def _unpostSlider(self, event=None):
  364. if not self._isPosted:
  365. # It is possible to get events on an unposted popup. For
  366. # example, by repeatedly pressing the space key to post
  367. # and unpost the popup. The <space> event may be
  368. # delivered to the popup window even though
  369. # Pmw.popgrab() has set the focus away from the
  370. # popup window. (Bug in Tk?)
  371. return
  372. # Restore the focus before withdrawing the window, since
  373. # otherwise the window manager may take the focus away so we
  374. # can't redirect it. Also, return the grab to the next active
  375. # window in the stack, if any.
  376. Pmw.popgrab(self._popup)
  377. self._popup.withdraw()
  378. self._isPosted = 0
  379. # Raise up arrow button
  380. self.interior()['relief'] = RAISED
  381. def _incrementValue(self, event):
  382. self.set(self.value + self.increment)
  383. def _bigIncrementValue(self, event):
  384. self.set(self.value + self.increment * 10.0)
  385. def _decrementValue(self, event):
  386. self.set(self.value - self.increment)
  387. def _bigDecrementValue(self, event):
  388. self.set(self.value - self.increment * 10.0)
  389. def _goToMin(self, event):
  390. self.set(self['min'])
  391. def _goToMax(self, event):
  392. self.set(self['max'])
  393. def _firstScaleCommand(self, val):
  394. """ Hack to avoid calling command on instantiation of Scale """
  395. self._widget['command'] = self._scaleCommand
  396. def _scaleCommand(self, val):
  397. self.set(string.atof(val))
  398. # Methods to modify floater characteristics
  399. def setMin(self):
  400. self._minLabel['text'] = self.formatString % self['min']
  401. if self['style'] == VALUATOR_FULL:
  402. self._widget['from_'] = self['min']
  403. self.updateIndicator(self.value)
  404. def setMax(self):
  405. self._maxLabel['text'] = self.formatString % self['max']
  406. if self['style'] == VALUATOR_FULL:
  407. self._widget['to'] = self['max']
  408. self.updateIndicator(self.value)
  409. def setNumDigits(self):
  410. self.formatString = '%0.' + ('%d' % self['numDigits']) + 'f'
  411. self._minLabel['text'] = self.formatString % self['min']
  412. self._maxLabel['text'] = self.formatString % self['max']
  413. self.updateIndicator(self.value)
  414. self.increment = pow(10, -self['numDigits'])
  415. def _getMarkerX(self):
  416. # Get marker triangle coordinates
  417. c = self._widget.coords(self._marker)
  418. # Marker postion defined as X position of third vertex
  419. return c[4]
  420. def setRelief(self):
  421. self.interior()['relief'] = self['relief']
  422. def setBorderwidth(self):
  423. self.interior()['borderwidth'] = self['borderwidth']
  424. def setBackground(self):
  425. self._widget['background'] = self['background']
  426. def highlightWidget(self, event):
  427. self._arrowBtn.itemconfigure('arrow', fill = 'black')
  428. def restoreWidget(self, event):
  429. self._arrowBtn.itemconfigure('arrow', fill = 'grey50')