DirectOptionMenu.py 13 KB

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