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