Mercurial > repos > rliterman > csp2
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) |