6 class NoteBook(Pmw.MegaArchetype):
8 def __init__(self, parent = None, **kw):
10 # Define the megawidget options.
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),
23 self.defineoptions(kw, optiondefs, dynamicGroups = ('Page', 'Tab'))
25 # Initialise the base class (after defining the options).
26 Pmw.MegaArchetype.__init__(self, parent, Tkinter.Canvas)
28 self.bind('<Map>', self._handleMap)
29 self.bind('<Configure>', self._handleConfigure)
31 tabpos = self['tabpos']
32 if tabpos is not None and tabpos != 'n':
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']
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
46 self._pending['size'] = 1
47 self._pending['borderColor'] = 1
48 self._pending['topPage'] = None
50 self._pending['tabs'] = 1
52 self._canvasSize = None # This gets set by <Configure> events
54 # Set initial height of space for tabs
60 self._lightBorderColor, self._darkBorderColor = \
61 Pmw.Color.bordercolors(self, self['hull_background'])
63 self._pageNames = [] # List of page names
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
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
93 # bottom and right shadow
94 # top (one or two items)
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)
102 # Create page border shadows.
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')
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')
120 # Check keywords and initialise options.
121 self.initialiseoptions()
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
128 # Do this early to catch bad <before> spec before creating any items.
129 beforeIndex = self.index(before, 1)
133 # Default tab button options.
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]
144 elif self._withTabs and key[:4] == 'tab_':
145 tabOptions[key[4:]] = kw[key]
148 raise KeyError, 'Unknown option "' + key + '"'
150 # Create the frame to contain the page.
151 page = apply(self.createcomponent, (pageName,
153 Tkinter.Frame, self._hull), pageOptions)
156 attributes['page'] = page
157 attributes['created'] = 0
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',
166 Tkinter.Button, self._hull), tabOptions)
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)
177 attributes['tabbutton'] = tab
178 attributes['tabreqwidth'] = tab.winfo_reqwidth()
179 attributes['tabreqheight'] = tab.winfo_reqheight()
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
191 self._pageAttrs[pageName] = attributes
192 self._pageNames.insert(beforeIndex, pageName)
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)
203 def add(self, pageName, **kw):
204 return apply(self.insert, (pageName, len(self._pageNames)), kw)
206 def delete(self, *pageNames):
208 for page in pageNames:
209 pageIndex = self.index(page)
210 pageName = self._pageNames[pageIndex]
211 pageInfo = self._pageAttrs[pageName]
213 if self.getcurselection() == pageName:
214 if len(self._pageNames) == 1:
216 self._pending['topPage'] = None
217 elif pageIndex == len(self._pageNames) - 1:
219 self._pending['topPage'] = self._pageNames[pageIndex - 1]
222 self._pending['topPage'] = self._pageNames[pageIndex + 1]
224 if self._topPageName == pageName:
225 self._hull.delete(self._topPageItem)
226 self._topPageName = None
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]
235 # If the old top page was deleted and there are still pages
236 # left in the notebook, call the create and raise callbacks.
238 pageName = self._pending['topPage']
239 self._raiseNewTop(pageName)
242 self._pending['tabs'] = 1
245 def page(self, pageIndex):
246 pageName = self._pageNames[self.index(pageIndex)]
247 return self._pageAttrs[pageName]['page']
250 return list(self._pageNames)
252 def getcurselection(self):
253 if self._pending.has_key('topPage'):
254 return self._pending['topPage']
256 return self._topPageName
258 def tab(self, pageIndex):
260 pageName = self._pageNames[self.index(pageIndex)]
261 return self._pageAttrs[pageName]['tabbutton']
265 def index(self, index, forInsert = 0):
266 listLength = len(self._pageNames)
267 if type(index) == types.IntType:
268 if forInsert and index <= listLength:
270 elif not forInsert and index < listLength:
273 raise ValueError, 'index "%s" is out of range' % index
274 elif index is Pmw.END:
278 return listLength - 1
280 raise ValueError, 'NoteBook has no pages'
281 elif index is Pmw.SELECT:
283 raise ValueError, 'NoteBook has no pages'
284 return self._pageNames.index(self.getcurselection())
286 if index in self._pageNames:
287 return self._pageNames.index(index)
288 validValues = 'a name, a number, Pmw.END or Pmw.SELECT'
290 'bad index "%s": must be %s' % (index, validValues)
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']
302 self._raiseNewTop(pageName)
306 # Set focus to the tab of new top page:
307 if self._withTabs and self['arrownavigation']:
308 self._pageAttrs[pageName]['tabbutton'].focus_set()
310 def previouspage(self, pageIndex = None):
311 if pageIndex is None:
312 curpage = self.index(Pmw.SELECT)
314 curpage = self.index(pageIndex)
316 self.selectpage(curpage - 1)
318 def nextpage(self, pageIndex = None):
319 if pageIndex is None:
320 curpage = self.index(Pmw.SELECT)
322 curpage = self.index(pageIndex)
323 if curpage < len(self._pageNames) - 1:
324 self.selectpage(curpage + 1)
326 def setnaturalsize(self, pageNames = None):
327 self.update_idletasks()
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()
339 if maxPageHeight < h:
341 pageBorder = self._borderWidth + self._pageMargin
342 width = maxPageWidth + pageBorder * 2
343 height = maxPageHeight + pageBorder * 2
347 for pageInfo in self._pageAttrs.values():
348 if maxTabHeight < pageInfo['tabreqheight']:
349 maxTabHeight = pageInfo['tabreqheight']
350 height = height + maxTabHeight + self._borderWidth * 1.5
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)
357 def recolorborders(self):
358 self._pending['borderColor'] = 1
361 def _handleMap(self, event):
364 def _handleConfigure(self, event):
365 self._canvasSize = (event.width, event.height)
366 self._pending['size'] = 1
369 def _raiseNewTop(self, pageName):
370 if not self._pageAttrs[pageName]['created']:
371 self._pageAttrs[pageName]['created'] = 1
372 cmd = self['createcommand']
375 cmd = self['raisecommand']
379 # This is the vertical layout of the notebook, from top (assuming
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)
389 # pagemargin (bottom)
390 # borderwidth (border below page)
391 # hull borderwidth (bottom)
392 # hull highlightthickness (bottom)
394 # canvasBorder is sum of top two elements.
395 # tabBottom is sum of top five elements.
397 # Horizontal layout (and also vertical layout when tabpos is None):
398 # hull highlightthickness
406 # hull highlightthickness
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.
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
422 if self._pending.has_key('borderColor'):
423 self._lightBorderColor, self._darkBorderColor = \
424 Pmw.Color.bordercolors(self, self['hull_background'])
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
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
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
448 numTabs = len(self._pageNames)
449 availableWidth = hullWidth - 2 * canvasBorder - \
450 numTabs * 2 * borderWidth
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']
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
473 # Position the tab's button canvas item.
474 self.coords(windowitem, x + borderWidth, tabTop3)
475 self.itemconfigure(windowitem,
476 width = tabwidth, height = maxTabHeight)
478 # Make a beautiful border around the tab.
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
486 self.coords(lightshadow,
487 left, tabBottom2, left, tabTop2, left2, tabTop,
488 right2, tabTop, right3, tabTop2, left3, tabTop2,
489 left2, tabTop3, left2, tabBottom,
491 self.coords(darkshadow,
492 right2, tabTop, right, tabTop2, right, tabBottom2,
493 right2, tabBottom, right2, tabTop3, right3, tabTop2,
495 pageInfo['left'] = left
496 pageInfo['right'] = right
498 x = x + tabwidth + 2 * borderWidth
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')):
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,
515 # Ignore second top border.
516 self.coords(self._pageTop2Border, 0, 0, 0, 0, 0, 0)
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,
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,
538 # Prevent bottom of dark border of tabs appearing over
540 self.tag_raise(self._pageTop1Border)
541 self.tag_raise(self._pageTop2Border)
543 # Position the page border shadows.
544 if self._pending.has_key('size') or oldTabBottom != self.tabBottom:
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,
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,
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,
577 if self._pending.has_key('borderColor'):
578 self.itemconfigure('lighttag', fill = self._lightBorderColor)
579 self.itemconfigure('darktag', fill = self._darkBorderColor)
581 newTopPage = self._pending.get('topPage')
582 pageBorder = borderWidth + self._pageMargin
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'],
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)
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),
614 # Need to do forwarding to get the pack, grid, etc methods.
615 # Unfortunately this means that all the other canvas methods are also
617 Pmw.forwardmethods(NoteBook, Tkinter.Canvas, '_hull')