DirectOptionMenu.py 13 KB

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