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

Source Code for Module teamwork.widgets.TreeWidget

   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  import tkFont 
  82   
  83  #------------------------------------------------------------------------------ 
84 -def report_callback_exception():
85 """report exception on sys.stderr.""" 86 import traceback 87 import sys 88 89 sys.stderr.write("Exception in Tree control callback\n") 90 traceback.print_exc()
91 92 #------------------------------------------------------------------------------
93 -class Struct:
94 """Helper object for add_node() method"""
95 - def __init__(self):
96 pass
97 98 #------------------------------------------------------------------------------
99 -class Node:
100 """Tree helper class that's instantiated for each element in the tree. It 101 has several useful attributes: 102 parent_node - immediate parent node 103 id - id assigned at creation 104 expanded_icon - image displayed when folder is expanded to display 105 children 106 collapsed_icon - image displayed when node is not a folder or folder is 107 collapsed. 108 parent_widget - reference to tree widget that contains node. 109 expandable_flag - is true when node is a folder that may be expanded or 110 collapsed. 111 expanded_flag - true to indicate node is currently expanded. 112 h_line - canvas line to left of node image. 113 v_line - canvas line below node image that connects children. 114 indic - expand/collapse canvas image. 115 label - canvas text label 116 symbol - current canvas image 117 118 Please note that methods prefixed PVT_* are not meant to be used by 119 client programs.""" 120
121 - def __init__(self, parent_node, id, collapsed_icon, x, y, 122 parent_widget=None, expanded_icon=None, label=None, 123 expandable_flag=0,expanded=0,action=None):
124 """Create node and initialize it. This also displays the node at the 125 given position on the canvas, and binds mouseclicks.""" 126 # immediate parent node 127 self.parent_node=parent_node 128 # internal name used to manipulate things 129 self.id=id 130 # bitmaps to be displayed 131 self.expanded_icon=expanded_icon 132 self.collapsed_icon=collapsed_icon 133 self.action=action 134 # tree widget we belong to 135 if parent_widget: 136 self.widget=parent_widget 137 else: 138 self.widget=parent_node.widget 139 # for speed 140 sw=self.widget 141 # our list of child nodes 142 self.child_nodes=[] 143 # flag that node can be expanded 144 self.expandable_flag=expandable_flag 145 self.expanded_flag=0 146 # add line 147 if parent_node and sw.line_flag: 148 self.h_line=sw.create_line(x, y, x-sw.dist_x, y) 149 else: 150 self.h_line=None 151 self.v_line=None 152 # draw approprate image 153 self.symbol=sw.create_image(x, y, image=self.collapsed_icon) 154 # add expand/collapse indicator 155 self.indic=None 156 self.font = sw.font 157 if expandable_flag and sw.line_flag and sw.plus_icon and sw.minus_icon: 158 self.indic=sw.create_image(x-sw.dist_x, y, image=sw.plus_icon) 159 if self.action: 160 font = self.font[:2]+('bold',) 161 else: 162 font = self.font 163 # add label 164 self.label=sw.create_text(x+sw.text_offset, y, text=label, anchor='w',font=font) 165 # single-click to expand/collapse 166 if self.indic: 167 sw.tag_bind(self.indic, '<1>', self.PVT_click) 168 else: 169 sw.tag_bind(self.symbol, '<1>', self.PVT_click) 170 if self.action: 171 sw.tag_bind(self.label,"<ButtonPress-1>", self.invoke) 172 else: 173 sw.tag_bind(self.label,"<ButtonPress-1>",self.highlight)
174 # for drag'n'drop target detection 175 # sw.tag_bind(self.symbol, '<Any-Enter>', self.PVT_enter) 176 # sw.tag_bind(self.label, '<Any-Enter>', self.PVT_enter) 177 178
179 - def invoke(self, event=None):
180 self.highlight() 181 if self.action: 182 self.action(self.id,event)
183
184 - def highlight(self,event=None):
185 self.widget.move_cursor(self)
186 187 # for testing (gotta make sure nodes get properly GC'ed) 188 #def __del__(self): 189 # print self.full_id(), 'deleted' 190 191 # ----- PUBLIC METHODS -----
192 - def set_collapsed_icon(self, icon):
193 """Set node's collapsed image""" 194 self.collapsed_icon=icon 195 if not self.expanded_flag: 196 self.widget.itemconfig(self.symbol, image=icon)
197
198 - def set_expanded_icon(self, icon):
199 """Set node's expanded image""" 200 self.expanded_icon=icon 201 if self.expanded_flag: 202 self.widget.itemconfig(self.symbol, image=icon)
203
204 - def parent(self):
205 """Return node's parent node""" 206 return self.parent_node
207
208 - def prev_sib(self):
209 """Return node's previous sibling (the child immediately above it)""" 210 i=self.parent_node.child_nodes.index(self)-1 211 if i >= 0: 212 return self.parent_node.child_nodes[i] 213 else: 214 return None
215
216 - def next_sib(self):
217 """Return node's next sibling (the child immediately below it)""" 218 i=self.parent_node.child_nodes.index(self)+1 219 if i < len(self.parent_node.child_nodes): 220 return self.parent_node.child_nodes[i] 221 else: 222 return None
223
224 - def next_visible(self):
225 """Return next lower visible node""" 226 n=self 227 if n.child_nodes: 228 # if you can go right, do so 229 return n.child_nodes[0] 230 while n.parent_node: 231 # move to next sibling 232 i=n.parent_node.child_nodes.index(n)+1 233 if i < len(n.parent_node.child_nodes): 234 return n.parent_node.child_nodes[i] 235 # if no siblings, move to parent's sibling 236 n=n.parent_node 237 # we're at bottom 238 return self
239
240 - def prev_visible(self):
241 """Return next higher visible node""" 242 n=self 243 if n.parent_node: 244 i=n.parent_node.child_nodes.index(n)-1 245 if i < 0: 246 return n.parent_node 247 else: 248 j=n.parent_node.child_nodes[i] 249 return j.PVT_last() 250 else: 251 return n
252
253 - def children(self):
254 """Return list of node's children""" 255 return self.child_nodes[:]
256
257 - def get_label(self):
258 """Return string containing text of current label""" 259 return self.widget.itemcget(self.label, 'text')
260
261 - def set_label(self, label):
262 """Set current text label""" 263 self.widget.itemconfig(self.label, text=label)
264
265 - def expanded(self):
266 """Returns true if node is currently expanded, false otherwise""" 267 return self.expanded_flag
268
269 - def expandable(self):
270 """Returns true if node can be expanded (i.e. if it's a folder)""" 271 return self.expandable_flag
272
273 - def full_id(self):
274 """Return list of IDs of all parents and node ID""" 275 if self.parent_node: 276 return self.parent_node.full_id()+(self.id,) 277 else: 278 return (self.id,)
279
280 - def expand(self):
281 """Expand node if possible""" 282 if not self.expanded_flag: 283 self.PVT_set_state(1)
284
285 - def collapse(self):
286 """Collapse node if possible""" 287 if self.expanded_flag: 288 self.PVT_set_state(0)
289
290 - def delete(self, me_too=1):
291 """Delete node from tree. ("me_too" is a hack not to be used by 292 external code, please!)""" 293 sw=self.widget 294 if not self.parent_node and me_too: 295 # can't delete the root node 296 raise ValueError, "can't delete root node" 297 self.PVT_delete_subtree() 298 # move everything up so that distance to next subnode is correct 299 n=self.next_visible() 300 x1, y1=sw.coords(self.symbol) 301 x2, y2=sw.coords(n.symbol) 302 if me_too: 303 dist=y2-y1 304 else: 305 dist=y2-y1-sw.dist_y 306 self.PVT_tag_move(-dist) 307 n=self 308 if me_too: 309 if sw.pos == self: 310 # move cursor if it points to current node 311 sw.move_cursor(self.parent_node) 312 self.PVT_unbind_all() 313 sw.delete(self.symbol) 314 sw.delete(self.label) 315 sw.delete(self.h_line) 316 sw.delete(self.v_line) 317 sw.delete(self.indic) 318 self.parent_node.child_nodes.remove(self) 319 # break circular ref now, so parent may be GC'ed later 320 n=self.parent_node 321 self.parent_node=None 322 n.PVT_cleanup_lines() 323 n.PVT_update_scrollregion()
324
325 - def insert_before(self, nodes):
326 """Insert list of nodes as siblings before this node. Call parent 327 node's add_node() function to generate the list of nodes.""" 328 i=self.parent_node.child_nodes.index(self) 329 self.parent_node.PVT_insert(nodes, i, self.prev_visible())
330
331 - def insert_after(self, nodes):
332 """Insert list of nodes as siblings after this node. Call parent 333 node's add_node() function to generate the list of nodes.""" 334 i=self.parent_node.child_nodes.index(self)+1 335 self.parent_node.PVT_insert(nodes, i, self.PVT_last())
336
337 - def insert_children(self, nodes):
338 """Insert list of nodes as children of this node. Call node's 339 add_node() function to generate the list of nodes.""" 340 self.PVT_insert(nodes, 0, self)
341
342 - def toggle_state(self):
343 """Toggle node's state between expanded and collapsed, if possible""" 344 if self.expandable_flag: 345 if self.expanded_flag: 346 self.PVT_set_state(0) 347 else: 348 self.PVT_set_state(1)
349 350 # ----- functions for drag'n'drop support -----
351 - def PVT_enter(self, event):
352 """detect mouse hover for drag'n'drop""" 353 self.widget.target=self
354
355 - def dnd_end(self, target, event):
356 """Notification that dnd processing has been ended. It DOES NOT imply 357 that we've been dropped somewhere useful, we could have just been 358 dropped into deep space and nothing happened to any data structures, 359 or it could have been just a plain mouse-click w/o any dragging.""" 360 if not self.widget.drag: 361 # if there's been no dragging, it was just a mouse click 362 #self.widget.move_cursor(self) 363 self.toggle_state() 364 self.widget.drag=0
365 366 # ----- PRIVATE METHODS (prefixed with "PVT_") ----- 367 # these methods are subject to change, so please try not to use them
368 - def PVT_last(self):
369 """Return bottom-most node in subtree""" 370 n=self 371 while n.child_nodes: 372 n=n.child_nodes[-1] 373 return n
374
375 - def PVT_find(self, search):
376 """Used by searching functions""" 377 if self.id != search[0]: 378 # this actually only goes tilt if root doesn't match 379 return None 380 if len(search) == 1: 381 return self 382 # get list of children IDs 383 i=map(lambda x: x.id, self.child_nodes) 384 # if there is a child that matches, search it 385 try: 386 return self.child_nodes[i.index(search[1])].PVT_find(search[1:]) 387 except: 388 return None
389
390 - def PVT_insert(self, nodes, pos, below):
391 """Create and insert new children. "nodes" is list previously created 392 via calls to add_list(). "pos" is index in the list of children where 393 the new nodes are inserted. "below" is node which new children should 394 appear immediately below.""" 395 if not self.expandable_flag: 396 raise TypeError, 'not an expandable node' 397 # for speed 398 sw=self.widget 399 # expand and insert children 400 children=[] 401 self.expanded_flag=1 402 sw.itemconfig(self.symbol, image=self.expanded_icon) 403 if sw.minus_icon and sw.line_flag: 404 sw.itemconfig(self.indic, image=sw.minus_icon) 405 if len(nodes): 406 # move stuff to make room 407 below.PVT_tag_move(sw.dist_y*len(nodes)) 408 # get position of first new child 409 xp, dummy=sw.coords(self.symbol) 410 dummy, yp=sw.coords(below.symbol) 411 xp=xp+sw.dist_x 412 yp=yp+sw.dist_y 413 # create vertical line 414 if sw.line_flag and not self.v_line: 415 self.v_line=sw.create_line( 416 xp, yp, 417 xp, yp+sw.dist_y*len(nodes)) 418 sw.tag_lower(self.v_line, self.symbol) 419 n=sw.node_class 420 for i in nodes: 421 # add new subnodes, they'll draw themselves 422 # this is a very expensive call 423 children.append( 424 n(parent_node=self, expandable_flag=i.flag, label=i.name, 425 id=i.id, collapsed_icon=i.collapsed_icon, 426 expanded_icon=i.expanded_icon, x=xp, y=yp,expanded=i.expanded,action=i.action)) 427 yp=yp+sw.dist_y 428 self.child_nodes[pos:pos]=children 429 self.PVT_cleanup_lines() 430 self.PVT_update_scrollregion() 431 sw.move_cursor(sw.pos)
432
433 - def PVT_set_state(self, state):
434 """Common code forexpanding/collapsing folders. It's not re-entrant, 435 and there are certain cases in which we can be called again before 436 we're done, so we use a mutex.""" 437 while self.widget.spinlock: 438 pass 439 self.widget.spinlock=1 440 # expand & draw our subtrees 441 if state: 442 self.child_nodes=[] 443 self.widget.new_nodes=[] 444 if self.widget.get_contents_callback: 445 # this callback needs to make multiple calls to add_node() 446 try: 447 self.widget.get_contents_callback(self) 448 except: 449 report_callback_exception() 450 self.PVT_insert(self.widget.new_nodes, 0, self) 451 # collapse and delete subtrees 452 else: 453 self.expanded_flag=0 454 self.widget.itemconfig(self.symbol, image=self.collapsed_icon) 455 if self.indic: 456 self.widget.itemconfig(self.indic, image=self.widget.plus_icon) 457 self.delete(0) 458 # release mutex 459 self.widget.spinlock=0
460
461 - def PVT_cleanup_lines(self):
462 """Resize connecting lines""" 463 if self.widget.line_flag: 464 n=self 465 while n: 466 if n.child_nodes: 467 x1, y1=self.widget.coords(n.symbol) 468 x2, y2=self.widget.coords(n.child_nodes[-1].symbol) 469 self.widget.coords(n.v_line, x1, y1, x1, y2) 470 n=n.parent_node
471
472 - def PVT_update_scrollregion(self):
473 """Update scroll region for new size""" 474 x1, y1, x2, y2=self.widget.bbox('all') 475 self.widget.configure(scrollregion=(x1, y1, x2+5, y2+5))
476
477 - def PVT_delete_subtree(self):
478 """Recursively delete subtree & clean up cyclic references to make 479 garbage collection happy""" 480 sw=self.widget 481 sw.delete(self.v_line) 482 self.v_line=None 483 for i in self.child_nodes: 484 # delete node's subtree, if any 485 i.PVT_delete_subtree() 486 i.PVT_unbind_all() 487 # delete widgets from canvas 488 sw.delete(i.symbol) 489 sw.delete(i.label) 490 sw.delete(i.h_line) 491 sw.delete(i.v_line) 492 sw.delete(i.indic) 493 # break circular reference 494 i.parent_node=None 495 # move cursor if it's in deleted subtree 496 if sw.pos in self.child_nodes: 497 sw.move_cursor(self) 498 # now subnodes will be properly garbage collected 499 self.child_nodes=[]
500
501 - def PVT_unbind_all(self):
502 """Unbind callbacks so node gets garbage-collected. This wasn't easy 503 to figure out the proper way to do this. See also tag_bind() for the 504 Tree widget itself.""" 505 for j in (self.symbol, self.label, self.indic, self.h_line, 506 self.v_line): 507 for k in self.widget.bindings.get(j, ()): 508 self.widget.tag_unbind(j, k[0], k[1])
509
510 - def PVT_tag_move(self, dist):
511 """Move everything below current icon, to make room for subtree using 512 the Disney magic of item tags. This is the secret of making 513 everything as fast as it is.""" 514 # mark everything below current node as movable 515 bbox1=self.widget.bbox(self.widget.root.symbol, self.label) 516 bbox2=self.widget.bbox('all') 517 self.widget.dtag('move') 518 self.widget.addtag('move', 'overlapping', 519 bbox2[0], bbox1[3], bbox2[2], bbox2[3]) 520 # untag cursor & node so they don't get moved too 521 self.widget.dtag(self.widget.cursor_box, 'move') 522 self.widget.dtag(self.symbol, 'move') 523 self.widget.dtag(self.label, 'move') 524 # now do the move of all the tagged objects 525 self.widget.move('move', 0, dist)
526
527 - def PVT_click(self, event):
528 """Handle mouse clicks by kicking off possible drag'n'drop 529 processing""" 530 if self.widget.drop_callback: 531 if Tkdnd.dnd_start(self, event): 532 x1, y1, x2, y2=self.widget.bbox(self.symbol) 533 self.x_off=(x1-x2)/2 534 self.y_off=(y1-y2)/2 535 else: 536 # no callback, don't bother with drag'n'drop 537 self.widget.drag=0 538 self.dnd_end(None, None)
539 540 #------------------------------------------------------------------------------
541 -class Tree(Canvas):
542 # do we have enough possible arguments?!?!?!
543 - def __init__(self, master, root_id, root_label='', 544 get_contents_callback=None, dist_x=15, dist_y=15, 545 text_offset=10, line_flag=1, expanded_icon=None, 546 collapsed_icon=None, regular_icon=None, plus_icon=None, 547 minus_icon=None, node_class=Node, root_action=None,drop_callback=None,font=None, 548 *args, **kw_args):
549 550 # pass args to superclass (new idiom from Python 2.2) 551 Canvas.__init__(self, master, *args, **kw_args) 552 553 # this allows to subclass Node and pass our class in 554 self.node_class=node_class 555 # keep track of node bindings 556 self.bindings={} 557 # cheap mutex spinlock 558 self.spinlock=0 559 # flag to see if there's been any d&d dragging 560 self.drag=0 561 562 # default images (BASE64-encoded GIF files) 563 # if expanded_icon == None: 564 # self.expanded_icon=PhotoImage( 565 # data='R0lGODlhEAANAKIAAAAAAMDAwICAgP//////ADAwMAAAAAAA' \ 566 # 'ACH5BAEAAAEALAAAAAAQAA0AAAM6GCrM+jCIQamIbw6ybXNSx3GVB' \ 567 # 'YRiygnA534Eq5UlO8jUqLYsquuy0+SXap1CxBHr+HoBjoGndDpNAAA7') 568 # else: 569 # self.expanded_icon=expanded_icon 570 self.expanded_icon=expanded_icon 571 # if collapsed_icon == None: 572 # self.collapsed_icon=PhotoImage( 573 # data='R0lGODlhDwANAKIAAAAAAMDAwICAgP//////ADAwMAAAAAAA' \ 574 # 'ACH5BAEAAAEALAAAAAAPAA0AAAMyGCHM+lAMMoeAT9Jtm5NDKI4Wo' \ 575 # 'FXcJphhipanq7Kvu8b1dLc5tcuom2foAQQAyKRSmQAAOw==') 576 # else: 577 # self.collapsed_icon=collapsed_icon 578 self.collapsed_icon=collapsed_icon 579 # if regular_icon == None: 580 # self.regular_icon=PhotoImage( 581 # data='R0lGODlhCwAOAJEAAAAAAICAgP///8DAwCH5BAEAAAMALAAA' \ 582 # 'AAALAA4AAAIphA+jA+JuVgtUtMQePJlWCgSN9oSTV5lkKQpo2q5W+' \ 583 # 'wbzuJrIHgw1WgAAOw==') 584 # else: 585 # self.regular_icon=regular_icon 586 self.regular_icon=regular_icon 587 588 if plus_icon == None: 589 self.plus_icon=PhotoImage( 590 data='R0lGODdhCQAJAPEAAAAAAH9/f////wAAACwAAAAACQAJAAAC' \ 591 'FIyPoiu2sJyCyoF7W3hxz850CFIA\nADs=') 592 else: 593 self.plus_icon=plus_icon 594 if minus_icon == None: 595 self.minus_icon=PhotoImage( 596 data='R0lGODdhCQAJAPEAAAAAAH9/f////wAAACwAAAAACQAJAAAC' \ 597 'EYyPoivG614LAlg7ZZbxoR8UADs=') 598 else: 599 self.minus_icon=minus_icon 600 601 self.font = font 602 603 #do some font metric magic 604 treeFont = tkFont.Font(font=font) 605 606 # horizontal distance that subtrees are indented 607 self.dist_x=dist_x 608 # vertical distance between rows 609 self.dist_y=treeFont.metrics()['linespace'] + 5 610 # how far to offset text label 611 self.text_offset=text_offset 612 # flag controlling connecting line display 613 self.line_flag=line_flag 614 # called just before subtree expand/collapse 615 self.get_contents_callback=get_contents_callback 616 # called after drag'n'drop 617 self.drop_callback=drop_callback 618 # create root node to get the ball rolling 619 self.root=node_class(parent_node=None, label=root_label, 620 id=root_id, expandable_flag=1, 621 collapsed_icon=self.collapsed_icon, 622 expanded_icon=self.expanded_icon, 623 x=dist_x, y=dist_y, parent_widget=self,action=root_action) 624 # configure for scrollbar(s) 625 x1, y1, x2, y2=self.bbox('all') 626 self.configure(scrollregion=(x1, y1, x2+5, y2+5)) 627 # add a cursor 628 self.cursor_box=self.create_rectangle(0, 0, 0, 0) 629 self.move_cursor(self.root) 630 # make it easy to point to control 631 self.bind('<Enter>', self.PVT_mousefocus) 632 # totally arbitrary yet hopefully intuitive default keybindings 633 # stole 'em from ones used by microsoft tree control 634 # page-up/page-down 635 self.bind('<Next>', self.pagedown) 636 self.bind('<Prior>', self.pageup) 637 # arrow-up/arrow-down 638 self.bind('<Down>', self.next) 639 self.bind('<Up>', self.prev) 640 # arrow-left/arrow-right 641 self.bind('<Left>', self.ascend) 642 # (hold this down and you expand the entire tree) 643 self.bind('<Right>', self.descend) 644 # home/end 645 self.bind('<Home>', self.first) 646 self.bind('<End>', self.last) 647 # space bar 648 self.bind('<Key-space>', self.toggle)
649 650 # ----- PRIVATE METHODS (prefixed with "PVT_") ----- 651 # these methods are subject to change, so please try not to use them
652 - def PVT_mousefocus(self, event):
653 """Soak up event argument when moused-over""" 654 self.focus_set()
655 656 # ----- PUBLIC METHODS -----
657 - def tag_bind(self, tag, seq, *args, **kw_args):
658 """Keep track of callback bindings so we can delete them later. I 659 shouldn't have to do this!!!!""" 660 # pass args to superclass 661 func_id=apply(Canvas.tag_bind, (self, tag, seq)+args, kw_args) 662 # save references 663 self.bindings[tag]=self.bindings.get(tag, [])+[(seq, func_id)]
664
665 - def add_list(self, list=None, name=None, id=None, flag=0, 666 expanded_icon=None, collapsed_icon=None, expanded=0, action=None):
667 """Add node construction info to list""" 668 n=Struct() 669 n.name=name 670 n.id=id 671 n.flag=flag 672 n.action=action 673 n.expanded=expanded 674 if collapsed_icon: 675 n.collapsed_icon=collapsed_icon 676 else: 677 if flag: 678 # it's expandable, use closed folder icon 679 n.collapsed_icon=self.collapsed_icon 680 else: 681 # it's not expandable, use regular file icon 682 n.collapsed_icon=self.regular_icon 683 if flag: 684 if expanded_icon: 685 n.expanded_icon=expanded_icon 686 else: 687 n.expanded_icon=self.expanded_icon 688 else: 689 # not expandable, don't need an icon 690 n.expanded_icon=None 691 if list == None: 692 list=[] 693 list.append(n) 694 return list
695
696 - def add_node(self, name=None, id=None, flag=0, expanded_icon=None, 697 collapsed_icon=None,expanded=0,action=None):
698 """Add a node during get_contents_callback()""" 699 self.add_list(self.new_nodes, name, id, flag, expanded_icon, 700 collapsed_icon,expanded,action)
701
702 - def find_full_id(self, search):
703 """Search for a node""" 704 return self.root.PVT_find(search)
705
706 - def cursor_node(self, search):
707 """Return node under cursor""" 708 return self.pos
709
710 - def see(self, *items):
711 """Scroll (in a series of nudges) so items are visible""" 712 x1, y1, x2, y2=apply(self.bbox, items) 713 while x2 > self.canvasx(0)+self.winfo_width(): 714 old=self.canvasx(0) 715 self.xview('scroll', 1, 'units') 716 # avoid endless loop if we can't scroll 717 if old == self.canvasx(0): 718 break 719 while y2 > self.canvasy(0)+self.winfo_height(): 720 old=self.canvasy(0) 721 self.yview('scroll', 1, 'units') 722 if old == self.canvasy(0): 723 break 724 # done in this order to ensure upper-left of object is visible 725 while x1 < self.canvasx(0): 726 old=self.canvasx(0) 727 self.xview('scroll', -1, 'units') 728 if old == self.canvasx(0): 729 break 730 while y1 < self.canvasy(0): 731 old=self.canvasy(0) 732 self.yview('scroll', -1, 'units') 733 if old == self.canvasy(0): 734 break
735
736 - def move_cursor(self, node):
737 """Move cursor to node""" 738 self.pos=node 739 x1, y1, x2, y2=self.bbox(node.symbol, node.label) 740 self.coords(self.cursor_box, x1-1, y1-1, x2+1, y2+1) 741 self.see(node.symbol, node.label)
742
743 - def toggle(self, event=None):
744 """Expand/collapse subtree""" 745 self.pos.toggle_state()
746
747 - def next(self, event=None):
748 """Move to next lower visible node""" 749 self.move_cursor(self.pos.next_visible())
750
751 - def prev(self, event=None):
752 """Move to next higher visible node""" 753 self.move_cursor(self.pos.prev_visible())
754
755 - def ascend(self, event=None):
756 """Move to immediate parent""" 757 if self.pos.parent_node: 758 # move to parent 759 self.move_cursor(self.pos.parent_node)
760
761 - def descend(self, event=None):
762 """Move right, expanding as we go""" 763 if self.pos.expandable_flag: 764 self.pos.expand() 765 if self.pos.child_nodes: 766 # move to first subnode 767 self.move_cursor(self.pos.child_nodes[0]) 768 return 769 # if no subnodes, move to next sibling 770 self.next()
771
772 - def first(self, event=None):
773 """Go to root node""" 774 # move to root node 775 self.move_cursor(self.root)
776
777 - def last(self, event=None):
778 """Go to last visible node""" 779 # move to bottom-most node 780 self.move_cursor(self.root.PVT_last())
781
782 - def pageup(self, event=None):
783 """Previous page""" 784 n=self.pos 785 j=self.winfo_height()/self.dist_y 786 for i in range(j-3): 787 n=n.prev_visible() 788 self.yview('scroll', -1, 'pages') 789 self.move_cursor(n)
790
791 - def pagedown(self, event=None):
792 """Next page""" 793 n=self.pos 794 j=self.winfo_height()/self.dist_y 795 for i in range(j-3): 796 n=n.next_visible() 797 self.yview('scroll', 1, 'pages') 798 self.move_cursor(n)
799 800 # ----- functions for drag'n'drop support -----
801 - def where(self, event):
802 """Determine drag location in canvas coordinates. event.x & event.y 803 don't seem to be what we want.""" 804 # where the corner of the canvas is relative to the screen: 805 x_org=self.winfo_rootx() 806 y_org=self.winfo_rooty() 807 # where the pointer is relative to the canvas widget, 808 # including scrolling 809 x=self.canvasx(event.x_root-x_org) 810 y=self.canvasy(event.y_root-y_org) 811 return x, y
812
813 - def dnd_accept(self, source, event):
814 """Accept dnd messages, i.e. we're a legit drop target, and we do 815 implement d&d functions.""" 816 self.target=None 817 return self
818
819 - def dnd_enter(self, source, event):
820 """Get ready to drag or drag has entered widget (create drag 821 object)""" 822 # this flag lets us know there's been drag motion 823 self.drag=1 824 x, y=self.where(event) 825 x1, y1, x2, y2=source.widget.bbox(source.symbol, source.label) 826 dx, dy=x2-x1, y2-y1 827 # create dragging icon 828 if source.expanded_flag: 829 self.dnd_symbol=self.create_image(x, y, 830 image=source.expanded_icon) 831 else: 832 self.dnd_symbol=self.create_image(x, y, 833 image=source.collapsed_icon) 834 self.dnd_label=self.create_text(x+self.text_offset, y, 835 text=source.get_label(), 836 justify='left', 837 anchor='w')
838
839 - def dnd_motion(self, source, event):
840 """Move drag icon""" 841 self.drag=1 842 x, y=self.where(event) 843 x1, y1, x2, y2=self.bbox(self.dnd_symbol, self.dnd_label) 844 self.move(self.dnd_symbol, x-x1+source.x_off, y-y1+source.y_off) 845 self.move(self.dnd_label, x-x1+source.x_off, y-y1+source.y_off)
846
847 - def dnd_leave(self, source, event):
848 """Finish dragging or drag has left widget (destroy drag object)""" 849 self.delete(self.dnd_symbol) 850 self.delete(self.dnd_label)
851
852 - def dnd_commit(self, source, event):
853 """Object has been dropped here""" 854 # call our own dnd_leave() to clean up 855 self.dnd_leave(source, event) 856 # process pending events to detect target node 857 # update_idletasks() doesn't do the trick if source & target are 858 # on different widgets 859 self.update() 860 if not self.target: 861 # no target node 862 return 863 # we must update data structures based on the drop 864 if self.drop_callback: 865 try: 866 # called with dragged node and target node 867 # this is where a file manager would move the actual file 868 # it must also move the nodes around as it wishes 869 self.drop_callback(source, self.target) 870 except: 871 report_callback_exception()
872 873 #------------------------------------------------------------------------------ 874 # the good 'ol test/demo code 875 if __name__ == '__main__': 876 import os 877 import sys 878 879 # default routine to get contents of subtree 880 # supply this for a different type of app 881 # argument is the node object being expanded 882 # should call add_node()
883 - def get_contents(node):
884 path=apply(os.path.join, node.full_id()) 885 for filename in os.listdir(path): 886 full=os.path.join(path, filename) 887 name=filename 888 folder=0 889 if os.path.isdir(full): 890 # it's a directory 891 folder=1 892 elif not os.path.isfile(full): 893 # but it's not a file 894 name=name+' (special)' 895 if os.path.islink(full): 896 # it's a link 897 name=name+' (link to '+os.readlink(full)+')' 898 node.widget.add_node(name=name, id=filename, flag=folder)
899 900 root=Tk() 901 root.title(os.path.basename(sys.argv[0])) 902 tree=os.sep 903 if sys.platform == 'win32': 904 # we could call the root "My Computer" and mess with get_contents() 905 # to return "A:", "B:", "C:", ... etc. as it's children, but that 906 # would just be terminally cute and I'd have to shoot myself 907 tree='C:'+os.sep 908 909 # create the control 910 t=Tree(master=root, 911 root_id=tree, 912 root_label=tree, 913 get_contents_callback=get_contents, 914 width=300) 915 t.grid(row=0, column=0, sticky='nsew') 916 917 # make expandable 918 root.grid_rowconfigure(0, weight=1) 919 root.grid_columnconfigure(0, weight=1) 920 921 # add scrollbars 922 sb=Scrollbar(root) 923 sb.grid(row=0, column=1, sticky='ns') 924 t.configure(yscrollcommand=sb.set) 925 sb.configure(command=t.yview) 926 927 sb=Scrollbar(root, orient=HORIZONTAL) 928 sb.grid(row=1, column=0, sticky='ew') 929 t.configure(xscrollcommand=sb.set) 930 sb.configure(command=t.xview) 931 932 # must get focus so keys work for demo 933 t.focus_set() 934 935 # we could do without this, but it's nice and friendly to have 936 Button(root, text='Quit', command=root.quit).grid(row=2, column=0, 937 columnspan=2) 938 939 # expand out the root 940 t.root.expand() 941 942 root.mainloop() 943 944 945 946 #-------------------------------------- Here's a bunch of custom wrapper code to make the tree a bit easier to use---------------- 947
948 -class EasyNode(dict):
949 - def __init__(self,d):
950 dict.__init__(self,d.copy())
951
952 - def childIndex(self,child):
953 for c,index in zip(self['children'],range(len(self['children']))): 954 if c['id'] == child['id']: 955 return index 956 return -1
957
958 - def deleteChildren(self):
959 for child in self['children'][:]: 960 child.delete()
961
962 - def delete(self,recursive=True):
963 if recursive: 964 for child in self['children'][:]: 965 child.delete() 966 n = self['widget'].find_full_id(self.full_id()) 967 if n: 968 n.delete() 969 if self['parent']: 970 self['parent']['children'].remove(self)
971 972 973
974 - def addChild(self,tree,node_id=None,label=None,isLeaf=False,debug=False):
975 child = EasyNode({'parent':self,'children':[],'label':label,'id':node_id,'isLeaf':isLeaf,'action':None,'expanded':False,'widget':tree}) 976 if not node_id: 977 node_id = id(child) 978 child['id'] = node_id 979 if not label: 980 label = node_id 981 child['label'] = label 982 983 self['children'].append(child) 984 985 if debug: 986 print "Adding child node to parent %s" % self['label'] 987 print "\tName: %s" % label 988 989 #if the parent is already in an expanded state, we should add the children directly to the tree too 990 pNode = tree.find_full_id(self.full_id()) 991 992 if pNode and pNode.expanded(): 993 if debug: 994 print "\tParent is in an expanded state" 995 oldersibling = None 996 if len(pNode.children()) > 0: 997 oldersibling = pNode.children()[len(pNode.children())-1] 998 oldersibling.insert_after(tree.add_list(list=None,name=child['label'],id=child['id'],flag=int(not child['isLeaf']),action=child['action'])) 999 else: 1000 pNode.insert_children(tree.add_list(list=None,name=child['label'],id=child['id'],flag=int(not child['isLeaf']),action=child['action'])) 1001 1002 return child
1003
1004 - def refresh(self,tree=None):
1005 1006 if not tree: 1007 tree = self['widget'] 1008 1009 #we need to keep track of whether this is the selected node or not 1010 selected=(tree.selected_node().full_id() == self.full_id()) 1011 1012 #we find the node's older and younger sibling then delete the node 1013 pNode = tree.find_full_id(self.full_id()) 1014 if pNode: 1015 parent = pNode.parent() 1016 if parent: 1017 siblings = parent.children() 1018 index = siblings.index(pNode) 1019 oldersibling = None 1020 youngersibling = None 1021 if index > 0: 1022 oldersibling = siblings[index-1] 1023 if not index == len(siblings)-1: 1024 youngersibling = siblings[index+1] 1025 pNode.delete() 1026 if oldersibling: 1027 oldersibling.insert_after(tree.add_list(list=None,name=self['label'],id=self['id'],flag=int(not self['isLeaf']),action=self['action'])) 1028 elif youngersibling: 1029 youngersibling.insert_before(tree.add_list(list=None,name=self['label'],id=self['id'],flag=int(not self['isLeaf']),action=self['action'])) 1030 else: 1031 parent.insert_children(tree.add_list(list=None,name=self['label'],id=self['id'],flag=int(not self['isLeaf']),action=self['action'])) 1032 else: 1033 # Root node (dp) 1034 pNode.set_label(self['label']) 1035 pNode.action = self['action'] 1036 if selected: 1037 node = tree.find_full_id(self.full_id()) 1038 tree.move_cursor(node)
1039 1040
1041 - def full_id(self):
1042 if self['parent']: 1043 return self['parent'].full_id() + (self['id'],) 1044 else: 1045 return (self['id'],)
1046
1047 -class EasyTree(Tree):
1048 - def __init__(self, master, root_id='root', root_label='root', 1049 get_contents_callback=None, dist_x=15, dist_y=30, 1050 text_offset=5, line_flag=1, expanded_icon=None, 1051 collapsed_icon=None, regular_icon=None, plus_icon=None, 1052 minus_icon=None, node_class=Node, root_action=None,drop_callback=None, 1053 *args, **kw_args):
1054 1055 if not get_contents_callback: 1056 get_contents_callback = self._getnodes_ 1057 1058 Tree.__init__(self,master,root_id,root_label=root_label, 1059 get_contents_callback=get_contents_callback,dist_x=dist_x,dist_y=dist_y, 1060 text_offset=text_offset,line_flag=line_flag,expanded_icon=expanded_icon, 1061 collapsed_icon=collapsed_icon,regular_icon=regular_icon,plus_icon=plus_icon, 1062 minus_icon=minus_icon,node_class=node_class,root_action=root_action,drop_callback=drop_callback, 1063 *args, **kw_args) 1064 1065 self.easyRoot = EasyNode({'parent':None,'children':[],'label':root_label,'id':root_id,'expanded':False,'widget': self})
1066
1067 - def selected_node(self):
1068 n = self.cursor_node(None) 1069 return self._find_internal_node_(n.full_id()[1:],self.easyRoot)
1070
1071 - def _find_internal_node_(self,id,search_node):
1072 if len(id) == 0: 1073 return search_node 1074 else: 1075 for child in search_node['children']: 1076 if child['id'] == id[0]: 1077 return self._find_internal_node_(id[1:],child) 1078 return None
1079
1080 - def _getnodes_(self,node):
1081 n = self._find_internal_node_(node.full_id()[1:],self.easyRoot) 1082 toexpand = [] 1083 children = [] 1084 for child in n['children']: 1085 self.add_list(list=children,name=child['label'],id=child['id'],flag=int(not child['isLeaf']),action=child['action']) 1086 if child['expanded']: 1087 toexpand.append(child) 1088 1089 node.insert_children(children) 1090 1091 for cnode in toexpand: 1092 childtoexpand = self.find_full_id(cnode.full_id()) 1093 cnode['expanded'] = False 1094 self._getnodes_(childtoexpand)
1095