seTree.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418
  1. #################################################################
  2. # seTree.py
  3. # Originally from Tree.py
  4. # Altered by Yi-Hong Lin, [email protected], 2004
  5. #
  6. # This class actually decides the behavior of the sceneGraphExplorer
  7. # You might feel it realy looks like the original one, but we actually did a lots of change in it.
  8. # such as, when selection happend in other place, such as picking directly inside the scene. or,
  9. # when user removed something by hot key.
  10. # The rename process has also been changed. It won't be rename in here anymore.
  11. # Instead, here we will send out a message to sceneEditor to reaname the target.
  12. #
  13. #################################################################
  14. import os, sys, string, Pmw, Tkinter
  15. from direct.showbase.DirectObject import DirectObject
  16. from Tkinter import IntVar, Menu, PhotoImage, Label, Frame, Entry
  17. from pandac.PandaModules import *
  18. # Initialize icon directory
  19. ICONDIR = getModelPath().findFile(Filename('icons')).toOsSpecific()
  20. if not os.path.isdir(ICONDIR):
  21. raise RuntimeError, "can't find DIRECT icon directory (%s)" % `ICONDIR`
  22. class TreeNode:
  23. def __init__(self, canvas, parent, item, menuList = []):
  24. self.canvas = canvas
  25. self.parent = parent
  26. self.item = item
  27. self.state = 'collapsed'
  28. self.selected = 0
  29. self.children = {}
  30. self.kidKeys = []
  31. self.x = self.y = None
  32. self.iconimages = {} # cache of PhotoImage instances for icons
  33. self.menuList = menuList
  34. self.menuVar = IntVar()
  35. self.menuVar.set(0)
  36. self._popupMenu = None
  37. self.image_id = None
  38. if self.menuList:
  39. if self.menuList[-1] == 'Separator':
  40. self.menuList = self.menuList[:-1]
  41. self._popupMenu = Menu(self.canvas, tearoff = 0)
  42. for i in range(len(self.menuList)):
  43. item = self.menuList[i]
  44. if item == 'Separator':
  45. self._popupMenu.add_separator()
  46. else:
  47. self._popupMenu.add_radiobutton(
  48. label = item,
  49. variable = self.menuVar,
  50. value = i,
  51. indicatoron = 0,
  52. command = self.popupMenuCommand)
  53. def destroy(self):
  54. for key in self.kidKeys:
  55. c = self.children[key]
  56. del self.children[key]
  57. c.destroy()
  58. self.parent = None
  59. def geticonimage(self, name):
  60. try:
  61. return self.iconimages[name]
  62. except KeyError:
  63. pass
  64. file, ext = os.path.splitext(name)
  65. ext = ext or ".gif"
  66. fullname = os.path.join(ICONDIR, file + ext)
  67. image = PhotoImage(master=self.canvas, file=fullname)
  68. self.iconimages[name] = image
  69. return image
  70. def select(self, event=None):
  71. if self.selected:
  72. return
  73. self.deselectall()
  74. self.selected = 1
  75. if self.parent != None:
  76. if self.parent.state == 'expanded':
  77. self.canvas.delete(self.image_id)
  78. self.drawicon()
  79. self.drawtext()
  80. self.item.OnSelect(event)
  81. def deselect(self, event=None):
  82. if not self.selected:
  83. return
  84. self.selected = 0
  85. if self.parent != None:
  86. if self.parent.state == 'expanded':
  87. self.canvas.delete(self.image_id)
  88. self.drawicon()
  89. self.drawtext()
  90. def deselectall(self):
  91. if self.parent:
  92. self.parent.deselectall()
  93. else:
  94. self.deselecttree()
  95. def deselecttree(self):
  96. if self.selected:
  97. self.deselect()
  98. for key in self.kidKeys:
  99. child = self.children[key]
  100. child.deselecttree()
  101. def flip(self, event=None):
  102. if self.state == 'expanded':
  103. self.collapse()
  104. else:
  105. self.expand()
  106. self.item.OnDoubleClick()
  107. return "break"
  108. def popupMenu(self, event=None):
  109. if self._popupMenu:
  110. self._popupMenu.post(event.widget.winfo_pointerx(),
  111. event.widget.winfo_pointery())
  112. return "break"
  113. def popupMenuCommand(self):
  114. command = self.menuList[self.menuVar.get()]
  115. self.item.MenuCommand(command)
  116. if self.parent and (command != 'Update Explorer'):
  117. # Update parent to try to keep explorer up to date
  118. self.parent.update()
  119. def expand(self, event=None):
  120. if not self.item.IsExpandable():
  121. return
  122. if self.state != 'expanded':
  123. self.state = 'expanded'
  124. self.update()
  125. self.view()
  126. def collapse(self, event=None):
  127. if self.state != 'collapsed':
  128. self.state = 'collapsed'
  129. self.update()
  130. def view(self):
  131. top = self.y - 2
  132. bottom = self.lastvisiblechild().y + 17
  133. height = bottom - top
  134. visible_top = self.canvas.canvasy(0)
  135. visible_height = self.canvas.winfo_height()
  136. visible_bottom = self.canvas.canvasy(visible_height)
  137. if visible_top <= top and bottom <= visible_bottom:
  138. return
  139. x0, y0, x1, y1 = self.canvas._getints(self.canvas['scrollregion'])
  140. if top >= visible_top and height <= visible_height:
  141. fraction = top + height - visible_height
  142. else:
  143. fraction = top
  144. fraction = float(fraction) / y1
  145. self.canvas.yview_moveto(fraction)
  146. def reveal(self):
  147. # Make sure all parent nodes are marked as expanded
  148. parent = self.parent
  149. while parent:
  150. if parent.state == 'collapsed':
  151. parent.state = 'expanded'
  152. parent = parent.parent
  153. else:
  154. break
  155. # Redraw tree accordingly
  156. self.update()
  157. # Bring this item into view
  158. self.view()
  159. def lastvisiblechild(self):
  160. if self.kidKeys and self.state == 'expanded':
  161. return self.children[self.kidKeys[-1]].lastvisiblechild()
  162. else:
  163. return self
  164. def update(self):
  165. if self.parent:
  166. self.parent.update()
  167. else:
  168. oldcursor = self.canvas['cursor']
  169. self.canvas['cursor'] = "watch"
  170. self.canvas.update()
  171. self.canvas.delete(Tkinter.ALL) # XXX could be more subtle
  172. self.draw(7, 2)
  173. x0, y0, x1, y1 = self.canvas.bbox(Tkinter.ALL)
  174. self.canvas.configure(scrollregion=(0, 0, x1, y1))
  175. self.canvas['cursor'] = oldcursor
  176. def draw(self, x, y):
  177. # XXX This hard-codes too many geometry constants!
  178. self.x, self.y = x, y
  179. self.drawicon()
  180. self.drawtext()
  181. if self.state != 'expanded':
  182. return y+17
  183. # draw children
  184. sublist = self.item._GetSubList()
  185. if not sublist:
  186. # IsExpandable() was mistaken; that's allowed
  187. return y+17
  188. self.kidKeys = []
  189. for item in sublist:
  190. key = item.GetKey()
  191. if self.children.has_key(key):
  192. child = self.children[key]
  193. else:
  194. child = TreeNode(self.canvas, self, item, self.menuList)
  195. self.children[key] = child
  196. self.kidKeys.append(key)
  197. # Remove unused children
  198. for key in self.children.keys():
  199. if key not in self.kidKeys:
  200. del(self.children[key])
  201. cx = x+20
  202. cy = y+17
  203. cylast = 0
  204. for key in self.kidKeys:
  205. child = self.children[key]
  206. cylast = cy
  207. self.canvas.create_line(x+9, cy+7, cx, cy+7, fill="gray50")
  208. cy = child.draw(cx, cy)
  209. if child.item.IsExpandable():
  210. if child.state == 'expanded':
  211. iconname = "minusnode"
  212. callback = child.collapse
  213. else:
  214. iconname = "plusnode"
  215. callback = child.expand
  216. image = self.geticonimage(iconname)
  217. id = self.canvas.create_image(x+9, cylast+7, image=image)
  218. # XXX This leaks bindings until canvas is deleted:
  219. self.canvas.tag_bind(id, "<1>", callback)
  220. self.canvas.tag_bind(id, "<Double-1>", lambda x: None)
  221. id = self.canvas.create_line(x+9, y+10, x+9, cylast+7,
  222. ##stipple="gray50", # XXX Seems broken in Tk 8.0.x
  223. fill="gray50")
  224. self.canvas.tag_lower(id) # XXX .lower(id) before Python 1.5.2
  225. return cy
  226. def drawicon(self):
  227. if self.selected:
  228. imagename = (self.item.GetSelectedIconName() or
  229. self.item.GetIconName() or
  230. "openfolder")
  231. else:
  232. imagename = self.item.GetIconName() or "folder"
  233. image = self.geticonimage(imagename)
  234. id = self.canvas.create_image(self.x, self.y, anchor="nw", image=image)
  235. self.image_id = id
  236. self.canvas.tag_bind(id, "<1>", self.select)
  237. self.canvas.tag_bind(id, "<Double-1>", self.flip)
  238. self.canvas.tag_bind(id, "<3>", self.popupMenu)
  239. def drawtext(self, text=None):
  240. textx = self.x+20-1
  241. texty = self.y-1
  242. labeltext = self.item.GetLabelText()
  243. if labeltext:
  244. id = self.canvas.create_text(textx, texty, anchor="nw",
  245. text=labeltext)
  246. self.canvas.tag_bind(id, "<1>", self.select)
  247. self.canvas.tag_bind(id, "<Double-1>", self.flip)
  248. x0, y0, x1, y1 = self.canvas.bbox(id)
  249. textx = max(x1, 200) + 10
  250. if text==None:
  251. text = self.item.GetText() or "<no text>"
  252. try:
  253. self.entry
  254. except AttributeError:
  255. pass
  256. else:
  257. self.edit_finish()
  258. try:
  259. label = self.label
  260. except AttributeError:
  261. # padding carefully selected (on Windows) to match Entry widget:
  262. self.label = Label(self.canvas, text=text, bd=0, padx=2, pady=2)
  263. if self.selected:
  264. self.label.configure(fg="white", bg="darkblue")
  265. else:
  266. fg = self.item.GetTextFg()
  267. self.label.configure(fg=fg, bg="white")
  268. id = self.canvas.create_window(textx, texty,
  269. anchor="nw", window=self.label)
  270. self.label.bind("<1>", self.select_or_edit)
  271. self.label.bind("<Double-1>", self.flip)
  272. self.label.bind("<3>", self.popupMenu)
  273. # Update text if necessary
  274. if text != self.label['text']:
  275. self.label['text'] = text
  276. self.text_id = id
  277. def select_or_edit(self, event=None):
  278. if self.selected and self.item.IsEditable():
  279. text = self.item.GetTextForEdit()
  280. self.label['text'] = text
  281. self.drawtext(text)
  282. self.edit(event)
  283. else:
  284. self.select(event)
  285. def edit(self, event=None):
  286. self.entry = Entry(self.label, bd=0, highlightthickness=1, width=0)
  287. self.entry.insert(0, self.label['text'])
  288. self.entry.selection_range(0, Tkinter.END)
  289. self.entry.pack(ipadx=5)
  290. self.entry.focus_set()
  291. self.entry.bind("<Return>", self.edit_finish)
  292. self.entry.bind("<Escape>", self.edit_cancel)
  293. def edit_finish(self, event=None):
  294. try:
  295. entry = self.entry
  296. del self.entry
  297. except AttributeError:
  298. return
  299. text = entry.get()
  300. entry.destroy()
  301. if text and text != self.item.GetText():
  302. self.item.SetText(text)
  303. text = self.item.GetText()
  304. self.label['text'] = text
  305. self.drawtext()
  306. self.canvas.focus_set()
  307. def edit_cancel(self, event=None):
  308. self.drawtext()
  309. self.canvas.focus_set()
  310. def find(self, searchKey):
  311. # Search for a node who's key matches the given key
  312. # Is it this node
  313. if searchKey == self.item.GetKey():
  314. return self
  315. # Nope, check the children
  316. sublist = self.item._GetSubList()
  317. for item in sublist:
  318. key = item.GetKey()
  319. # Use existing child or create new TreeNode if none exists
  320. if self.children.has_key(key):
  321. child = self.children[key]
  322. else:
  323. child = TreeNode(self.canvas, self, item, self.menuList)
  324. # Update local list of children and keys
  325. self.children[key] = child
  326. self.kidKeys.append(key)
  327. # See if node is child (or one of child's descendants)
  328. retVal = child.find(searchKey)
  329. if retVal:
  330. return retVal
  331. # Not here
  332. return None
  333. class TreeItem:
  334. """Abstract class representing tree items.
  335. Methods should typically be overridden, otherwise a default action
  336. is used.
  337. """
  338. def __init__(self):
  339. """Constructor. Do whatever you need to do."""
  340. def GetText(self):
  341. """Return text string to display."""
  342. def GetTextFg(self):
  343. return "black"
  344. def GetLabelText(self):
  345. """Return label text string to display in front of text (if any)."""
  346. def IsExpandable(self):
  347. """Return whether there are subitems."""
  348. return 1
  349. def _GetSubList(self):
  350. """Do not override! Called by TreeNode."""
  351. if not self.IsExpandable():
  352. return []
  353. sublist = self.GetSubList()
  354. return sublist
  355. def IsEditable(self):
  356. """Return whether the item's text may be edited."""
  357. def SetText(self, text):
  358. """Change the item's text (if it is editable)."""
  359. def GetIconName(self):
  360. """Return name of icon to be displayed normally."""
  361. def GetSelectedIconName(self):
  362. """Return name of icon to be displayed when selected."""
  363. def GetSubList(self):
  364. """Return list of items forming sublist."""
  365. def OnDoubleClick(self):
  366. """Called on a double-click on the item."""
  367. def OnSelect(self):
  368. """Called when item selected."""
  369. def GetTextForEdit(self):
  370. """Called before editting the item."""