1import string 2import types 3import Tkinter 4import Pmw 5 6class NoteBook(Pmw.MegaArchetype): 7 8 def __init__(self, parent = None, **kw): 9 10 # Define the megawidget options. 11 INITOPT = Pmw.INITOPT 12 optiondefs = ( 13 ('hull_highlightthickness', 0, None), 14 ('hull_borderwidth', 0, None), 15 ('arrownavigation', 1, INITOPT), 16 ('borderwidth', 2, INITOPT), 17 ('createcommand', None, None), 18 ('lowercommand', None, None), 19 ('pagemargin', 4, INITOPT), 20 ('raisecommand', None, None), 21 ('tabpos', 'n', INITOPT), 22 ) 23 self.defineoptions(kw, optiondefs, dynamicGroups = ('Page', 'Tab')) 24 25 # Initialise the base class (after defining the options). 26 Pmw.MegaArchetype.__init__(self, parent, Tkinter.Canvas) 27 28 self.bind('<Map>', self._handleMap) 29 self.bind('<Configure>', self._handleConfigure) 30 31 tabpos = self['tabpos'] 32 if tabpos is not None and tabpos != 'n': 33 raise ValueError, \ 34 'bad tabpos option %s: should be n or None' % repr(tabpos) 35 self._withTabs = (tabpos is not None) 36 self._pageMargin = self['pagemargin'] 37 self._borderWidth = self['borderwidth'] 38 39 # Use a dictionary as a set of bits indicating what needs to 40 # be redisplayed the next time _layout() is called. If 41 # dictionary contains 'topPage' key, the value is the new top 42 # page to be displayed. None indicates that all pages have 43 # been deleted and that _layout() should draw a border under where 44 # the tabs should be. 45 self._pending = {} 46 self._pending['size'] = 1 47 self._pending['borderColor'] = 1 48 self._pending['topPage'] = None 49 if self._withTabs: 50 self._pending['tabs'] = 1 51 52 self._canvasSize = None # This gets set by <Configure> events 53 54 # Set initial height of space for tabs 55 if self._withTabs: 56 self.tabBottom = 35 57 else: 58 self.tabBottom = 0 59 60 self._lightBorderColor, self._darkBorderColor = \ 61 Pmw.Color.bordercolors(self, self['hull_background']) 62 63 self._pageNames = [] # List of page names 64 65 # Map from page name to page info. Each item is itself a 66 # dictionary containing the following items: 67 # page the Tkinter.Frame widget for the page 68 # created set to true the first time the page is raised 69 # tabbutton the Tkinter.Button widget for the button (if any) 70 # tabreqwidth requested width of the tab 71 # tabreqheight requested height of the tab 72 # tabitems the canvas items for the button: the button 73 # window item, the lightshadow and the darkshadow 74 # left the left and right canvas coordinates of the tab 75 # right 76 self._pageAttrs = {} 77 78 # Name of page currently on top (actually displayed, using 79 # create_window, not pending). Ignored if current top page 80 # has been deleted or new top page is pending. None indicates 81 # no pages in notebook. 82 self._topPageName = None 83 84 # Canvas items used: 85 # Per tab: 86 # top and left shadow 87 # right shadow 88 # button 89 # Per notebook: 90 # page 91 # top page 92 # left shadow 93 # bottom and right shadow 94 # top (one or two items) 95 96 # Canvas tags used: 97 # lighttag - top and left shadows of tabs and page 98 # darktag - bottom and right shadows of tabs and page 99 # (if no tabs then these are reversed) 100 # (used to color the borders by recolorborders) 101 102 # Create page border shadows. 103 if self._withTabs: 104 self._pageLeftBorder = self.create_polygon(0, 0, 0, 0, 0, 0, 105 fill = self._lightBorderColor, tags = 'lighttag') 106 self._pageBottomRightBorder = self.create_polygon(0, 0, 0, 0, 0, 0, 107 fill = self._darkBorderColor, tags = 'darktag') 108 self._pageTop1Border = self.create_polygon(0, 0, 0, 0, 0, 0, 109 fill = self._darkBorderColor, tags = 'lighttag') 110 self._pageTop2Border = self.create_polygon(0, 0, 0, 0, 0, 0, 111 fill = self._darkBorderColor, tags = 'lighttag') 112 else: 113 self._pageLeftBorder = self.create_polygon(0, 0, 0, 0, 0, 0, 114 fill = self._darkBorderColor, tags = 'darktag') 115 self._pageBottomRightBorder = self.create_polygon(0, 0, 0, 0, 0, 0, 116 fill = self._lightBorderColor, tags = 'lighttag') 117 self._pageTopBorder = self.create_polygon(0, 0, 0, 0, 0, 0, 118 fill = self._darkBorderColor, tags = 'darktag') 119 120 # Check keywords and initialise options. 121 self.initialiseoptions() 122 123 def insert(self, pageName, before = 0, **kw): 124 if self._pageAttrs.has_key(pageName): 125 msg = 'Page "%s" already exists.' % pageName 126 raise ValueError, msg 127 128 # Do this early to catch bad <before> spec before creating any items. 129 beforeIndex = self.index(before, 1) 130 131 pageOptions = {} 132 if self._withTabs: 133 # Default tab button options. 134 tabOptions = { 135 'text' : pageName, 136 'borderwidth' : 0, 137 } 138 139 # Divide the keyword options into the 'page_' and 'tab_' options. 140 for key in kw.keys(): 141 if key[:5] == 'page_': 142 pageOptions[key[5:]] = kw[key] 143 del kw[key] 144 elif self._withTabs and key[:4] == 'tab_': 145 tabOptions[key[4:]] = kw[key] 146 del kw[key] 147 else: 148 raise KeyError, 'Unknown option "' + key + '"' 149 150 # Create the frame to contain the page. 151 page = apply(self.createcomponent, (pageName, 152 (), 'Page', 153 Tkinter.Frame, self._hull), pageOptions) 154 155 attributes = {} 156 attributes['page'] = page 157 attributes['created'] = 0 158 159 if self._withTabs: 160 # Create the button for the tab. 161 def raiseThisPage(self = self, pageName = pageName): 162 self.selectpage(pageName) 163 tabOptions['command'] = raiseThisPage 164 tab = apply(self.createcomponent, (pageName + '-tab', 165 (), 'Tab', 166 Tkinter.Button, self._hull), tabOptions) 167 168 if self['arrownavigation']: 169 # Allow the use of the arrow keys for Tab navigation: 170 def next(event, self = self, pageName = pageName): 171 self.nextpage(pageName) 172 def prev(event, self = self, pageName = pageName): 173 self.previouspage(pageName) 174 tab.bind('<Left>', prev) 175 tab.bind('<Right>', next) 176 177 attributes['tabbutton'] = tab 178 attributes['tabreqwidth'] = tab.winfo_reqwidth() 179 attributes['tabreqheight'] = tab.winfo_reqheight() 180 181 # Create the canvas item to manage the tab's button and the items 182 # for the tab's shadow. 183 windowitem = self.create_window(0, 0, window = tab, anchor = 'nw') 184 lightshadow = self.create_polygon(0, 0, 0, 0, 0, 0, 185 tags = 'lighttag', fill = self._lightBorderColor) 186 darkshadow = self.create_polygon(0, 0, 0, 0, 0, 0, 187 tags = 'darktag', fill = self._darkBorderColor) 188 attributes['tabitems'] = (windowitem, lightshadow, darkshadow) 189 self._pending['tabs'] = 1 190 191 self._pageAttrs[pageName] = attributes 192 self._pageNames.insert(beforeIndex, pageName) 193 194 # If this is the first page added, make it the new top page 195 # and call the create and raise callbacks. 196 if self.getcurselection() is None: 197 self._pending['topPage'] = pageName 198 self._raiseNewTop(pageName) 199 200 self._layout() 201 return page 202 203 def add(self, pageName, **kw): 204 return apply(self.insert, (pageName, len(self._pageNames)), kw) 205 206 def delete(self, *pageNames): 207 newTopPage = 0 208 for page in pageNames: 209 pageIndex = self.index(page) 210 pageName = self._pageNames[pageIndex] 211 pageInfo = self._pageAttrs[pageName] 212 213 if self.getcurselection() == pageName: 214 if len(self._pageNames) == 1: 215 newTopPage = 0 216 self._pending['topPage'] = None 217 elif pageIndex == len(self._pageNames) - 1: 218 newTopPage = 1 219 self._pending['topPage'] = self._pageNames[pageIndex - 1] 220 else: 221 newTopPage = 1 222 self._pending['topPage'] = self._pageNames[pageIndex + 1] 223 224 if self._topPageName == pageName: 225 self._hull.delete(self._topPageItem) 226 self._topPageName = None 227 228 if self._withTabs: 229 self.destroycomponent(pageName + '-tab') 230 apply(self._hull.delete, pageInfo['tabitems']) 231 self.destroycomponent(pageName) 232 del self._pageAttrs[pageName] 233 del self._pageNames[pageIndex] 234 235 # If the old top page was deleted and there are still pages 236 # left in the notebook, call the create and raise callbacks. 237 if newTopPage: 238 pageName = self._pending['topPage'] 239 self._raiseNewTop(pageName) 240 241 if self._withTabs: 242 self._pending['tabs'] = 1 243 self._layout() 244 245 def page(self, pageIndex): 246 pageName = self._pageNames[self.index(pageIndex)] 247 return self._pageAttrs[pageName]['page'] 248 249 def pagenames(self): 250 return list(self._pageNames) 251 252 def getcurselection(self): 253 if self._pending.has_key('topPage'): 254 return self._pending['topPage'] 255 else: 256 return self._topPageName 257 258 def tab(self, pageIndex): 259 if self._withTabs: 260 pageName = self._pageNames[self.index(pageIndex)] 261 return self._pageAttrs[pageName]['tabbutton'] 262 else: 263 return None 264 265 def index(self, index, forInsert = 0): 266 listLength = len(self._pageNames) 267 if type(index) == types.IntType: 268 if forInsert and index <= listLength: 269 return index 270 elif not forInsert and index < listLength: 271 return index 272 else: 273 raise ValueError, 'index "%s" is out of range' % index 274 elif index is Pmw.END: 275 if forInsert: 276 return listLength 277 elif listLength > 0: 278 return listLength - 1 279 else: 280 raise ValueError, 'NoteBook has no pages' 281 elif index is Pmw.SELECT: 282 if listLength == 0: 283 raise ValueError, 'NoteBook has no pages' 284 return self._pageNames.index(self.getcurselection()) 285 else: 286 if index in self._pageNames: 287 return self._pageNames.index(index) 288 validValues = 'a name, a number, Pmw.END or Pmw.SELECT' 289 raise ValueError, \ 290 'bad index "%s": must be %s' % (index, validValues) 291 292 def selectpage(self, page): 293 pageName = self._pageNames[self.index(page)] 294 oldTopPage = self.getcurselection() 295 if pageName != oldTopPage: 296 self._pending['topPage'] = pageName 297 if oldTopPage == self._topPageName: 298 self._hull.delete(self._topPageItem) 299 cmd = self['lowercommand'] 300 if cmd is not None: 301 cmd(oldTopPage) 302 self._raiseNewTop(pageName) 303 304 self._layout() 305 306 # Set focus to the tab of new top page: 307 if self._withTabs and self['arrownavigation']: 308 self._pageAttrs[pageName]['tabbutton'].focus_set() 309 310 def previouspage(self, pageIndex = None): 311 if pageIndex is None: 312 curpage = self.index(Pmw.SELECT) 313 else: 314 curpage = self.index(pageIndex) 315 if curpage > 0: 316 self.selectpage(curpage - 1) 317 318 def nextpage(self, pageIndex = None): 319 if pageIndex is None: 320 curpage = self.index(Pmw.SELECT) 321 else: 322 curpage = self.index(pageIndex) 323 if curpage < len(self._pageNames) - 1: 324 self.selectpage(curpage + 1) 325 326 def setnaturalsize(self, pageNames = None): 327 self.update_idletasks() 328 maxPageWidth = 1 329 maxPageHeight = 1 330 if pageNames is None: 331 pageNames = self.pagenames() 332 for pageName in pageNames: 333 pageInfo = self._pageAttrs[pageName] 334 page = pageInfo['page'] 335 w = page.winfo_reqwidth() 336 h = page.winfo_reqheight() 337 if maxPageWidth < w: 338 maxPageWidth = w 339 if maxPageHeight < h: 340 maxPageHeight = h 341 pageBorder = self._borderWidth + self._pageMargin 342 width = maxPageWidth + pageBorder * 2 343 height = maxPageHeight + pageBorder * 2 344 345 if self._withTabs: 346 maxTabHeight = 0 347 for pageInfo in self._pageAttrs.values(): 348 if maxTabHeight < pageInfo['tabreqheight']: 349 maxTabHeight = pageInfo['tabreqheight'] 350 height = height + maxTabHeight + self._borderWidth * 1.5 351 352 # Note that, since the hull is a canvas, the width and height 353 # options specify the geometry *inside* the borderwidth and 354 # highlightthickness. 355 self.configure(hull_width = width, hull_height = height) 356 357 def recolorborders(self): 358 self._pending['borderColor'] = 1 359 self._layout() 360 361 def _handleMap(self, event): 362 self._layout() 363 364 def _handleConfigure(self, event): 365 self._canvasSize = (event.width, event.height) 366 self._pending['size'] = 1 367 self._layout() 368 369 def _raiseNewTop(self, pageName): 370 if not self._pageAttrs[pageName]['created']: 371 self._pageAttrs[pageName]['created'] = 1 372 cmd = self['createcommand'] 373 if cmd is not None: 374 cmd(pageName) 375 cmd = self['raisecommand'] 376 if cmd is not None: 377 cmd(pageName) 378 379 # This is the vertical layout of the notebook, from top (assuming 380 # tabpos is 'n'): 381 # hull highlightthickness (top) 382 # hull borderwidth (top) 383 # borderwidth (top border of tabs) 384 # borderwidth * 0.5 (space for bevel) 385 # tab button (maximum of requested height of all tab buttons) 386 # borderwidth (border between tabs and page) 387 # pagemargin (top) 388 # the page itself 389 # pagemargin (bottom) 390 # borderwidth (border below page) 391 # hull borderwidth (bottom) 392 # hull highlightthickness (bottom) 393 # 394 # canvasBorder is sum of top two elements. 395 # tabBottom is sum of top five elements. 396 # 397 # Horizontal layout (and also vertical layout when tabpos is None): 398 # hull highlightthickness 399 # hull borderwidth 400 # borderwidth 401 # pagemargin 402 # the page itself 403 # pagemargin 404 # borderwidth 405 # hull borderwidth 406 # hull highlightthickness 407 # 408 def _layout(self): 409 if not self.winfo_ismapped() or self._canvasSize is None: 410 # Don't layout if the window is not displayed, or we 411 # haven't yet received a <Configure> event. 412 return 413 414 hullWidth, hullHeight = self._canvasSize 415 borderWidth = self._borderWidth 416 canvasBorder = string.atoi(self._hull['borderwidth']) + \ 417 string.atoi(self._hull['highlightthickness']) 418 if not self._withTabs: 419 self.tabBottom = canvasBorder 420 oldTabBottom = self.tabBottom 421 422 if self._pending.has_key('borderColor'): 423 self._lightBorderColor, self._darkBorderColor = \ 424 Pmw.Color.bordercolors(self, self['hull_background']) 425 426 # Draw all the tabs. 427 if self._withTabs and (self._pending.has_key('tabs') or 428 self._pending.has_key('size')): 429 # Find total requested width and maximum requested height 430 # of tabs. 431 sumTabReqWidth = 0 432 maxTabHeight = 0 433 for pageInfo in self._pageAttrs.values(): 434 sumTabReqWidth = sumTabReqWidth + pageInfo['tabreqwidth'] 435 if maxTabHeight < pageInfo['tabreqheight']: 436 maxTabHeight = pageInfo['tabreqheight'] 437 if maxTabHeight != 0: 438 # Add the top tab border plus a bit for the angled corners 439 self.tabBottom = canvasBorder + maxTabHeight + borderWidth * 1.5 440 441 # Prepare for drawing the border around each tab button. 442 tabTop = canvasBorder 443 tabTop2 = tabTop + borderWidth 444 tabTop3 = tabTop + borderWidth * 1.5 445 tabBottom2 = self.tabBottom 446 tabBottom = self.tabBottom + borderWidth 447 448 numTabs = len(self._pageNames) 449 availableWidth = hullWidth - 2 * canvasBorder - \ 450 numTabs * 2 * borderWidth 451 x = canvasBorder 452 cumTabReqWidth = 0 453 cumTabWidth = 0 454 455 # Position all the tabs. 456 for pageName in self._pageNames: 457 pageInfo = self._pageAttrs[pageName] 458 (windowitem, lightshadow, darkshadow) = pageInfo['tabitems'] 459 if sumTabReqWidth <= availableWidth: 460 tabwidth = pageInfo['tabreqwidth'] 461 else: 462 # This ugly calculation ensures that, when the 463 # notebook is not wide enough for the requested 464 # widths of the tabs, the total width given to 465 # the tabs exactly equals the available width, 466 # without rounding errors. 467 cumTabReqWidth = cumTabReqWidth + pageInfo['tabreqwidth'] 468 tmp = (2*cumTabReqWidth*availableWidth + sumTabReqWidth) \ 469 / (2 * sumTabReqWidth) 470 tabwidth = tmp - cumTabWidth 471 cumTabWidth = tmp 472 473 # Position the tab's button canvas item. 474 self.coords(windowitem, x + borderWidth, tabTop3) 475 self.itemconfigure(windowitem, 476 width = tabwidth, height = maxTabHeight) 477 478 # Make a beautiful border around the tab. 479 left = x 480 left2 = left + borderWidth 481 left3 = left + borderWidth * 1.5 482 right = left + tabwidth + 2 * borderWidth 483 right2 = left + tabwidth + borderWidth 484 right3 = left + tabwidth + borderWidth * 0.5 485 486 self.coords(lightshadow, 487 left, tabBottom2, left, tabTop2, left2, tabTop, 488 right2, tabTop, right3, tabTop2, left3, tabTop2, 489 left2, tabTop3, left2, tabBottom, 490 ) 491 self.coords(darkshadow, 492 right2, tabTop, right, tabTop2, right, tabBottom2, 493 right2, tabBottom, right2, tabTop3, right3, tabTop2, 494 ) 495 pageInfo['left'] = left 496 pageInfo['right'] = right 497 498 x = x + tabwidth + 2 * borderWidth 499 500 # Redraw shadow under tabs so that it appears that tab for old 501 # top page is lowered and that tab for new top page is raised. 502 if self._withTabs and (self._pending.has_key('topPage') or 503 self._pending.has_key('tabs') or self._pending.has_key('size')): 504 505 if self.getcurselection() is None: 506 # No pages, so draw line across top of page area. 507 self.coords(self._pageTop1Border, 508 canvasBorder, self.tabBottom, 509 hullWidth - canvasBorder, self.tabBottom, 510 hullWidth - canvasBorder - borderWidth, 511 self.tabBottom + borderWidth, 512 borderWidth + canvasBorder, self.tabBottom + borderWidth, 513 ) 514 515 # Ignore second top border. 516 self.coords(self._pageTop2Border, 0, 0, 0, 0, 0, 0) 517 else: 518 # Draw two lines, one on each side of the tab for the 519 # top page, so that the tab appears to be raised. 520 pageInfo = self._pageAttrs[self.getcurselection()] 521 left = pageInfo['left'] 522 right = pageInfo['right'] 523 self.coords(self._pageTop1Border, 524 canvasBorder, self.tabBottom, 525 left, self.tabBottom, 526 left + borderWidth, self.tabBottom + borderWidth, 527 canvasBorder + borderWidth, self.tabBottom + borderWidth, 528 ) 529 530 self.coords(self._pageTop2Border, 531 right, self.tabBottom, 532 hullWidth - canvasBorder, self.tabBottom, 533 hullWidth - canvasBorder - borderWidth, 534 self.tabBottom + borderWidth, 535 right - borderWidth, self.tabBottom + borderWidth, 536 ) 537 538 # Prevent bottom of dark border of tabs appearing over 539 # page top border. 540 self.tag_raise(self._pageTop1Border) 541 self.tag_raise(self._pageTop2Border) 542 543 # Position the page border shadows. 544 if self._pending.has_key('size') or oldTabBottom != self.tabBottom: 545 546 self.coords(self._pageLeftBorder, 547 canvasBorder, self.tabBottom, 548 borderWidth + canvasBorder, 549 self.tabBottom + borderWidth, 550 borderWidth + canvasBorder, 551 hullHeight - canvasBorder - borderWidth, 552 canvasBorder, hullHeight - canvasBorder, 553 ) 554 555 self.coords(self._pageBottomRightBorder, 556 hullWidth - canvasBorder, self.tabBottom, 557 hullWidth - canvasBorder, hullHeight - canvasBorder, 558 canvasBorder, hullHeight - canvasBorder, 559 borderWidth + canvasBorder, 560 hullHeight - canvasBorder - borderWidth, 561 hullWidth - canvasBorder - borderWidth, 562 hullHeight - canvasBorder - borderWidth, 563 hullWidth - canvasBorder - borderWidth, 564 self.tabBottom + borderWidth, 565 ) 566 567 if not self._withTabs: 568 self.coords(self._pageTopBorder, 569 canvasBorder, self.tabBottom, 570 hullWidth - canvasBorder, self.tabBottom, 571 hullWidth - canvasBorder - borderWidth, 572 self.tabBottom + borderWidth, 573 borderWidth + canvasBorder, self.tabBottom + borderWidth, 574 ) 575 576 # Color borders. 577 if self._pending.has_key('borderColor'): 578 self.itemconfigure('lighttag', fill = self._lightBorderColor) 579 self.itemconfigure('darktag', fill = self._darkBorderColor) 580 581 newTopPage = self._pending.get('topPage') 582 pageBorder = borderWidth + self._pageMargin 583 584 # Raise new top page. 585 if newTopPage is not None: 586 self._topPageName = newTopPage 587 self._topPageItem = self.create_window( 588 pageBorder + canvasBorder, self.tabBottom + pageBorder, 589 window = self._pageAttrs[newTopPage]['page'], 590 anchor = 'nw', 591 ) 592 593 # Change position of top page if tab height has changed. 594 if self._topPageName is not None and oldTabBottom != self.tabBottom: 595 self.coords(self._topPageItem, 596 pageBorder + canvasBorder, self.tabBottom + pageBorder) 597 598 # Change size of top page if, 599 # 1) there is a new top page. 600 # 2) canvas size has changed, but not if there is no top 601 # page (eg: initially or when all pages deleted). 602 # 3) tab height has changed, due to difference in the height of a tab 603 if (newTopPage is not None or \ 604 self._pending.has_key('size') and self._topPageName is not None 605 or oldTabBottom != self.tabBottom): 606 self.itemconfigure(self._topPageItem, 607 width = hullWidth - 2 * canvasBorder - pageBorder * 2, 608 height = hullHeight - 2 * canvasBorder - pageBorder * 2 - 609 (self.tabBottom - canvasBorder), 610 ) 611 612 self._pending = {} 613 614# Need to do forwarding to get the pack, grid, etc methods. 615# Unfortunately this means that all the other canvas methods are also 616# forwarded. 617Pmw.forwardmethods(NoteBook, Tkinter.Canvas, '_hull') 618