Tree.py 12 KB

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