PyXR

c:\python24\lib \ idlelib \ TreeWidget.py



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
SourceForge.net Logo