DirectOptionMenu.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. """Undocumented Module"""
  2. __all__ = ['DirectOptionMenu']
  3. import types
  4. from panda3d.core import *
  5. import DirectGuiGlobals as DGG
  6. from DirectButton import *
  7. from DirectLabel import *
  8. from DirectFrame import *
  9. class DirectOptionMenu(DirectButton):
  10. """
  11. DirectOptionMenu(parent) - Create a DirectButton which pops up a
  12. menu which can be used to select from a list of items.
  13. Execute button command (passing the selected item through) if defined
  14. To cancel the popup menu click anywhere on the screen outside of the
  15. popup menu. No command is executed in this case.
  16. """
  17. def __init__(self, parent = None, **kw):
  18. # Inherits from DirectButton
  19. optiondefs = (
  20. # List of items to display on the popup menu
  21. ('items', [], self.setItems),
  22. # Initial item to display on menu button
  23. # Can be an interger index or the same string as the button
  24. ('initialitem', None, DGG.INITOPT),
  25. # Amount of padding to place around popup button indicator
  26. ('popupMarkerBorder', (.1, .1), None),
  27. # Background color to use to highlight popup menu items
  28. ('highlightColor', (.5, .5, .5, 1), None),
  29. # Extra scale to use on highlight popup menu items
  30. ('highlightScale', (1, 1), None),
  31. # Alignment to use for text on popup menu button
  32. # Changing this breaks button layout
  33. ('text_align', TextNode.ALeft, None),
  34. # Remove press effect because it looks a bit funny
  35. ('pressEffect', 0, DGG.INITOPT),
  36. )
  37. # Merge keyword options with default options
  38. self.defineoptions(kw, optiondefs)
  39. # Initialize superclasses
  40. DirectButton.__init__(self, parent)
  41. # Record any user specified frame size
  42. self.initFrameSize = self['frameSize']
  43. # Create a small rectangular marker to distinguish this button
  44. # as a popup menu button
  45. self.popupMarker = self.createcomponent(
  46. 'popupMarker', (), None,
  47. DirectFrame, (self,),
  48. frameSize = (-0.5, 0.5, -0.2, 0.2),
  49. scale = 0.4,
  50. relief = DGG.RAISED)
  51. # This needs to popup the menu too
  52. self.popupMarker.bind(DGG.B1PRESS, self.showPopupMenu)
  53. # Check if item is highlighted on release and select it if it is
  54. self.popupMarker.bind(DGG.B1RELEASE, self.selectHighlightedIndex)
  55. # Make popup marker have the same click sound
  56. if self['clickSound']:
  57. self.popupMarker.guiItem.setSound(
  58. DGG.B1PRESS + self.popupMarker.guiId, self['clickSound'])
  59. else:
  60. self.popupMarker.guiItem.clearSound(DGG.B1PRESS + self.popupMarker.guiId)
  61. # This is created when you set the menu's items
  62. self.popupMenu = None
  63. self.selectedIndex = None
  64. self.highlightedIndex = None
  65. # A big screen encompassing frame to catch the cancel clicks
  66. self.cancelFrame = self.createcomponent(
  67. 'cancelframe', (), None,
  68. DirectFrame, (self,),
  69. frameSize = (-1, 1, -1, 1),
  70. relief = None,
  71. state = 'normal')
  72. # Make sure this is on top of all the other widgets
  73. self.cancelFrame.setBin('gui-popup', 0)
  74. self.cancelFrame.bind(DGG.B1PRESS, self.hidePopupMenu)
  75. # Default action on press is to show popup menu
  76. self.bind(DGG.B1PRESS, self.showPopupMenu)
  77. # Check if item is highlighted on release and select it if it is
  78. self.bind(DGG.B1RELEASE, self.selectHighlightedIndex)
  79. # Call option initialization functions
  80. self.initialiseoptions(DirectOptionMenu)
  81. # Need to call this since we explicitly set frame size
  82. self.resetFrameSize()
  83. def setItems(self):
  84. """
  85. self['items'] = itemList
  86. Create new popup menu to reflect specified set of items
  87. """
  88. # Remove old component if it exits
  89. if self.popupMenu != None:
  90. self.destroycomponent('popupMenu')
  91. # Create new component
  92. self.popupMenu = self.createcomponent('popupMenu', (), None,
  93. DirectFrame,
  94. (self,),
  95. relief = 'raised',
  96. )
  97. # Make sure it is on top of all the other gui widgets
  98. self.popupMenu.setBin('gui-popup', 0)
  99. if not self['items']:
  100. return
  101. # Create a new component for each item
  102. # Find the maximum extents of all items
  103. itemIndex = 0
  104. self.minX = self.maxX = self.minZ = self.maxZ = None
  105. for item in self['items']:
  106. c = self.createcomponent(
  107. 'item%d' % itemIndex, (), 'item',
  108. DirectButton, (self.popupMenu,),
  109. text = item, text_align = TextNode.ALeft,
  110. command = lambda i = itemIndex: self.set(i))
  111. bounds = c.getBounds()
  112. if self.minX == None:
  113. self.minX = bounds[0]
  114. elif bounds[0] < self.minX:
  115. self.minX = bounds[0]
  116. if self.maxX == None:
  117. self.maxX = bounds[1]
  118. elif bounds[1] > self.maxX:
  119. self.maxX = bounds[1]
  120. if self.minZ == None:
  121. self.minZ = bounds[2]
  122. elif bounds[2] < self.minZ:
  123. self.minZ = bounds[2]
  124. if self.maxZ == None:
  125. self.maxZ = bounds[3]
  126. elif bounds[3] > self.maxZ:
  127. self.maxZ = bounds[3]
  128. itemIndex += 1
  129. # Calc max width and height
  130. self.maxWidth = self.maxX - self.minX
  131. self.maxHeight = self.maxZ - self.minZ
  132. # Adjust frame size for each item and bind actions to mouse events
  133. for i in range(itemIndex):
  134. item = self.component('item%d' %i)
  135. # So entire extent of item's slot on popup is reactive to mouse
  136. item['frameSize'] = (self.minX, self.maxX, self.minZ, self.maxZ)
  137. # Move it to its correct position on the popup
  138. item.setPos(-self.minX, 0, -self.maxZ - i * self.maxHeight)
  139. item.bind(DGG.B1RELEASE, self.hidePopupMenu)
  140. # Highlight background when mouse is in item
  141. item.bind(DGG.WITHIN,
  142. lambda x, i=i, item=item:self._highlightItem(item, i))
  143. # Restore specified color upon exiting
  144. fc = item['frameColor']
  145. item.bind(DGG.WITHOUT,
  146. lambda x, item=item, fc=fc: self._unhighlightItem(item, fc))
  147. # Set popup menu frame size to encompass all items
  148. f = self.component('popupMenu')
  149. f['frameSize'] = (0, self.maxWidth, -self.maxHeight * itemIndex, 0)
  150. # Determine what initial item to display and set text accordingly
  151. if self['initialitem']:
  152. self.set(self['initialitem'], fCommand = 0)
  153. else:
  154. # No initial item specified, just use first item
  155. self.set(0, fCommand = 0)
  156. # Position popup Marker to the right of the button
  157. pm = self.popupMarker
  158. pmw = (pm.getWidth() * pm.getScale()[0] +
  159. 2 * self['popupMarkerBorder'][0])
  160. if self.initFrameSize:
  161. # Use specified frame size
  162. bounds = list(self.initFrameSize)
  163. else:
  164. # Or base it upon largest item
  165. bounds = [self.minX, self.maxX, self.minZ, self.maxZ]
  166. pm.setPos(bounds[1] + pmw/2.0, 0,
  167. bounds[2] + (bounds[3] - bounds[2])/2.0)
  168. # Adjust popup menu button to fit all items (or use user specified
  169. # frame size
  170. bounds[1] += pmw
  171. self['frameSize'] = (bounds[0], bounds[1], bounds[2], bounds[3])
  172. # Set initial state
  173. self.hidePopupMenu()
  174. def showPopupMenu(self, event = None):
  175. """
  176. Make popup visible and try to position it just to right of
  177. mouse click with currently selected item aligned with button.
  178. Adjust popup position if default position puts it outside of
  179. visible screen region
  180. """
  181. # Show the menu
  182. self.popupMenu.show()
  183. # Make sure its at the right scale
  184. self.popupMenu.setScale(self, VBase3(1))
  185. # Compute bounds
  186. b = self.getBounds()
  187. fb = self.popupMenu.getBounds()
  188. # Position menu at midpoint of button
  189. xPos = (b[1] - b[0])/2.0 - fb[0]
  190. self.popupMenu.setX(self, xPos)
  191. # Try to set height to line up selected item with button
  192. self.popupMenu.setZ(
  193. self, self.minZ + (self.selectedIndex + 1)*self.maxHeight)
  194. # Make sure the whole popup menu is visible
  195. pos = self.popupMenu.getPos(render2d)
  196. scale = self.popupMenu.getScale(render2d)
  197. # How are we doing relative to the right side of the screen
  198. maxX = pos[0] + fb[1] * scale[0]
  199. if maxX > 1.0:
  200. # Need to move menu to the left
  201. self.popupMenu.setX(render2d, pos[0] + (1.0 - maxX))
  202. # How about up and down?
  203. minZ = pos[2] + fb[2] * scale[2]
  204. maxZ = pos[2] + fb[3] * scale[2]
  205. if minZ < -1.0:
  206. # Menu too low, move it up
  207. self.popupMenu.setZ(render2d, pos[2] + (-1.0 - minZ))
  208. elif maxZ > 1.0:
  209. # Menu too high, move it down
  210. self.popupMenu.setZ(render2d, pos[2] + (1.0 - maxZ))
  211. # Also display cancel frame to catch clicks outside of the popup
  212. self.cancelFrame.show()
  213. # Position and scale cancel frame to fill entire window
  214. self.cancelFrame.setPos(render2d, 0, 0, 0)
  215. self.cancelFrame.setScale(render2d, 1, 1, 1)
  216. def hidePopupMenu(self, event = None):
  217. """ Put away popup and cancel frame """
  218. self.popupMenu.hide()
  219. self.cancelFrame.hide()
  220. def _highlightItem(self, item, index):
  221. """ Set frame color of highlighted item, record index """
  222. item['frameColor'] = self['highlightColor']
  223. item['frameSize'] = (self['highlightScale'][0]*self.minX, self['highlightScale'][0]*self.maxX, self['highlightScale'][1]*self.minZ, self['highlightScale'][1]*self.maxZ)
  224. item['text_scale'] = self['highlightScale']
  225. self.highlightedIndex = index
  226. def _unhighlightItem(self, item, frameColor):
  227. """ Clear frame color, clear highlightedIndex """
  228. item['frameColor'] = frameColor
  229. item['frameSize'] = (self.minX, self.maxX, self.minZ, self.maxZ)
  230. item['text_scale'] = (1,1)
  231. self.highlightedIndex = None
  232. def selectHighlightedIndex(self, event = None):
  233. """
  234. Check to see if item is highlighted (by cursor being within
  235. that item). If so, selected it. If not, do nothing
  236. """
  237. if self.highlightedIndex is not None:
  238. self.set(self.highlightedIndex)
  239. self.hidePopupMenu()
  240. def index(self, index):
  241. intIndex = None
  242. if isinstance(index, types.IntType):
  243. intIndex = index
  244. elif index in self['items']:
  245. i = 0
  246. for item in self['items']:
  247. if item == index:
  248. intIndex = i
  249. break
  250. i += 1
  251. return intIndex
  252. def set(self, index, fCommand = 1):
  253. # Item was selected, record item and call command if any
  254. newIndex = self.index(index)
  255. if newIndex is not None:
  256. self.selectedIndex = newIndex
  257. item = self['items'][self.selectedIndex]
  258. self['text'] = item
  259. if fCommand and self['command']:
  260. # Pass any extra args to command
  261. apply(self['command'], [item] + self['extraArgs'])
  262. def get(self):
  263. """ Get currently selected item """
  264. return self['items'][self.selectedIndex]
  265. def commandFunc(self, event):
  266. """
  267. Override popup menu button's command func
  268. Command is executed in response to selecting menu items
  269. """
  270. pass