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