DirectScrolledList.py 20 KB

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