You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

477 lines
15 KiB

26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
  1. # XXX TO DO:
  2. # - popup menu
  3. # - support partial or total redisplay
  4. # - key bindings (instead of quick-n-dirty bindings on Canvas):
  5. # - up/down arrow keys to move focus around
  6. # - ditto for page up/down, home/end
  7. # - left/right arrows to expand/collapse & move out/in
  8. # - more doc strings
  9. # - add icons for "file", "module", "class", "method"; better "python" icon
  10. # - callback for selection???
  11. # - multiple-item selection
  12. # - tooltips
  13. # - redo geometry without magic numbers
  14. # - keep track of object ids to allow more careful cleaning
  15. # - optimize tree redraw after expand of subnode
  16. import os
  17. from tkinter import *
  18. import imp
  19. from idlelib import ZoomHeight
  20. from idlelib.configHandler import idleConf
  21. ICONDIR = "Icons"
  22. # Look for Icons subdirectory in the same directory as this module
  23. try:
  24. _icondir = os.path.join(os.path.dirname(__file__), ICONDIR)
  25. except NameError:
  26. _icondir = ICONDIR
  27. if os.path.isdir(_icondir):
  28. ICONDIR = _icondir
  29. elif not os.path.isdir(ICONDIR):
  30. raise RuntimeError("can't find icon directory (%r)" % (ICONDIR,))
  31. def listicons(icondir=ICONDIR):
  32. """Utility to display the available icons."""
  33. root = Tk()
  34. import glob
  35. list = glob.glob(os.path.join(icondir, "*.gif"))
  36. list.sort()
  37. images = []
  38. row = column = 0
  39. for file in list:
  40. name = os.path.splitext(os.path.basename(file))[0]
  41. image = PhotoImage(file=file, master=root)
  42. images.append(image)
  43. label = Label(root, image=image, bd=1, relief="raised")
  44. label.grid(row=row, column=column)
  45. label = Label(root, text=name)
  46. label.grid(row=row+1, column=column)
  47. column = column + 1
  48. if column >= 10:
  49. row = row+2
  50. column = 0
  51. root.images = images
  52. class TreeNode:
  53. def __init__(self, canvas, parent, item):
  54. self.canvas = canvas
  55. self.parent = parent
  56. self.item = item
  57. self.state = 'collapsed'
  58. self.selected = False
  59. self.children = []
  60. self.x = self.y = None
  61. self.iconimages = {} # cache of PhotoImage instances for icons
  62. def destroy(self):
  63. for c in self.children[:]:
  64. self.children.remove(c)
  65. c.destroy()
  66. self.parent = None
  67. def geticonimage(self, name):
  68. try:
  69. return self.iconimages[name]
  70. except KeyError:
  71. pass
  72. file, ext = os.path.splitext(name)
  73. ext = ext or ".gif"
  74. fullname = os.path.join(ICONDIR, file + ext)
  75. image = PhotoImage(master=self.canvas, file=fullname)
  76. self.iconimages[name] = image
  77. return image
  78. def select(self, event=None):
  79. if self.selected:
  80. return
  81. self.deselectall()
  82. self.selected = True
  83. self.canvas.delete(self.image_id)
  84. self.drawicon()
  85. self.drawtext()
  86. def deselect(self, event=None):
  87. if not self.selected:
  88. return
  89. self.selected = False
  90. self.canvas.delete(self.image_id)
  91. self.drawicon()
  92. self.drawtext()
  93. def deselectall(self):
  94. if self.parent:
  95. self.parent.deselectall()
  96. else:
  97. self.deselecttree()
  98. def deselecttree(self):
  99. if self.selected:
  100. self.deselect()
  101. for child in self.children:
  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 expand(self, event=None):
  111. if not self.item._IsExpandable():
  112. return
  113. if self.state != 'expanded':
  114. self.state = 'expanded'
  115. self.update()
  116. self.view()
  117. def collapse(self, event=None):
  118. if self.state != 'collapsed':
  119. self.state = 'collapsed'
  120. self.update()
  121. def view(self):
  122. top = self.y - 2
  123. bottom = self.lastvisiblechild().y + 17
  124. height = bottom - top
  125. visible_top = self.canvas.canvasy(0)
  126. visible_height = self.canvas.winfo_height()
  127. visible_bottom = self.canvas.canvasy(visible_height)
  128. if visible_top <= top and bottom <= visible_bottom:
  129. return
  130. x0, y0, x1, y1 = self.canvas._getints(self.canvas['scrollregion'])
  131. if top >= visible_top and height <= visible_height:
  132. fraction = top + height - visible_height
  133. else:
  134. fraction = top
  135. fraction = float(fraction) / y1
  136. self.canvas.yview_moveto(fraction)
  137. def lastvisiblechild(self):
  138. if self.children and self.state == 'expanded':
  139. return self.children[-1].lastvisiblechild()
  140. else:
  141. return self
  142. def update(self):
  143. if self.parent:
  144. self.parent.update()
  145. else:
  146. oldcursor = self.canvas['cursor']
  147. self.canvas['cursor'] = "watch"
  148. self.canvas.update()
  149. self.canvas.delete(ALL) # XXX could be more subtle
  150. self.draw(7, 2)
  151. x0, y0, x1, y1 = self.canvas.bbox(ALL)
  152. self.canvas.configure(scrollregion=(0, 0, x1, y1))
  153. self.canvas['cursor'] = oldcursor
  154. def draw(self, x, y):
  155. # XXX This hard-codes too many geometry constants!
  156. self.x, self.y = x, y
  157. self.drawicon()
  158. self.drawtext()
  159. if self.state != 'expanded':
  160. return y+17
  161. # draw children
  162. if not self.children:
  163. sublist = self.item._GetSubList()
  164. if not sublist:
  165. # _IsExpandable() was mistaken; that's allowed
  166. return y+17
  167. for item in sublist:
  168. child = self.__class__(self.canvas, self, item)
  169. self.children.append(child)
  170. cx = x+20
  171. cy = y+17
  172. cylast = 0
  173. for child in self.children:
  174. cylast = cy
  175. self.canvas.create_line(x+9, cy+7, cx, cy+7, fill="gray50")
  176. cy = child.draw(cx, cy)
  177. if child.item._IsExpandable():
  178. if child.state == 'expanded':
  179. iconname = "minusnode"
  180. callback = child.collapse
  181. else:
  182. iconname = "plusnode"
  183. callback = child.expand
  184. image = self.geticonimage(iconname)
  185. id = self.canvas.create_image(x+9, cylast+7, image=image)
  186. # XXX This leaks bindings until canvas is deleted:
  187. self.canvas.tag_bind(id, "<1>", callback)
  188. self.canvas.tag_bind(id, "<Double-1>", lambda x: None)
  189. id = self.canvas.create_line(x+9, y+10, x+9, cylast+7,
  190. ##stipple="gray50", # XXX Seems broken in Tk 8.0.x
  191. fill="gray50")
  192. self.canvas.tag_lower(id) # XXX .lower(id) before Python 1.5.2
  193. return cy
  194. def drawicon(self):
  195. if self.selected:
  196. imagename = (self.item.GetSelectedIconName() or
  197. self.item.GetIconName() or
  198. "openfolder")
  199. else:
  200. imagename = self.item.GetIconName() or "folder"
  201. image = self.geticonimage(imagename)
  202. id = self.canvas.create_image(self.x, self.y, anchor="nw", image=image)
  203. self.image_id = id
  204. self.canvas.tag_bind(id, "<1>", self.select)
  205. self.canvas.tag_bind(id, "<Double-1>", self.flip)
  206. def drawtext(self):
  207. textx = self.x+20-1
  208. texty = self.y-1
  209. labeltext = self.item.GetLabelText()
  210. if labeltext:
  211. id = self.canvas.create_text(textx, texty, anchor="nw",
  212. text=labeltext)
  213. self.canvas.tag_bind(id, "<1>", self.select)
  214. self.canvas.tag_bind(id, "<Double-1>", self.flip)
  215. x0, y0, x1, y1 = self.canvas.bbox(id)
  216. textx = max(x1, 200) + 10
  217. text = self.item.GetText() or "<no text>"
  218. try:
  219. self.entry
  220. except AttributeError:
  221. pass
  222. else:
  223. self.edit_finish()
  224. try:
  225. label = self.label
  226. except AttributeError:
  227. # padding carefully selected (on Windows) to match Entry widget:
  228. self.label = Label(self.canvas, text=text, bd=0, padx=2, pady=2)
  229. theme = idleConf.GetOption('main','Theme','name')
  230. if self.selected:
  231. self.label.configure(idleConf.GetHighlight(theme, 'hilite'))
  232. else:
  233. self.label.configure(idleConf.GetHighlight(theme, 'normal'))
  234. id = self.canvas.create_window(textx, texty,
  235. anchor="nw", window=self.label)
  236. self.label.bind("<1>", self.select_or_edit)
  237. self.label.bind("<Double-1>", self.flip)
  238. self.text_id = id
  239. def select_or_edit(self, event=None):
  240. if self.selected and self.item.IsEditable():
  241. self.edit(event)
  242. else:
  243. self.select(event)
  244. def edit(self, event=None):
  245. self.entry = Entry(self.label, bd=0, highlightthickness=1, width=0)
  246. self.entry.insert(0, self.label['text'])
  247. self.entry.selection_range(0, END)
  248. self.entry.pack(ipadx=5)
  249. self.entry.focus_set()
  250. self.entry.bind("<Return>", self.edit_finish)
  251. self.entry.bind("<Escape>", self.edit_cancel)
  252. def edit_finish(self, event=None):
  253. try:
  254. entry = self.entry
  255. del self.entry
  256. except AttributeError:
  257. return
  258. text = entry.get()
  259. entry.destroy()
  260. if text and text != self.item.GetText():
  261. self.item.SetText(text)
  262. text = self.item.GetText()
  263. self.label['text'] = text
  264. self.drawtext()
  265. self.canvas.focus_set()
  266. def edit_cancel(self, event=None):
  267. try:
  268. entry = self.entry
  269. del self.entry
  270. except AttributeError:
  271. return
  272. entry.destroy()
  273. self.drawtext()
  274. self.canvas.focus_set()
  275. class TreeItem:
  276. """Abstract class representing tree items.
  277. Methods should typically be overridden, otherwise a default action
  278. is used.
  279. """
  280. def __init__(self):
  281. """Constructor. Do whatever you need to do."""
  282. def GetText(self):
  283. """Return text string to display."""
  284. def GetLabelText(self):
  285. """Return label text string to display in front of text (if any)."""
  286. expandable = None
  287. def _IsExpandable(self):
  288. """Do not override! Called by TreeNode."""
  289. if self.expandable is None:
  290. self.expandable = self.IsExpandable()
  291. return self.expandable
  292. def IsExpandable(self):
  293. """Return whether there are subitems."""
  294. return 1
  295. def _GetSubList(self):
  296. """Do not override! Called by TreeNode."""
  297. if not self.IsExpandable():
  298. return []
  299. sublist = self.GetSubList()
  300. if not sublist:
  301. self.expandable = 0
  302. return sublist
  303. def IsEditable(self):
  304. """Return whether the item's text may be edited."""
  305. def SetText(self, text):
  306. """Change the item's text (if it is editable)."""
  307. def GetIconName(self):
  308. """Return name of icon to be displayed normally."""
  309. def GetSelectedIconName(self):
  310. """Return name of icon to be displayed when selected."""
  311. def GetSubList(self):
  312. """Return list of items forming sublist."""
  313. def OnDoubleClick(self):
  314. """Called on a double-click on the item."""
  315. # Example application
  316. class FileTreeItem(TreeItem):
  317. """Example TreeItem subclass -- browse the file system."""
  318. def __init__(self, path):
  319. self.path = path
  320. def GetText(self):
  321. return os.path.basename(self.path) or self.path
  322. def IsEditable(self):
  323. return os.path.basename(self.path) != ""
  324. def SetText(self, text):
  325. newpath = os.path.dirname(self.path)
  326. newpath = os.path.join(newpath, text)
  327. if os.path.dirname(newpath) != os.path.dirname(self.path):
  328. return
  329. try:
  330. os.rename(self.path, newpath)
  331. self.path = newpath
  332. except os.error:
  333. pass
  334. def GetIconName(self):
  335. if not self.IsExpandable():
  336. return "python" # XXX wish there was a "file" icon
  337. def IsExpandable(self):
  338. return os.path.isdir(self.path)
  339. def GetSubList(self):
  340. try:
  341. names = os.listdir(self.path)
  342. except os.error:
  343. return []
  344. names.sort(key = os.path.normcase)
  345. sublist = []
  346. for name in names:
  347. item = FileTreeItem(os.path.join(self.path, name))
  348. sublist.append(item)
  349. return sublist
  350. # A canvas widget with scroll bars and some useful bindings
  351. class ScrolledCanvas:
  352. def __init__(self, master, **opts):
  353. if 'yscrollincrement' not in opts:
  354. opts['yscrollincrement'] = 17
  355. self.master = master
  356. self.frame = Frame(master)
  357. self.frame.rowconfigure(0, weight=1)
  358. self.frame.columnconfigure(0, weight=1)
  359. self.canvas = Canvas(self.frame, **opts)
  360. self.canvas.grid(row=0, column=0, sticky="nsew")
  361. self.vbar = Scrollbar(self.frame, name="vbar")
  362. self.vbar.grid(row=0, column=1, sticky="nse")
  363. self.hbar = Scrollbar(self.frame, name="hbar", orient="horizontal")
  364. self.hbar.grid(row=1, column=0, sticky="ews")
  365. self.canvas['yscrollcommand'] = self.vbar.set
  366. self.vbar['command'] = self.canvas.yview
  367. self.canvas['xscrollcommand'] = self.hbar.set
  368. self.hbar['command'] = self.canvas.xview
  369. self.canvas.bind("<Key-Prior>", self.page_up)
  370. self.canvas.bind("<Key-Next>", self.page_down)
  371. self.canvas.bind("<Key-Up>", self.unit_up)
  372. self.canvas.bind("<Key-Down>", self.unit_down)
  373. #if isinstance(master, Toplevel) or isinstance(master, Tk):
  374. self.canvas.bind("<Alt-Key-2>", self.zoom_height)
  375. self.canvas.focus_set()
  376. def page_up(self, event):
  377. self.canvas.yview_scroll(-1, "page")
  378. return "break"
  379. def page_down(self, event):
  380. self.canvas.yview_scroll(1, "page")
  381. return "break"
  382. def unit_up(self, event):
  383. self.canvas.yview_scroll(-1, "unit")
  384. return "break"
  385. def unit_down(self, event):
  386. self.canvas.yview_scroll(1, "unit")
  387. return "break"
  388. def zoom_height(self, event):
  389. ZoomHeight.zoom_height(self.master)
  390. return "break"
  391. # Testing functions
  392. def test():
  393. from idlelib import PyShell
  394. root = Toplevel(PyShell.root)
  395. root.configure(bd=0, bg="yellow")
  396. root.focus_set()
  397. sc = ScrolledCanvas(root, bg="white", highlightthickness=0, takefocus=1)
  398. sc.frame.pack(expand=1, fill="both")
  399. item = FileTreeItem("C:/windows/desktop")
  400. node = TreeNode(sc.canvas, None, item)
  401. node.expand()
  402. def test2():
  403. # test w/o scrolling canvas
  404. root = Tk()
  405. root.configure(bd=0)
  406. canvas = Canvas(root, bg="white", highlightthickness=0)
  407. canvas.pack(expand=1, fill="both")
  408. item = FileTreeItem(os.curdir)
  409. node = TreeNode(canvas, None, item)
  410. node.update()
  411. canvas.focus_set()
  412. if __name__ == '__main__':
  413. test()