DirectDialog.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429
  1. """This module defines various dialog windows for the DirectGUI system.
  2. See the :ref:`directdialog` page in the programming manual for a more
  3. in-depth explanation and an example of how to use this class.
  4. """
  5. __all__ = [
  6. 'findDialog', 'cleanupDialog', 'DirectDialog', 'OkDialog',
  7. 'OkCancelDialog', 'YesNoDialog', 'YesNoCancelDialog', 'RetryCancelDialog',
  8. ]
  9. from panda3d.core import *
  10. from direct.showbase import ShowBaseGlobal
  11. from . import DirectGuiGlobals as DGG
  12. from .DirectFrame import *
  13. from .DirectButton import *
  14. import types
  15. def findDialog(uniqueName):
  16. """
  17. Returns the panel whose uniqueName is given. This is mainly
  18. useful for debugging, to get a pointer to the current onscreen
  19. panel of a particular type.
  20. """
  21. if uniqueName in DirectDialog.AllDialogs:
  22. return DirectDialog.AllDialogs[uniqueName]
  23. return None
  24. def cleanupDialog(uniqueName):
  25. """cleanupPanel(string uniqueName)
  26. Cleans up (removes) the panel with the given uniqueName. This
  27. may be useful when some panels know about each other and know
  28. that opening panel A should automatically close panel B, for
  29. instance.
  30. """
  31. if uniqueName in DirectDialog.AllDialogs:
  32. # calling cleanup() will remove it out of the AllDialogs dict
  33. # This way it will get removed from the dict even it we did
  34. # not clean it up using this interface (ie somebody called
  35. # self.cleanup() directly
  36. DirectDialog.AllDialogs[uniqueName].cleanup()
  37. class DirectDialog(DirectFrame):
  38. AllDialogs = {}
  39. PanelIndex = 0
  40. def __init__(self, parent=None, **kw):
  41. """Creates a popup dialog to alert and/or interact with user.
  42. Some of the main keywords that can be used to customize the dialog:
  43. Parameters:
  44. text (str): Text message/query displayed to user
  45. geom: Geometry to be displayed in dialog
  46. buttonTextList: List of text to show on each button
  47. buttonGeomList: List of geometry to show on each button
  48. buttonImageList: List of images to show on each button
  49. buttonValueList: List of values sent to dialog command for
  50. each button. If value is [] then the ordinal rank of
  51. the button is used as its value.
  52. buttonHotKeyList: List of hotkeys to bind to each button.
  53. Typing the hotkey is equivalent to pressing the
  54. corresponding button.
  55. suppressKeys: Set to true if you wish to suppress keys
  56. (i.e. Dialog eats key event), false if you wish Dialog
  57. to pass along key event.
  58. buttonSize: 4-tuple used to specify custom size for each
  59. button (to make bigger then geom/text for example)
  60. pad: Space between border and interior graphics
  61. topPad: Extra space added above text/geom/image
  62. midPad: Extra space added between text/buttons
  63. sidePad: Extra space added to either side of text/buttons
  64. buttonPadSF: Scale factor used to expand/contract button
  65. horizontal spacing
  66. command: Callback command used when a button is pressed.
  67. Value supplied to command depends on values in
  68. buttonValueList.
  69. Note:
  70. The number of buttons on the dialog depends on the maximum
  71. length of any button[Text|Geom|Image|Value]List specified.
  72. Values of None are substituted for lists that are shorter
  73. than the max length
  74. """
  75. # Inherits from DirectFrame
  76. optiondefs = (
  77. # Define type of DirectGuiWidget
  78. ('dialogName', 'DirectDialog_' + repr(DirectDialog.PanelIndex), DGG.INITOPT),
  79. # Default position is slightly forward in Y, so as not to
  80. # intersect the near plane, which is incorrectly set to 0
  81. # in DX for some reason.
  82. ('pos', (0, 0.1, 0), None),
  83. ('pad', (0.1, 0.1), None),
  84. ('text', '', None),
  85. ('text_align', TextNode.ALeft, None),
  86. ('text_scale', 0.06, None),
  87. ('image', DGG.getDefaultDialogGeom(), None),
  88. ('relief', DGG.getDefaultDialogRelief(), None),
  89. ('borderWidth', (0.01, 0.01), None),
  90. ('buttonTextList', [], DGG.INITOPT),
  91. ('buttonGeomList', [], DGG.INITOPT),
  92. ('buttonImageList', [], DGG.INITOPT),
  93. ('buttonValueList', [], DGG.INITOPT),
  94. ('buttonHotKeyList', [], DGG.INITOPT),
  95. ('button_borderWidth', (.01, .01), None),
  96. ('button_pad', (.01, .01), None),
  97. ('button_relief', DGG.RAISED, None),
  98. ('button_text_scale', 0.06, None),
  99. ('buttonSize', None, DGG.INITOPT),
  100. ('topPad', 0.06, DGG.INITOPT),
  101. ('midPad', 0.12, DGG.INITOPT),
  102. ('sidePad', 0., DGG.INITOPT),
  103. ('buttonPadSF', 1.1, DGG.INITOPT),
  104. # Alpha of fade screen behind dialog
  105. ('fadeScreen', 0, None),
  106. ('command', None, None),
  107. ('extraArgs', [], None),
  108. ('sortOrder', DGG.NO_FADE_SORT_INDEX, None),
  109. )
  110. # Merge keyword options with default options
  111. self.defineoptions(kw, optiondefs, dynamicGroups = ("button",))
  112. # Initialize superclasses
  113. DirectFrame.__init__(self, parent)
  114. #if not self['dialogName']:
  115. # self['dialogName'] = 'DirectDialog_' + repr(DirectDialog.PanelIndex)
  116. # Clean up any previously existing panel with the same unique
  117. # name. We don't allow any two panels with the same name to
  118. # coexist.
  119. cleanupDialog(self['dialogName'])
  120. # Store this panel in our map of all open panels.
  121. DirectDialog.AllDialogs[self['dialogName']] = self
  122. DirectDialog.PanelIndex += 1
  123. # Determine number of buttons
  124. self.numButtons = max(len(self['buttonTextList']),
  125. len(self['buttonGeomList']),
  126. len(self['buttonImageList']),
  127. len(self['buttonValueList']))
  128. # Create buttons
  129. self.buttonList = []
  130. index = 0
  131. for i in range(self.numButtons):
  132. name = 'Button' + repr(i)
  133. try:
  134. text = self['buttonTextList'][i]
  135. except IndexError:
  136. text = None
  137. try:
  138. geom = self['buttonGeomList'][i]
  139. except IndexError:
  140. geom = None
  141. try:
  142. image = self['buttonImageList'][i]
  143. except IndexError:
  144. image = None
  145. try:
  146. value = self['buttonValueList'][i]
  147. except IndexError:
  148. value = i
  149. self['buttonValueList'].append(i)
  150. try:
  151. hotKey = self['buttonHotKeyList'][i]
  152. except IndexError:
  153. hotKey = None
  154. button = self.createcomponent(
  155. name, (), "button",
  156. DirectButton, (self,),
  157. text = text,
  158. geom = geom,
  159. image = image,
  160. suppressKeys = self['suppressKeys'],
  161. frameSize = self['buttonSize'],
  162. command = lambda s = self, v = value: s.buttonCommand(v)
  163. )
  164. self.buttonList.append(button)
  165. # Update dialog when everything has been initialised
  166. self.postInitialiseFuncList.append(self.configureDialog)
  167. self.initialiseoptions(DirectDialog)
  168. def configureDialog(self):
  169. # Set up hot key bindings
  170. bindList = zip(self.buttonList, self['buttonHotKeyList'],
  171. self['buttonValueList'])
  172. for button, hotKey, value in bindList:
  173. if ((type(hotKey) == list) or
  174. (type(hotKey) == tuple)):
  175. for key in hotKey:
  176. button.bind('press-' + key + '-', self.buttonCommand,
  177. extraArgs = [value])
  178. self.bind('press-' + key + '-', self.buttonCommand,
  179. extraArgs = [value])
  180. else:
  181. button.bind('press-' + hotKey + '-', self.buttonCommand,
  182. extraArgs = [value])
  183. self.bind('press-' + hotKey + '-', self.buttonCommand,
  184. extraArgs = [value])
  185. # Position buttons and text
  186. pad = self['pad']
  187. if self.hascomponent('image0'):
  188. image = self.component('image0')
  189. else:
  190. image = None
  191. # Get size of text/geom without image (for state 0)
  192. if image:
  193. image.reparentTo(ShowBaseGlobal.hidden)
  194. bounds = self.stateNodePath[0].getTightBounds()
  195. if image:
  196. image.reparentTo(self.stateNodePath[0])
  197. if bounds is None:
  198. l = 0
  199. r = 0
  200. b = 0
  201. t = 0
  202. else:
  203. l = bounds[0][0]
  204. r = bounds[1][0]
  205. b = bounds[0][2]
  206. t = bounds[1][2]
  207. # Center text and geom around origin
  208. # How far is center of text from origin?
  209. xOffset = -(l+r)*0.5
  210. zOffset = -(b+t)*0.5
  211. # Update bounds to reflect text movement
  212. l += xOffset
  213. r += xOffset
  214. b += zOffset
  215. t += zOffset
  216. # Offset text and geom to center
  217. if self['text']:
  218. self['text_pos'] = (self['text_pos'][0] + xOffset,
  219. self['text_pos'][1] + zOffset)
  220. if self['geom']:
  221. self['geom_pos'] = Point3(self['geom_pos'][0] + xOffset,
  222. self['geom_pos'][1],
  223. self['geom_pos'][2] + zOffset)
  224. if self.numButtons != 0:
  225. bpad = self['button_pad']
  226. # Get button size
  227. if self['buttonSize']:
  228. # Either use given size
  229. buttonSize = self['buttonSize']
  230. bl = buttonSize[0]
  231. br = buttonSize[1]
  232. bb = buttonSize[2]
  233. bt = buttonSize[3]
  234. else:
  235. # Or get bounds of union of buttons
  236. bl = br = bb = bt = 0
  237. for button in self.buttonList:
  238. bounds = button.stateNodePath[0].getTightBounds()
  239. if bounds is None:
  240. bl = 0
  241. br = 0
  242. bb = 0
  243. bt = 0
  244. else:
  245. bl = min(bl, bounds[0][0])
  246. br = max(br, bounds[1][0])
  247. bb = min(bb, bounds[0][2])
  248. bt = max(bt, bounds[1][2])
  249. bl -= bpad[0]
  250. br += bpad[0]
  251. bb -= bpad[1]
  252. bt += bpad[1]
  253. # Now resize buttons to match largest
  254. for button in self.buttonList:
  255. button['frameSize'] = (bl, br, bb, bt)
  256. # Must compensate for scale
  257. scale = self['button_scale']
  258. # Can either be a Vec3 or a tuple of 3 values
  259. if (isinstance(scale, Vec3) or
  260. (type(scale) == list) or
  261. (type(scale) == tuple)):
  262. sx = scale[0]
  263. sz = scale[2]
  264. elif ((type(scale) == int) or
  265. (type(scale) == float)):
  266. sx = sz = scale
  267. else:
  268. sx = sz = 1
  269. bl *= sx
  270. br *= sx
  271. bb *= sz
  272. bt *= sz
  273. # Position buttons
  274. # Calc button width and height
  275. bHeight = bt - bb
  276. bWidth = br - bl
  277. # Add pad between buttons
  278. bSpacing = self['buttonPadSF'] * bWidth
  279. bPos = -bSpacing * (self.numButtons - 1)*0.5
  280. index = 0
  281. for button in self.buttonList:
  282. button.setPos(bPos + index * bSpacing, 0,
  283. b - self['midPad'] - bpad[1] - bt)
  284. index += 1
  285. bMax = bPos + bSpacing * (self.numButtons - 1)
  286. else:
  287. bpad = 0
  288. bl = br = bb = bt = 0
  289. bPos = 0
  290. bMax = 0
  291. bpad = (0, 0)
  292. bHeight = bWidth = 0
  293. # Resize frame to fit text and buttons
  294. l = min(bPos + bl, l) - pad[0]
  295. r = max(bMax + br, r) + pad[0]
  296. sidePad = self['sidePad']
  297. l -= sidePad
  298. r += sidePad
  299. # reduce bottom by pad, button height and 2*button pad
  300. b = min(b - self['midPad'] - bpad[1] - bHeight - bpad[1], b) - pad[1]
  301. t = t + self['topPad'] + pad[1]
  302. if self['frameSize'] is None:
  303. self['frameSize'] = (l, r, b, t)
  304. self['image_scale'] = (r - l, 1, t - b)
  305. # Center frame about text and buttons
  306. self['image_pos'] = ((l+r)*0.5, 0.0, (b+t)*0.5)
  307. self.resetFrameSize()
  308. def show(self):
  309. if self['fadeScreen']:
  310. base.transitions.fadeScreen(self['fadeScreen'])
  311. self.setBin('gui-popup', 0)
  312. NodePath.show(self)
  313. def hide(self):
  314. if self['fadeScreen']:
  315. base.transitions.noTransitions()
  316. NodePath.hide(self)
  317. def buttonCommand(self, value, event = None):
  318. if self['command']:
  319. self['command'](value, *self['extraArgs'])
  320. def setMessage(self, message):
  321. self['text'] = message
  322. self.configureDialog()
  323. def cleanup(self):
  324. # Remove this panel out of the AllDialogs list
  325. uniqueName = self['dialogName']
  326. if uniqueName in DirectDialog.AllDialogs:
  327. del DirectDialog.AllDialogs[uniqueName]
  328. self.destroy()
  329. def destroy(self):
  330. if self['fadeScreen']:
  331. base.transitions.noTransitions()
  332. for button in self.buttonList:
  333. button.destroy()
  334. DirectFrame.destroy(self)
  335. class OkDialog(DirectDialog):
  336. def __init__(self, parent = None, **kw):
  337. # Inherits from DirectFrame
  338. optiondefs = (
  339. # Define type of DirectGuiWidget
  340. ('buttonTextList', ['OK'], DGG.INITOPT),
  341. ('buttonValueList', [DGG.DIALOG_OK], DGG.INITOPT),
  342. )
  343. # Merge keyword options with default options
  344. self.defineoptions(kw, optiondefs)
  345. DirectDialog.__init__(self, parent)
  346. self.initialiseoptions(OkDialog)
  347. class OkCancelDialog(DirectDialog):
  348. def __init__(self, parent = None, **kw):
  349. # Inherits from DirectFrame
  350. optiondefs = (
  351. # Define type of DirectGuiWidget
  352. ('buttonTextList', ['OK','Cancel'], DGG.INITOPT),
  353. ('buttonValueList', [DGG.DIALOG_OK, DGG.DIALOG_CANCEL], DGG.INITOPT),
  354. )
  355. # Merge keyword options with default options
  356. self.defineoptions(kw, optiondefs)
  357. DirectDialog.__init__(self, parent)
  358. self.initialiseoptions(OkCancelDialog)
  359. class YesNoDialog(DirectDialog):
  360. def __init__(self, parent = None, **kw):
  361. # Inherits from DirectFrame
  362. optiondefs = (
  363. # Define type of DirectGuiWidget
  364. ('buttonTextList', ['Yes', 'No'], DGG.INITOPT),
  365. ('buttonValueList', [DGG.DIALOG_YES, DGG.DIALOG_NO], DGG.INITOPT),
  366. )
  367. # Merge keyword options with default options
  368. self.defineoptions(kw, optiondefs)
  369. DirectDialog.__init__(self, parent)
  370. self.initialiseoptions(YesNoDialog)
  371. class YesNoCancelDialog(DirectDialog):
  372. def __init__(self, parent = None, **kw):
  373. # Inherits from DirectFrame
  374. optiondefs = (
  375. # Define type of DirectGuiWidget
  376. ('buttonTextList', ['Yes', 'No', 'Cancel'], DGG.INITOPT),
  377. ('buttonValueList', [DGG.DIALOG_YES, DGG.DIALOG_NO, DGG.DIALOG_CANCEL],
  378. DGG.INITOPT),
  379. )
  380. # Merge keyword options with default options
  381. self.defineoptions(kw, optiondefs)
  382. DirectDialog.__init__(self, parent)
  383. self.initialiseoptions(YesNoCancelDialog)
  384. class RetryCancelDialog(DirectDialog):
  385. def __init__(self, parent = None, **kw):
  386. # Inherits from DirectFrame
  387. optiondefs = (
  388. # Define type of DirectGuiWidget
  389. ('buttonTextList', ['Retry','Cancel'], DGG.INITOPT),
  390. ('buttonValueList', [DGG.DIALOG_RETRY, DGG.DIALOG_CANCEL], DGG.INITOPT),
  391. )
  392. # Merge keyword options with default options
  393. self.defineoptions(kw, optiondefs)
  394. DirectDialog.__init__(self, parent)
  395. self.initialiseoptions(RetryCancelDialog)