Index: configDialog.py =================================================================== --- configDialog.py (revision 52836) +++ configDialog.py (working copy) @@ -15,7 +15,7 @@ from configHandler import idleConf from dynOptionMenuWidget import DynOptionMenu -from tabpage import TabPageSet +from tabbedPages import TabbedPageSet from keybindingDialog import GetKeysDialog from configSectionNameDialog import GetCfgSectionNameDialog from configHelpSourceEdit import GetHelpSourceDialog @@ -61,28 +61,32 @@ self.wait_window() def CreateWidgets(self): - self.tabPages = TabPageSet(self, - pageNames=['Fonts/Tabs','Highlighting','Keys','General']) - self.tabPages.ChangePage()#activates default (first) page - frameActionButtons = Frame(self) + self.tabPages = TabbedPageSet(self, + page_names=['Fonts/Tabs','Highlighting','Keys','General']) + frameActionButtons = Frame(self,pady=2) #action buttons self.buttonHelp = Button(frameActionButtons,text='Help', - command=self.Help,takefocus=FALSE) + command=self.Help,takefocus=FALSE, + padx=6,pady=3) self.buttonOk = Button(frameActionButtons,text='Ok', - command=self.Ok,takefocus=FALSE) + command=self.Ok,takefocus=FALSE, + padx=6,pady=3) self.buttonApply = Button(frameActionButtons,text='Apply', - command=self.Apply,takefocus=FALSE) + command=self.Apply,takefocus=FALSE, + padx=6,pady=3) self.buttonCancel = Button(frameActionButtons,text='Cancel', - command=self.Cancel,takefocus=FALSE) + command=self.Cancel,takefocus=FALSE, + padx=6,pady=3) self.CreatePageFontTab() self.CreatePageHighlight() self.CreatePageKeys() self.CreatePageGeneral() - self.buttonHelp.pack(side=RIGHT,padx=5,pady=5) - self.buttonOk.pack(side=LEFT,padx=5,pady=5) - self.buttonApply.pack(side=LEFT,padx=5,pady=5) - self.buttonCancel.pack(side=LEFT,padx=5,pady=5) + self.buttonHelp.pack(side=RIGHT,padx=5) + self.buttonOk.pack(side=LEFT,padx=5) + self.buttonApply.pack(side=LEFT,padx=5) + self.buttonCancel.pack(side=LEFT,padx=5) frameActionButtons.pack(side=BOTTOM) + Frame(self, border=0).pack(side=BOTTOM,pady=2) self.tabPages.pack(side=TOP,expand=TRUE,fill=BOTH) def CreatePageFontTab(self): @@ -94,7 +98,7 @@ self.editFont=tkFont.Font(self,('courier',10,'normal')) ##widget creation #body frame - frame=self.tabPages.pages['Fonts/Tabs']['page'] + frame=self.tabPages.pages['Fonts/Tabs'].frame #body section frames frameFont=Frame(frame,borderwidth=2,relief=GROOVE) frameIndent=Frame(frame,borderwidth=2,relief=GROOVE) @@ -128,8 +132,8 @@ tickinterval=2, from_=2, to=16) #widget packing #body - frameFont.pack(side=LEFT,padx=5,pady=10,expand=TRUE,fill=BOTH) - frameIndent.pack(side=LEFT,padx=5,pady=10,fill=Y) + frameFont.pack(side=LEFT,padx=5,pady=5,expand=TRUE,fill=BOTH) + frameIndent.pack(side=LEFT,padx=5,pady=5,fill=Y) #frameFont labelFontTitle.pack(side=TOP,anchor=W,padx=5,pady=5) frameFontName.pack(side=TOP,padx=5,pady=5,fill=X) @@ -158,7 +162,7 @@ self.highlightTarget=StringVar(self) ##widget creation #body frame - frame=self.tabPages.pages['Highlighting']['page'] + frame=self.tabPages.pages['Highlighting'].frame #body section frames frameCustom=Frame(frame,borderwidth=2,relief=GROOVE) frameTheme=Frame(frame,borderwidth=2,relief=GROOVE) @@ -216,8 +220,8 @@ command=self.DeleteCustomTheme) ##widget packing #body - frameCustom.pack(side=LEFT,padx=5,pady=10,expand=TRUE,fill=BOTH) - frameTheme.pack(side=LEFT,padx=5,pady=10,fill=Y) + frameCustom.pack(side=LEFT,padx=5,pady=5,expand=TRUE,fill=BOTH) + frameTheme.pack(side=LEFT,padx=5,pady=5,fill=Y) #frameCustom labelCustomTitle.pack(side=TOP,anchor=W,padx=5,pady=5) self.frameColourSet.pack(side=TOP,padx=5,pady=5,expand=TRUE,fill=X) @@ -248,7 +252,7 @@ self.keyBinding=StringVar(self) ##widget creation #body frame - frame=self.tabPages.pages['Keys']['page'] + frame=self.tabPages.pages['Keys'].frame #body section frames frameCustom=Frame(frame,borderwidth=2,relief=GROOVE) frameKeySets=Frame(frame,borderwidth=2,relief=GROOVE) @@ -320,7 +324,7 @@ self.helpBrowser=StringVar(self) #widget creation #body - frame=self.tabPages.pages['General']['page'] + frame=self.tabPages.pages['General'].frame #body section frames frameRun=Frame(frame,borderwidth=2,relief=GROOVE) frameSave=Frame(frame,borderwidth=2,relief=GROOVE) Index: tabbedPages.py =================================================================== --- tabbedPages.py (revision 0) +++ tabbedPages.py (revision 0) @@ -0,0 +1,362 @@ +"""An implementation of tabbed pages using only standard Tkinter. + +Originially developed for use in IDLE. Based on tabpage.py. + +Classes exported: +TabbedPageSet -- A Tkinter implementation of a tabbed-page widget. +TabBarSet -- A widget containing tabs (buttons) in one or more rows. + +""" +from Tkinter import * + +class InvalidNameError(Exception): pass +class AlreadyExistsError(Exception): pass + +class _Command(object): + """Utility class. Useful for defining callbacks with arguments.""" + def __init__(self, func, *args, **kw): + self.func = func + self.args = args[:] + self.kw = kw.copy() + + def __call__(self, *args, **kw): + args += self.args + kw.update(self.kw) + return self.func(*args, **kw) + +class TabBarSet(Frame): + """A widget containing tabs (buttons) in one or more rows. + + Only one tab may be selected at a time. + + """ + def __init__(self, parent, select_command, + tabs=[], n_rows=1, max_tabs_per_row=5, **kw): + """Constructor arguments: + + select_command -- A callable which will be called when a tab is + selected. It is called with the name of the selected tab as an + argument. + + tabs -- A list of strings, the names of the tabs. Should be specified in + the desired tab order. The first tab will be the default and first + active tab. If tabs is None or empty, the TabBarSet will be initialized + empty. + + n_rows -- Number of rows of tabs to be shown. If n_rows <= 0 or is + None, then the number of rows will be decided by TabBarSet. See + _arrange_tabs() for details. + + max_tabs_per_row -- Used for deciding how many rows of tabs are needed, + when the number of rows is not constant. See _arrange_tabs() for + details. + + """ + Frame.__init__(self, parent, **kw) + self.select_command = select_command + self.n_rows = n_rows + self.max_tabs_per_row = max_tabs_per_row + + self._tabs = {} + self._tab_names = tabs[:] + self._active_tab = None + self._tab_rows = [] + + self._arrange_tabs() + + def add_tab(self, tab_name): + """Add a new tab with the name given in tab_name.""" + if not tab_name: + raise InvalidNameError("Invalid Tab name: '%s'" % tab_name) + if tab_name in self._tab_names: + raise AlreadyExistsError("Tab named '%s' already exists" %tab_name) + + self._tab_names.append(tab_name) + self._arrange_tabs() + + def remove_tab(self, tab_name): + """Remove the tab with the name given in tab_name.""" + if not tab_name in self._tab_names: + raise KeyError("No such Tab: '%s" % page_name) + + self._tab_names.remove(tab_name) + self._arrange_tabs() + + def select_tab(self, tab_name): + """Select the tab with the name given in tab_name.""" + if tab_name == self._active_tab: + return + + if self._active_tab is not None: + self._tabs[self._active_tab].set_normal() + + if tab_name is not None: + if tab_name in self._tabs: + self._active_tab = tab_name + self._tabs[tab_name].set_selected() + else: + raise KeyError("No such Tab: '%s" % page_name) + else: + self._active_tab = None + + def _add_tab_row(self): + tab_row = Frame(self) + tab_row.pack(side=TOP, fill=X, expand=0) + self._tab_rows.append(tab_row) + return tab_row + + def _reset_tab_rows(self): + while self._tab_rows: + tab_row = self._tab_rows.pop() + tab_row.destroy() + + def _arrange_tabs(self): + """ + Arrange the tabs in rows, in the order in which they were added. + + If n_rows >= 1, this will be the number of rows used. Otherwise the + number of rows will be calculated according to the number of tabs and + max_tabs_per_row. In this case, the number of rows may change when + adding/removing tabs. + + """ + if self.n_rows is None or self.n_rows <=0: + # calculate the required number of rows + n_bars = (len(self._tab_names) - 1) // self.max_tabs_per_row + 1 + else: + n_bars = self.n_rows + + # remove all tabs and rows + for tab_name in self._tabs: + self._tabs[tab_name].destroy() + self._tabs = {} + self._reset_tab_rows() + + i = 0 + for row_index in xrange(n_bars): + tab_row = self._add_tab_row() + n_tabs = (len(self._tab_names) - i - 1) // (n_bars - row_index) + 1 + for tab_name in self._tab_names[i:i + n_tabs]: + tab_command = _Command(self.select_command, tab_name) + tab = TabBarSet.TabButton(tab_row, tab_name, tab_command) + tab.pack(side=LEFT, fill=X, expand=1) + self._tabs[tab_name] = tab + i += n_tabs + + active = self._active_tab + if active in self._tab_names: + self._tabs[active].set_selected() + + class TabButton(Frame): + """A simple tab-like widget.""" + def __init__(self, parent, name, command): + """Constructor arguments: + + name -- The tab's name, which will appear in its button. + + command -- The command to be called upon selection of the tab. It + is called with the tab's name as an argument. + + """ + Frame.__init__(self, parent, borderwidth=2) + self.set_normal() + self.button = Radiobutton(self, text=name, command=command, + padx=5, pady=5, takefocus=FALSE, indicatoron=FALSE, + highlightthickness=0, selectcolor='', borderwidth=0) + self.button.pack(fill=X, expand=True) + + def set_selected(self): + """Assume selected look""" + self.config(relief=RAISED) + + def set_normal(self): + """Assume normal look""" + self.config(relief=RIDGE) + + +class TabbedPageSet(Frame): + """A Tkinter tabbed-pane widget. + + Constains set of 'pages' (or 'panes') with tabs above for selecting which + page is displayed. Only one page will be displayed at a time. + + Pages may be accessed through the 'pages' attribute, which is a dictionary + of pages, using the name given as the key. A page is an instance of a + subclass of Tk's Frame widget. + + The page widgets will be created (and destroyed when required) by the + TabbedPageSet. Do not call the page's pack/place/grid/destroy methods. + + Pages may be added or removed at any time using the add_page() and + remove_page() methods. + + """ + MECH_LIFT = 0 + MECH_REMOVE = 1 + + def __init__(self, parent, page_names=[], n_rows=1, max_tabs_per_row=5, + mechanism=MECH_LIFT, **kw): + """Constructor arguments: + + page_names -- A list of strings, each will be the dictionary key to a + page's widget, and the name displayed on the page's tab. Should be + specified in the desired page order. The first page will be the default + and first active page. If page_names is None or empty, the + TabbedPageSet will be initialized empty. + + n_rows, max_tabs_per_row -- Parameters for the TabBarSet which will + manage the tabs. See TabBarSet's docs for details. + + mechanism -- Pages can be shown/hidden using two mechanisms: + + * MECH_LIFT - All pages will be rendered one on top of the other. When + a page is selected, it will be brought to the top, thus hiding all + other pages. Using this method, the TabbedPageSet will not be resized + when pages are switched. (It may still be resized when pages are + added/removed.) + + * MECH_REMOVE - When a page is selected, the currently showing page is + hidden, and the new page shown in its place. Using this method, the + TabbedPageSet may resize when pages are changed. + + """ + Frame.__init__(self, parent, kw) + self.grid_location(0, 0) + self.columnconfigure(0, weight=1) + self.rowconfigure(1, weight=1) + + if mechanism == TabbedPageSet.MECH_LIFT: + self.page_class = TabbedPageSet.PageLift + elif mechanism == TabbedPageSet.MECH_REMOVE: + self.page_class = TabbedPageSet.PageRemove + + self.pages = {} + self._pages_order = [] + self._current_page = None + self._default_page = None + + # the order of the following commands is important + self._tab_set = TabBarSet(self, self.change_page, n_rows=n_rows, + max_tabs_per_row=max_tabs_per_row) + if page_names: + for name in page_names: + self.add_page(name) + self._tab_set.grid(row=0,column=0,sticky=EW) + self._tab_set._arrange_tabs() + self.change_page(self._default_page) + + def add_page(self, page_name): + """Add a new page with the name given in page_name.""" + if not page_name: + raise InvalidNameError("Invalid TabPage name: '%s'" % page_name) + if page_name in self.pages: + raise AlreadyExistsError( + "TabPage named '%s' already exists" % page_name) + + self.pages[page_name] = self.page_class(self) + self._pages_order.append(page_name) + self._tab_set.add_tab(page_name) + + if len(self.pages) == 1: # adding first page + self._default_page = page_name + self.change_page(page_name) + + self._tab_set._arrange_tabs() + + def remove_page(self, page_name): + """Destroy the page whose name is given in page_name.""" + if not page_name in self.pages: + raise KeyError("No such TabPage: '%s" % page_name) + + self._pages_order.remove(page_name) + + # handle removing last remaining, default, or currently shown page + if len(self._pages_order) > 0: + if page_name == self._default_page: + # set a new default page + self._default_page = self._pages_order[0] + else: + self._default_page = None + + if page_name == self._current_page: + self.change_page(self._default_page) + + self._tab_set.remove_tab(page_name) + page = self.pages.pop(page_name) + page.frame.destroy() + + def change_page(self, page_name): + """Show the page whose name is given in page_name.""" + if self._current_page == page_name: + return + + if self._current_page is not None: + self.pages[self._current_page]._hide() + + if self._tab_set is not None: + self._tab_set.select_tab(page_name) + + if page_name is not None: + if page_name in self.pages: + self._current_page = page_name + self.pages[page_name]._show() + else: + raise KeyError("No such TabPage: '%s" % page_name) + else: + self._current_page = page_name + + class Page(object): + """Abstract base class for TabbedPageSet's pages. + + Subclasses must override the _show() and _hide() methods. + + """ + def __init__(self, page_set): + self.frame = Frame(page_set, borderwidth=2, relief=RAISED) + + def _show(self): + raise NotImplementedError + + def _hide(self): + raise NotImplementedError + + class PageRemove(Page): + """Page class implementing the GRID_REMOVE mechanism.""" + def _show(self): + self.frame.grid(row=1, column=0, sticky=NSEW) + + def _hide(self): + self.frame.grid_remove() + + class PageLift(Page): + """Page class implementing the GRID_LIFT mechanism.""" + def __init__(self, page_set): + super(TabbedPageSet.PageLift, self).__init__(page_set) + self.frame.grid(row=1, column=0, sticky=NSEW) + + def _show(self): + self.frame.lift() + + def _hide(self): + pass + + +if __name__ == '__main__': + #test dialog + root=Tk() + tabPage=TabbedPageSet(root,page_names=['Foobar','Baz'], rows=0) + tabPage.pack(expand=TRUE,fill=BOTH) + Label(tabPage.pages['Foobar'].frame,text='Foo',pady=20).pack() + Label(tabPage.pages['Foobar'].frame,text='Bar',pady=20).pack() + Label(tabPage.pages['Baz'].frame,text='Baz').pack() + entryPgName=Entry(root) + buttonAdd=Button(root,text='Add Page', + command=lambda:tabPage.add_page(entryPgName.get())) + buttonRemove=Button(root,text='Remove Page', + command=lambda:tabPage.remove_page(entryPgName.get())) + labelPgName=Label(root,text='name of page to add/remove:') + buttonAdd.pack(padx=5,pady=5) + buttonRemove.pack(padx=5,pady=5) + labelPgName.pack(padx=5) + entryPgName.pack(padx=5) + root.mainloop()