comparison CSP2/CSP2_env/env-d9b9114564458d9d-741b3de822f2aaca6c6caa4325c4afce/lib/python3.8/idlelib/tree.py @ 69:33d812a61356

planemo upload commit 2e9511a184a1ca667c7be0c6321a36dc4e3d116d
author jpayne
date Tue, 18 Mar 2025 17:55:14 -0400
parents
children
comparison
equal deleted inserted replaced
67:0e9998148a16 69:33d812a61356
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("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 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 TreeNode:
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 TreeItem:
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 FileTreeItem(TreeItem):
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 ScrolledCanvas:
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 if __name__ == '__main__':
496 from unittest import main
497 main('idlelib.idle_test.test_tree', verbosity=2, exit=False)
498
499 from idlelib.idle_test.htest import run
500 run(_tree_widget)