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('', self._handleMap) self.bind('', 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 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 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('', prev) tab.bind('', 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 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')