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