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