Package teamwork :: Package widgets :: Module Tree
[hide private]
[frames] | no frames]

Source Code for Module teamwork.widgets.Tree

  1  # Highly optimized Tkinter tree control 
  2  # by Charles E. "Gene" Cash 
  3  # 
  4  # This is documented more fully on my homepage at 
  5  # http://home.cfl.rr.com/genecash/ and if it's not there, look in the Vaults 
  6  # of Parnassus at http://www.vex.net/parnassus/ which I promise to keep 
  7  # updated. 
  8  # 
  9  # Thanks to Laurent Claustre <claustre@esrf.fr> for sending lots of helpful 
 10  # bug reports. 
 11  # 
 12  # This copyright license is intended to be similar to the FreeBSD license.  
 13  # 
 14  # Copyright 1998 Gene Cash All rights reserved.  
 15  # 
 16  # Redistribution and use in source and binary forms, with or without 
 17  # modification, are permitted provided that the following conditions are 
 18  # met: 
 19  # 
 20  #    1. Redistributions of source code must retain the above copyright 
 21  #       notice, this list of conditions and the following disclaimer. 
 22  #    2. Redistributions in binary form must reproduce the above copyright 
 23  #       notice, this list of conditions and the following disclaimer in the 
 24  #       documentation and/or other materials provided with the 
 25  #       distribution. 
 26  # 
 27  # THIS SOFTWARE IS PROVIDED BY GENE CASH ``AS IS'' AND ANY EXPRESS OR 
 28  # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 
 29  # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
 30  # DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR 
 31  # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 
 32  # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 
 33  # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 
 34  # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 
 35  # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 
 36  # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
 37  # POSSIBILITY OF SUCH DAMAGE. 
 38  # 
 39  # This means you may do anything you want with this code, except claim you 
 40  # wrote it. Also, if it breaks you get to keep both pieces. 
 41  # 
 42  # 02-DEC-98 Started writing code. 
 43  # 22-NOV-99 Changed garbage collection to a better algorithm. 
 44  # 28-AUG-01 Added logic to deal with exceptions in user callbacks. 
 45  # 02-SEP-01 Fixed hang when closing last node. 
 46  # 07-SEP-01 Added binding tracking so nodes got garbage-collected. 
 47  #           Also fixed subclass call to initialize Canvas to properly deal 
 48  #           with variable arguments and keyword arguments. 
 49  # 11-SEP-01 Bugfix for unbinding code. 
 50  # 13-OCT-01 Added delete & insert methods for nodes (by email request). 
 51  #           LOTS of code cleanup. 
 52  #           Changed leading double underscores to PVT nomenclature. 
 53  #           Added ability to pass Node subclass to Tree constructor. 
 54  #           Removed after_callback since subclassing Node is better idea. 
 55  # 15-OCT-01 Finally added drag'n'drop support.  It consisted of a simple 
 56  #           change to the Node PVT_click method, and addition of logic like 
 57  #           the example in Tkdnd.py.  It took 3 days to grok the Tkdnd 
 58  #           example and 2 hours to make the code changes.  Plus another 1/2 
 59  #           day to get a working where() function. 
 60  # 16-OCT-01 Incorporated fixes to delete() and dnd_commit() bugs by 
 61  #           Laurent Claustre <claustre@esrf.fr>. 
 62  # 17-OCT-01 Added find_full_id() and cursor_node() methods. 
 63  # 18-OCT-01 Fixes to delete() on root during collapse and with 
 64  #           drag-in-progress flag by Laurent Claustre <claustre@esrf.fr>. 
 65  # 10-FEB-02 Fix to prev_visible() by Nicolas Pascal <pascal@esrf.fr>. 
 66  #           Fixes which made insert_before()/insert_after() actually work. 
 67  #           Also added expand/collapse indicators like Internet Explorer 
 68  #           as requested by Nicolas. 
 69  # 11-FEB-02 Another fix to prev_visible().  It works this time.  Honest. 
 70  # 31-MAY-02 Added documentation strings so the new PYthon 2.2 help function 
 71  #           is a little more useful. 
 72  # 19-AUG-02 Minor fix to eliminate crash in "treedemo-icons.py" caused by 
 73  #           referencing expand/collapse indicators when lines are turned off. 
 74  # 15-OCT-02 Used new idiom for calling Canvas superclass. 
 75  # 18-NOV-02 Fixed bug discovered by Amanjit Gill <amanjit.gill@gmx.de>, where 
 76  #           I didn't pass "master" properly to the Canvas superclass. Sigh. 
 77  #           One step forward, one step back. 
 78   
 79  import Tkdnd 
 80  from Tkinter import * 
 81   
 82  #------------------------------------------------------------------------------ 
