Tree.py 17 KB

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