1 |
2 |
drasko |
# tree.py -- highly optimized tkinter tree control
|
2 |
|
|
# by Charles E. "Gene" Cash (gcash@magicnet.net)
|
3 |
|
|
#
|
4 |
|
|
# 98/12/02 CEC started
|
5 |
|
|
# 99/??/?? CEC release to comp.lang.python.announce
|
6 |
|
|
# Trimmed for CML2 by ESR, December 2001 (cut-paste support removed).
|
7 |
|
|
import os, string
|
8 |
|
|
from Tkinter import *
|
9 |
|
|
|
10 |
|
|
# this is initialized later, after Tkinter is started
|
11 |
|
|
open_icon=None
|
12 |
|
|
|
13 |
|
|
# tree node helper class
|
14 |
|
|
class Node:
|
15 |
|
|
# initialization creates node, draws it, and binds mouseclicks
|
16 |
|
|
def __init__(self, parent, name, id, myclosed_icon, myopen_icon, x, y,
|
17 |
|
|
parentwidget):
|
18 |
|
|
self.parent=parent # immediate parent node
|
19 |
|
|
self.name=name # name displayed on the label
|
20 |
|
|
self.id=id # internal id used to manipulate things
|
21 |
|
|
self.open_icon=myopen_icon # bitmaps to be displayed
|
22 |
|
|
self.closed_icon=myclosed_icon
|
23 |
|
|
self.widget=parentwidget # tree widget we belong to
|
24 |
|
|
self.subnodes=[] # our list of child nodes
|
25 |
|
|
self.spinlock=0 # cheap mutex spinlock
|
26 |
|
|
self.open_flag=0 # closed to start with
|
27 |
|
|
# draw horizontal connecting lines
|
28 |
|
|
if self.widget.lineflag:
|
29 |
|
|
self.line=self.widget.create_line(x-self.widget.distx, y, x, y)
|
30 |
|
|
# draw approprate image
|
31 |
|
|
if self.open_flag:
|
32 |
|
|
self.symbol=self.widget.create_image(x, y, image=self.open_icon)
|
33 |
|
|
else:
|
34 |
|
|
self.symbol=self.widget.create_image(x, y, image=self.closed_icon)
|
35 |
|
|
# add label
|
36 |
|
|
self.label=self.widget.create_text(x+self.widget.textoff, y,
|
37 |
|
|
text=self.name, justify='left',
|
38 |
|
|
anchor='w' )
|
39 |
|
|
# single-click to expand/collapse
|
40 |
|
|
self.widget.tag_bind(self.symbol, '<1>', self.click)
|
41 |
|
|
self.widget.tag_bind(self.label, '<1>', self.click) # njr
|
42 |
|
|
# call customization hook
|
43 |
|
|
if self.widget.init_hook:
|
44 |
|
|
self.widget.init_hook(self)
|
45 |
|
|
|
46 |
|
|
# def __repr__(self):
|
47 |
|
|
# return 'Node: %s Parent: %s (%d children)' % \
|
48 |
|
|
# (self.name, self.parent.name, len(self.subnodes))
|
49 |
|
|
|
50 |
|
|
# recursively delete subtree & clean up cyclic references
|
51 |
|
|
def _delete(self):
|
52 |
|
|
for i in self.subnodes:
|
53 |
|
|
if i.open_flag and i.subnodes:
|
54 |
|
|
# delete vertical connecting line
|
55 |
|
|
if self.widget.lineflag:
|
56 |
|
|
self.widget.delete(i.tree)
|
57 |
|
|
# delete node's subtree, if any
|
58 |
|
|
i._delete()
|
59 |
|
|
# the following unbinding hassle is because tkinter
|
60 |
|
|
# keeps a callback reference for each binding
|
61 |
|
|
# so if we want things GC'd...
|
62 |
|
|
for j in (i.symbol, i.label):
|
63 |
|
|
for k in self.widget.tag_bind(j):
|
64 |
|
|
self.widget.tag_unbind(j, k)
|
65 |
|
|
try:
|
66 |
|
|
for k in self.widget._tagcommands.get(j, []):
|
67 |
|
|
self.widget.deletecommand(k)
|
68 |
|
|
self.widget._tagcommands[j].remove(k)
|
69 |
|
|
except: # XXX not needed in >= 1.6?
|
70 |
|
|
pass
|
71 |
|
|
# delete widgets from canvas
|
72 |
|
|
self.widget.delete(i.symbol, i.label)
|
73 |
|
|
if self.widget.lineflag:
|
74 |
|
|
self.widget.delete(i.line)
|
75 |
|
|
# break cyclic reference
|
76 |
|
|
i.parent=None
|
77 |
|
|
# move cursor if it's in deleted subtree
|
78 |
|
|
if self.widget.pos in self.subnodes:
|
79 |
|
|
self.widget.move_cursor(self)
|
80 |
|
|
# now subnodes will be properly garbage collected
|
81 |
|
|
self.subnodes=[]
|
82 |
|
|
|
83 |
|
|
# move everything below current icon, to make room for subtree
|
84 |
|
|
# using the magic of item tags
|
85 |
|
|
def _tagmove(self, dist):
|
86 |
|
|
# mark everything below current node as movable
|
87 |
|
|
bbox1=self.widget.bbox(self.widget.root.symbol, self.label)
|
88 |
|
|
bbox2=self.widget.bbox('all')
|
89 |
|
|
self.widget.dtag('move')
|
90 |
|
|
self.widget.addtag('move', 'overlapping',
|
91 |
|
|
bbox2[0], bbox1[3], bbox2[2], bbox2[3])
|
92 |
|
|
# untag cursor & node so they don't get moved too
|
93 |
|
|
# this has to be done under Tk on X11
|
94 |
|
|
self.widget.dtag(self.widget.cursor_box, 'move')
|
95 |
|
|
self.widget.dtag(self.symbol, 'move')
|
96 |
|
|
self.widget.dtag(self.label, 'move')
|
97 |
|
|
# now do the move of all the tagged objects
|
98 |
|
|
self.widget.move('move', 0, dist)
|
99 |
|
|
# fix up connecting lines
|
100 |
|
|
if self.widget.lineflag:
|
101 |
|
|
n=self
|
102 |
|
|
while n:
|
103 |
|
|
if len(n.subnodes):
|
104 |
|
|
# position of current icon
|
105 |
|
|
x1, y1=self.widget.coords(n.symbol)
|
106 |
|
|
# position of last node in subtree
|
107 |
|
|
x2, y2=self.widget.coords(n.subnodes[-1:][0].symbol)
|
108 |
|
|
self.widget.coords(n.tree, x1, y1, x1, y2)
|
109 |
|
|
n=n.parent
|
110 |
|
|
|
111 |
|
|
# return list of subnodes that are expanded (not including self)
|
112 |
|
|
# only includes unique leaf nodes (e.g. /home and /home/root won't
|
113 |
|
|
# both be included) so expand() doesn't get called unnecessarily
|
114 |
|
|
# thank $DEITY for Dr. Dutton's Data Structures classes at UCF!
|
115 |
|
|
def expanded(self):
|
116 |
|
|
# push initial node into stack
|
117 |
|
|
stack=[(self, (self.id,))]
|
118 |
|
|
list=[]
|
119 |
|
|
while stack:
|
120 |
|
|
# pop from stack
|
121 |
|
|
p, i=stack[-1:][0]
|
122 |
|
|
del stack[-1:]
|
123 |
|
|
# flag to discard non-unique sub paths
|
124 |
|
|
flag=1
|
125 |
|
|
# check all children
|
126 |
|
|
for n in p.subnodes:
|
127 |
|
|
# if expanded, push onto stack
|
128 |
|
|
if n.open_flag:
|
129 |
|
|
flag=0
|
130 |
|
|
stack.append((n, i+(n.id,)))
|
131 |
|
|
# if we reached end of path, add to list
|
132 |
|
|
if flag:
|
133 |
|
|
list.append(i[1:])
|
134 |
|
|
return list
|
135 |
|
|
|
136 |
|
|
# get full name, including names of all parents
|
137 |
|
|
def full_id(self):
|
138 |
|
|
if self.parent:
|
139 |
|
|
return self.parent.full_id()+(self.id,)
|
140 |
|
|
else:
|
141 |
|
|
return (self.id,)
|
142 |
|
|
|
143 |
|
|
# expanding/collapsing folders
|
144 |
|
|
def toggle_state(self, state=None):
|
145 |
|
|
if self.widget.toggle_init_hook:
|
146 |
|
|
self.widget.toggle_init_hook(self)
|
147 |
|
|
if not self.open_icon:
|
148 |
|
|
return # not an expandable folder
|
149 |
|
|
if state == None:
|
150 |
|
|
state = not self.open_flag # toggle to other state
|
151 |
|
|
else:
|
152 |
|
|
# are we already in the state we want to be?
|
153 |
|
|
if (not state) == (not self.open_flag):
|
154 |
|
|
return
|
155 |
|
|
# not re-entrant
|
156 |
|
|
# acquire mutex
|
157 |
|
|
while self.spinlock:
|
158 |
|
|
pass
|
159 |
|
|
self.spinlock=1
|
160 |
|
|
# call customization hook
|
161 |
|
|
if self.widget.before_hook:
|
162 |
|
|
self.widget.before_hook(self)
|
163 |
|
|
# if we're closed, expand & draw our subtrees
|
164 |
|
|
if not self.open_flag:
|
165 |
|
|
self.open_flag=1
|
166 |
|
|
self.widget.itemconfig(self.symbol, image=self.open_icon)
|
167 |
|
|
# get contents of subdirectory or whatever
|
168 |
|
|
contents=self.widget.get_contents(self)
|
169 |
|
|
# move stuff to make room
|
170 |
|
|
self._tagmove(self.widget.disty*len(contents))
|
171 |
|
|
# now draw subtree
|
172 |
|
|
self.subnodes=[]
|
173 |
|
|
# get current position of icon
|
174 |
|
|
x, y=self.widget.coords(self.symbol)
|
175 |
|
|
yp=y
|
176 |
|
|
for i in contents:
|
177 |
|
|
# add new subnodes, they'll draw themselves
|
178 |
|
|
yp=yp+self.widget.disty
|
179 |
|
|
self.subnodes.append(Node(self, i[0], i[1], i[2], i[3],
|
180 |
|
|
x+self.widget.distx, yp,
|
181 |
|
|
self.widget))
|
182 |
|
|
# the vertical line spanning the subtree
|
183 |
|
|
if self.subnodes and self.widget.lineflag:
|
184 |
|
|
self.tree=self.widget.create_line(x, y,
|
185 |
|
|
x, y+self.widget.disty*len(self.subnodes))
|
186 |
|
|
self.widget.lower(self.tree, self.symbol)
|
187 |
|
|
# if we're open, collapse and delete subtrees
|
188 |
|
|
elif self.open_flag:
|
189 |
|
|
self.open_flag=0
|
190 |
|
|
self.widget.itemconfig(self.symbol, image=self.closed_icon)
|
191 |
|
|
# if we have any children
|
192 |
|
|
if self.subnodes:
|
193 |
|
|
# recursively delete subtree icons
|
194 |
|
|
self._delete()
|
195 |
|
|
# delete vertical line
|
196 |
|
|
if self.widget.lineflag:
|
197 |
|
|
self.widget.delete(self.tree)
|
198 |
|
|
# find next (vertically-speaking) node
|
199 |
|
|
n=self
|
200 |
|
|
while n.parent:
|
201 |
|
|
# position of next sibling in parent's list
|
202 |
|
|
i=n.parent.subnodes.index(n)+1
|
203 |
|
|
if i < len(n.parent.subnodes):
|
204 |
|
|
n=n.parent.subnodes[i]
|
205 |
|
|
break
|
206 |
|
|
n=n.parent
|
207 |
|
|
if n.parent:
|
208 |
|
|
# move everything up so that distance to next subnode is
|
209 |
|
|
# correct
|
210 |
|
|
x1, y1=self.widget.coords(self.symbol)
|
211 |
|
|
x2, y2=self.widget.coords(n.symbol)
|
212 |
|
|
dist=y2-y1-self.widget.disty
|
213 |
|
|
self._tagmove(-dist)
|
214 |
|
|
# update scroll region for new size
|
215 |
|
|
x1, y1, x2, y2=self.widget.bbox('all')
|
216 |
|
|
self.widget.configure(scrollregion=(x1, y1, x2+5, y2+5))
|
217 |
|
|
# call customization hook
|
218 |
|
|
if self.widget.after_hook:
|
219 |
|
|
print 'calling after_hook'
|
220 |
|
|
self.widget.after_hook(self)
|
221 |
|
|
# release mutex
|
222 |
|
|
self.spinlock=0
|
223 |
|
|
|
224 |
|
|
# expand this subnode
|
225 |
|
|
# doesn't have to exist, it expands what part of the path DOES exist
|
226 |
|
|
def expand(self, dirs):
|
227 |
|
|
# if collapsed, then expand
|
228 |
|
|
self.toggle_state(1)
|
229 |
|
|
# find next subnode
|
230 |
|
|
if dirs:
|
231 |
|
|
for n in self.subnodes:
|
232 |
|
|
if n.id == dirs[0]:
|
233 |
|
|
return n.expand(dirs[1:])
|
234 |
|
|
print "Can't find path %s in %s" % (dirs, self.id)
|
235 |
|
|
print "- Available subnodes: %s" % map(lambda n: n.id, self.subnodes)
|
236 |
|
|
return self
|
237 |
|
|
|
238 |
|
|
# handle mouse clicks by moving cursor and toggling folder state
|
239 |
|
|
def click(self, dummy):
|
240 |
|
|
self.widget.move_cursor(self)
|
241 |
|
|
self.toggle_state()
|
242 |
|
|
|
243 |
|
|
# return next lower visible node
|
244 |
|
|
def next(self):
|
245 |
|
|
n=self
|
246 |
|
|
if n.subnodes:
|
247 |
|
|
# if you can go right, do so
|
248 |
|
|
return n.subnodes[0]
|
249 |
|
|
while n.parent:
|
250 |
|
|
# move to next sibling
|
251 |
|
|
i=n.parent.subnodes.index(n)+1
|
252 |
|
|
if i < len(n.parent.subnodes):
|
253 |
|
|
return n.parent.subnodes[i]
|
254 |
|
|
# if no siblings, move to parent's sibling
|
255 |
|
|
n=n.parent
|
256 |
|
|
# we're at bottom
|
257 |
|
|
return self
|
258 |
|
|
|
259 |
|
|
# return next higher visible node
|
260 |
|
|
def prev(self):
|
261 |
|
|
n=self
|
262 |
|
|
if n.parent:
|
263 |
|
|
# move to previous sibling
|
264 |
|
|
i=n.parent.subnodes.index(n)-1
|
265 |
|
|
if i >= 0:
|
266 |
|
|
# move to last child
|
267 |
|
|
n=n.parent.subnodes[i]
|
268 |
|
|
while n.subnodes:
|
269 |
|
|
n=n.subnodes[-1]
|
270 |
|
|
else:
|
271 |
|
|
# punt if there's no previous sibling
|
272 |
|
|
if n.parent:
|
273 |
|
|
n=n.parent
|
274 |
|
|
return n
|
275 |
|
|
|
276 |
|
|
class Tree(Canvas):
|
277 |
|
|
def __init__(self, master, rootname, rootlabel=None, openicon=None,
|
278 |
|
|
shuticon=None, getcontents=None, init=None,
|
279 |
|
|
toggle_init=None,before=None, after=None, cut=None, paste=None,
|
280 |
|
|
distx=15, disty=15, textoff=10, lineflag=1, **kw_args):
|
281 |
|
|
global open_icon, shut_icon, file_icon,yes_icon,no_icon
|
282 |
|
|
# pass args to superclass
|
283 |
|
|
apply(Canvas.__init__, (self, master), kw_args)
|
284 |
|
|
# try creating an image, work around Tkinter bug
|
285 |
|
|
# ('global' should do it, but it doesn't)
|
286 |
|
|
if open_icon is not None:
|
287 |
|
|
try:
|
288 |
|
|
item = self.create_image(0,0,image=open_icon)
|
289 |
|
|
self.delete(item)
|
290 |
|
|
except:
|
291 |
|
|
print "recreating Tree PhotoImages"
|
292 |
|
|
open_icon = None # need to recreate PhotoImages
|
293 |
|
|
# default images (BASE64-encoded GIF files)
|
294 |
|
|
# we have to delay initialization until Tk starts up or PhotoImage()
|
295 |
|
|
# complains (otherwise I'd just put it up top)
|
296 |
|
|
if open_icon == None:
|
297 |
|
|
open_icon=PhotoImage(
|
298 |
|
|
data='R0lGODlhEAANAKIAAAAAAMDAwICAgP//////ADAwMAAAAAAA' \
|
299 |
|
|
'ACH5BAEAAAEALAAAAAAQAA0AAAM6GCrM+jCIQamIbw6ybXNSx3GVB' \
|
300 |
|
|
'YRiygnA534Eq5UlO8jUqLYsquuy0+SXap1CxBHr+HoBjoGndDpNAAA7')
|
301 |
|
|
shut_icon=PhotoImage(
|
302 |
|
|
data='R0lGODlhDwANAKIAAAAAAMDAwICAgP//////ADAwMAAAAAAA' \
|
303 |
|
|
'ACH5BAEAAAEALAAAAAAPAA0AAAMyGCHM+lAMMoeAT9Jtm5NDKI4Wo' \
|
304 |
|
|
'FXcJphhipanq7Kvu8b1dLc5tcuom2foAQQAyKRSmQAAOw==')
|
305 |
|
|
file_icon=PhotoImage(
|
306 |
|
|
data='R0lGODlhCwAOAJEAAAAAAICAgP///8DAwCH5BAEAAAMALAAA' \
|
307 |
|
|
'AAALAA4AAAIphA+jA+JuVgtUtMQePJlWCgSN9oSTV5lkKQpo2q5W+' \
|
308 |
|
|
'wbzuJrIHgw1WgAAOw==')
|
309 |
|
|
yes_icon=PhotoImage(
|
310 |
|
|
data='R0lGODlhDAAPAKEAAP////9FRAAAAP///yH5BAEKAAMALAAA' \
|
311 |
|
|
'AAAMAA8AAAIrhI8zyKAWUARCQGnqPVODuXlg0FkQ+WUmUzYpZYKv9'\
|
312 |
|
|
'5Eg7VZKxffC7usVAAA7')
|
313 |
|
|
no_icon=PhotoImage(
|
314 |
|
|
data='R0lGODlhDAAPAKEAAP///wAAAERe/////yH+FUNyZWF0ZWQgd' \
|
315 |
|
|
'2l0aCBUaGUgR0lNUAAh+QQBCgADACwAAAAADAAPAAACLISPM8i' \
|
316 |
|
|
'gjUIAolILpDB70zxxnieJBxl6n1FiGLiuW4tgJcSGuKRI/h/oAX8FADs=')
|
317 |
|
|
# function to return subnodes (not very much use w/o this)
|
318 |
|
|
if not getcontents:
|
319 |
|
|
raise ValueError, 'must have "get_contents" function'
|
320 |
|
|
self.get_contents=getcontents
|
321 |
|
|
# horizontal distance that subtrees are indented
|
322 |
|
|
self.distx=distx
|
323 |
|
|
# vertical distance between rows
|
324 |
|
|
self.disty=disty
|
325 |
|
|
# how far to offset text label
|
326 |
|
|
self.textoff=textoff
|
327 |
|
|
# called after new node initialization
|
328 |
|
|
self.init_hook=init
|
329 |
|
|
# called right after toggle state
|
330 |
|
|
self.toggle_init_hook=toggle_init
|
331 |
|
|
# called just before subtree expand/collapse
|
332 |
|
|
self.before_hook=before
|
333 |
|
|
# called just after subtree expand/collapse
|
334 |
|
|
self.after_hook=after
|
335 |
|
|
# flag to display lines
|
336 |
|
|
self.lineflag=lineflag
|
337 |
|
|
# create root node to get the ball rolling
|
338 |
|
|
if openicon:
|
339 |
|
|
oi = openicon
|
340 |
|
|
else:
|
341 |
|
|
oi = open_icon
|
342 |
|
|
if shuticon:
|
343 |
|
|
si = shuticon
|
344 |
|
|
else:
|
345 |
|
|
si = shut_icon
|
346 |
|
|
if rootlabel:
|
347 |
|
|
self.root=Node(None, rootlabel, rootname, si, oi, 11, 11, self)
|
348 |
|
|
else:
|
349 |
|
|
self.root=Node(None, rootname, rootname, si, oi, 11, 11, self)
|
350 |
|
|
# configure for scrollbar(s)
|
351 |
|
|
x1, y1, x2, y2=self.bbox('all')
|
352 |
|
|
self.configure(scrollregion=(x1, y1, x2+5, y2+5))
|
353 |
|
|
# add a cursor
|
354 |
|
|
self.cursor_box=self.create_rectangle(0, 0, 0, 0)
|
355 |
|
|
self.move_cursor(self.root)
|
356 |
|
|
# make it easy to point to control
|
357 |
|
|
self.bind('<Enter>', self.mousefocus)
|
358 |
|
|
# bindings similar to those used by Microsoft tree control
|
359 |
|
|
# page-up/page-down
|
360 |
|
|
self.bind('<Next>', self.pagedown)
|
361 |
|
|
self.bind('<Prior>', self.pageup)
|
362 |
|
|
# arrow-up/arrow-down
|
363 |
|
|
self.bind('<Down>', self.next)
|
364 |
|
|
self.bind('<Up>', self.prev)
|
365 |
|
|
# arrow-left/arrow-right
|
366 |
|
|
self.bind('<Left>', self.ascend)
|
367 |
|
|
# (hold this down and you expand the entire tree)
|
368 |
|
|
self.bind('<Right>', self.descend)
|
369 |
|
|
# home/end
|
370 |
|
|
self.bind('<Home>', self.first)
|
371 |
|
|
self.bind('<End>', self.last)
|
372 |
|
|
# space bar
|
373 |
|
|
self.bind('<Key-space>', self.toggle)
|
374 |
|
|
|
375 |
|
|
# scroll (in a series of nudges) so items are visible
|
376 |
|
|
def see(self, *items):
|
377 |
|
|
x1, y1, x2, y2=apply(self.bbox, items)
|
378 |
|
|
while x2 > self.canvasx(0)+self.winfo_width():
|
379 |
|
|
old=self.canvasx(0)
|
380 |
|
|
self.xview('scroll', 1, 'units')
|
381 |
|
|
# avoid endless loop if we can't scroll
|
382 |
|
|
if old == self.canvasx(0):
|
383 |
|
|
break
|
384 |
|
|
while y2 > self.canvasy(0)+self.winfo_height():
|
385 |
|
|
old=self.canvasy(0)
|
386 |
|
|
self.yview('scroll', 1, 'units')
|
387 |
|
|
if old == self.canvasy(0):
|
388 |
|
|
break
|
389 |
|
|
# done in this order to ensure upper-left of object is visible
|
390 |
|
|
while x1 < self.canvasx(0):
|
391 |
|
|
old=self.canvasx(0)
|
392 |
|
|
self.xview('scroll', -1, 'units')
|
393 |
|
|
if old == self.canvasx(0):
|
394 |
|
|
break
|
395 |
|
|
while y1 < self.canvasy(0):
|
396 |
|
|
old=self.canvasy(0)
|
397 |
|
|
self.yview('scroll', -1, 'units')
|
398 |
|
|
if old == self.canvasy(0):
|
399 |
|
|
break
|
400 |
|
|
|
401 |
|
|
# move cursor to node
|
402 |
|
|
def move_cursor(self, node):
|
403 |
|
|
self.pos=node
|
404 |
|
|
x1, y1, x2, y2=self.bbox(node.symbol, node.label)
|
405 |
|
|
self.coords(self.cursor_box, x1-1, y1-1, x2+1, y2+1)
|
406 |
|
|
self.see(node.symbol, node.label)
|
407 |
|
|
|
408 |
|
|
# expand given path
|
409 |
|
|
# note that the convention used in this program to identify a
|
410 |
|
|
# particular node is to give a tuple listing it's id and parent ids
|
411 |
|
|
# so you probably want to use os.path.split() a lot
|
412 |
|
|
def expand(self, path):
|
413 |
|
|
return self.root.expand(path[1:])
|
414 |
|
|
|
415 |
|
|
# soak up event argument when moused-over
|
416 |
|
|
# could've used lambda but didn't...
|
417 |
|
|
def mousefocus(self, event):
|
418 |
|
|
self.focus_set()
|
419 |
|
|
|
420 |
|
|
# open/close subtree
|
421 |
|
|
def toggle(self, event=None):
|
422 |
|
|
self.pos.toggle_state()
|
423 |
|
|
|
424 |
|
|
# move to next lower visible node
|
425 |
|
|
def next(self, event=None):
|
426 |
|
|
self.move_cursor(self.pos.next())
|
427 |
|
|
|
428 |
|
|
# move to next higher visible node
|
429 |
|
|
def prev(self, event=None):
|
430 |
|
|
self.move_cursor(self.pos.prev())
|
431 |
|
|
|
432 |
|
|
# move to immediate parent
|
433 |
|
|
def ascend(self, event=None):
|
434 |
|
|
if self.pos.parent:
|
435 |
|
|
# move to parent
|
436 |
|
|
self.move_cursor(self.pos.parent)
|
437 |
|
|
|
438 |
|
|
# move right, expanding as we go
|
439 |
|
|
def descend(self, event=None):
|
440 |
|
|
self.pos.toggle_state(1)
|
441 |
|
|
if self.pos.subnodes:
|
442 |
|
|
# move to first subnode
|
443 |
|
|
self.move_cursor(self.pos.subnodes[0])
|
444 |
|
|
else:
|
445 |
|
|
# if no subnodes, move to next sibling
|
446 |
|
|
self.next()
|
447 |
|
|
|
448 |
|
|
# go to root
|
449 |
|
|
def first(self, event=None):
|
450 |
|
|
# move to root node
|
451 |
|
|
self.move_cursor(self.root)
|
452 |
|
|
|
453 |
|
|
# go to last visible node
|
454 |
|
|
def last(self, event=None):
|
455 |
|
|
# move to bottom-most node
|
456 |
|
|
n=self.root
|
457 |
|
|
while n.subnodes:
|
458 |
|
|
n=n.subnodes[-1]
|
459 |
|
|
self.move_cursor(n)
|
460 |
|
|
|
461 |
|
|
# previous page
|
462 |
|
|
def pageup(self, event=None):
|
463 |
|
|
n=self.pos
|
464 |
|
|
j=self.winfo_height()/self.disty
|
465 |
|
|
for i in range(j-3):
|
466 |
|
|
n=n.prev()
|
467 |
|
|
self.yview('scroll', -1, 'pages')
|
468 |
|
|
self.move_cursor(n)
|
469 |
|
|
|
470 |
|
|
# next page
|
471 |
|
|
def pagedown(self, event=None):
|
472 |
|
|
n=self.pos
|
473 |
|
|
j=self.winfo_height()/self.disty
|
474 |
|
|
for i in range(j-3):
|
475 |
|
|
n=n.next()
|
476 |
|
|
self.yview('scroll', 1, 'pages')
|
477 |
|
|
self.move_cursor(n)
|
478 |
|
|
|
479 |
|
|
# End
|