Tree.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483
  1. """Undocumented Module"""
  2. __all__ = ['TreeNode', 'TreeItem']
  3. # ADAPTED FROM IDLE TreeWidget.py
  4. # XXX TO DO:
  5. # - popup menu
  6. # - support partial or total redisplay
  7. # - key bindings (instead of quick-n-dirty bindings on Canvas):
  8. # - up/down arrow keys to move focus around
  9. # - ditto for page up/down, home/end
  10. # - left/right arrows to expand/collapse and move out/in
  11. # - more doc strings
  12. # - add icons for "file", "module", "class", "method"; better "python" icon
  13. # - callback for selection???
  14. # - multiple-item selection
  15. # - tooltips
  16. # - redo geometry without magic numbers
  17. # - keep track of object ids to allow more careful cleaning
  18. # - optimize tree redraw after expand of subnode
  19. import os
  20. import sys
  21. import string
  22. from direct.showbase.TkGlobal import *
  23. from Tkinter import *
  24. import Pmw
  25. from pandac.PandaModules import *
  26. # Initialize icon directory
  27. ICONDIR = ConfigVariableSearchPath('model-path').findFile(Filename('icons')).toOsSpecific()
  28. if not os.path.isdir(ICONDIR):
  29. raise RuntimeError, "can't find DIRECT icon directory (%s)" % `ICONDIR`
  30. class TreeNode:
  31. def __init__(self, canvas, parent, item, menuList = []):
  32. self.canvas = canvas
  33. self.parent = parent
  34. self.item = item
  35. self.state = 'collapsed'
  36. self.selected = 0
  37. self.children = {}
  38. self.kidKeys = []
  39. self.x = self.y = None
  40. self.iconimages = {} # cache of PhotoImage instances for icons
  41. self.menuList = menuList
  42. if self.menuList:
  43. if self.menuList[-1] == 'Separator':
  44. self.menuList = self.menuList[:-1]
  45. self.menuVar = IntVar()
  46. self.menuVar.set(0)
  47. self._popupMenu = None
  48. self.fSortChildren = False # [gjeon] flag for sorting children or not
  49. self.fModeChildrenTag = 0 # [gjeon] 0: hide by given tag, 1: show by given tag
  50. self.childrenTag = None
  51. # [gjeon] to set fSortChildren
  52. def setFSortChildren(self, fSortChildren):
  53. self.fSortChildren = fSortChildren
  54. def setChildrenTag(self, tag, fModeChildrenTag):
  55. self.childrenTag = tag
  56. self.fModeChildrenTag = fModeChildrenTag
  57. def destroy(self):
  58. if self._popupMenu:
  59. self._popupMenu.destroy()
  60. for key in self.kidKeys:
  61. c = self.children[key]
  62. del self.children[key]
  63. c.destroy()
  64. self.parent = None
  65. def geticonimage(self, name):
  66. try:
  67. return self.iconimages[name]
  68. except KeyError:
  69. pass
  70. file, ext = os.path.splitext(name)
  71. ext = ext or ".gif"
  72. fullname = os.path.join(ICONDIR, file + ext)
  73. image = PhotoImage(master=self.canvas, file=fullname)
  74. self.iconimages[name] = image
  75. return image
  76. def select(self, event=None):
  77. if self.selected:
  78. return
  79. self.deselectall()
  80. self.selected = 1
  81. self.canvas.delete(self.image_id)
  82. self.drawicon()
  83. self.drawtext()
  84. self.item.OnSelect()
  85. def deselect(self, event=None):
  86. if not self.selected:
  87. return
  88. self.selected = 0
  89. self.canvas.delete(self.image_id)
  90. self.drawicon()
  91. self.drawtext()
  92. def deselectall(self):
  93. if self.parent:
  94. self.parent.deselectall()
  95. else:
  96. self.deselecttree()
  97. def deselecttree(self):
  98. if self.selected:
  99. self.deselect()
  100. for key in self.kidKeys:
  101. child = self.children[key]
  102. child.deselecttree()
  103. def flip(self, event=None):
  104. if self.state == 'expanded':
  105. self.collapse()
  106. else:
  107. self.expand()
  108. self.item.OnDoubleClick()
  109. return "break"
  110. def createPopupMenu(self):
  111. if self.menuList:
  112. self._popupMenu = Menu(self.canvas, tearoff = 0)
  113. for i in range(len(self.menuList)):
  114. item = self.menuList[i]
  115. if item == 'Separator':
  116. self._popupMenu.add_separator()
  117. else:
  118. self._popupMenu.add_radiobutton(
  119. label = item,
  120. variable = self.menuVar,
  121. value = i,
  122. indicatoron = 0,
  123. command = self.popupMenuCommand)
  124. def popupMenu(self, event=None):
  125. if not self._popupMenu:
  126. self.createPopupMenu()
  127. if self._popupMenu:
  128. self._popupMenu.post(event.widget.winfo_pointerx(),
  129. event.widget.winfo_pointery())
  130. return "break"
  131. def popupMenuCommand(self):
  132. command = self.menuList[self.menuVar.get()]
  133. if (command == 'Expand All'):
  134. self.update(fExpandMode = 1)
  135. ## elif (command == 'Collapse All'):
  136. ## self.update(fExpandMode = 2)
  137. ## elif (command == 'Expand'):
  138. ## self.expand()
  139. ## elif (command == 'Collapse'):
  140. ## self.collapse()
  141. else:
  142. self.item.MenuCommand(command)
  143. if self.parent and (command != 'Update Explorer'):
  144. # Update parent to try to keep explorer up to date
  145. self.parent.update()
  146. def expand(self, event=None):
  147. if not self.item.IsExpandable():
  148. return
  149. if self.state != 'expanded':
  150. self.state = 'expanded'
  151. self.update()
  152. self.view()
  153. def collapse(self, event=None):
  154. if self.state != 'collapsed':
  155. self.state = 'collapsed'
  156. self.update()
  157. def view(self):
  158. top = self.y - 2
  159. bottom = self.lastvisiblechild().y + 17
  160. height = bottom - top
  161. visible_top = self.canvas.canvasy(0)
  162. visible_height = self.canvas.winfo_height()
  163. visible_bottom = self.canvas.canvasy(visible_height)
  164. if visible_top <= top and bottom <= visible_bottom:
  165. return
  166. x0, y0, x1, y1 = self.canvas._getints(self.canvas['scrollregion'])
  167. if top >= visible_top and height <= visible_height:
  168. fraction = top + height - visible_height
  169. else:
  170. fraction = top
  171. fraction = float(fraction) / y1
  172. self.canvas.yview_moveto(fraction)
  173. def reveal(self):
  174. # Make sure all parent nodes are marked as expanded
  175. parent = self.parent
  176. while parent:
  177. if parent.state == 'collapsed':
  178. parent.state = 'expanded'
  179. parent = parent.parent
  180. else:
  181. break
  182. # Redraw tree accordingly
  183. self.update()
  184. # Bring this item into view
  185. self.view()
  186. def lastvisiblechild(self):
  187. if self.kidKeys and self.state == 'expanded':
  188. return self.children[self.kidKeys[-1]].lastvisiblechild()
  189. else:
  190. return self
  191. def update(self, fUseCachedChildren = 1, fExpandMode = 0):
  192. if self.parent:
  193. self.parent.update(fUseCachedChildren, fExpandMode = fExpandMode)
  194. else:
  195. oldcursor = self.canvas['cursor']
  196. self.canvas['cursor'] = "watch"
  197. self.canvas.update()
  198. self.canvas.delete(ALL) # XXX could be more subtle
  199. self.draw(7, 2, fUseCachedChildren, fExpandMode = fExpandMode)
  200. x0, y0, x1, y1 = self.canvas.bbox(ALL)
  201. self.canvas.configure(scrollregion=(0, 0, x1, y1))
  202. self.canvas['cursor'] = oldcursor
  203. def draw(self, x, y, fUseCachedChildren = 1, fExpandMode = 0):
  204. # XXX This hard-codes too many geometry constants!
  205. self.x, self.y = x, y
  206. self.drawicon()
  207. self.drawtext()
  208. if fExpandMode == 1: # [gjeon] expand all
  209. self.state = 'expanded'
  210. elif fExpandMode == 2: # [gjeon] collapse all
  211. self.state = 'collapsed'
  212. if self.state != 'expanded':
  213. return y+17
  214. # draw children
  215. sublist = self.item._GetSubList()
  216. if not sublist:
  217. # IsExpandable() was mistaken; that's allowed
  218. return y+17
  219. self.kidKeys = []
  220. # [gjeon] to sort children
  221. if self.fSortChildren:
  222. def compareText(x, y):
  223. textX = x.GetText()
  224. textY = y.GetText()
  225. if (textX > textY):
  226. return 1
  227. elif (textX == textY):
  228. return 0
  229. else: # textX < textY
  230. return -1
  231. sublist.sort(compareText)
  232. for item in sublist:
  233. key = item.GetKey()
  234. if fUseCachedChildren and self.children.has_key(key):
  235. child = self.children[key]
  236. else:
  237. child = TreeNode(self.canvas, self, item, self.menuList)
  238. # [gjeon] to set flag recursively
  239. child.setFSortChildren(self.fSortChildren)
  240. child.setChildrenTag(self.childrenTag, self.fModeChildrenTag)
  241. self.children[key] = child
  242. self.kidKeys.append(key)
  243. # [gjeon] to filter by given tag
  244. if self.childrenTag:
  245. #if not item.nodePath.hasTag('GameArea'):
  246. if self.fModeChildrenTag != item.nodePath.hasTag(self.childrenTag):
  247. self.kidKeys.remove(key)
  248. # Remove unused children
  249. for key in self.children.keys():
  250. if key not in self.kidKeys:
  251. del(self.children[key])
  252. cx = x+20
  253. cy = y+17
  254. cylast = 0
  255. for key in self.kidKeys:
  256. child = self.children[key]
  257. cylast = cy
  258. self.canvas.create_line(x+9, cy+7, cx, cy+7, fill="gray50")
  259. cy = child.draw(cx, cy, fUseCachedChildren, fExpandMode = fExpandMode)
  260. if child.item.IsExpandable():
  261. if child.state == 'expanded':
  262. iconname = "minusnode"
  263. callback = child.collapse
  264. else:
  265. iconname = "plusnode"
  266. callback = child.expand
  267. image = self.geticonimage(iconname)
  268. id = self.canvas.create_image(x+9, cylast+7, image=image)
  269. # XXX This leaks bindings until canvas is deleted:
  270. self.canvas.tag_bind(id, "<1>", callback)
  271. self.canvas.tag_bind(id, "<Double-1>", lambda x: None)
  272. id = self.canvas.create_line(x+9, y+10, x+9, cylast+7,
  273. ##stipple="gray50", # XXX Seems broken in Tk 8.0.x
  274. fill="gray50")
  275. self.canvas.tag_lower(id) # XXX .lower(id) before Python 1.5.2
  276. return cy
  277. def drawicon(self):
  278. if self.selected:
  279. imagename = (self.item.GetSelectedIconName() or
  280. self.item.GetIconName() or
  281. "openfolder")
  282. else:
  283. imagename = self.item.GetIconName() or "folder"
  284. image = self.geticonimage(imagename)
  285. id = self.canvas.create_image(self.x, self.y, anchor="nw", image=image)
  286. self.image_id = id
  287. self.canvas.tag_bind(id, "<1>", self.select)
  288. self.canvas.tag_bind(id, "<Double-1>", self.flip)
  289. self.canvas.tag_bind(id, "<3>", self.popupMenu)
  290. def drawtext(self):
  291. textx = self.x+20-1
  292. texty = self.y-1
  293. labeltext = self.item.GetLabelText()
  294. if labeltext:
  295. id = self.canvas.create_text(textx, texty, anchor="nw",
  296. text=labeltext)
  297. self.canvas.tag_bind(id, "<1>", self.select)
  298. self.canvas.tag_bind(id, "<Double-1>", self.flip)
  299. x0, y0, x1, y1 = self.canvas.bbox(id)
  300. textx = max(x1, 200) + 10
  301. text = self.item.GetText() or "<no text>"
  302. try:
  303. self.entry
  304. except AttributeError:
  305. pass
  306. else:
  307. self.edit_finish()
  308. try:
  309. label = self.label
  310. except AttributeError:
  311. # padding carefully selected (on Windows) to match Entry widget:
  312. self.label = Label(self.canvas, text=text, bd=0, padx=2, pady=2)
  313. if self.selected:
  314. self.label.configure(fg="white", bg="darkblue")
  315. else:
  316. fg = self.item.GetTextFg()
  317. self.label.configure(fg=fg, bg="white")
  318. id = self.canvas.create_window(textx, texty,
  319. anchor="nw", window=self.label)
  320. self.label.bind("<1>", self.select_or_edit)
  321. self.label.bind("<Double-1>", self.flip)
  322. self.label.bind("<3>", self.popupMenu)
  323. # Update text if necessary
  324. if text != self.label['text']:
  325. self.label['text'] = text
  326. self.text_id = id
  327. def select_or_edit(self, event=None):
  328. if self.selected and self.item.IsEditable():
  329. self.edit(event)
  330. else:
  331. self.select(event)
  332. def edit(self, event=None):
  333. self.entry = Entry(self.label, bd=0, highlightthickness=1, width=0)
  334. self.entry.insert(0, self.label['text'])
  335. self.entry.selection_range(0, END)
  336. self.entry.pack(ipadx=5)
  337. self.entry.focus_set()
  338. self.entry.bind("<Return>", self.edit_finish)
  339. self.entry.bind("<Escape>", self.edit_cancel)
  340. def edit_finish(self, event=None):
  341. try:
  342. entry = self.entry
  343. del self.entry
  344. except AttributeError:
  345. return
  346. text = entry.get()
  347. entry.destroy()
  348. if text and text != self.item.GetText():
  349. self.item.SetText(text)
  350. text = self.item.GetText()
  351. self.label['text'] = text
  352. self.drawtext()
  353. self.canvas.focus_set()
  354. def edit_cancel(self, event=None):
  355. self.drawtext()
  356. self.canvas.focus_set()
  357. def find(self, searchKey):
  358. # Search for a node who's key matches the given key
  359. # Is it this node
  360. if searchKey == self.item.GetKey():
  361. # [gjeon] to filter by given tag
  362. if self.childrenTag:
  363. if self.fModeChildrenTag != self.item.nodePath.hasTag(self.childrenTag):
  364. return None
  365. return self
  366. # Nope, check the children
  367. sublist = self.item._GetSubList()
  368. for item in sublist:
  369. key = item.GetKey()
  370. # Use existing child or create new TreeNode if none exists
  371. if self.children.has_key(key):
  372. child = self.children[key]
  373. else:
  374. child = TreeNode(self.canvas, self, item, self.menuList)
  375. # Update local list of children and keys
  376. self.children[key] = child
  377. self.kidKeys.append(key)
  378. # [gjeon] to set flag recursively
  379. child.setChildrenTag(self.childrenTag, self.fModeChildrenTag)
  380. # See if node is child (or one of child's descendants)
  381. retVal = child.find(searchKey)
  382. if retVal:
  383. return retVal
  384. # Not here
  385. return None
  386. class TreeItem:
  387. """Abstract class representing tree items.
  388. Methods should typically be overridden, otherwise a default action
  389. is used.
  390. """
  391. def __init__(self):
  392. """Constructor. Do whatever you need to do."""
  393. def GetText(self):
  394. """Return text string to display."""
  395. def GetTextFg(self):
  396. return "black"
  397. def GetLabelText(self):
  398. """Return label text string to display in front of text (if any)."""
  399. def IsExpandable(self):
  400. """Return whether there are subitems."""
  401. return 1
  402. def _GetSubList(self):
  403. """Do not override! Called by TreeNode."""
  404. if not self.IsExpandable():
  405. return []
  406. sublist = self.GetSubList()
  407. return sublist
  408. def IsEditable(self):
  409. """Return whether the item's text may be edited."""
  410. def SetText(self, text):
  411. """Change the item's text (if it is editable)."""
  412. def GetIconName(self):
  413. """Return name of icon to be displayed normally."""
  414. def GetSelectedIconName(self):
  415. """Return name of icon to be displayed when selected."""
  416. def GetSubList(self):
  417. """Return list of items forming sublist."""
  418. def OnDoubleClick(self):
  419. """Called on a double-click on the item."""
  420. def OnSelect(self):
  421. """Called when item selected."""