DirectScrolledList.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525
  1. """Undocumented Module"""
  2. __all__ = ['DirectScrolledListItem', 'DirectScrolledList']
  3. from panda3d.core import *
  4. from . import DirectGuiGlobals as DGG
  5. from direct.directnotify import DirectNotifyGlobal
  6. from direct.task.Task import Task
  7. from .DirectFrame import *
  8. from .DirectButton import *
  9. class DirectScrolledListItem(DirectButton):
  10. """
  11. While you are not required to use a DirectScrolledListItem for a
  12. DirectScrolledList, doing so takes care of the highlighting and
  13. unhighlighting of the list items.
  14. """
  15. notify = DirectNotifyGlobal.directNotify.newCategory("DirectScrolledListItem")
  16. def __init__(self, parent=None, **kw):
  17. assert self.notify.debugStateCall(self)
  18. self._parent = parent
  19. if "command" in kw:
  20. self.nextCommand = kw.get("command")
  21. del kw["command"]
  22. if "extraArgs" in kw:
  23. self.nextCommandExtraArgs = kw.get("extraArgs")
  24. del kw["extraArgs"]
  25. optiondefs = (
  26. ('parent', self._parent, None),
  27. ('command', self.select, None),
  28. )
  29. # Merge keyword options with default options
  30. self.defineoptions(kw, optiondefs)
  31. DirectButton.__init__(self)
  32. self.initialiseoptions(DirectScrolledListItem)
  33. def select(self):
  34. assert self.notify.debugStateCall(self)
  35. self.nextCommand(*self.nextCommandExtraArgs)
  36. self._parent.selectListItem(self)
  37. class DirectScrolledList(DirectFrame):
  38. notify = DirectNotifyGlobal.directNotify.newCategory("DirectScrolledList")
  39. def __init__(self, parent = None, **kw):
  40. assert self.notify.debugStateCall(self)
  41. self.index = 0
  42. self.__forceHeight = None
  43. """ If one were to want a scrolledList that makes and adds its items
  44. as needed, simply pass in an items list of strings (type 'str')
  45. and when that item is needed, itemMakeFunction will be called
  46. with the text, the index, and itemMakeExtraArgs. If itemMakeFunction
  47. is not specified, it will create a DirectFrame with the text."""
  48. # if 'items' is a list of strings, make a copy for our use
  49. # so we can modify it without mangling the user's list
  50. if 'items' in kw:
  51. for item in kw['items']:
  52. if type(item) != type(''):
  53. break
  54. else:
  55. # we get here if every item in 'items' is a string
  56. # make a copy
  57. kw['items'] = kw['items'][:]
  58. self.nextItemID = 10
  59. # Inherits from DirectFrame
  60. optiondefs = (
  61. # Define type of DirectGuiWidget
  62. ('items', [], None),
  63. ('itemsAlign', TextNode.ACenter, DGG.INITOPT),
  64. ('itemsWordwrap', None, DGG.INITOPT),
  65. ('command', None, None),
  66. ('extraArgs', [], None),
  67. ('itemMakeFunction', None, None),
  68. ('itemMakeExtraArgs', [], None),
  69. ('numItemsVisible', 1, self.setNumItemsVisible),
  70. ('scrollSpeed', 8, self.setScrollSpeed),
  71. ('forceHeight', None, self.setForceHeight),
  72. ('incButtonCallback', None, self.setIncButtonCallback),
  73. ('decButtonCallback', None, self.setDecButtonCallback),
  74. )
  75. # Merge keyword options with default options
  76. self.defineoptions(kw, optiondefs)
  77. # Initialize superclasses
  78. DirectFrame.__init__(self, parent)
  79. self.incButton = self.createcomponent("incButton", (), None,
  80. DirectButton, (self,),
  81. )
  82. self.incButton.bind(DGG.B1PRESS, self.__incButtonDown)
  83. self.incButton.bind(DGG.B1RELEASE, self.__buttonUp)
  84. self.decButton = self.createcomponent("decButton", (), None,
  85. DirectButton, (self,),
  86. )
  87. self.decButton.bind(DGG.B1PRESS, self.__decButtonDown)
  88. self.decButton.bind(DGG.B1RELEASE, self.__buttonUp)
  89. self.itemFrame = self.createcomponent("itemFrame", (), None,
  90. DirectFrame, (self,),
  91. )
  92. for item in self["items"]:
  93. if item.__class__.__name__ != 'str':
  94. item.reparentTo(self.itemFrame)
  95. self.initialiseoptions(DirectScrolledList)
  96. self.recordMaxHeight()
  97. self.scrollTo(0)
  98. def setForceHeight(self):
  99. assert self.notify.debugStateCall(self)
  100. self.__forceHeight = self["forceHeight"]
  101. def recordMaxHeight(self):
  102. assert self.notify.debugStateCall(self)
  103. if self.__forceHeight is not None:
  104. self.maxHeight = self.__forceHeight
  105. else:
  106. self.maxHeight = 0.0
  107. for item in self["items"]:
  108. if item.__class__.__name__ != 'str':
  109. self.maxHeight = max(self.maxHeight, item.getHeight())
  110. def setScrollSpeed(self):
  111. assert self.notify.debugStateCall(self)
  112. # Items per second to move
  113. self.__scrollSpeed = self["scrollSpeed"]
  114. if self.__scrollSpeed <= 0:
  115. self.__scrollSpeed = 1
  116. def setNumItemsVisible(self):
  117. assert self.notify.debugStateCall(self)
  118. # Items per second to move
  119. self.__numItemsVisible = self["numItemsVisible"]
  120. def destroy(self):
  121. assert self.notify.debugStateCall(self)
  122. taskMgr.remove(self.taskName("scroll"))
  123. if hasattr(self, "currentSelected"):
  124. del self.currentSelected
  125. if self.__incButtonCallback:
  126. self.__incButtonCallback = None
  127. if self.__decButtonCallback:
  128. self.__decButtonCallback = None
  129. self.incButton.destroy()
  130. self.decButton.destroy()
  131. DirectFrame.destroy(self)
  132. def selectListItem(self, item):
  133. assert self.notify.debugStateCall(self)
  134. if hasattr(self, "currentSelected"):
  135. self.currentSelected['state']=DGG.NORMAL
  136. item['state']=DGG.DISABLED
  137. self.currentSelected=item
  138. def scrollBy(self, delta):
  139. assert self.notify.debugStateCall(self)
  140. #print "scrollBy[", delta,"]"
  141. return self.scrollTo(self.index + delta)
  142. def getItemIndexForItemID(self, itemID):
  143. assert self.notify.debugStateCall(self)
  144. #for i in range(len(self["items"])):
  145. # print "buttontext[", i,"]", self["items"][i]["text"]
  146. if len(self["items"]) == 0:
  147. return 0
  148. if type(self["items"][0]) == type(''):
  149. self.notify.warning("getItemIndexForItemID: cant find itemID for non-class list items!")
  150. return 0
  151. for i in range(len(self["items"])):
  152. if(self["items"][i].itemID == itemID):
  153. return i
  154. self.notify.warning("getItemIndexForItemID: item not found!")
  155. return 0
  156. def scrollToItemID(self, itemID, centered=0):
  157. assert self.notify.debugStateCall(self)
  158. self.scrollTo(self.getItemIndexForItemID(itemID), centered)
  159. def scrollTo(self, index, centered=0):
  160. """ scrolls list so selected index is at top, or centered in box"""
  161. assert self.notify.debugStateCall(self)
  162. #print "scrollTo[", index,"] called, len(self[items])=", len(self["items"])," self[numItemsVisible]=", self["numItemsVisible"]
  163. try:
  164. self["numItemsVisible"]
  165. except:
  166. # RAU hack to kill 27633
  167. self.notify.info('crash 27633 fixed!')
  168. return
  169. numItemsVisible = self["numItemsVisible"]
  170. numItemsTotal = len(self["items"])
  171. if(centered):
  172. self.index = index - (numItemsVisible/2)
  173. else:
  174. self.index = index
  175. # Not enough items to even worry about scrolling,
  176. # just disable the buttons and do nothing
  177. if (len(self["items"]) <= numItemsVisible):
  178. self.incButton['state'] = DGG.DISABLED
  179. self.decButton['state'] = DGG.DISABLED
  180. # Hmm.. just reset self.index to 0 and bail out
  181. self.index = 0
  182. ret = 0
  183. else:
  184. if (self.index <= 0):
  185. self.index = 0
  186. #print "at list start, ", len(self["items"])," ", self["numItemsVisible"]
  187. self.decButton['state'] = DGG.DISABLED
  188. self.incButton['state'] = DGG.NORMAL
  189. ret = 0
  190. elif (self.index >= (numItemsTotal - numItemsVisible)):
  191. self.index = numItemsTotal - numItemsVisible
  192. #print "at list end, ", len(self["items"])," ", self["numItemsVisible"]
  193. self.incButton['state'] = DGG.DISABLED
  194. self.decButton['state'] = DGG.NORMAL
  195. ret = 0
  196. else:
  197. # deal with an edge condition - make sure any tasks are removed from the disabled arrows.
  198. if (self.incButton['state'] == DGG.DISABLED) or (self.decButton['state'] == DGG.DISABLED):
  199. #print "leaving list start/end, ", len(self["items"])," ", self["numItemsVisible"]
  200. self.__buttonUp(0)
  201. self.incButton['state'] = DGG.NORMAL
  202. self.decButton['state'] = DGG.NORMAL
  203. ret = 1
  204. #print "self.index set to ", self.index
  205. # Hide them all
  206. for item in self["items"]:
  207. if item.__class__.__name__ != 'str':
  208. item.hide()
  209. # Then show the ones in range, and stack their positions
  210. upperRange = min(numItemsTotal, numItemsVisible)
  211. for i in range(self.index, self.index + upperRange):
  212. item = self["items"][i]
  213. #print "stacking buttontext[", i,"]", self["items"][i]["text"]
  214. # If the item is a 'str', then it has not been created (scrolled list is 'as needed')
  215. # Therefore, use the the function given to make it or just make it a frame
  216. if item.__class__.__name__ == 'str':
  217. if self['itemMakeFunction']:
  218. # If there is a function to create the item
  219. item = self['itemMakeFunction'](item, i, self['itemMakeExtraArgs'])
  220. else:
  221. item = DirectFrame(text = item,
  222. text_align = self['itemsAlign'],
  223. text_wordwrap = self['itemsWordwrap'],
  224. relief = None)
  225. #print "str stacking buttontext[", i,"]", self["items"][i]["text"]
  226. # Then add the newly formed item back into the normal item list
  227. self["items"][i] = item
  228. item.reparentTo(self.itemFrame)
  229. self.recordMaxHeight()
  230. item.show()
  231. item.setPos(0, 0, -(i-self.index) * self.maxHeight)
  232. #print 'height bug tracker: i-%s idx-%s h-%s' % (i, self.index, self.maxHeight)
  233. if self['command']:
  234. # Pass any extra args to command
  235. self['command'](*self['extraArgs'])
  236. return ret
  237. def makeAllItems(self):
  238. assert self.notify.debugStateCall(self)
  239. for i in range(len(self['items'])):
  240. item = self["items"][i]
  241. # If the item is a 'str', then it has not been created
  242. # Therefore, use the the function given to make it or
  243. # just make it a frame
  244. #print "Making " + str(item)
  245. if item.__class__.__name__ == 'str':
  246. if self['itemMakeFunction']:
  247. # If there is a function to create the item
  248. item = self['itemMakeFunction'](item, i, self['itemMakeExtraArgs'])
  249. else:
  250. item = DirectFrame(text = item,
  251. text_align = self['itemsAlign'],
  252. text_wordwrap = self['itemsWordwrap'],
  253. relief = None)
  254. # Then add the newly formed item back into the normal item list
  255. self["items"][i] = item
  256. item.reparentTo(self.itemFrame)
  257. self.recordMaxHeight()
  258. def __scrollByTask(self, task):
  259. assert self.notify.debugStateCall(self)
  260. if ((task.time - task.prevTime) < task.delayTime):
  261. return Task.cont
  262. else:
  263. ret = self.scrollBy(task.delta)
  264. task.prevTime = task.time
  265. if ret:
  266. return Task.cont
  267. else:
  268. return Task.done
  269. def __incButtonDown(self, event):
  270. assert self.notify.debugStateCall(self)
  271. task = Task(self.__scrollByTask)
  272. task.setDelay(1.0 / self.__scrollSpeed)
  273. task.prevTime = 0.0
  274. task.delta = 1
  275. taskName = self.taskName("scroll")
  276. #print "incButtonDown: adding ", taskName
  277. taskMgr.add(task, taskName)
  278. self.scrollBy(task.delta)
  279. messenger.send('wakeup')
  280. if self.__incButtonCallback:
  281. self.__incButtonCallback()
  282. def __decButtonDown(self, event):
  283. assert self.notify.debugStateCall(self)
  284. task = Task(self.__scrollByTask)
  285. task.setDelay(1.0 / self.__scrollSpeed)
  286. task.prevTime = 0.0
  287. task.delta = -1
  288. taskName = self.taskName("scroll")
  289. #print "decButtonDown: adding ", taskName
  290. taskMgr.add(task, taskName)
  291. self.scrollBy(task.delta)
  292. messenger.send('wakeup')
  293. if self.__decButtonCallback:
  294. self.__decButtonCallback()
  295. def __buttonUp(self, event):
  296. assert self.notify.debugStateCall(self)
  297. taskName = self.taskName("scroll")
  298. #print "buttonUp: removing ", taskName
  299. taskMgr.remove(taskName)
  300. def addItem(self, item, refresh=1):
  301. """
  302. Add this string and extraArg to the list
  303. """
  304. assert self.notify.debugStateCall(self)
  305. if type(item) != type(''):
  306. # cant add attribs to non-classes (like strings & ints)
  307. item.itemID = self.nextItemID
  308. self.nextItemID += 1
  309. self['items'].append(item)
  310. if type(item) != type(''):
  311. item.reparentTo(self.itemFrame)
  312. if refresh:
  313. self.refresh()
  314. if type(item) != type(''):
  315. return item.itemID # to pass to scrollToItemID
  316. def removeItem(self, item, refresh=1):
  317. """
  318. Remove this item from the panel
  319. """
  320. assert self.notify.debugStateCall(self)
  321. #print "remove item called", item
  322. #print "items list", self['items']
  323. if item in self["items"]:
  324. #print "removing item", item
  325. if hasattr(self, "currentSelected") and self.currentSelected is item:
  326. del self.currentSelected
  327. self["items"].remove(item)
  328. if type(item) != type(''):
  329. item.reparentTo(hidden)
  330. self.refresh()
  331. return 1
  332. else:
  333. return 0
  334. def removeAndDestroyItem(self, item, refresh = 1):
  335. """
  336. Remove and destroy this item from the panel.
  337. """
  338. assert self.notify.debugStateCall(self)
  339. if item in self["items"]:
  340. if hasattr(self, "currentSelected") and self.currentSelected is item:
  341. del self.currentSelected
  342. if (hasattr(item, 'destroy') and hasattr(item.destroy, '__call__')):
  343. item.destroy()
  344. self["items"].remove(item)
  345. if type(item) != type(''):
  346. item.reparentTo(hidden)
  347. self.refresh()
  348. return 1
  349. else:
  350. return 0
  351. def removeAllItems(self, refresh=1):
  352. """
  353. Remove this item from the panel
  354. Warning 2006_10_19 tested only in the trolley metagame
  355. """
  356. assert self.notify.debugStateCall(self)
  357. retval = 0
  358. #print "remove item called", item
  359. #print "items list", self['items']
  360. while len (self["items"]):
  361. item = self['items'][0]
  362. #print "removing item", item
  363. if hasattr(self, "currentSelected") and self.currentSelected is item:
  364. del self.currentSelected
  365. self["items"].remove(item)
  366. if type(item) != type(''):
  367. #RAU possible leak here, let's try to do the right thing
  368. #item.reparentTo(hidden)
  369. item.removeNode()
  370. retval = 1
  371. if (refresh):
  372. self.refresh()
  373. return retval
  374. def removeAndDestroyAllItems(self, refresh = 1):
  375. """
  376. Remove and destroy all items from the panel.
  377. Warning 2006_10_19 tested only in the trolley metagame
  378. """
  379. assert self.notify.debugStateCall(self)
  380. retval = 0
  381. while len (self["items"]):
  382. item = self['items'][0]
  383. if hasattr(self, "currentSelected") and self.currentSelected is item:
  384. del self.currentSelected
  385. if (hasattr(item, 'destroy') and hasattr(item.destroy, '__call__')):
  386. item.destroy()
  387. self["items"].remove(item)
  388. if type(item) != type(''):
  389. #RAU possible leak here, let's try to do the right thing
  390. #item.reparentTo(hidden)
  391. item.removeNode()
  392. retval = 1
  393. if (refresh):
  394. self.refresh()
  395. return retval
  396. def refresh(self):
  397. """
  398. Update the list - useful when adding or deleting items
  399. or changing properties that would affect the scrolling
  400. """
  401. assert self.notify.debugStateCall(self)
  402. self.recordMaxHeight()
  403. #print "refresh called"
  404. self.scrollTo(self.index)
  405. def getSelectedIndex(self):
  406. assert self.notify.debugStateCall(self)
  407. return self.index
  408. def getSelectedText(self):
  409. assert self.notify.debugStateCall(self)
  410. if self['items'][self.index].__class__.__name__ == 'str':
  411. return self['items'][self.index]
  412. else:
  413. return self['items'][self.index]['text']
  414. def setIncButtonCallback(self):
  415. assert self.notify.debugStateCall(self)
  416. self.__incButtonCallback = self["incButtonCallback"]
  417. def setDecButtonCallback(self):
  418. assert self.notify.debugStateCall(self)
  419. self.__decButtonCallback = self["decButtonCallback"]
  420. """
  421. from DirectGui import *
  422. def makeButton(itemName, itemNum, *extraArgs):
  423. def buttonCommand():
  424. print itemName, itemNum
  425. return DirectButton(text = itemName,
  426. relief = DGG.RAISED,
  427. frameSize = (-3.5, 3.5, -0.2, 0.8),
  428. scale = 0.85,
  429. command = buttonCommand)
  430. s = scrollList = DirectScrolledList(
  431. parent = aspect2d,
  432. relief = None,
  433. # Use the default dialog box image as the background
  434. image = DGG.getDefaultDialogGeom(),
  435. # Scale it to fit around everyting
  436. image_scale = (0.7, 1, .8),
  437. # Give it a label
  438. text = "Scrolled List Example",
  439. text_scale = 0.06,
  440. text_align = TextNode.ACenter,
  441. text_pos = (0, 0.3),
  442. text_fg = (0, 0, 0, 1),
  443. # inc and dec are DirectButtons
  444. # They can contain a combination of text, geometry and images
  445. # Just a simple text one for now
  446. incButton_text = 'Increment',
  447. incButton_relief = DGG.RAISED,
  448. incButton_pos = (0.0, 0.0, -0.36),
  449. incButton_scale = 0.1,
  450. # Same for the decrement button
  451. decButton_text = 'Decrement',
  452. decButton_relief = DGG.RAISED,
  453. decButton_pos = (0.0, 0.0, 0.175),
  454. decButton_scale = 0.1,
  455. # each item is a button with text on it
  456. numItemsVisible = 4,
  457. itemMakeFunction = makeButton,
  458. items = ['Able', 'Baker', 'Charlie', 'Delta', 'Echo', 'Foxtrot',
  459. 'Golf', 'Hotel', 'India', 'Juliet', 'Kilo', 'Lima'],
  460. # itemFrame is a DirectFrame
  461. # Use it to scale up or down the items and to place it relative
  462. # to eveything else
  463. itemFrame_pos = (0, 0, 0.06),
  464. itemFrame_scale = 0.1,
  465. itemFrame_frameSize = (-3.1, 3.1, -3.3, 0.8),
  466. itemFrame_relief = DGG.GROOVE,
  467. )
  468. """