File: PmwNoteBook.py

package info (click to toggle)
python-pmw 1.2-3
  • links: PTS
  • area: main
  • in suites: sarge
  • size: 2,024 kB
  • ctags: 3,802
  • sloc: python: 17,143; makefile: 41
file content (617 lines) | stat: -rw-r--r-- 24,817 bytes parent folder | download | duplicates (5)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
import string
import types
import Tkinter
import Pmw

class NoteBook(Pmw.MegaArchetype):

    def __init__(self, parent = None, **kw):

	# Define the megawidget options.
	INITOPT = Pmw.INITOPT
        optiondefs = (
	    ('hull_highlightthickness',  0,           None),
	    ('hull_borderwidth',         0,           None),
            ('arrownavigation',          1,           INITOPT),
            ('borderwidth',              2,           INITOPT),
            ('createcommand',            None,        None),
            ('lowercommand',             None,        None),
            ('pagemargin',               4,           INITOPT),
            ('raisecommand',             None,        None),
	    ('tabpos',                   'n',         INITOPT),
        )
	self.defineoptions(kw, optiondefs, dynamicGroups = ('Page', 'Tab'))

	# Initialise the base class (after defining the options).
	Pmw.MegaArchetype.__init__(self, parent, Tkinter.Canvas)

        self.bind('<Map>', self._handleMap)
        self.bind('<Configure>', self._handleConfigure)

        tabpos = self['tabpos']
	if tabpos is not None and tabpos != 'n':
            raise ValueError, \
                'bad tabpos option %s:  should be n or None' % repr(tabpos)
        self._withTabs = (tabpos is not None)
        self._pageMargin = self['pagemargin']
        self._borderWidth = self['borderwidth']

        # Use a dictionary as a set of bits indicating what needs to
        # be redisplayed the next time _layout() is called.  If
        # dictionary contains 'topPage' key, the value is the new top
        # page to be displayed.  None indicates that all pages have
        # been deleted and that _layout() should draw a border under where
        # the tabs should be.
        self._pending = {}
        self._pending['size'] = 1
        self._pending['borderColor'] = 1
        self._pending['topPage'] = None
        if self._withTabs:
            self._pending['tabs'] = 1

        self._canvasSize = None       # This gets set by <Configure> events

        # Set initial height of space for tabs
        if self._withTabs:
            self.tabBottom = 35
        else:
            self.tabBottom = 0

        self._lightBorderColor, self._darkBorderColor = \
                Pmw.Color.bordercolors(self, self['hull_background'])

	self._pageNames   = []        # List of page names

        # Map from page name to page info.  Each item is itself a
        # dictionary containing the following items:
        #   page           the Tkinter.Frame widget for the page
        #   created        set to true the first time the page is raised
        #   tabbutton      the Tkinter.Button widget for the button (if any)
        #   tabreqwidth    requested width of the tab
        #   tabreqheight   requested height of the tab
        #   tabitems       the canvas items for the button: the button
        #                  window item, the lightshadow and the darkshadow
        #   left           the left and right canvas coordinates of the tab
        #   right
	self._pageAttrs   = {}

        # Name of page currently on top (actually displayed, using
        # create_window, not pending).  Ignored if current top page
        # has been deleted or new top page is pending.  None indicates
        # no pages in notebook.
	self._topPageName = None

        # Canvas items used:
        #   Per tab: 
        #       top and left shadow
        #       right shadow
        #       button
        #   Per notebook: 
        #       page
        #       top page
        #       left shadow
        #       bottom and right shadow
        #       top (one or two items)

        # Canvas tags used:
        #   lighttag      - top and left shadows of tabs and page
        #   darktag       - bottom and right shadows of tabs and page
        #                   (if no tabs then these are reversed)
        #                   (used to color the borders by recolorborders)

        # Create page border shadows.
        if self._withTabs:
            self._pageLeftBorder = self.create_polygon(0, 0, 0, 0, 0, 0,
                fill = self._lightBorderColor, tags = 'lighttag')
            self._pageBottomRightBorder = self.create_polygon(0, 0, 0, 0, 0, 0,
                fill = self._darkBorderColor, tags = 'darktag')
            self._pageTop1Border = self.create_polygon(0, 0, 0, 0, 0, 0,
                fill = self._darkBorderColor, tags = 'lighttag')
            self._pageTop2Border = self.create_polygon(0, 0, 0, 0, 0, 0,
                fill = self._darkBorderColor, tags = 'lighttag')
        else:
            self._pageLeftBorder = self.create_polygon(0, 0, 0, 0, 0, 0,
                fill = self._darkBorderColor, tags = 'darktag')
            self._pageBottomRightBorder = self.create_polygon(0, 0, 0, 0, 0, 0,
                fill = self._lightBorderColor, tags = 'lighttag')
            self._pageTopBorder = self.create_polygon(0, 0, 0, 0, 0, 0,
                fill = self._darkBorderColor, tags = 'darktag')

	# Check keywords and initialise options.
	self.initialiseoptions()

    def insert(self, pageName, before = 0, **kw):
	if self._pageAttrs.has_key(pageName):
	    msg = 'Page "%s" already exists.' % pageName
	    raise ValueError, msg

        # Do this early to catch bad <before> spec before creating any items.
	beforeIndex = self.index(before, 1)

        pageOptions = {}
        if self._withTabs:
            # Default tab button options.
            tabOptions = {
                'text' : pageName,
                'borderwidth' : 0,
            }

        # Divide the keyword options into the 'page_' and 'tab_' options.
        for key in kw.keys():
            if key[:5] == 'page_':
                pageOptions[key[5:]] = kw[key]
                del kw[key]
            elif self._withTabs and key[:4] == 'tab_':
                tabOptions[key[4:]] = kw[key]
                del kw[key]
            else:
		raise KeyError, 'Unknown option "' + key + '"'

        # Create the frame to contain the page.
	page = apply(self.createcomponent, (pageName,
		(), 'Page',
		Tkinter.Frame, self._hull), pageOptions)

        attributes = {}
        attributes['page'] = page
        attributes['created'] = 0

        if self._withTabs:
            # Create the button for the tab.
            def raiseThisPage(self = self, pageName = pageName):
                self.selectpage(pageName)
            tabOptions['command'] = raiseThisPage
            tab = apply(self.createcomponent, (pageName + '-tab',
                    (), 'Tab',
                    Tkinter.Button, self._hull), tabOptions)

            if self['arrownavigation']:
                # Allow the use of the arrow keys for Tab navigation:
                def next(event, self = self, pageName = pageName):
                    self.nextpage(pageName)
                def prev(event, self = self, pageName = pageName):
                    self.previouspage(pageName)
                tab.bind('<Left>', prev)
                tab.bind('<Right>', next)

            attributes['tabbutton'] = tab
            attributes['tabreqwidth'] = tab.winfo_reqwidth()
            attributes['tabreqheight'] = tab.winfo_reqheight()

            # Create the canvas item to manage the tab's button and the items
            # for the tab's shadow.
            windowitem = self.create_window(0, 0, window = tab, anchor = 'nw')
            lightshadow = self.create_polygon(0, 0, 0, 0, 0, 0,
                tags = 'lighttag', fill = self._lightBorderColor)
            darkshadow = self.create_polygon(0, 0, 0, 0, 0, 0,
                tags = 'darktag', fill = self._darkBorderColor)
            attributes['tabitems'] = (windowitem, lightshadow, darkshadow)
            self._pending['tabs'] = 1

        self._pageAttrs[pageName] = attributes
	self._pageNames.insert(beforeIndex, pageName)

        # If this is the first page added, make it the new top page
        # and call the create and raise callbacks.
        if self.getcurselection() is None:
            self._pending['topPage'] = pageName
            self._raiseNewTop(pageName)

        self._layout()
        return page
  		
    def add(self, pageName, **kw):
        return apply(self.insert, (pageName, len(self._pageNames)), kw)

    def delete(self, *pageNames):
        newTopPage = 0
        for page in pageNames:
            pageIndex = self.index(page)
            pageName = self._pageNames[pageIndex]
            pageInfo = self._pageAttrs[pageName]

            if self.getcurselection() == pageName:
                if len(self._pageNames) == 1:
                    newTopPage = 0
                    self._pending['topPage'] = None
                elif pageIndex == len(self._pageNames) - 1:
                    newTopPage = 1
                    self._pending['topPage'] = self._pageNames[pageIndex - 1]
                else:
                    newTopPage = 1
                    self._pending['topPage'] = self._pageNames[pageIndex + 1]

            if self._topPageName == pageName:
                self._hull.delete(self._topPageItem)
                self._topPageName = None
                                
            if self._withTabs:
                self.destroycomponent(pageName + '-tab')
                apply(self._hull.delete, pageInfo['tabitems'])
            self.destroycomponent(pageName)
            del self._pageAttrs[pageName]
            del self._pageNames[pageIndex]

        # If the old top page was deleted and there are still pages
        # left in the notebook, call the create and raise callbacks.
        if newTopPage:
            pageName = self._pending['topPage']
            self._raiseNewTop(pageName)

        if self._withTabs:
            self._pending['tabs'] = 1
        self._layout()

    def page(self, pageIndex):
        pageName = self._pageNames[self.index(pageIndex)]
	return self._pageAttrs[pageName]['page']

    def pagenames(self):
	return list(self._pageNames)

    def getcurselection(self):
        if self._pending.has_key('topPage'):
            return self._pending['topPage']
        else:
            return self._topPageName

    def tab(self, pageIndex):
        if self._withTabs:
            pageName = self._pageNames[self.index(pageIndex)]
            return self._pageAttrs[pageName]['tabbutton']
        else:
            return None

    def index(self, index, forInsert = 0):
	listLength = len(self._pageNames)
	if type(index) == types.IntType:
	    if forInsert and index <= listLength:
		return index
	    elif not forInsert and index < listLength:
		return index
	    else:
		raise ValueError, 'index "%s" is out of range' % index
	elif index is Pmw.END:
	    if forInsert:
		return listLength
	    elif listLength > 0:
		return listLength - 1
	    else:
		raise ValueError, 'NoteBook has no pages'
	elif index is Pmw.SELECT:
	    if listLength == 0:
		raise ValueError, 'NoteBook has no pages'
            return self._pageNames.index(self.getcurselection())
	else:
            if index in self._pageNames:
                return self._pageNames.index(index)
	    validValues = 'a name, a number, Pmw.END or Pmw.SELECT'
	    raise ValueError, \
                'bad index "%s": must be %s' % (index, validValues)

    def selectpage(self, page):
        pageName = self._pageNames[self.index(page)]
        oldTopPage = self.getcurselection()
        if pageName != oldTopPage:
            self._pending['topPage'] = pageName
            if oldTopPage == self._topPageName:
                self._hull.delete(self._topPageItem)
            cmd = self['lowercommand']
            if cmd is not None:
                cmd(oldTopPage)
            self._raiseNewTop(pageName)

            self._layout()

        # Set focus to the tab of new top page:
        if self._withTabs and self['arrownavigation']:
            self._pageAttrs[pageName]['tabbutton'].focus_set()

    def previouspage(self, pageIndex = None):
        if pageIndex is None:
            curpage = self.index(Pmw.SELECT)
        else:
            curpage = self.index(pageIndex)
	if curpage > 0:
	    self.selectpage(curpage - 1)

    def nextpage(self, pageIndex = None):
        if pageIndex is None:
            curpage = self.index(Pmw.SELECT)
        else:
            curpage = self.index(pageIndex)
	if curpage < len(self._pageNames) - 1:
	    self.selectpage(curpage + 1)

    def setnaturalsize(self, pageNames = None):
        self.update_idletasks()
        maxPageWidth = 1
        maxPageHeight = 1
        if pageNames is None:
            pageNames = self.pagenames()
        for pageName in pageNames:
            pageInfo = self._pageAttrs[pageName]
            page = pageInfo['page']
            w = page.winfo_reqwidth()
            h = page.winfo_reqheight()
            if maxPageWidth < w:
                maxPageWidth = w
            if maxPageHeight < h:
                maxPageHeight = h
        pageBorder = self._borderWidth + self._pageMargin
        width = maxPageWidth + pageBorder * 2
        height = maxPageHeight + pageBorder * 2

        if self._withTabs:
            maxTabHeight = 0
            for pageInfo in self._pageAttrs.values():
                if maxTabHeight < pageInfo['tabreqheight']:
                    maxTabHeight = pageInfo['tabreqheight']
            height = height + maxTabHeight + self._borderWidth * 1.5

        # Note that, since the hull is a canvas, the width and height
        # options specify the geometry *inside* the borderwidth and
        # highlightthickness.
        self.configure(hull_width = width, hull_height = height)

    def recolorborders(self):
        self._pending['borderColor'] = 1
        self._layout()

    def _handleMap(self, event):
        self._layout()

    def _handleConfigure(self, event):
        self._canvasSize = (event.width, event.height)
        self._pending['size'] = 1
        self._layout()

    def _raiseNewTop(self, pageName):
        if not self._pageAttrs[pageName]['created']:
            self._pageAttrs[pageName]['created'] = 1
            cmd = self['createcommand']
            if cmd is not None:
                cmd(pageName)
        cmd = self['raisecommand']
        if cmd is not None:
            cmd(pageName)

    # This is the vertical layout of the notebook, from top (assuming
    # tabpos is 'n'):
    #     hull highlightthickness (top)
    #     hull borderwidth (top)
    #     borderwidth (top border of tabs)
    #     borderwidth * 0.5 (space for bevel)
    #     tab button (maximum of requested height of all tab buttons)
    #     borderwidth (border between tabs and page)
    #     pagemargin (top)
    #     the page itself
    #     pagemargin (bottom)
    #     borderwidth (border below page)
    #     hull borderwidth (bottom)
    #     hull highlightthickness (bottom)
    #
    # canvasBorder is sum of top two elements.
    # tabBottom is sum of top five elements.
    #
    # Horizontal layout (and also vertical layout when tabpos is None):
    #     hull highlightthickness
    #     hull borderwidth
    #     borderwidth
    #     pagemargin
    #     the page itself
    #     pagemargin
    #     borderwidth
    #     hull borderwidth
    #     hull highlightthickness
    #
    def _layout(self):
        if not self.winfo_ismapped() or self._canvasSize is None:
            # Don't layout if the window is not displayed, or we
            # haven't yet received a <Configure> event.
            return

        hullWidth, hullHeight = self._canvasSize
        borderWidth = self._borderWidth
        canvasBorder = string.atoi(self._hull['borderwidth']) + \
            string.atoi(self._hull['highlightthickness'])
        if not self._withTabs:
            self.tabBottom = canvasBorder
        oldTabBottom = self.tabBottom

        if self._pending.has_key('borderColor'):
            self._lightBorderColor, self._darkBorderColor = \
                    Pmw.Color.bordercolors(self, self['hull_background'])

        # Draw all the tabs.
        if self._withTabs and (self._pending.has_key('tabs') or
                self._pending.has_key('size')):
            # Find total requested width and maximum requested height
            # of tabs.
            sumTabReqWidth = 0
            maxTabHeight = 0
            for pageInfo in self._pageAttrs.values():
                sumTabReqWidth = sumTabReqWidth + pageInfo['tabreqwidth']
                if maxTabHeight < pageInfo['tabreqheight']:
                    maxTabHeight = pageInfo['tabreqheight']
            if maxTabHeight != 0:
                # Add the top tab border plus a bit for the angled corners
                self.tabBottom = canvasBorder + maxTabHeight + borderWidth * 1.5

            # Prepare for drawing the border around each tab button.
            tabTop = canvasBorder
            tabTop2 = tabTop + borderWidth
            tabTop3 = tabTop + borderWidth * 1.5
            tabBottom2 = self.tabBottom
            tabBottom = self.tabBottom + borderWidth

            numTabs = len(self._pageNames)
            availableWidth = hullWidth - 2 * canvasBorder - \
                numTabs * 2 * borderWidth
            x = canvasBorder
            cumTabReqWidth = 0
            cumTabWidth = 0

            # Position all the tabs.
            for pageName in self._pageNames:
                pageInfo = self._pageAttrs[pageName]
                (windowitem, lightshadow, darkshadow) = pageInfo['tabitems']
                if sumTabReqWidth <= availableWidth:
                    tabwidth = pageInfo['tabreqwidth']
                else:
                    # This ugly calculation ensures that, when the
                    # notebook is not wide enough for the requested
                    # widths of the tabs, the total width given to
                    # the tabs exactly equals the available width,
                    # without rounding errors.
                    cumTabReqWidth = cumTabReqWidth + pageInfo['tabreqwidth']
                    tmp = (2*cumTabReqWidth*availableWidth + sumTabReqWidth) \
                            / (2 * sumTabReqWidth)
                    tabwidth = tmp - cumTabWidth
                    cumTabWidth = tmp

                # Position the tab's button canvas item.
                self.coords(windowitem, x + borderWidth, tabTop3)
                self.itemconfigure(windowitem,
                    width = tabwidth, height = maxTabHeight)

                # Make a beautiful border around the tab.
                left = x
                left2 = left + borderWidth
                left3 = left + borderWidth * 1.5
                right = left + tabwidth + 2 * borderWidth
                right2 = left + tabwidth + borderWidth
                right3 = left + tabwidth + borderWidth * 0.5

                self.coords(lightshadow, 
                    left, tabBottom2, left, tabTop2, left2, tabTop,
                    right2, tabTop, right3, tabTop2, left3, tabTop2,
                    left2, tabTop3, left2, tabBottom,
                    )
                self.coords(darkshadow, 
                    right2, tabTop, right, tabTop2, right, tabBottom2,
                    right2, tabBottom, right2, tabTop3, right3, tabTop2,
                    )
                pageInfo['left'] = left
                pageInfo['right'] = right

                x = x + tabwidth + 2 * borderWidth

        # Redraw shadow under tabs so that it appears that tab for old
        # top page is lowered and that tab for new top page is raised.
        if self._withTabs and (self._pending.has_key('topPage') or
                self._pending.has_key('tabs') or self._pending.has_key('size')):

            if self.getcurselection() is None:
                # No pages, so draw line across top of page area.
                self.coords(self._pageTop1Border,
                    canvasBorder, self.tabBottom,
                    hullWidth - canvasBorder, self.tabBottom,
                    hullWidth - canvasBorder - borderWidth,
                        self.tabBottom + borderWidth,
                    borderWidth + canvasBorder, self.tabBottom + borderWidth,
                    )

                # Ignore second top border.
                self.coords(self._pageTop2Border, 0, 0, 0, 0, 0, 0)
            else:
                # Draw two lines, one on each side of the tab for the
                # top page, so that the tab appears to be raised.
                pageInfo = self._pageAttrs[self.getcurselection()]
                left = pageInfo['left']
                right = pageInfo['right']
                self.coords(self._pageTop1Border,
                    canvasBorder, self.tabBottom,
                    left, self.tabBottom,
                    left + borderWidth, self.tabBottom + borderWidth,
                    canvasBorder + borderWidth, self.tabBottom + borderWidth,
                    )

                self.coords(self._pageTop2Border,
                    right, self.tabBottom,
                    hullWidth - canvasBorder, self.tabBottom,
                    hullWidth - canvasBorder - borderWidth,
                        self.tabBottom + borderWidth,
                    right - borderWidth, self.tabBottom + borderWidth,
                    )

            # Prevent bottom of dark border of tabs appearing over
            # page top border.
            self.tag_raise(self._pageTop1Border)
            self.tag_raise(self._pageTop2Border)

        # Position the page border shadows.
        if self._pending.has_key('size') or oldTabBottom != self.tabBottom:

            self.coords(self._pageLeftBorder,
                canvasBorder, self.tabBottom,
                borderWidth + canvasBorder,
                    self.tabBottom + borderWidth,
                borderWidth + canvasBorder,
                    hullHeight - canvasBorder - borderWidth,
                canvasBorder, hullHeight - canvasBorder,
                )

            self.coords(self._pageBottomRightBorder,
                hullWidth - canvasBorder, self.tabBottom,
                hullWidth - canvasBorder, hullHeight - canvasBorder,
                canvasBorder, hullHeight - canvasBorder,
                borderWidth + canvasBorder,
                    hullHeight - canvasBorder - borderWidth,
                hullWidth - canvasBorder - borderWidth,
                    hullHeight - canvasBorder - borderWidth,
                hullWidth - canvasBorder - borderWidth,
                    self.tabBottom + borderWidth,
                )

            if not self._withTabs:
                self.coords(self._pageTopBorder,
                    canvasBorder, self.tabBottom,
                    hullWidth - canvasBorder, self.tabBottom,
                    hullWidth - canvasBorder - borderWidth,
                        self.tabBottom + borderWidth,
                    borderWidth + canvasBorder, self.tabBottom + borderWidth,
                    )

        # Color borders.
        if self._pending.has_key('borderColor'):
            self.itemconfigure('lighttag', fill = self._lightBorderColor)
            self.itemconfigure('darktag', fill = self._darkBorderColor)

        newTopPage = self._pending.get('topPage')
        pageBorder = borderWidth + self._pageMargin

        # Raise new top page.
        if newTopPage is not None:
            self._topPageName = newTopPage
            self._topPageItem = self.create_window(
                pageBorder + canvasBorder, self.tabBottom + pageBorder,
                window = self._pageAttrs[newTopPage]['page'],
                anchor = 'nw',
                )

        # Change position of top page if tab height has changed.
        if self._topPageName is not None and oldTabBottom != self.tabBottom:
            self.coords(self._topPageItem,
                    pageBorder + canvasBorder, self.tabBottom + pageBorder)

        # Change size of top page if,
        #   1) there is a new top page.
        #   2) canvas size has changed, but not if there is no top
        #      page (eg:  initially or when all pages deleted).
        #   3) tab height has changed, due to difference in the height of a tab
        if (newTopPage is not None or \
                self._pending.has_key('size') and self._topPageName is not None
                or oldTabBottom != self.tabBottom):
            self.itemconfigure(self._topPageItem,
                width = hullWidth - 2 * canvasBorder - pageBorder * 2,
                height = hullHeight - 2 * canvasBorder - pageBorder * 2 -
                    (self.tabBottom - canvasBorder),
                )

        self._pending = {}

# Need to do forwarding to get the pack, grid, etc methods. 
# Unfortunately this means that all the other canvas methods are also
# forwarded.
Pmw.forwardmethods(NoteBook, Tkinter.Canvas, '_hull')