# PanedWidget # a frame which may contain several resizable sub-frames import sys import types import Tkinter import Pmw class PanedWidget(Pmw.MegaWidget): def __init__(self, parent = None, **kw): # Define the megawidget options. INITOPT = Pmw.INITOPT optiondefs = ( ('command', None, None), ('orient', 'vertical', INITOPT), ('separatorrelief', 'sunken', INITOPT), ) self.defineoptions(kw, optiondefs) # Initialise the base class (after defining the options). Pmw.MegaWidget.__init__(self, parent) self.bind('<Configure>', self._handleConfigure) self._timerId = None self._item = [] self._frame = {} self._separator = [] self._button = [] self._totalSize = 0 self._movePending = 0 self._relsize = {} self._relmin = {} self._relmax = {} self._size = {} self._min = {} self._max = {} self._rootp = None self._curSize = None self._beforeLimit = None self._afterLimit = None self._buttonIsDown = 0 self._majorSize = 100 self._minorSize = 100 # Check keywords and initialise options. self.initialiseoptions(PanedWidget) def insert(self, name, before = 0, **kw): # Set defaults. self._size[name] = 0 self._relsize[name] = None self._min[name] = 0 self._relmin[name] = None self._max[name] = 100000 self._relmax[name] = None # Parse <kw> for options. self._parseOptions(name, kw) insertPos = self._nameToIndex(before) atEnd = (insertPos == len(self._item)) # Add the frame. self._item[insertPos:insertPos] = [name] self._frame[name] = self.createcomponent(name, (), 'Frame', Tkinter.Frame, (self.interior(),)) # Add separator, if necessary. if len(self._item) > 1: self._addSeparator() else: self._separator.append(None) self._button.append(None) # Add the new frame and adjust the PanedWidget if atEnd: size = self._size[name] if size > 0 or self._relsize[name] is not None: if self['orient'] == 'vertical': self._frame[name].place(x=0, relwidth=1, height=size, y=self._totalSize) else: self._frame[name].place(y=0, relheight=1, width=size, x=self._totalSize) else: if self['orient'] == 'vertical': self._frame[name].place(x=0, relwidth=1, y=self._totalSize) else: self._frame[name].place(y=0, relheight=1, x=self._totalSize) else: self._updateSizes() self._totalSize = self._totalSize + self._size[name] return self._frame[name] def add(self, name, **kw): return apply(self.insert, (name, len(self._item)), kw) def remove(self, name): removePos = self._nameToIndex(name) name = self._item[removePos] self.destroycomponent(name) del self._item[removePos] del self._frame[name] del self._size[name] del self._min[name] del self._max[name] del self._relsize[name] del self._relmin[name] del self._relmax[name] last = len(self._item) del self._separator[last] del self._button[last] self.destroycomponent(self._sepName(last)) self.destroycomponent(self._buttonName(last)) self._plotHandles() def _nameToIndex(self, name): try: pos = self._item.index(name) except: sys.exc_traceback = None # Clean up object references pos = name return pos def _parseOptions(self, name, args): # Parse <args> for options. for arg, value in args.items(): if type(value) == types.FloatType: relvalue = value value = self._absSize(relvalue) else: relvalue = None if arg == 'size': self._size[name], self._relsize[name] = value, relvalue elif arg == 'min': self._min[name], self._relmin[name] = value, relvalue elif arg == 'max': self._max[name], self._relmax[name] = value, relvalue else: raise ValueError, 'keyword must be "size", "min", or "max"' def _absSize(self, relvalue): return int(round(relvalue * self._majorSize)) def _sepName(self, n): return 'separator-%d' % n def _buttonName(self, n): return 'handle-%d' % n def _addSeparator(self): n = len(self._item) - 1 downFunc = lambda event, s = self, num=n: s._btnDown(event, num) upFunc = lambda event, s = self, num=n: s._btnUp(event, num) moveFunc = lambda event, s = self, num=n: s._btnMove(event, num) # Create the line dividing the panes. sep = self.createcomponent(self._sepName(n), (), 'Separator', Tkinter.Frame, (self.interior(),), borderwidth = 1, relief = self['separatorrelief']) self._separator.append(sep) sep.bind('<ButtonPress-1>', downFunc) sep.bind('<Any-ButtonRelease-1>', upFunc) sep.bind('<B1-Motion>', moveFunc) if self['orient'] == 'vertical': cursor = 'sb_v_double_arrow' sep.configure(height = 2, width = 10000, cursor = cursor) else: cursor = 'sb_h_double_arrow' sep.configure(width = 2, height = 10000, cursor = cursor) self._totalSize = self._totalSize + 2 # Create the handle on the dividing line. handle = self.createcomponent(self._buttonName(n), (), 'Handle', Tkinter.Frame, (self.interior(),), relief = 'raised', borderwidth = 1, width = 8, height = 8, cursor = cursor, ) self._button.append(handle) handle.bind('<ButtonPress-1>', downFunc) handle.bind('<Any-ButtonRelease-1>', upFunc) handle.bind('<B1-Motion>', moveFunc) self._plotHandles() for i in range(1, len(self._item)): self._separator[i].tkraise() for i in range(1, len(self._item)): self._button[i].tkraise() def _btnUp(self, event, item): self._buttonIsDown = 0 self._updateSizes() try: self._button[item].configure(relief='raised') except: sys.exc_traceback = None # Clean up object references def _btnDown(self, event, item): self._button[item].configure(relief='sunken') self._getMotionLimit(item) self._buttonIsDown = 1 self._movePending = 0 def _handleConfigure(self, event = None): self._getNaturalSizes() if self._totalSize == 0: return iterRange = list(self._item) iterRange.reverse() if self._majorSize > self._totalSize: n = self._majorSize - self._totalSize self._iterate(iterRange, self._grow, n) elif self._majorSize < self._totalSize: n = self._totalSize - self._majorSize self._iterate(iterRange, self._shrink, n) self._plotHandles() self._updateSizes() def _getNaturalSizes(self): # must call this in order to get correct reqwidth, reqheight self.update_idletasks() self._totalSize = 0 if self['orient'] == 'vertical': self._majorSize = self.winfo_height() self._minorSize = self.winfo_width() majorspec = Tkinter.Frame.winfo_reqheight else: self._majorSize = self.winfo_width() self._minorSize = self.winfo_height() majorspec = Tkinter.Frame.winfo_reqwidth if self._majorSize < 0: self._majorSize = 0 if self._minorSize < 0: self._minorSize = 0 for name in self._item: # adjust the absolute sizes first... if self._relsize[name] is None: #special case if self._size[name] == 0: self._size[name] = apply(majorspec, (self._frame[name],)) self._setrel(name) else: self._size[name] = self._absSize(self._relsize[name]) if self._relmin[name] is not None: self._min[name] = self._absSize(self._relmin[name]) if self._relmax[name] is not None: self._max[name] = self._absSize(self._relmax[name]) # now adjust sizes if self._size[name] < self._min[name]: self._size[name] = self._min[name] self._setrel(name) if self._size[name] > self._max[name]: self._size[name] = self._max[name] self._setrel(name) self._totalSize = self._totalSize + self._size[name] # adjust for separators self._totalSize = self._totalSize + len(self._item) * 2 - 2 def _setrel(self, name): if self._relsize[name] is not None: if self._majorSize != 0: self._relsize[name] = round(self._size[name]) / self._majorSize def _iterate(self, names, proc, n): for i in names: n = apply(proc, (i, n)) if n == 0: break def _grow(self, name, n): canGrow = self._max[name] - self._size[name] if canGrow > n: self._size[name] = self._size[name] + n self._setrel(name) return 0 elif canGrow > 0: self._size[name] = self._max[name] self._setrel(name) n = n - canGrow return n def _shrink(self, name, n): canShrink = self._size[name] - self._min[name] if canShrink > n: self._size[name] = self._size[name] - n self._setrel(name) return 0 elif canShrink > 0: self._size[name] = self._min[name] self._setrel(name) n = n - canShrink return n def _updateSizes(self): totalSize = 0 for name in self._item: size = self._size[name] if self['orient'] == 'vertical': self._frame[name].place(x = 0, relwidth = 1, y = totalSize, height = size) else: self._frame[name].place(y = 0, relheight = 1, x = totalSize, width = size) totalSize = totalSize + size + 2 # Invoke the callback command cmd = self['command'] if callable(cmd): cmd(map(lambda x, s = self: s._size[x], self._item)) def _plotHandles(self): if len(self._item) == 0: return if self['orient'] == 'vertical': btnp = self._minorSize - 13 else: h = self._minorSize if h > 18: btnp = 9 else: btnp = h - 9 firstPane = self._item[0] totalSize = self._size[firstPane] first = 1 last = len(self._item) - 1 # loop from first to last, inclusive for i in range(1, last + 1): handlepos = totalSize - 3 prevSize = self._size[self._item[i - 1]] nextSize = self._size[self._item[i]] offset1 = 0 if i == first: if prevSize < 4: offset1 = 4 - prevSize else: if prevSize < 8: offset1 = (8 - prevSize) / 2 offset2 = 0 if i == last: if nextSize < 4: offset2 = nextSize - 4 else: if nextSize < 8: offset2 = (nextSize - 8) / 2 handlepos = handlepos + offset1 if self['orient'] == 'vertical': height = 8 - offset1 + offset2 if height > 1: self._button[i].configure(height = height) self._button[i].place(x = btnp, y = handlepos) else: self._button[i].place_forget() self._separator[i].place(x = 0, y = totalSize, relwidth = 1) else: width = 8 - offset1 + offset2 if width > 1: self._button[i].configure(width = width) self._button[i].place(y = btnp, x = handlepos) else: self._button[i].place_forget() self._separator[i].place(y = 0, x = totalSize, relheight = 1) totalSize = totalSize + nextSize + 2 def pane(self, name): return self._frame[self._item[self._nameToIndex(name)]] # Return the name of all panes def panes(self): return list(self._item) def configurepane(self, name, **kw): name = self._item[self._nameToIndex(name)] self._parseOptions(name, kw) self._handleConfigure() def _getMotionLimit(self, item): curBefore = item * 2 - 2 minBefore, maxBefore = curBefore, curBefore for name in self._item[:item]: curBefore = curBefore + self._size[name] minBefore = minBefore + self._min[name] maxBefore = maxBefore + self._max[name] curAfter = (len(self._item) - item) * 2 minAfter, maxAfter = curAfter, curAfter for name in self._item[item:]: curAfter = curAfter + self._size[name] minAfter = minAfter + self._min[name] maxAfter = maxAfter + self._max[name] beforeToGo = min(curBefore - minBefore, maxAfter - curAfter) afterToGo = min(curAfter - minAfter, maxBefore - curBefore) self._beforeLimit = curBefore - beforeToGo self._afterLimit = curBefore + afterToGo self._curSize = curBefore self._plotHandles() # Compress the motion so that update is quick even on slow machines # # theRootp = root position (either rootx or rooty) def _btnMove(self, event, item): self._rootp = event if self._movePending == 0: self._timerId = self.after_idle( lambda s = self, i = item: s._btnMoveCompressed(i)) self._movePending = 1 def destroy(self): if self._timerId is not None: self.after_cancel(self._timerId) self._timerId = None Pmw.MegaWidget.destroy(self) def _btnMoveCompressed(self, item): if not self._buttonIsDown: return if self['orient'] == 'vertical': p = self._rootp.y_root - self.winfo_rooty() else: p = self._rootp.x_root - self.winfo_rootx() if p == self._curSize: self._movePending = 0 return if p < self._beforeLimit: p = self._beforeLimit if p >= self._afterLimit: p = self._afterLimit self._calculateChange(item, p) self.update_idletasks() self._movePending = 0 # Calculate the change in response to mouse motions def _calculateChange(self, item, p): if p < self._curSize: self._moveBefore(item, p) elif p > self._curSize: self._moveAfter(item, p) self._plotHandles() def _moveBefore(self, item, p): n = self._curSize - p # Shrink the frames before iterRange = list(self._item[:item]) iterRange.reverse() self._iterate(iterRange, self._shrink, n) # Adjust the frames after iterRange = self._item[item:] self._iterate(iterRange, self._grow, n) self._curSize = p def _moveAfter(self, item, p): n = p - self._curSize # Shrink the frames after iterRange = self._item[item:] self._iterate(iterRange, self._shrink, n) # Adjust the frames before iterRange = list(self._item[:item]) iterRange.reverse() self._iterate(iterRange, self._grow, n) self._curSize = p