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