83 -def report_callback_exception():
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 #------------------------------------------------------------------------------
92 -class Struct:
93 """Helper object for add_node() method"""
94 - def __init__(self):
95 pass
96 97 #------------------------------------------------------------------------------
98 -class Node:
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 # immediate parent node 123 self.parent_node=parent_node 124 # internal name used to manipulate things 125 self.id=id 126 # bitmaps to be displayed 127 self.expanded_icon=expanded_icon 128 self.collapsed_icon=collapsed_icon 129 # tree widget we belong to 130 if parent_widget: 131 self.widget=parent_widget 132 else: 133 self.widget=parent_node.widget 134 # for speed 135 sw=self.widget 136 # our list of child nodes 137 self.child_nodes=[] 138 # flag that node can be expanded 139 self.expandable_flag=expandable_flag 140 self.expanded_flag=0 141 # add line 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 # draw approprate image 148 self.symbol=sw.create_image(x, y, image=self.collapsed_icon) 149 # add expand/collapse indicator 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 # add label 154 self.label=sw.create_text(x+sw.text_offset, y, text=label, anchor='w') 155 # single-click to expand/collapse 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 # for drag'n'drop target detection 161 sw.tag_bind(self.symbol, '<Any-Enter>', self.PVT_enter) 162 sw.tag_bind(self.label, '<Any-Enter>', self.PVT_enter)
163 164 # for testing (gotta make sure nodes get properly GC'ed) 165 #def __del__(self): 166 # print self.full_id(), 'deleted' 167 168 # ----- PUBLIC METHODS -----
169 - def set_collapsed_icon(self, icon):
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
175 - def set_expanded_icon(self, icon):
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
181 - def parent(self):
182 """Return node's parent node""" 183 return self.parent_node
184
185 - def prev_sib(self):
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
193 - def next_sib(self):
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
201 - def next_visible(self):
202 """Return next lower visible node""" 203 n=self 204 if n.child_nodes: 205 # if you can go right, do so 206 return n.child_nodes[0] 207 while n.parent_node: 208 # move to next sibling 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 # if no siblings, move to parent's sibling 213 n=n.parent_node 214 # we're at bottom 215 return self
216
217 - def prev_visible(self):
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
230 - def children(self):
231 """Return list of node's children""" 232 return self.child_nodes[:]
233
234 - def get_label(self):
235 """Return string containing text of current label""" 236 return self.widget.itemcget(self.label, 'text')
237
238 - def set_label(self, label):
239 """Set current text label""" 240 self.widget.itemconfig(self.label, text=label)
241
242 - def expanded(self):
243 """Returns true if node is currently expanded, false otherwise""" 244 return self.expanded_flag
245
246 - def expandable(self):
247 """Returns true if node can be expanded (i.e. if it's a folder)""" 248 return self.expandable_flag
249
250 - def full_id(self):
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
257 - def expand(self):
258 """Expand node if possible""" 259 if not self.expanded_flag: 260 self.PVT_set_state(1)
261
262 - def collapse(self):
263 """Collapse node if possible""" 264 if self.expanded_flag: 265 self.PVT_set_state(0)
266
267 - def delete(self, me_too=1):
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 # can't delete the root node 273 raise ValueError, "can't delete root node" 274 self.PVT_delete_subtree() 275 # move everything up so that distance to next subnode is correct 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 # move cursor if it points to current node 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 # break circular ref now, so parent may be GC'ed later 297 n=self.parent_node 298 self.parent_node=None 299 n.PVT_cleanup_lines() 300 n.PVT_update_scrollregion()
301
302 - def insert_before(self, nodes):
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
308 - def insert_after(self, nodes):
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
314 - def insert_children(self, nodes):
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
319 - def toggle_state(self):
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 # ----- functions for drag'n'drop support -----
328 - def PVT_enter(self, event):
329 """detect mouse hover for drag'n'drop""" 330 self.widget.target=self
331
332 - def dnd_end(self, target, event):
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 # if there's been no dragging, it was just a mouse click 339 self.widget.move_cursor(self) 340 self.toggle_state() 341 self.widget.drag=0
342 343 # ----- PRIVATE METHODS (prefixed with "PVT_") ----- 344 # these methods are subject to change, so please try not to use them
345 - def PVT_last(self):
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
352 - def PVT_find(self, search):
353 """Used by searching functions""" 354 if self.id != search[0]: 355 # this actually only goes tilt if root doesn't match 356 return None 357 if len(search) == 1: 358 return self 359 # get list of children IDs 360 i=map(lambda x: x.id, self.child_nodes) 361 # if there is a child that matches, search it 362 try: 363 return self.child_nodes[i.index(search[1])].PVT_find(search[1:]) 364 except: 365 return None
366
367 - def PVT_insert(self, nodes, pos, below):
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 # for speed 375 sw=self.widget 376 # expand and insert children 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 # move stuff to make room 384 below.PVT_tag_move(sw.dist_y*len(nodes)) 385 # get position of first new child 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 # create vertical line 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 # add new subnodes, they'll draw themselves 399 # this is a very expensive call 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
410 - def PVT_set_state(self, state):
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 # expand & draw our subtrees 418 if state: 419 self.child_nodes=[] 420 self.widget.new_nodes=[] 421 if self.widget.get_contents_callback: 422 # this callback needs to make multiple calls to add_node() 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 # collapse and delete subtrees 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 # release mutex 436 self.widget.spinlock=0
437
438 - def PVT_cleanup_lines(self):
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
449 - def PVT_update_scrollregion(self):
450 """Update scroll region for new size""" 451 x1, y1, x2, y2=self.widget.bbox('all') 452 self.widget.configure(scrollregion=(x1, y1, x2+5, y2+5))
453
454 - def PVT_delete_subtree(self):
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 # delete node's subtree, if any 462 i.PVT_delete_subtree() 463 i.PVT_unbind_all() 464 # delete widgets from canvas 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 # break circular reference 471 i.parent_node=None 472 # move cursor if it's in deleted subtree 473 if sw.pos in self.child_nodes: 474 sw.move_cursor(self) 475 # now subnodes will be properly garbage collected 476 self.child_nodes=[]
477
478 - def PVT_unbind_all(self):
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
487 - def PVT_tag_move(self, dist):
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 # mark everything below current node as movable 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 # untag cursor & node so they don't get moved too 498 self.widget.dtag(self.widget.cursor_box, 'move') 499 self.widget.dtag(self.symbol, 'move') 500 self.widget.dtag(self.label, 'move') 501 # now do the move of all the tagged objects 502 self.widget.move('move', 0, dist)
503
504 - def PVT_click(self, event):
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 # no callback, don't bother with drag'n'drop 514 self.widget.drag=0 515 self.dnd_end(None, None)
516 517 #------------------------------------------------------------------------------
518 -class Tree(Canvas):
519 # do we have enough possible arguments?!?!?!
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 # pass args to superclass (new idiom from Python 2.2) 527 Canvas.__init__(self, master, *args, **kw_args) 528 529 # this allows to subclass Node and pass our class in 530 self.node_class=node_class 531 # keep track of node bindings 532 self.bindings={} 533 # cheap mutex spinlock 534 self.spinlock=0 535 # flag to see if there's been any d&d dragging 536 self.drag=0 537 # default images (BASE64-encoded GIF files) 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 # horizontal distance that subtrees are indented 572 self.dist_x=dist_x 573 # vertical distance between rows 574 self.dist_y=dist_y 575 # how far to offset text label 576 self.text_offset=text_offset 577 # flag controlling connecting line display 578 self.line_flag=line_flag 579 # called just before subtree expand/collapse 580 self.get_contents_callback=get_contents_callback 581 # called after drag'n'drop 582 self.drop_callback=drop_callback 583 # create root node to get the ball rolling 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 # configure for scrollbar(s) 590 x1, y1, x2, y2=self.bbox('all') 591 self.configure(scrollregion=(x1, y1, x2+5, y2+5)) 592 # add a cursor 593 self.cursor_box=self.create_rectangle(0, 0, 0, 0) 594 self.move_cursor(self.root) 595 # make it easy to point to control 596 self.bind('<Enter>', self.PVT_mousefocus) 597 # totally arbitrary yet hopefully intuitive default keybindings 598 # stole 'em from ones used by microsoft tree control 599 # page-up/page-down 600 self.bind('<Next>', self.pagedown) 601 self.bind('<Prior>', self.pageup) 602 # arrow-up/arrow-down 603 self.bind('<Down>', self.next) 604 self.bind('<Up>', self.prev) 605 # arrow-left/arrow-right 606 self.bind('<Left>', self.ascend) 607 # (hold this down and you expand the entire tree) 608 self.bind('<Right>', self.descend) 609 # home/end 610 self.bind('<Home>', self.first) 611 self.bind('<End>', self.last) 612 # space bar 613 self.bind('<Key-space>', self.toggle)
614 615 # ----- PRIVATE METHODS (prefixed with "PVT_") ----- 616 # these methods are subject to change, so please try not to use them
617 - def PVT_mousefocus(self, event):
618 """Soak up event argument when moused-over""" 619 self.focus_set()
620 621 # ----- PUBLIC METHODS -----
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 # pass args to superclass 626 func_id=apply(Canvas.tag_bind, (self, tag, seq)+args, kw_args) 627 # save references 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 # it's expandable, use closed folder icon 642 n.collapsed_icon=self.collapsed_icon 643 else: 644 # it's not expandable, use regular file icon 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 # not expandable, don't need an icon 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
665 - def find_full_id(self, search):
666 """Search for a node""" 667 return self.root.PVT_find(search)
668
669 - def cursor_node(self, search):
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 # avoid endless loop if we can't scroll 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 # done in this order to ensure upper-left of object is visible 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
699 - def move_cursor(self, node):
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):
711 """Move to next lower visible node""" 712 self.move_cursor(self.pos.next_visible())
713
714 - def prev(self, event=None):
715 """Move to next higher visible node""" 716 self.move_cursor(self.pos.prev_visible())
717
718 - def ascend(self, event=None):
719 """Move to immediate parent""" 720 if self.pos.parent_node: 721 # move to parent 722 self.move_cursor(self.pos.parent_node)
723
724 - def descend(self, event=None):
725 """Move right, expanding as we go""" 726 if self.pos.expandable_flag: 727 self.pos.expand() 728 if self.pos.child_nodes: 729 # move to first subnode 730 self.move_cursor(self.pos.child_nodes[0]) 731 return 732 # if no subnodes, move to next sibling 733 self.next()
734
735 - def first(self, event=None):
736 """Go to root node""" 737 # move to root node 738 self.move_cursor(self.root)
739
740 - def last(self, event=None):
741 """Go to last visible node""" 742 # move to bottom-most node 743 self.move_cursor(self.root.PVT_last())
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 # ----- functions for drag'n'drop support -----
764 - def where(self, event):
765 """Determine drag location in canvas coordinates. event.x & event.y 766 don't seem to be what we want.""" 767 # where the corner of the canvas is relative to the screen: 768 x_org=self.winfo_rootx() 769 y_org=self.winfo_rooty() 770 # where the pointer is relative to the canvas widget, 771 # including scrolling 772 x=self.canvasx(event.x_root-x_org) 773 y=self.canvasy(event.y_root-y_org) 774 return x, y
775
776 - def dnd_accept(self, source, event):
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
782 - def dnd_enter(self, source, event):
783 """Get ready to drag or drag has entered widget (create drag 784 object)""" 785 # this flag lets us know there's been drag motion 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 # create dragging icon 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
802 - def dnd_motion(self, source, event):
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
810 - def dnd_leave(self, source, event):
811 """Finish dragging or drag has left widget (destroy drag object)""" 812 self.delete(self.dnd_symbol) 813 self.delete(self.dnd_label)
814
815 - def dnd_commit(self, source, event):
816 """Object has been dropped here""" 817 # call our own dnd_leave() to clean up 818 self.dnd_leave(source, event) 819 # process pending events to detect target node 820 # update_idletasks() doesn't do the trick if source & target are 821 # on different widgets 822 self.update() 823 if not self.target: 824 # no target node 825 return 826 # we must update data structures based on the drop 827 if self.drop_callback: 828 try: 829 # called with dragged node and target node 830 # this is where a file manager would move the actual file 831 # it must also move the nodes around as it wishes 832 self.drop_callback(source, self.target) 833 except: 834 report_callback_exception()
835 836 #------------------------------------------------------------------------------ 837 # the good 'ol test/demo code 838 if __name__ == '__main__': 839 import os 840 import sys 841 842 # default routine to get contents of subtree 843 # supply this for a different type of app 844 # argument is the node object being expanded 845 # should call add_node()
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 # it's a directory 854 folder=1 855 elif not os.path.isfile(full): 856 # but it's not a file 857 name=name+' (special)' 858 if os.path.islink(full): 859 # it's a link 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 # we could call the root "My Computer" and mess with get_contents() 868 # to return "A:", "B:", "C:", ... etc. as it's children, but that 869 # would just be terminally cute and I'd have to shoot myself 870 tree='C:'+os.sep 871 872 # create the control 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 # make expandable 881 root.grid_rowconfigure(0, weight=1) 882 root.grid_columnconfigure(0, weight=1) 883 884 # add scrollbars 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 # must get focus so keys work for demo 896 t.focus_set() 897 898 # we could do without this, but it's nice and friendly to have 899 Button(root, text='Quit', command=root.quit).grid(row=2, column=0, 900 columnspan=2) 901 902 # expand out the root 903 t.root.expand() 904 905 root.mainloop() 906