1# PanedWidget
2# a frame which may contain several resizable sub-frames
3
4import string
5import sys
6import types
7import Tkinter
8import Pmw
9
10class PanedWidget(Pmw.MegaWidget):
11
12    def __init__(self, parent = None, **kw):
13
14	# Define the megawidget options.
15	INITOPT = Pmw.INITOPT
16	optiondefs = (
17            ('command',            None,         None),
18            ('orient',             'vertical',   INITOPT),
19            ('separatorrelief',    'sunken',     INITOPT),
20            ('separatorthickness', 2,            INITOPT),
21            ('handlesize',         8,            INITOPT),
22            ('hull_width',         400,          None),
23            ('hull_height',        400,          None),
24	)
25	self.defineoptions(kw, optiondefs,
26                dynamicGroups = ('Frame', 'Separator', 'Handle'))
27
28	# Initialise the base class (after defining the options).
29	Pmw.MegaWidget.__init__(self, parent)
30
31	self.bind('<Configure>', self._handleConfigure)
32
33	if self['orient'] not in ('horizontal', 'vertical'):
34	    raise ValueError, 'bad orient option ' + repr(self['orient']) + \
35		': must be either \'horizontal\' or \'vertical\''
36
37        self._separatorThickness = self['separatorthickness']
38        self._handleSize = self['handlesize']
39	self._paneNames = []            # List of pane names
40	self._paneAttrs = {}            # Map from pane name to pane info
41
42	self._timerId = None
43	self._frame = {}
44	self._separator = []
45	self._button = []
46	self._totalSize = 0
47	self._movePending = 0
48	self._relsize = {}
49	self._relmin = {}
50	self._relmax = {}
51	self._size = {}
52	self._min = {}
53	self._max = {}
54	self._rootp = None
55	self._curSize = None
56	self._beforeLimit = None
57	self._afterLimit = None
58	self._buttonIsDown = 0
59	self._majorSize = 100
60	self._minorSize = 100
61
62	# Check keywords and initialise options.
63	self.initialiseoptions()
64
65    def insert(self, name, before = 0, **kw):
66	# Parse <kw> for options.
67        self._initPaneOptions(name)
68	self._parsePaneOptions(name, kw)
69
70	insertPos = self._nameToIndex(before)
71	atEnd = (insertPos == len(self._paneNames))
72
73	# Add the frame.
74	self._paneNames[insertPos:insertPos] = [name]
75	self._frame[name] = self.createcomponent(name,
76		(), 'Frame',
77		Tkinter.Frame, (self.interior(),))
78
79	# Add separator, if necessary.
80	if len(self._paneNames) > 1:
81	    self._addSeparator()
82	else:
83	    self._separator.append(None)
84	    self._button.append(None)
85
86	# Add the new frame and adjust the PanedWidget
87	if atEnd:
88	    size = self._size[name]
89	    if size > 0 or self._relsize[name] is not None:
90		if self['orient'] == 'vertical':
91		    self._frame[name].place(x=0, relwidth=1,
92					    height=size, y=self._totalSize)
93		else:
94		    self._frame[name].place(y=0, relheight=1,
95					    width=size, x=self._totalSize)
96	    else:
97		if self['orient'] == 'vertical':
98		    self._frame[name].place(x=0, relwidth=1,
99					    y=self._totalSize)
100		else:
101		    self._frame[name].place(y=0, relheight=1,
102					    x=self._totalSize)
103	else:
104	    self._updateSizes()
105
106	self._totalSize = self._totalSize + self._size[name]
107	return self._frame[name]
108
109    def add(self, name, **kw):
110        return apply(self.insert, (name, len(self._paneNames)), kw)
111
112    def delete(self, name):
113	deletePos = self._nameToIndex(name)
114	name = self._paneNames[deletePos]
115	self.destroycomponent(name)
116	del self._paneNames[deletePos]
117	del self._frame[name]
118	del self._size[name]
119	del self._min[name]
120	del self._max[name]
121	del self._relsize[name]
122	del self._relmin[name]
123	del self._relmax[name]
124
125	last = len(self._paneNames)
126	del self._separator[last]
127	del self._button[last]
128        if last > 0:
129            self.destroycomponent(self._sepName(last))
130            self.destroycomponent(self._buttonName(last))
131
132	self._plotHandles()
133
134    def setnaturalsize(self):
135        self.update_idletasks()
136        totalWidth = 0
137        totalHeight = 0
138        maxWidth = 0
139        maxHeight = 0
140	for name in self._paneNames:
141            frame = self._frame[name]
142            w = frame.winfo_reqwidth()
143            h = frame.winfo_reqheight()
144            totalWidth = totalWidth + w
145            totalHeight = totalHeight + h
146            if maxWidth < w:
147                maxWidth = w
148            if maxHeight < h:
149                maxHeight = h
150
151        # Note that, since the hull is a frame, the width and height
152        # options specify the geometry *outside* the borderwidth and
153        # highlightthickness.
154        bw = string.atoi(str(self.cget('hull_borderwidth')))
155        hl = string.atoi(str(self.cget('hull_highlightthickness')))
156        extra = (bw + hl) * 2
157        if str(self.cget('orient')) == 'horizontal':
158            totalWidth = totalWidth + extra
159            maxHeight = maxHeight + extra
160            self.configure(hull_width = totalWidth, hull_height = maxHeight)
161        else:
162            totalHeight = (totalHeight + extra +
163                    (len(self._paneNames) - 1) * self._separatorThickness)
164            maxWidth = maxWidth + extra
165            self.configure(hull_width = maxWidth, hull_height = totalHeight)
166
167    def move(self, name, newPos, newPosOffset = 0):
168
169        # see if we can spare ourselves some work
170        numPanes = len(self._paneNames)
171        if numPanes < 2:
172            return
173
174        newPos = self._nameToIndex(newPos) + newPosOffset
175        if newPos < 0 or newPos >=numPanes:
176            return
177
178        deletePos = self._nameToIndex(name)
179
180        if deletePos == newPos:
181            # inserting over ourself is a no-op
182            return
183
184        # delete name from old position in list
185        name = self._paneNames[deletePos]
186        del self._paneNames[deletePos]
187
188        # place in new position
189        self._paneNames[newPos:newPos] = [name]
190
191        # force everything to redraw
192        self._plotHandles()
193        self._updateSizes()
194
195    def _nameToIndex(self, nameOrIndex):
196	try:
197	    pos = self._paneNames.index(nameOrIndex)
198	except ValueError:
199	    pos = nameOrIndex
200
201	return pos
202
203    def _initPaneOptions(self, name):
204	# Set defaults.
205	self._size[name] = 0
206	self._relsize[name] = None
207	self._min[name] = 0
208	self._relmin[name] = None
209	self._max[name] = 100000
210	self._relmax[name] = None
211
212    def _parsePaneOptions(self, name, args):
213	# Parse <args> for options.
214	for arg, value in args.items():
215	    if type(value) == types.FloatType:
216		relvalue = value
217		value = self._absSize(relvalue)
218	    else:
219		relvalue = None
220
221	    if arg == 'size':
222		self._size[name], self._relsize[name] = value, relvalue
223	    elif arg == 'min':
224		self._min[name], self._relmin[name] = value, relvalue
225	    elif arg == 'max':
226		self._max[name], self._relmax[name] = value, relvalue
227	    else:
228		raise ValueError, 'keyword must be "size", "min", or "max"'
229
230    def _absSize(self, relvalue):
231	return int(round(relvalue * self._majorSize))
232
233    def _sepName(self, n):
234	return 'separator-%d' % n
235
236    def _buttonName(self, n):
237	return 'handle-%d' % n
238
239    def _addSeparator(self):
240	n = len(self._paneNames) - 1
241
242	downFunc = lambda event, s = self, num=n: s._btnDown(event, num)
243	upFunc = lambda event, s = self, num=n: s._btnUp(event, num)
244	moveFunc = lambda event, s = self, num=n: s._btnMove(event, num)
245
246	# Create the line dividing the panes.
247	sep = self.createcomponent(self._sepName(n),
248		(), 'Separator',
249		Tkinter.Frame, (self.interior(),),
250		borderwidth = 1,
251		relief = self['separatorrelief'])
252	self._separator.append(sep)
253
254	sep.bind('<ButtonPress-1>', downFunc)
255	sep.bind('<Any-ButtonRelease-1>', upFunc)
256	sep.bind('<B1-Motion>', moveFunc)
257
258	if self['orient'] == 'vertical':
259	    cursor = 'sb_v_double_arrow'
260	    sep.configure(height = self._separatorThickness,
261                    width = 10000, cursor = cursor)
262	else:
263	    cursor = 'sb_h_double_arrow'
264	    sep.configure(width = self._separatorThickness,
265                    height = 10000, cursor = cursor)
266
267	self._totalSize = self._totalSize + self._separatorThickness
268
269	# Create the handle on the dividing line.
270	handle = self.createcomponent(self._buttonName(n),
271		(), 'Handle',
272		Tkinter.Frame, (self.interior(),),
273		    relief = 'raised',
274		    borderwidth = 1,
275		    width = self._handleSize,
276		    height = self._handleSize,
277		    cursor = cursor,
278		)
279	self._button.append(handle)
280
281	handle.bind('<ButtonPress-1>', downFunc)
282	handle.bind('<Any-ButtonRelease-1>', upFunc)
283	handle.bind('<B1-Motion>', moveFunc)
284
285	self._plotHandles()
286
287	for i in range(1, len(self._paneNames)):
288	    self._separator[i].tkraise()
289	for i in range(1, len(self._paneNames)):
290	    self._button[i].tkraise()
291
292    def _btnUp(self, event, item):
293	self._buttonIsDown = 0
294	self._updateSizes()
295	try:
296	    self._button[item].configure(relief='raised')
297	except:
298	    pass
299
300    def _btnDown(self, event, item):
301	self._button[item].configure(relief='sunken')
302	self._getMotionLimit(item)
303	self._buttonIsDown = 1
304	self._movePending = 0
305
306    def _handleConfigure(self, event = None):
307	self._getNaturalSizes()
308	if self._totalSize == 0:
309	    return
310
311	iterRange = list(self._paneNames)
312	iterRange.reverse()
313	if self._majorSize > self._totalSize:
314	    n = self._majorSize - self._totalSize
315	    self._iterate(iterRange, self._grow, n)
316	elif self._majorSize < self._totalSize:
317	    n = self._totalSize - self._majorSize
318	    self._iterate(iterRange, self._shrink, n)
319
320	self._plotHandles()
321	self._updateSizes()
322
323    def _getNaturalSizes(self):
324	# Must call this in order to get correct winfo_width, winfo_height
325	self.update_idletasks()
326
327	self._totalSize = 0
328
329	if self['orient'] == 'vertical':
330	    self._majorSize = self.winfo_height()
331	    self._minorSize = self.winfo_width()
332	    majorspec = Tkinter.Frame.winfo_reqheight
333	else:
334	    self._majorSize = self.winfo_width()
335	    self._minorSize = self.winfo_height()
336	    majorspec = Tkinter.Frame.winfo_reqwidth
337
338        bw = string.atoi(str(self.cget('hull_borderwidth')))
339        hl = string.atoi(str(self.cget('hull_highlightthickness')))
340        extra = (bw + hl) * 2
341        self._majorSize = self._majorSize - extra
342        self._minorSize = self._minorSize - extra
343
344	if self._majorSize < 0:
345	    self._majorSize = 0
346	if self._minorSize < 0:
347	    self._minorSize = 0
348
349	for name in self._paneNames:
350	    # adjust the absolute sizes first...
351	    if self._relsize[name] is None:
352		#special case
353		if self._size[name] == 0:
354		    self._size[name] = apply(majorspec, (self._frame[name],))
355		    self._setrel(name)
356	    else:
357		self._size[name] = self._absSize(self._relsize[name])
358
359	    if self._relmin[name] is not None:
360		self._min[name] = self._absSize(self._relmin[name])
361	    if self._relmax[name] is not None:
362		self._max[name] = self._absSize(self._relmax[name])
363
364	    # now adjust sizes
365	    if self._size[name] < self._min[name]:
366		self._size[name] = self._min[name]
367		self._setrel(name)
368
369	    if self._size[name] > self._max[name]:
370		self._size[name] = self._max[name]
371		self._setrel(name)
372
373	    self._totalSize = self._totalSize + self._size[name]
374
375	# adjust for separators
376	self._totalSize = (self._totalSize +
377                (len(self._paneNames) - 1) * self._separatorThickness)
378
379    def _setrel(self, name):
380	if self._relsize[name] is not None:
381	    if self._majorSize != 0:
382		self._relsize[name] = round(self._size[name]) / self._majorSize
383
384    def _iterate(self, names, proc, n):
385	for i in names:
386	    n = apply(proc, (i, n))
387	    if n == 0:
388		break
389
390    def _grow(self, name, n):
391	canGrow = self._max[name] - self._size[name]
392
393	if canGrow > n:
394	    self._size[name] = self._size[name] + n
395	    self._setrel(name)
396	    return 0
397	elif canGrow > 0:
398	    self._size[name] = self._max[name]
399	    self._setrel(name)
400	    n = n - canGrow
401
402	return n
403
404    def _shrink(self, name, n):
405	canShrink = self._size[name] - self._min[name]
406
407	if canShrink > n:
408	    self._size[name] = self._size[name] - n
409	    self._setrel(name)
410	    return 0
411	elif canShrink > 0:
412	    self._size[name] = self._min[name]
413	    self._setrel(name)
414	    n = n - canShrink
415
416	return n
417
418    def _updateSizes(self):
419	totalSize = 0
420
421	for name in self._paneNames:
422	    size = self._size[name]
423	    if self['orient'] == 'vertical':
424		self._frame[name].place(x = 0, relwidth = 1,
425					y = totalSize,
426					height = size)
427	    else:
428		self._frame[name].place(y = 0, relheight = 1,
429					x = totalSize,
430					width = size)
431
432	    totalSize = totalSize + size + self._separatorThickness
433
434	# Invoke the callback command
435	cmd = self['command']
436	if callable(cmd):
437	    cmd(map(lambda x, s = self: s._size[x], self._paneNames))
438
439    def _plotHandles(self):
440	if len(self._paneNames) == 0:
441	    return
442
443	if self['orient'] == 'vertical':
444	    btnp = self._minorSize - 13
445	else:
446	    h = self._minorSize
447
448	    if h > 18:
449		btnp = 9
450	    else:
451		btnp = h - 9
452
453	firstPane = self._paneNames[0]
454	totalSize = self._size[firstPane]
455
456	first = 1
457	last = len(self._paneNames) - 1
458
459	# loop from first to last, inclusive
460	for i in range(1, last + 1):
461
462	    handlepos = totalSize - 3
463	    prevSize = self._size[self._paneNames[i - 1]]
464	    nextSize = self._size[self._paneNames[i]]
465
466	    offset1 = 0
467
468	    if i == first:
469		if prevSize < 4:
470		    offset1 = 4 - prevSize
471	    else:
472		if prevSize < 8:
473		    offset1 = (8 - prevSize) / 2
474
475	    offset2 = 0
476
477	    if i == last:
478		if nextSize < 4:
479		    offset2 = nextSize - 4
480	    else:
481		if nextSize < 8:
482		    offset2 = (nextSize - 8) / 2
483
484	    handlepos = handlepos + offset1
485
486	    if self['orient'] == 'vertical':
487		height = 8 - offset1 + offset2
488
489		if height > 1:
490		    self._button[i].configure(height = height)
491		    self._button[i].place(x = btnp, y = handlepos)
492		else:
493		    self._button[i].place_forget()
494
495		self._separator[i].place(x = 0, y = totalSize,
496					 relwidth = 1)
497	    else:
498		width = 8 - offset1 + offset2
499
500		if width > 1:
501		    self._button[i].configure(width = width)
502		    self._button[i].place(y = btnp, x = handlepos)
503		else:
504		    self._button[i].place_forget()
505
506		self._separator[i].place(y = 0, x = totalSize,
507					 relheight = 1)
508
509	    totalSize = totalSize + nextSize + self._separatorThickness
510
511    def pane(self, name):
512	return self._frame[self._paneNames[self._nameToIndex(name)]]
513
514    # Return the name of all panes
515    def panes(self):
516	return list(self._paneNames)
517
518    def configurepane(self, name, **kw):
519	name = self._paneNames[self._nameToIndex(name)]
520	self._parsePaneOptions(name, kw)
521	self._handleConfigure()
522
523    def updatelayout(self):
524	self._handleConfigure()
525
526    def _getMotionLimit(self, item):
527	curBefore = (item - 1) * self._separatorThickness
528	minBefore, maxBefore = curBefore, curBefore
529
530	for name in self._paneNames[:item]:
531	    curBefore = curBefore + self._size[name]
532	    minBefore = minBefore + self._min[name]
533	    maxBefore = maxBefore + self._max[name]
534
535	curAfter = (len(self._paneNames) - item) * self._separatorThickness
536	minAfter, maxAfter = curAfter, curAfter
537	for name in self._paneNames[item:]:
538	    curAfter = curAfter + self._size[name]
539	    minAfter = minAfter + self._min[name]
540	    maxAfter = maxAfter + self._max[name]
541
542	beforeToGo = min(curBefore - minBefore, maxAfter - curAfter)
543	afterToGo = min(curAfter - minAfter, maxBefore - curBefore)
544
545	self._beforeLimit = curBefore - beforeToGo
546	self._afterLimit = curBefore + afterToGo
547	self._curSize = curBefore
548
549	self._plotHandles()
550
551    # Compress the motion so that update is quick even on slow machines
552    #
553    # theRootp = root position (either rootx or rooty)
554    def _btnMove(self, event, item):
555	self._rootp = event
556
557	if self._movePending == 0:
558	    self._timerId = self.after_idle(
559		    lambda s = self, i = item: s._btnMoveCompressed(i))
560	    self._movePending = 1
561
562    def destroy(self):
563        if self._timerId is not None:
564          self.after_cancel(self._timerId)
565	  self._timerId = None
566        Pmw.MegaWidget.destroy(self)
567
568    def _btnMoveCompressed(self, item):
569	if not self._buttonIsDown:
570	    return
571
572	if self['orient'] == 'vertical':
573	    p = self._rootp.y_root - self.winfo_rooty()
574	else:
575	    p = self._rootp.x_root - self.winfo_rootx()
576
577	if p == self._curSize:
578	    self._movePending = 0
579	    return
580
581	if p < self._beforeLimit:
582	    p = self._beforeLimit
583
584	if p >= self._afterLimit:
585	    p = self._afterLimit
586
587	self._calculateChange(item, p)
588	self.update_idletasks()
589	self._movePending = 0
590
591    # Calculate the change in response to mouse motions
592    def _calculateChange(self, item, p):
593
594	if p < self._curSize:
595	    self._moveBefore(item, p)
596	elif p > self._curSize:
597	    self._moveAfter(item, p)
598
599	self._plotHandles()
600
601    def _moveBefore(self, item, p):
602	n = self._curSize - p
603
604	# Shrink the frames before
605	iterRange = list(self._paneNames[:item])
606	iterRange.reverse()
607	self._iterate(iterRange, self._shrink, n)
608
609	# Adjust the frames after
610	iterRange = self._paneNames[item:]
611	self._iterate(iterRange, self._grow, n)
612
613	self._curSize = p
614
615    def _moveAfter(self, item, p):
616	n = p - self._curSize
617
618	# Shrink the frames after
619	iterRange = self._paneNames[item:]
620	self._iterate(iterRange, self._shrink, n)
621
622	# Adjust the frames before
623	iterRange = list(self._paneNames[:item])
624	iterRange.reverse()
625	self._iterate(iterRange, self._grow, n)
626
627	self._curSize = p
628