1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79 import Tkdnd
80 from Tkinter import *
81
82
84 """report exception on sys.stderr."""
85 import traceback
86 import sys
87
88 sys.stderr.write("Exception in Tree control callback\n")
89 traceback.print_exc()
90
91
93 """Helper object for add_node() method"""
96
97
99 """Tree helper class that's instantiated for each element in the tree. It
100 has several useful attributes:
101 parent_node - immediate parent node
102 id - id assigned at creation
103 expanded_icon - image displayed when folder is expanded to display children
104 collapsed_icon - image displayed when node is not a folder or folder is collapsed.
105 parent_widget - reference to tree widget that contains node.
106 expandable_flag - is true when node is a folder that may be expanded or collapsed.
107 expanded_flag - true to indicate node is currently expanded.
108 h_line - canvas line to left of node image.
109 v_line - canvas line below node image that connects children.
110 indic - expand/collapse canvas image.
111 label - canvas text label
112 symbol - current canvas image
113
114 Please note that methods prefixed PVT_* are not meant to be used by
115 client programs."""
116
117 - def __init__(self, parent_node, id, collapsed_icon, x, y,
118 parent_widget=None, expanded_icon=None, label=None,
119 expandable_flag=0):
120 """Create node and initialize it. This also displays the node at the
121 given position on the canvas, and binds mouseclicks."""
122
123 self.parent_node=parent_node
124
125 self.id=id
126
127 self.expanded_icon=expanded_icon
128 self.collapsed_icon=collapsed_icon
129
130 if parent_widget:
131 self.widget=parent_widget
132 else:
133 self.widget=parent_node.widget
134
135 sw=self.widget
136
137 self.child_nodes=[]
138
139 self.expandable_flag=expandable_flag
140 self.expanded_flag=0
141
142 if parent_node and sw.line_flag:
143 self.h_line=sw.create_line(x, y, x-sw.dist_x, y)
144 else:
145 self.h_line=None
146 self.v_line=None
147
148 self.symbol=sw.create_image(x, y, image=self.collapsed_icon)
149
150 self.indic=None
151 if expandable_flag and sw.line_flag and sw.plus_icon and sw.minus_icon:
152 self.indic=sw.create_image(x-sw.dist_x, y, image=sw.plus_icon)
153
154 self.label=sw.create_text(x+sw.text_offset, y, text=label, anchor='w')
155
156 if self.indic:
157 sw.tag_bind(self.indic, '<1>', self.PVT_click)
158 else:
159 sw.tag_bind(self.symbol, '<1>', self.PVT_click)
160
161 sw.tag_bind(self.symbol, '<Any-Enter>', self.PVT_enter)
162 sw.tag_bind(self.label, '<Any-Enter>', self.PVT_enter)
163
164
165
166
167
168
170 """Set node's collapsed image"""
171 self.collapsed_icon=icon
172 if not self.expanded_flag:
173 self.widget.itemconfig(self.symbol, image=icon)
174
176 """Set node's expanded image"""
177 self.expanded_icon=icon
178 if self.expanded_flag:
179 self.widget.itemconfig(self.symbol, image=icon)
180
182 """Return node's parent node"""
183 return self.parent_node
184
186 """Return node's previous sibling (the child immediately above it)"""
187 i=self.parent_node.child_nodes.index(self)-1
188 if i >= 0:
189 return self.parent_node.child_nodes[i]
190 else:
191 return None
192
194 """Return node's next sibling (the child immediately below it)"""
195 i=self.parent_node.child_nodes.index(self)+1
196 if i < len(self.parent_node.child_nodes):
197 return self.parent_node.child_nodes[i]
198 else:
199 return None
200
202 """Return next lower visible node"""
203 n=self
204 if n.child_nodes:
205
206 return n.child_nodes[0]
207 while n.parent_node:
208
209 i=n.parent_node.child_nodes.index(n)+1
210 if i < len(n.parent_node.child_nodes):
211 return n.parent_node.child_nodes[i]
212
213 n=n.parent_node
214
215 return self
216
218 """Return next higher visible node"""
219 n=self
220 if n.parent_node:
221 i=n.parent_node.child_nodes.index(n)-1
222 if i < 0:
223 return n.parent_node
224 else:
225 j=n.parent_node.child_nodes[i]
226 return j.PVT_last()
227 else:
228 return n
229
231 """Return list of node's children"""
232 return self.child_nodes[:]
233
235 """Return string containing text of current label"""
236 return self.widget.itemcget(self.label, 'text')
237
239 """Set current text label"""
240 self.widget.itemconfig(self.label, text=label)
241
243 """Returns true if node is currently expanded, false otherwise"""
244 return self.expanded_flag
245
247 """Returns true if node can be expanded (i.e. if it's a folder)"""
248 return self.expandable_flag
249
251 """Return list of IDs of all parents and node ID"""
252 if self.parent_node:
253 return self.parent_node.full_id()+(self.id,)
254 else:
255 return (self.id,)
256
258 """Expand node if possible"""
259 if not self.expanded_flag:
260 self.PVT_set_state(1)
261
263 """Collapse node if possible"""
264 if self.expanded_flag:
265 self.PVT_set_state(0)
266
268 """Delete node from tree. ("me_too" is a hack not to be used by
269 external code, please!)"""
270 sw=self.widget
271 if not self.parent_node and me_too:
272
273 raise ValueError, "can't delete root node"
274 self.PVT_delete_subtree()
275
276 n=self.next_visible()
277 x1, y1=sw.coords(self.symbol)
278 x2, y2=sw.coords(n.symbol)
279 if me_too:
280 dist=y2-y1
281 else:
282 dist=y2-y1-sw.dist_y
283 self.PVT_tag_move(-dist)
284 n=self
285 if me_too:
286 if sw.pos == self:
287
288 sw.move_cursor(self.parent_node)
289 self.PVT_unbind_all()
290 sw.delete(self.symbol)
291 sw.delete(self.label)
292 sw.delete(self.h_line)
293 sw.delete(self.v_line)
294 sw.delete(self.indic)
295 self.parent_node.child_nodes.remove(self)
296
297 n=self.parent_node
298 self.parent_node=None
299 n.PVT_cleanup_lines()
300 n.PVT_update_scrollregion()
301
303 """Insert list of nodes as siblings before this node. Call parent
304 node's add_node() function to generate the list of nodes."""
305 i=self.parent_node.child_nodes.index(self)
306 self.parent_node.PVT_insert(nodes, i, self.prev_visible())
307
309 """Insert list of nodes as siblings after this node. Call parent
310 node's add_node() function to generate the list of nodes."""
311 i=self.parent_node.child_nodes.index(self)+1
312 self.parent_node.PVT_insert(nodes, i, self.PVT_last())
313
315 """Insert list of nodes as children of this node. Call node's
316 add_node() function to generate the list of nodes."""
317 self.PVT_insert(nodes, 0, self)
318
320 """Toggle node's state between expanded and collapsed, if possible"""
321 if self.expandable_flag:
322 if self.expanded_flag:
323 self.PVT_set_state(0)
324 else:
325 self.PVT_set_state(1)
326
327
329 """detect mouse hover for drag'n'drop"""
330 self.widget.target=self
331
333 """Notification that dnd processing has been ended. It DOES NOT imply
334 that we've been dropped somewhere useful, we could have just been
335 dropped into deep space and nothing happened to any data structures,
336 or it could have been just a plain mouse-click w/o any dragging."""
337 if not self.widget.drag:
338
339 self.widget.move_cursor(self)
340 self.toggle_state()
341 self.widget.drag=0
342
343
344
346 """Return bottom-most node in subtree"""
347 n=self
348 while n.child_nodes:
349 n=n.child_nodes[-1]
350 return n
351
353 """Used by searching functions"""
354 if self.id != search[0]:
355
356 return None
357 if len(search) == 1:
358 return self
359
360 i=map(lambda x: x.id, self.child_nodes)
361
362 try:
363 return self.child_nodes[i.index(search[1])].PVT_find(search[1:])
364 except:
365 return None
366
368 """Create and insert new children. "nodes" is list previously created
369 via calls to add_list(). "pos" is index in the list of children where
370 the new nodes are inserted. "below" is node which new children should
371 appear immediately below."""
372 if not self.expandable_flag:
373 raise TypeError, 'not an expandable node'
374
375 sw=self.widget
376
377 children=[]
378 self.expanded_flag=1
379 sw.itemconfig(self.symbol, image=self.expanded_icon)
380 if sw.minus_icon and sw.line_flag:
381 sw.itemconfig(self.indic, image=sw.minus_icon)
382 if len(nodes):
383
384 below.PVT_tag_move(sw.dist_y*len(nodes))
385
386 xp, dummy=sw.coords(self.symbol)
387 dummy, yp=sw.coords(below.symbol)
388 xp=xp+sw.dist_x
389 yp=yp+sw.dist_y
390
391 if sw.line_flag and not self.v_line:
392 self.v_line=sw.create_line(
393 xp, yp,
394 xp, yp+sw.dist_y*len(nodes))
395 sw.tag_lower(self.v_line, self.symbol)
396 n=sw.node_class
397 for i in nodes:
398
399
400 children.append(
401 n(parent_node=self, expandable_flag=i.flag, label=i.name,
402 id=i.id, collapsed_icon=i.collapsed_icon,
403 expanded_icon=i.expanded_icon, x=xp, y=yp))
404 yp=yp+sw.dist_y
405 self.child_nodes[pos:pos]=children
406 self.PVT_cleanup_lines()
407 self.PVT_update_scrollregion()
408 sw.move_cursor(sw.pos)
409
411 """Common code forexpanding/collapsing folders. It's not re-entrant,
412 and there are certain cases in which we can be called again before
413 we're done, so we use a mutex."""
414 while self.widget.spinlock:
415 pass
416 self.widget.spinlock=1
417
418 if state:
419 self.child_nodes=[]
420 self.widget.new_nodes=[]
421 if self.widget.get_contents_callback:
422
423 try:
424 self.widget.get_contents_callback(self)
425 except:
426 report_callback_exception()
427 self.PVT_insert(self.widget.new_nodes, 0, self)
428
429 else:
430 self.expanded_flag=0
431 self.widget.itemconfig(self.symbol, image=self.collapsed_icon)
432 if self.indic:
433 self.widget.itemconfig(self.indic, image=self.widget.plus_icon)
434 self.delete(0)
435
436 self.widget.spinlock=0
437
439 """Resize connecting lines"""
440 if self.widget.line_flag:
441 n=self
442 while n:
443 if n.child_nodes:
444 x1, y1=self.widget.coords(n.symbol)
445 x2, y2=self.widget.coords(n.child_nodes[-1].symbol)
446 self.widget.coords(n.v_line, x1, y1, x1, y2)
447 n=n.parent_node
448
453
455 """Recursively delete subtree & clean up cyclic references to make
456 garbage collection happy"""
457 sw=self.widget
458 sw.delete(self.v_line)
459 self.v_line=None
460 for i in self.child_nodes:
461
462 i.PVT_delete_subtree()
463 i.PVT_unbind_all()
464
465 sw.delete(i.symbol)
466 sw.delete(i.label)
467 sw.delete(i.h_line)
468 sw.delete(i.v_line)
469 sw.delete(i.indic)
470
471 i.parent_node=None
472
473 if sw.pos in self.child_nodes:
474 sw.move_cursor(self)
475
476 self.child_nodes=[]
477
479 """Unbind callbacks so node gets garbage-collected. This wasn't easy
480 to figure out the proper way to do this. See also tag_bind() for the
481 Tree widget itself."""
482 for j in (self.symbol, self.label, self.indic, self.h_line,
483 self.v_line):
484 for k in self.widget.bindings.get(j, ()):
485 self.widget.tag_unbind(j, k[0], k[1])
486
488 """Move everything below current icon, to make room for subtree using
489 the Disney magic of item tags. This is the secret of making
490 everything as fast as it is."""
491
492 bbox1=self.widget.bbox(self.widget.root.symbol, self.label)
493 bbox2=self.widget.bbox('all')
494 self.widget.dtag('move')
495 self.widget.addtag('move', 'overlapping',
496 bbox2[0], bbox1[3], bbox2[2], bbox2[3])
497
498 self.widget.dtag(self.widget.cursor_box, 'move')
499 self.widget.dtag(self.symbol, 'move')
500 self.widget.dtag(self.label, 'move')
501
502 self.widget.move('move', 0, dist)
503
505 """Handle mouse clicks by kicking off possible drag'n'drop
506 processing"""
507 if self.widget.drop_callback:
508 if Tkdnd.dnd_start(self, event):
509 x1, y1, x2, y2=self.widget.bbox(self.symbol)
510 self.x_off=(x1-x2)/2
511 self.y_off=(y1-y2)/2
512 else:
513
514 self.widget.drag=0
515 self.dnd_end(None, None)
516
517
519
520 - def __init__(self, master, root_id, root_label='',
521 get_contents_callback=None, dist_x=15, dist_y=15,
522 text_offset=10, line_flag=1, expanded_icon=None,
523 collapsed_icon=None, regular_icon=None, plus_icon=None,
524 minus_icon=None, node_class=Node, drop_callback=None,
525 *args, **kw_args):
526
527 Canvas.__init__(self, master, *args, **kw_args)
528
529
530 self.node_class=node_class
531
532 self.bindings={}
533
534 self.spinlock=0
535
536 self.drag=0
537
538 if expanded_icon == None:
539 self.expanded_icon=PhotoImage(
540 data='R0lGODlhEAANAKIAAAAAAMDAwICAgP//////ADAwMAAAAAAA' \
541 'ACH5BAEAAAEALAAAAAAQAA0AAAM6GCrM+jCIQamIbw6ybXNSx3GVB' \
542 'YRiygnA534Eq5UlO8jUqLYsquuy0+SXap1CxBHr+HoBjoGndDpNAAA7')
543 else:
544 self.expanded_icon=expanded_icon
545 if collapsed_icon == None:
546 self.collapsed_icon=PhotoImage(
547 data='R0lGODlhDwANAKIAAAAAAMDAwICAgP//////ADAwMAAAAAAA' \
548 'ACH5BAEAAAEALAAAAAAPAA0AAAMyGCHM+lAMMoeAT9Jtm5NDKI4Wo' \
549 'FXcJphhipanq7Kvu8b1dLc5tcuom2foAQQAyKRSmQAAOw==')
550 else:
551 self.collapsed_icon=collapsed_icon
552 if regular_icon == None:
553 self.regular_icon=PhotoImage(
554 data='R0lGODlhCwAOAJEAAAAAAICAgP///8DAwCH5BAEAAAMALAAA' \
555 'AAALAA4AAAIphA+jA+JuVgtUtMQePJlWCgSN9oSTV5lkKQpo2q5W+' \
556 'wbzuJrIHgw1WgAAOw==')
557 else:
558 self.regular_icon=regular_icon
559 if plus_icon == None:
560 self.plus_icon=PhotoImage(
561 data='R0lGODdhCQAJAPEAAAAAAH9/f////wAAACwAAAAACQAJAAAC' \
562 'FIyPoiu2sJyCyoF7W3hxz850CFIA\nADs=')
563 else:
564 self.plus_icon=plus_icon
565 if minus_icon == None:
566 self.minus_icon=PhotoImage(
567 data='R0lGODdhCQAJAPEAAAAAAH9/f////wAAACwAAAAACQAJAAAC' \
568 'EYyPoivG614LAlg7ZZbxoR8UADs=')
569 else:
570 self.minus_icon=minus_icon
571
572 self.dist_x=dist_x
573
574 self.dist_y=dist_y
575
576 self.text_offset=text_offset
577
578 self.line_flag=line_flag
579
580 self.get_contents_callback=get_contents_callback
581
582 self.drop_callback=drop_callback
583
584 self.root=node_class(parent_node=None, label=root_label,
585 id=root_id, expandable_flag=1,
586 collapsed_icon=self.collapsed_icon,
587 expanded_icon=self.expanded_icon,
588 x=dist_x, y=dist_y, parent_widget=self)
589
590 x1, y1, x2, y2=self.bbox('all')
591 self.configure(scrollregion=(x1, y1, x2+5, y2+5))
592
593 self.cursor_box=self.create_rectangle(0, 0, 0, 0)
594 self.move_cursor(self.root)
595
596 self.bind('<Enter>', self.PVT_mousefocus)
597
598
599
600 self.bind('<Next>', self.pagedown)
601 self.bind('<Prior>', self.pageup)
602
603 self.bind('<Down>', self.next)
604 self.bind('<Up>', self.prev)
605
606 self.bind('<Left>', self.ascend)
607
608 self.bind('<Right>', self.descend)
609
610 self.bind('<Home>', self.first)
611 self.bind('<End>', self.last)
612
613 self.bind('<Key-space>', self.toggle)
614
615
616
618 """Soak up event argument when moused-over"""
619 self.focus_set()
620
621
622 - def tag_bind(self, tag, seq, *args, **kw_args):
623 """Keep track of callback bindings so we can delete them later. I
624 shouldn't have to do this!!!!"""
625
626 func_id=apply(Canvas.tag_bind, (self, tag, seq)+args, kw_args)
627
628 self.bindings[tag]=self.bindings.get(tag, [])+[(seq, func_id)]
629
630 - def add_list(self, list=None, name=None, id=None, flag=0,
631 expanded_icon=None, collapsed_icon=None):
632 """Add node construction info to list"""
633 n=Struct()
634 n.name=name
635 n.id=id
636 n.flag=flag
637 if collapsed_icon:
638 n.collapsed_icon=collapsed_icon
639 else:
640 if flag:
641
642 n.collapsed_icon=self.collapsed_icon
643 else:
644
645 n.collapsed_icon=self.regular_icon
646 if flag:
647 if expanded_icon:
648 n.expanded_icon=expanded_icon
649 else:
650 n.expanded_icon=self.expanded_icon
651 else:
652
653 n.expanded_icon=None
654 if list == None:
655 list=[]
656 list.append(n)
657 return list
658
659 - def add_node(self, name=None, id=None, flag=0, expanded_icon=None,
660 collapsed_icon=None):
661 """Add a node during get_contents_callback()"""
662 self.add_list(self.new_nodes, name, id, flag, expanded_icon,
663 collapsed_icon)
664
668
670 """Return node under cursor"""
671 return self.pos
672
673 - def see(self, *items):
674 """Scroll (in a series of nudges) so items are visible"""
675 x1, y1, x2, y2=apply(self.bbox, items)
676 while x2 > self.canvasx(0)+self.winfo_width():
677 old=self.canvasx(0)
678 self.xview('scroll', 1, 'units')
679
680 if old == self.canvasx(0):
681 break
682 while y2 > self.canvasy(0)+self.winfo_height():
683 old=self.canvasy(0)
684 self.yview('scroll', 1, 'units')
685 if old == self.canvasy(0):
686 break
687
688 while x1 < self.canvasx(0):
689 old=self.canvasx(0)
690 self.xview('scroll', -1, 'units')
691 if old == self.canvasx(0):
692 break
693 while y1 < self.canvasy(0):
694 old=self.canvasy(0)
695 self.yview('scroll', -1, 'units')
696 if old == self.canvasy(0):
697 break
698
700 """Move cursor to node"""
701 self.pos=node
702 x1, y1, x2, y2=self.bbox(node.symbol, node.label)
703 self.coords(self.cursor_box, x1-1, y1-1, x2+1, y2+1)
704 self.see(node.symbol, node.label)
705
706 - def toggle(self, event=None):
707 """Expand/collapse subtree"""
708 self.pos.toggle_state()
709
710 - def next(self, event=None):
713
714 - def prev(self, event=None):
717
718 - def ascend(self, event=None):
719 """Move to immediate parent"""
720 if self.pos.parent_node:
721
722 self.move_cursor(self.pos.parent_node)
723
725 """Move right, expanding as we go"""
726 if self.pos.expandable_flag:
727 self.pos.expand()
728 if self.pos.child_nodes:
729
730 self.move_cursor(self.pos.child_nodes[0])
731 return
732
733 self.next()
734
735 - def first(self, event=None):
736 """Go to root node"""
737
738 self.move_cursor(self.root)
739
740 - def last(self, event=None):
744
745 - def pageup(self, event=None):
746 """Previous page"""
747 n=self.pos
748 j=self.winfo_height()/self.dist_y
749 for i in range(j-3):
750 n=n.prev_visible()
751 self.yview('scroll', -1, 'pages')
752 self.move_cursor(n)
753
754 - def pagedown(self, event=None):
755 """Next page"""
756 n=self.pos
757 j=self.winfo_height()/self.dist_y
758 for i in range(j-3):
759 n=n.next_visible()
760 self.yview('scroll', 1, 'pages')
761 self.move_cursor(n)
762
763
765 """Determine drag location in canvas coordinates. event.x & event.y
766 don't seem to be what we want."""
767
768 x_org=self.winfo_rootx()
769 y_org=self.winfo_rooty()
770
771
772 x=self.canvasx(event.x_root-x_org)
773 y=self.canvasy(event.y_root-y_org)
774 return x, y
775
777 """Accept dnd messages, i.e. we're a legit drop target, and we do
778 implement d&d functions."""
779 self.target=None
780 return self
781
783 """Get ready to drag or drag has entered widget (create drag
784 object)"""
785
786 self.drag=1
787 x, y=self.where(event)
788 x1, y1, x2, y2=source.widget.bbox(source.symbol, source.label)
789 dx, dy=x2-x1, y2-y1
790
791 if source.expanded_flag:
792 self.dnd_symbol=self.create_image(x, y,
793 image=source.expanded_icon)
794 else:
795 self.dnd_symbol=self.create_image(x, y,
796 image=source.collapsed_icon)
797 self.dnd_label=self.create_text(x+self.text_offset, y,
798 text=source.get_label(),
799 justify='left',
800 anchor='w')
801
803 """Move drag icon"""
804 self.drag=1
805 x, y=self.where(event)
806 x1, y1, x2, y2=self.bbox(self.dnd_symbol, self.dnd_label)
807 self.move(self.dnd_symbol, x-x1+source.x_off, y-y1+source.y_off)
808 self.move(self.dnd_label, x-x1+source.x_off, y-y1+source.y_off)
809
811 """Finish dragging or drag has left widget (destroy drag object)"""
812 self.delete(self.dnd_symbol)
813 self.delete(self.dnd_label)
814
816 """Object has been dropped here"""
817
818 self.dnd_leave(source, event)
819
820
821
822 self.update()
823 if not self.target:
824
825 return
826
827 if self.drop_callback:
828 try:
829
830
831
832 self.drop_callback(source, self.target)
833 except:
834 report_callback_exception()
835
836
837
838 if __name__ == '__main__':
839 import os
840 import sys
841
842
843
844
845
846 - def get_contents(node):
847 path=apply(os.path.join, node.full_id())
848 for filename in os.listdir(path):
849 full=os.path.join(path, filename)
850 name=filename
851 folder=0
852 if os.path.isdir(full):
853
854 folder=1
855 elif not os.path.isfile(full):
856
857 name=name+' (special)'
858 if os.path.islink(full):
859
860 name=name+' (link to '+os.readlink(full)+')'
861 node.widget.add_node(name=name, id=filename, flag=folder)
862
863 root=Tk()
864 root.title(os.path.basename(sys.argv[0]))
865 tree=os.sep
866 if sys.platform == 'win32':
867
868
869
870 tree='C:'+os.sep
871
872
873 t=Tree(master=root,
874 root_id=tree,
875 root_label=tree,
876 get_contents_callback=get_contents,
877 width=300)
878 t.grid(row=0, column=0, sticky='nsew')
879
880
881 root.grid_rowconfigure(0, weight=1)
882 root.grid_columnconfigure(0, weight=1)
883
884
885 sb=Scrollbar(root)
886 sb.grid(row=0, column=1, sticky='ns')
887 t.configure(yscrollcommand=sb.set)
888 sb.configure(command=t.yview)
889
890 sb=Scrollbar(root, orient=HORIZONTAL)
891 sb.grid(row=1, column=0, sticky='ew')
892 t.configure(xscrollcommand=sb.set)
893 sb.configure(command=t.xview)
894
895
896 t.focus_set()
897
898
899 Button(root, text='Quit', command=root.quit).grid(row=2, column=0,
900 columnspan=2)
901
902
903 t.root.expand()
904
905 root.mainloop()
906