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