class NoteBook(Pmw
.MegaArchetype
):
def __init__(self
, parent
= None, **kw
):
# Define the megawidget options.
('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
)
if tabpos
is not None and tabpos
!= 'n':
'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
self
._pending
['size'] = 1
self
._pending
['borderColor'] = 1
self
._pending
['topPage'] = None
self
._pending
['tabs'] = 1
self
._canvasSize
= None # This gets set by <Configure> events
# Set initial height of space for tabs
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
# 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
# bottom and right shadow
# 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.
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
._pageTop
1Border
= self
.create_polygon(0, 0, 0, 0, 0, 0,
fill
= self
._darkBorderColor
, tags
= 'lighttag')
self
._pageTop
2Border
= self
.create_polygon(0, 0, 0, 0, 0, 0,
fill
= self
._darkBorderColor
, tags
= 'lighttag')
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.
def insert(self
, pageName
, before
= 0, **kw
):
if self
._pageAttrs
.has_key(pageName
):
msg
= 'Page "%s" already exists.' % pageName
# Do this early to catch bad <before> spec before creating any items.
beforeIndex
= self
.index(before
, 1)
# Default tab button options.
# Divide the keyword options into the 'page_' and 'tab_' options.
pageOptions
[key
[5:]] = kw
[key
]
elif self
._withTabs
and key
[:4] == 'tab_':
tabOptions
[key
[4:]] = kw
[key
]
raise KeyError, 'Unknown option "' + key
+ '"'
# Create the frame to contain the page.
page
= apply(self
.createcomponent
, (pageName
,
Tkinter
.Frame
, self
._hull
), pageOptions
)
attributes
['page'] = page
attributes
['created'] = 0
# Create the button for the tab.
def raiseThisPage(self
= self
, pageName
= pageName
):
self
.selectpage(pageName
)
tabOptions
['command'] = raiseThisPage
tab
= apply(self
.createcomponent
, (pageName
+ '-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
):
def prev(event
, self
= self
, pageName
= pageName
):
self
.previouspage(pageName
)
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
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
)
def add(self
, pageName
, **kw
):
return apply(self
.insert
, (pageName
, len(self
._pageNames
)), kw
)
def delete(self
, *pageNames
):
pageIndex
= self
.index(page
)
pageName
= self
._pageNames
[pageIndex
]
pageInfo
= self
._pageAttrs
[pageName
]
if self
.getcurselection() == pageName
:
if len(self
._pageNames
) == 1:
self
._pending
['topPage'] = None
elif pageIndex
== len(self
._pageNames
) - 1:
self
._pending
['topPage'] = self
._pageNames
[pageIndex
- 1]
self
._pending
['topPage'] = self
._pageNames
[pageIndex
+ 1]
if self
._topPageName
== pageName
:
self
._hull
.delete(self
._topPageItem
)
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.
pageName
= self
._pending
['topPage']
self
._raiseNewTop
(pageName
)
self
._pending
['tabs'] = 1
def page(self
, pageIndex
):
pageName
= self
._pageNames
[self
.index(pageIndex
)]
return self
._pageAttrs
[pageName
]['page']
return list(self
._pageNames
)
def getcurselection(self
):
if self
._pending
.has_key('topPage'):
return self
._pending
['topPage']
def tab(self
, pageIndex
):
pageName
= self
._pageNames
[self
.index(pageIndex
)]
return self
._pageAttrs
[pageName
]['tabbutton']
def index(self
, index
, forInsert
= 0):
listLength
= len(self
._pageNames
)
if type(index
) == types
.IntType
:
if forInsert
and index
<= listLength
:
elif not forInsert
and index
< listLength
:
raise ValueError, 'index "%s" is out of range' % index
raise ValueError, 'NoteBook has no pages'
elif index
is Pmw
.SELECT
:
raise ValueError, 'NoteBook has no pages'
return self
._pageNames
.index(self
.getcurselection())
if index
in self
._pageNames
:
return self
._pageNames
.index(index
)
validValues
= 'a name, a number, Pmw.END or Pmw.SELECT'
'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']
self
._raiseNewTop
(pageName
)
# 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):
curpage
= self
.index(Pmw
.SELECT
)
curpage
= self
.index(pageIndex
)
self
.selectpage(curpage
- 1)
def nextpage(self
, pageIndex
= None):
curpage
= self
.index(Pmw
.SELECT
)
curpage
= self
.index(pageIndex
)
if curpage
< len(self
._pageNames
) - 1:
self
.selectpage(curpage
+ 1)
def setnaturalsize(self
, pageNames
= None):
pageNames
= self
.pagenames()
for pageName
in pageNames
:
pageInfo
= self
._pageAttrs
[pageName
]
w
= page
.winfo_reqwidth()
h
= page
.winfo_reqheight()
pageBorder
= self
._borderWidth
+ self
._pageMargin
width
= maxPageWidth
+ pageBorder
* 2
height
= maxPageHeight
+ pageBorder
* 2
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
self
.configure(hull_width
= width
, hull_height
= height
)
def recolorborders(self
):
self
._pending
['borderColor'] = 1
def _handleMap(self
, event
):
def _handleConfigure(self
, event
):
self
._canvasSize
= (event
.width
, event
.height
)
self
._pending
['size'] = 1
def _raiseNewTop(self
, pageName
):
if not self
._pageAttrs
[pageName
]['created']:
self
._pageAttrs
[pageName
]['created'] = 1
cmd
= self
['createcommand']
cmd
= self
['raisecommand']
# This is the vertical layout of the notebook, from top (assuming
# hull highlightthickness (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)
# 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 highlightthickness
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.
hullWidth
, hullHeight
= self
._canvasSize
borderWidth
= self
._borderWidth
canvasBorder
= string
.atoi(self
._hull
['borderwidth']) + \
string
.atoi(self
._hull
['highlightthickness'])
self
.tabBottom
= canvasBorder
oldTabBottom
= self
.tabBottom
if self
._pending
.has_key('borderColor'):
self
._lightBorderColor
, self
._darkBorderColor
= \
Pmw
.Color
.bordercolors(self
, self
['hull_background'])
if self
._withTabs
and (self
._pending
.has_key('tabs') or
self
._pending
.has_key('size')):
# Find total requested width and maximum requested height
for pageInfo
in self
._pageAttrs
.values():
sumTabReqWidth
= sumTabReqWidth
+ pageInfo
['tabreqwidth']
if maxTabHeight
< pageInfo
['tabreqheight']:
maxTabHeight
= pageInfo
['tabreqheight']
# 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.
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
for pageName
in self
._pageNames
:
pageInfo
= self
._pageAttrs
[pageName
]
(windowitem
, lightshadow
, darkshadow
) = pageInfo
['tabitems']
if sumTabReqWidth
<= availableWidth
:
tabwidth
= pageInfo
['tabreqwidth']
# 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
) \
tabwidth
= tmp
- cumTabWidth
# 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.
left2
= left
+ borderWidth
left3
= left
+ borderWidth
* 1.5
right
= left
+ tabwidth
+ 2 * borderWidth
right2
= left
+ tabwidth
+ borderWidth
right3
= left
+ tabwidth
+ borderWidth
* 0.5
left
, tabBottom2
, left
, tabTop2
, left2
, tabTop
,
right2
, tabTop
, right3
, tabTop2
, left3
, tabTop2
,
left2
, tabTop3
, left2
, tabBottom
,
right2
, tabTop
, right
, tabTop2
, right
, tabBottom2
,
right2
, tabBottom
, right2
, tabTop3
, right3
, tabTop2
,
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
._pageTop
1Border
,
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
._pageTop
2Border
, 0, 0, 0, 0, 0, 0)
# 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()]
right
= pageInfo
['right']
self
.coords(self
._pageTop
1Border
,
canvasBorder
, self
.tabBottom
,
left
+ borderWidth
, self
.tabBottom
+ borderWidth
,
canvasBorder
+ borderWidth
, self
.tabBottom
+ borderWidth
,
self
.coords(self
._pageTop
2Border
,
hullWidth
- canvasBorder
, self
.tabBottom
,
hullWidth
- canvasBorder
- borderWidth
,
self
.tabBottom
+ borderWidth
,
right
- borderWidth
, self
.tabBottom
+ borderWidth
,
# Prevent bottom of dark border of tabs appearing over
self
.tag_raise(self
._pageTop
1Border
)
self
.tag_raise(self
._pageTop
2Border
)
# 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
,
self
.coords(self
._pageTopBorder
,
canvasBorder
, self
.tabBottom
,
hullWidth
- canvasBorder
, self
.tabBottom
,
hullWidth
- canvasBorder
- borderWidth
,
self
.tabBottom
+ borderWidth
,
borderWidth
+ canvasBorder
, self
.tabBottom
+ borderWidth
,
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
if newTopPage
is not None:
self
._topPageName
= newTopPage
self
._topPageItem
= self
.create_window(
pageBorder
+ canvasBorder
, self
.tabBottom
+ pageBorder
,
window
= self
._pageAttrs
[newTopPage
]['page'],
# 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
),
# Need to do forwarding to get the pack, grid, etc methods.
# Unfortunately this means that all the other canvas methods are also
Pmw
.forwardmethods(NoteBook
, Tkinter
.Canvas
, '_hull')