0001 # XXX TO DO: 0002 # - popup menu 0003 # - support partial or total redisplay 0004 # - key bindings (instead of quick-n-dirty bindings on Canvas): 0005 # - up/down arrow keys to move focus around 0006 # - ditto for page up/down, home/end 0007 # - left/right arrows to expand/collapse & move out/in 0008 # - more doc strings 0009 # - add icons for "file", "module", "class", "method"; better "python" icon 0010 # - callback for selection??? 0011 # - multiple-item selection 0012 # - tooltips 0013 # - redo geometry without magic numbers 0014 # - keep track of object ids to allow more careful cleaning 0015 # - optimize tree redraw after expand of subnode 0016 0017 import os 0018 import sys 0019 from Tkinter import * 0020 import imp 0021 0022 import ZoomHeight 0023 from configHandler import idleConf 0024 0025 ICONDIR = "Icons" 0026 0027 # Look for Icons subdirectory in the same directory as this module 0028 try: 0029 _icondir = os.path.join(os.path.dirname(__file__), ICONDIR) 0030 except NameError: 0031 _icondir = ICONDIR 0032 if os.path.isdir(_icondir): 0033 ICONDIR = _icondir 0034 elif not os.path.isdir(ICONDIR): 0035 raise RuntimeError, "can't find icon directory (%r)" % (ICONDIR,) 0036 0037 def listicons(icondir=ICONDIR): 0038 """Utility to display the available icons.""" 0039 root = Tk() 0040 import glob 0041 list = glob.glob(os.path.join(icondir, "*.gif")) 0042 list.sort() 0043 images = [] 0044 row = column = 0 0045 for file in list: 0046 name = os.path.splitext(os.path.basename(file))[0] 0047 image = PhotoImage(file=file, master=root) 0048 images.append(image) 0049 label = Label(root, image=image, bd=1, relief="raised") 0050 label.grid(row=row, column=column) 0051 label = Label(root, text=name) 0052 label.grid(row=row+1, column=column) 0053 column = column + 1 0054 if column >= 10: 0055 row = row+2 0056 column = 0 0057 root.images = images 0058 0059 0060 class TreeNode: 0061 0062 def __init__(self, canvas, parent, item): 0063 self.canvas = canvas 0064 self.parent = parent 0065 self.item = item 0066 self.state = 'collapsed' 0067 self.selected = False 0068 self.children = [] 0069 self.x = self.y = None 0070 self.iconimages = {} # cache of PhotoImage instances for icons 0071 0072 def destroy(self): 0073 for c in self.children[:]: 0074 self.children.remove(c) 0075 c.destroy() 0076 self.parent = None 0077 0078 def geticonimage(self, name): 0079 try: 0080 return self.iconimages[name] 0081 except KeyError: 0082 pass 0083 file, ext = os.path.splitext(name) 0084 ext = ext or ".gif" 0085 fullname = os.path.join(ICONDIR, file + ext) 0086 image = PhotoImage(master=self.canvas, file=fullname) 0087 self.iconimages[name] = image 0088 return image 0089 0090 def select(self, event=None): 0091 if self.selected: 0092 return 0093 self.deselectall() 0094 self.selected = True 0095 self.canvas.delete(self.image_id) 0096 self.drawicon() 0097 self.drawtext() 0098 0099 def deselect(self, event=None): 0100 if not self.selected: 0101 return 0102 self.selected = False 0103 self.canvas.delete(self.image_id) 0104 self.drawicon() 0105 self.drawtext() 0106 0107 def deselectall(self): 0108 if self.parent: 0109 self.parent.deselectall() 0110 else: 0111 self.deselecttree() 0112 0113 def deselecttree(self): 0114 if self.selected: 0115 self.deselect() 0116 for child in self.children: 0117 child.deselecttree() 0118 0119 def flip(self, event=None): 0120 if self.state == 'expanded': 0121 self.collapse() 0122 else: 0123 self.expand() 0124 self.item.OnDoubleClick() 0125 return "break" 0126 0127 def expand(self, event=None): 0128 if not self.item._IsExpandable(): 0129 return 0130 if self.state != 'expanded': 0131 self.state = 'expanded' 0132 self.update() 0133 self.view() 0134 0135 def collapse(self, event=None): 0136 if self.state != 'collapsed': 0137 self.state = 'collapsed' 0138 self.update() 0139 0140 def view(self): 0141 top = self.y - 2 0142 bottom = self.lastvisiblechild().y + 17 0143 height = bottom - top 0144 visible_top = self.canvas.canvasy(0) 0145 visible_height = self.canvas.winfo_height() 0146 visible_bottom = self.canvas.canvasy(visible_height) 0147 if visible_top <= top and bottom <= visible_bottom: 0148 return 0149 x0, y0, x1, y1 = self.canvas._getints(self.canvas['scrollregion']) 0150 if top >= visible_top and height <= visible_height: 0151 fraction = top + height - visible_height 0152 else: 0153 fraction = top 0154 fraction = float(fraction) / y1 0155 self.canvas.yview_moveto(fraction) 0156 0157 def lastvisiblechild(self): 0158 if self.children and self.state == 'expanded': 0159 return self.children[-1].lastvisiblechild() 0160 else: 0161 return self 0162 0163 def update(self): 0164 if self.parent: 0165 self.parent.update() 0166 else: 0167 oldcursor = self.canvas['cursor'] 0168 self.canvas['cursor'] = "watch" 0169 self.canvas.update() 0170 self.canvas.delete(ALL) # XXX could be more subtle 0171 self.draw(7, 2) 0172 x0, y0, x1, y1 = self.canvas.bbox(ALL) 0173 self.canvas.configure(scrollregion=(0, 0, x1, y1)) 0174 self.canvas['cursor'] = oldcursor 0175 0176 def draw(self, x, y): 0177 # XXX This hard-codes too many geometry constants! 0178 self.x, self.y = x, y 0179 self.drawicon() 0180 self.drawtext() 0181 if self.state != 'expanded': 0182 return y+17 0183 # draw children 0184 if not self.children: 0185 sublist = self.item._GetSubList() 0186 if not sublist: 0187 # _IsExpandable() was mistaken; that's allowed 0188 return y+17 0189 for item in sublist: 0190 child = self.__class__(self.canvas, self, item) 0191 self.children.append(child) 0192 cx = x+20 0193 cy = y+17 0194 cylast = 0 0195 for child in self.children: 0196 cylast = cy 0197 self.canvas.create_line(x+9, cy+7, cx, cy+7, fill="gray50") 0198 cy = child.draw(cx, cy) 0199 if child.item._IsExpandable(): 0200 if child.state == 'expanded': 0201 iconname = "minusnode" 0202 callback = child.collapse 0203 else: 0204 iconname = "plusnode" 0205 callback = child.expand 0206 image = self.geticonimage(iconname) 0207 id = self.canvas.create_image(x+9, cylast+7, image=image) 0208 # XXX This leaks bindings until canvas is deleted: 0209 self.canvas.tag_bind(id, "<1>", callback) 0210 self.canvas.tag_bind(id, "<Double-1>", lambda x: None) 0211 id = self.canvas.create_line(x+9, y+10, x+9, cylast+7, 0212 ##stipple="gray50", # XXX Seems broken in Tk 8.0.x 0213 fill="gray50") 0214 self.canvas.tag_lower(id) # XXX .lower(id) before Python 1.5.2 0215 return cy 0216 0217 def drawicon(self): 0218 if self.selected: 0219 imagename = (self.item.GetSelectedIconName() or 0220 self.item.GetIconName() or 0221 "openfolder") 0222 else: 0223 imagename = self.item.GetIconName() or "folder" 0224 image = self.geticonimage(imagename) 0225 id = self.canvas.create_image(self.x, self.y, anchor="nw", image=image) 0226 self.image_id = id 0227 self.canvas.tag_bind(id, "<1>", self.select) 0228 self.canvas.tag_bind(id, "<Double-1>", self.flip) 0229 0230 def drawtext(self): 0231 textx = self.x+20-1 0232 texty = self.y-1 0233 labeltext = self.item.GetLabelText() 0234 if labeltext: 0235 id = self.canvas.create_text(textx, texty, anchor="nw", 0236 text=labeltext) 0237 self.canvas.tag_bind(id, "<1>", self.select) 0238 self.canvas.tag_bind(id, "<Double-1>", self.flip) 0239 x0, y0, x1, y1 = self.canvas.bbox(id) 0240 textx = max(x1, 200) + 10 0241 text = self.item.GetText() or "<no text>" 0242 try: 0243 self.entry 0244 except AttributeError: 0245 pass 0246 else: 0247 self.edit_finish() 0248 try: 0249 label = self.label 0250 except AttributeError: 0251 # padding carefully selected (on Windows) to match Entry widget: 0252 self.label = Label(self.canvas, text=text, bd=0, padx=2, pady=2) 0253 theme = idleConf.GetOption('main','Theme','name') 0254 if self.selected: 0255 self.label.configure(idleConf.GetHighlight(theme, 'hilite')) 0256 else: 0257 self.label.configure(idleConf.GetHighlight(theme, 'normal')) 0258 id = self.canvas.create_window(textx, texty, 0259 anchor="nw", window=self.label) 0260 self.label.bind("<1>", self.select_or_edit) 0261 self.label.bind("<Double-1>", self.flip) 0262 self.text_id = id 0263 0264 def select_or_edit(self, event=None): 0265 if self.selected and self.item.IsEditable(): 0266 self.edit(event) 0267 else: 0268 self.select(event) 0269 0270 def edit(self, event=None): 0271 self.entry = Entry(self.label, bd=0, highlightthickness=1, width=0) 0272 self.entry.insert(0, self.label['text']) 0273 self.entry.selection_range(0, END) 0274 self.entry.pack(ipadx=5) 0275 self.entry.focus_set() 0276 self.entry.bind("<Return>", self.edit_finish) 0277 self.entry.bind("<Escape>", self.edit_cancel) 0278 0279 def edit_finish(self, event=None): 0280 try: 0281 entry = self.entry 0282 del self.entry 0283 except AttributeError: 0284 return 0285 text = entry.get() 0286 entry.destroy() 0287 if text and text != self.item.GetText(): 0288 self.item.SetText(text) 0289 text = self.item.GetText() 0290 self.label['text'] = text 0291 self.drawtext() 0292 self.canvas.focus_set() 0293 0294 def edit_cancel(self, event=None): 0295 try: 0296 entry = self.entry 0297 del self.entry 0298 except AttributeError: 0299 return 0300 entry.destroy() 0301 self.drawtext() 0302 self.canvas.focus_set() 0303 0304 0305 class TreeItem: 0306 0307 """Abstract class representing tree items. 0308 0309 Methods should typically be overridden, otherwise a default action 0310 is used. 0311 0312 """ 0313 0314 def __init__(self): 0315 """Constructor. Do whatever you need to do.""" 0316 0317 def GetText(self): 0318 """Return text string to display.""" 0319 0320 def GetLabelText(self): 0321 """Return label text string to display in front of text (if any).""" 0322 0323 expandable = None 0324 0325 def _IsExpandable(self): 0326 """Do not override! Called by TreeNode.""" 0327 if self.expandable is None: 0328 self.expandable = self.IsExpandable() 0329 return self.expandable 0330 0331 def IsExpandable(self): 0332 """Return whether there are subitems.""" 0333 return 1 0334 0335 def _GetSubList(self): 0336 """Do not override! Called by TreeNode.""" 0337 if not self.IsExpandable(): 0338 return [] 0339 sublist = self.GetSubList() 0340 if not sublist: 0341 self.expandable = 0 0342 return sublist 0343 0344 def IsEditable(self): 0345 """Return whether the item's text may be edited.""" 0346 0347 def SetText(self, text): 0348 """Change the item's text (if it is editable).""" 0349 0350 def GetIconName(self): 0351 """Return name of icon to be displayed normally.""" 0352 0353 def GetSelectedIconName(self): 0354 """Return name of icon to be displayed when selected.""" 0355 0356 def GetSubList(self): 0357 """Return list of items forming sublist.""" 0358 0359 def OnDoubleClick(self): 0360 """Called on a double-click on the item.""" 0361 0362 0363 # Example application 0364 0365 class FileTreeItem(TreeItem): 0366 0367 """Example TreeItem subclass -- browse the file system.""" 0368 0369 def __init__(self, path): 0370 self.path = path 0371 0372 def GetText(self): 0373 return os.path.basename(self.path) or self.path 0374 0375 def IsEditable(self): 0376 return os.path.basename(self.path) != "" 0377 0378 def SetText(self, text): 0379 newpath = os.path.dirname(self.path) 0380 newpath = os.path.join(newpath, text) 0381 if os.path.dirname(newpath) != os.path.dirname(self.path): 0382 return 0383 try: 0384 os.rename(self.path, newpath) 0385 self.path = newpath 0386 except os.error: 0387 pass 0388 0389 def GetIconName(self): 0390 if not self.IsExpandable(): 0391 return "python" # XXX wish there was a "file" icon 0392 0393 def IsExpandable(self): 0394 return os.path.isdir(self.path) 0395 0396 def GetSubList(self): 0397 try: 0398 names = os.listdir(self.path) 0399 except os.error: 0400 return [] 0401 names.sort(lambda a, b: cmp(os.path.normcase(a), os.path.normcase(b))) 0402 sublist = [] 0403 for name in names: 0404 item = FileTreeItem(os.path.join(self.path, name)) 0405 sublist.append(item) 0406 return sublist 0407 0408 0409 # A canvas widget with scroll bars and some useful bindings 0410 0411 class ScrolledCanvas: 0412 def __init__(self, master, **opts): 0413 if not opts.has_key('yscrollincrement'): 0414 opts['yscrollincrement'] = 17 0415 self.master = master 0416 self.frame = Frame(master) 0417 self.frame.rowconfigure(0, weight=1) 0418 self.frame.columnconfigure(0, weight=1) 0419 self.canvas = Canvas(self.frame, **opts) 0420 self.canvas.grid(row=0, column=0, sticky="nsew") 0421 self.vbar = Scrollbar(self.frame, name="vbar") 0422 self.vbar.grid(row=0, column=1, sticky="nse") 0423 self.hbar = Scrollbar(self.frame, name="hbar", orient="horizontal") 0424 self.hbar.grid(row=1, column=0, sticky="ews") 0425 self.canvas['yscrollcommand'] = self.vbar.set 0426 self.vbar['command'] = self.canvas.yview 0427 self.canvas['xscrollcommand'] = self.hbar.set 0428 self.hbar['command'] = self.canvas.xview 0429 self.canvas.bind("<Key-Prior>", self.page_up) 0430 self.canvas.bind("<Key-Next>", self.page_down) 0431 self.canvas.bind("<Key-Up>", self.unit_up) 0432 self.canvas.bind("<Key-Down>", self.unit_down) 0433 #if isinstance(master, Toplevel) or isinstance(master, Tk): 0434 self.canvas.bind("<Alt-Key-2>", self.zoom_height) 0435 self.canvas.focus_set() 0436 def page_up(self, event): 0437 self.canvas.yview_scroll(-1, "page") 0438 return "break" 0439 def page_down(self, event): 0440 self.canvas.yview_scroll(1, "page") 0441 return "break" 0442 def unit_up(self, event): 0443 self.canvas.yview_scroll(-1, "unit") 0444 return "break" 0445 def unit_down(self, event): 0446 self.canvas.yview_scroll(1, "unit") 0447 return "break" 0448 def zoom_height(self, event): 0449 ZoomHeight.zoom_height(self.master) 0450 return "break" 0451 0452 0453 # Testing functions 0454 0455 def test(): 0456 import PyShell 0457 root = Toplevel(PyShell.root) 0458 root.configure(bd=0, bg="yellow") 0459 root.focus_set() 0460 sc = ScrolledCanvas(root, bg="white", highlightthickness=0, takefocus=1) 0461 sc.frame.pack(expand=1, fill="both") 0462 item = FileTreeItem("C:/windows/desktop") 0463 node = TreeNode(sc.canvas, None, item) 0464 node.expand() 0465 0466 def test2(): 0467 # test w/o scrolling canvas 0468 root = Tk() 0469 root.configure(bd=0) 0470 canvas = Canvas(root, bg="white", highlightthickness=0) 0471 canvas.pack(expand=1, fill="both") 0472 item = FileTreeItem(os.curdir) 0473 node = TreeNode(canvas, None, item) 0474 node.update() 0475 canvas.focus_set() 0476 0477 if __name__ == '__main__': 0478 test() 0479
Generated by PyXR 0.9.4