Home | History | Annotate | Download | only in idlelib
      1 """An implementation of tabbed pages using only standard Tkinter.
      2 
      3 Originally developed for use in IDLE. Based on tabpage.py.
      4 
      5 Classes exported:
      6 TabbedPageSet -- A Tkinter implementation of a tabbed-page widget.
      7 TabSet -- A widget containing tabs (buttons) in one or more rows.
      8 
      9 """
     10 from Tkinter import *
     11 
     12 class InvalidNameError(Exception): pass
     13 class AlreadyExistsError(Exception): pass
     14 
     15 
     16 class TabSet(Frame):
     17     """A widget containing tabs (buttons) in one or more rows.
     18 
     19     Only one tab may be selected at a time.
     20 
     21     """
     22     def __init__(self, page_set, select_command,
     23                  tabs=None, n_rows=1, max_tabs_per_row=5,
     24                  expand_tabs=False, **kw):
     25         """Constructor arguments:
     26 
     27         select_command -- A callable which will be called when a tab is
     28         selected. It is called with the name of the selected tab as an
     29         argument.
     30 
     31         tabs -- A list of strings, the names of the tabs. Should be specified in
     32         the desired tab order. The first tab will be the default and first
     33         active tab. If tabs is None or empty, the TabSet will be initialized
     34         empty.
     35 
     36         n_rows -- Number of rows of tabs to be shown. If n_rows <= 0 or is
     37         None, then the number of rows will be decided by TabSet. See
     38         _arrange_tabs() for details.
     39 
     40         max_tabs_per_row -- Used for deciding how many rows of tabs are needed,
     41         when the number of rows is not constant. See _arrange_tabs() for
     42         details.
     43 
     44         """
     45         Frame.__init__(self, page_set, **kw)
     46         self.select_command = select_command
     47         self.n_rows = n_rows
     48         self.max_tabs_per_row = max_tabs_per_row
     49         self.expand_tabs = expand_tabs
     50         self.page_set = page_set
     51 
     52         self._tabs = {}
     53         self._tab2row = {}
     54         if tabs:
     55             self._tab_names = list(tabs)
     56         else:
     57             self._tab_names = []
     58         self._selected_tab = None
     59         self._tab_rows = []
     60 
     61         self.padding_frame = Frame(self, height=2,
     62                                    borderwidth=0, relief=FLAT,
     63                                    background=self.cget('background'))
     64         self.padding_frame.pack(side=TOP, fill=X, expand=False)
     65 
     66         self._arrange_tabs()
     67 
     68     def add_tab(self, tab_name):
     69         """Add a new tab with the name given in tab_name."""
     70         if not tab_name:
     71             raise InvalidNameError("Invalid Tab name: '%s'" % tab_name)
     72         if tab_name in self._tab_names:
     73             raise AlreadyExistsError("Tab named '%s' already exists" %tab_name)
     74 
     75         self._tab_names.append(tab_name)
     76         self._arrange_tabs()
     77 
     78     def remove_tab(self, tab_name):
     79         """Remove the tab named <tab_name>"""
     80         if not tab_name in self._tab_names:
     81             raise KeyError("No such Tab: '%s" % page_name)
     82 
     83         self._tab_names.remove(tab_name)
     84         self._arrange_tabs()
     85 
     86     def set_selected_tab(self, tab_name):
     87         """Show the tab named <tab_name> as the selected one"""
     88         if tab_name == self._selected_tab:
     89             return
     90         if tab_name is not None and tab_name not in self._tabs:
     91             raise KeyError("No such Tab: '%s" % page_name)
     92 
     93         # deselect the current selected tab
     94         if self._selected_tab is not None:
     95             self._tabs[self._selected_tab].set_normal()
     96         self._selected_tab = None
     97 
     98         if tab_name is not None:
     99             # activate the tab named tab_name
    100             self._selected_tab = tab_name
    101             tab = self._tabs[tab_name]
    102             tab.set_selected()
    103             # move the tab row with the selected tab to the bottom
    104             tab_row = self._tab2row[tab]
    105             tab_row.pack_forget()
    106             tab_row.pack(side=TOP, fill=X, expand=0)
    107 
    108     def _add_tab_row(self, tab_names, expand_tabs):
    109         if not tab_names:
    110             return
    111 
    112         tab_row = Frame(self)
    113         tab_row.pack(side=TOP, fill=X, expand=0)
    114         self._tab_rows.append(tab_row)
    115 
    116         for tab_name in tab_names:
    117             tab = TabSet.TabButton(tab_name, self.select_command,
    118                                    tab_row, self)
    119             if expand_tabs:
    120                 tab.pack(side=LEFT, fill=X, expand=True)
    121             else:
    122                 tab.pack(side=LEFT)
    123             self._tabs[tab_name] = tab
    124             self._tab2row[tab] = tab_row
    125 
    126         # tab is the last one created in the above loop
    127         tab.is_last_in_row = True
    128 
    129     def _reset_tab_rows(self):
    130         while self._tab_rows:
    131             tab_row = self._tab_rows.pop()
    132             tab_row.destroy()
    133         self._tab2row = {}
    134 
    135     def _arrange_tabs(self):
    136         """
    137         Arrange the tabs in rows, in the order in which they were added.
    138 
    139         If n_rows >= 1, this will be the number of rows used. Otherwise the
    140         number of rows will be calculated according to the number of tabs and
    141         max_tabs_per_row. In this case, the number of rows may change when
    142         adding/removing tabs.
    143 
    144         """
    145         # remove all tabs and rows
    146         for tab_name in self._tabs.keys():
    147             self._tabs.pop(tab_name).destroy()
    148         self._reset_tab_rows()
    149 
    150         if not self._tab_names:
    151             return
    152 
    153         if self.n_rows is not None and self.n_rows > 0:
    154             n_rows = self.n_rows
    155         else:
    156             # calculate the required number of rows
    157             n_rows = (len(self._tab_names) - 1) // self.max_tabs_per_row + 1
    158 
    159         # not expanding the tabs with more than one row is very ugly
    160         expand_tabs = self.expand_tabs or n_rows > 1
    161         i = 0 # index in self._tab_names
    162         for row_index in xrange(n_rows):
    163             # calculate required number of tabs in this row
    164             n_tabs = (len(self._tab_names) - i - 1) // (n_rows - row_index) + 1
    165             tab_names = self._tab_names[i:i + n_tabs]
    166             i += n_tabs
    167             self._add_tab_row(tab_names, expand_tabs)
    168 
    169         # re-select selected tab so it is properly displayed
    170         selected = self._selected_tab
    171         self.set_selected_tab(None)
    172         if selected in self._tab_names:
    173             self.set_selected_tab(selected)
    174 
    175     class TabButton(Frame):
    176         """A simple tab-like widget."""
    177 
    178         bw = 2 # borderwidth
    179 
    180         def __init__(self, name, select_command, tab_row, tab_set):
    181             """Constructor arguments:
    182 
    183             name -- The tab's name, which will appear in its button.
    184 
    185             select_command -- The command to be called upon selection of the
    186             tab. It is called with the tab's name as an argument.
    187 
    188             """
    189             Frame.__init__(self, tab_row, borderwidth=self.bw, relief=RAISED)
    190 
    191             self.name = name
    192             self.select_command = select_command
    193             self.tab_set = tab_set
    194             self.is_last_in_row = False
    195 
    196             self.button = Radiobutton(
    197                 self, text=name, command=self._select_event,
    198                 padx=5, pady=1, takefocus=FALSE, indicatoron=FALSE,
    199                 highlightthickness=0, selectcolor='', borderwidth=0)
    200             self.button.pack(side=LEFT, fill=X, expand=True)
    201 
    202             self._init_masks()
    203             self.set_normal()
    204 
    205         def _select_event(self, *args):
    206             """Event handler for tab selection.
    207 
    208             With TabbedPageSet, this calls TabbedPageSet.change_page, so that
    209             selecting a tab changes the page.
    210 
    211             Note that this does -not- call set_selected -- it will be called by
    212             TabSet.set_selected_tab, which should be called when whatever the
    213             tabs are related to changes.
    214 
    215             """
    216             self.select_command(self.name)
    217             return
    218 
    219         def set_selected(self):
    220             """Assume selected look"""
    221             self._place_masks(selected=True)
    222 
    223         def set_normal(self):
    224             """Assume normal look"""
    225             self._place_masks(selected=False)
    226 
    227         def _init_masks(self):
    228             page_set = self.tab_set.page_set
    229             background = page_set.pages_frame.cget('background')
    230             # mask replaces the middle of the border with the background color
    231             self.mask = Frame(page_set, borderwidth=0, relief=FLAT,
    232                               background=background)
    233             # mskl replaces the bottom-left corner of the border with a normal
    234             # left border
    235             self.mskl = Frame(page_set, borderwidth=0, relief=FLAT,
    236                               background=background)
    237             self.mskl.ml = Frame(self.mskl, borderwidth=self.bw,
    238                                  relief=RAISED)
    239             self.mskl.ml.place(x=0, y=-self.bw,
    240                                width=2*self.bw, height=self.bw*4)
    241             # mskr replaces the bottom-right corner of the border with a normal
    242             # right border
    243             self.mskr = Frame(page_set, borderwidth=0, relief=FLAT,
    244                               background=background)
    245             self.mskr.mr = Frame(self.mskr, borderwidth=self.bw,
    246                                  relief=RAISED)
    247 
    248         def _place_masks(self, selected=False):
    249             height = self.bw
    250             if selected:
    251                 height += self.bw
    252 
    253             self.mask.place(in_=self,
    254                             relx=0.0, x=0,
    255                             rely=1.0, y=0,
    256                             relwidth=1.0, width=0,
    257                             relheight=0.0, height=height)
    258 
    259             self.mskl.place(in_=self,
    260                             relx=0.0, x=-self.bw,
    261                             rely=1.0, y=0,
    262                             relwidth=0.0, width=self.bw,
    263                             relheight=0.0, height=height)
    264 
    265             page_set = self.tab_set.page_set
    266             if selected and ((not self.is_last_in_row) or
    267                              (self.winfo_rootx() + self.winfo_width() <
    268                               page_set.winfo_rootx() + page_set.winfo_width())
    269                              ):
    270                 # for a selected tab, if its rightmost edge isn't on the
    271                 # rightmost edge of the page set, the right mask should be one
    272                 # borderwidth shorter (vertically)
    273                 height -= self.bw
    274 
    275             self.mskr.place(in_=self,
    276                             relx=1.0, x=0,
    277                             rely=1.0, y=0,
    278                             relwidth=0.0, width=self.bw,
    279                             relheight=0.0, height=height)
    280 
    281             self.mskr.mr.place(x=-self.bw, y=-self.bw,
    282                                width=2*self.bw, height=height + self.bw*2)
    283 
    284             # finally, lower the tab set so that all of the frames we just
    285             # placed hide it
    286             self.tab_set.lower()
    287 
    288 class TabbedPageSet(Frame):
    289     """A Tkinter tabbed-pane widget.
    290 
    291     Constains set of 'pages' (or 'panes') with tabs above for selecting which
    292     page is displayed. Only one page will be displayed at a time.
    293 
    294     Pages may be accessed through the 'pages' attribute, which is a dictionary
    295     of pages, using the name given as the key. A page is an instance of a
    296     subclass of Tk's Frame widget.
    297 
    298     The page widgets will be created (and destroyed when required) by the
    299     TabbedPageSet. Do not call the page's pack/place/grid/destroy methods.
    300 
    301     Pages may be added or removed at any time using the add_page() and
    302     remove_page() methods.
    303 
    304     """
    305     class Page(object):
    306         """Abstract base class for TabbedPageSet's pages.
    307 
    308         Subclasses must override the _show() and _hide() methods.
    309 
    310         """
    311         uses_grid = False
    312 
    313         def __init__(self, page_set):
    314             self.frame = Frame(page_set, borderwidth=2, relief=RAISED)
    315 
    316         def _show(self):
    317             raise NotImplementedError
    318 
    319         def _hide(self):
    320             raise NotImplementedError
    321 
    322     class PageRemove(Page):
    323         """Page class using the grid placement manager's "remove" mechanism."""
    324         uses_grid = True
    325 
    326         def _show(self):
    327             self.frame.grid(row=0, column=0, sticky=NSEW)
    328 
    329         def _hide(self):
    330             self.frame.grid_remove()
    331 
    332     class PageLift(Page):
    333         """Page class using the grid placement manager's "lift" mechanism."""
    334         uses_grid = True
    335 
    336         def __init__(self, page_set):
    337             super(TabbedPageSet.PageLift, self).__init__(page_set)
    338             self.frame.grid(row=0, column=0, sticky=NSEW)
    339             self.frame.lower()
    340 
    341         def _show(self):
    342             self.frame.lift()
    343 
    344         def _hide(self):
    345             self.frame.lower()
    346 
    347     class PagePackForget(Page):
    348         """Page class using the pack placement manager's "forget" mechanism."""
    349         def _show(self):
    350             self.frame.pack(fill=BOTH, expand=True)
    351 
    352         def _hide(self):
    353             self.frame.pack_forget()
    354 
    355     def __init__(self, parent, page_names=None, page_class=PageLift,
    356                  n_rows=1, max_tabs_per_row=5, expand_tabs=False,
    357                  **kw):
    358         """Constructor arguments:
    359 
    360         page_names -- A list of strings, each will be the dictionary key to a
    361         page's widget, and the name displayed on the page's tab. Should be
    362         specified in the desired page order. The first page will be the default
    363         and first active page. If page_names is None or empty, the
    364         TabbedPageSet will be initialized empty.
    365 
    366         n_rows, max_tabs_per_row -- Parameters for the TabSet which will
    367         manage the tabs. See TabSet's docs for details.
    368 
    369         page_class -- Pages can be shown/hidden using three mechanisms:
    370 
    371         * PageLift - All pages will be rendered one on top of the other. When
    372           a page is selected, it will be brought to the top, thus hiding all
    373           other pages. Using this method, the TabbedPageSet will not be resized
    374           when pages are switched. (It may still be resized when pages are
    375           added/removed.)
    376 
    377         * PageRemove - When a page is selected, the currently showing page is
    378           hidden, and the new page shown in its place. Using this method, the
    379           TabbedPageSet may resize when pages are changed.
    380 
    381         * PagePackForget - This mechanism uses the pack placement manager.
    382           When a page is shown it is packed, and when it is hidden it is
    383           unpacked (i.e. pack_forget). This mechanism may also cause the
    384           TabbedPageSet to resize when the page is changed.
    385 
    386         """
    387         Frame.__init__(self, parent, **kw)
    388 
    389         self.page_class = page_class
    390         self.pages = {}
    391         self._pages_order = []
    392         self._current_page = None
    393         self._default_page = None
    394 
    395         self.columnconfigure(0, weight=1)
    396         self.rowconfigure(1, weight=1)
    397 
    398         self.pages_frame = Frame(self)
    399         self.pages_frame.grid(row=1, column=0, sticky=NSEW)
    400         if self.page_class.uses_grid:
    401             self.pages_frame.columnconfigure(0, weight=1)
    402             self.pages_frame.rowconfigure(0, weight=1)
    403 
    404         # the order of the following commands is important
    405         self._tab_set = TabSet(self, self.change_page, n_rows=n_rows,
    406                                max_tabs_per_row=max_tabs_per_row,
    407                                expand_tabs=expand_tabs)
    408         if page_names:
    409             for name in page_names:
    410                 self.add_page(name)
    411         self._tab_set.grid(row=0, column=0, sticky=NSEW)
    412 
    413         self.change_page(self._default_page)
    414 
    415     def add_page(self, page_name):
    416         """Add a new page with the name given in page_name."""
    417         if not page_name:
    418             raise InvalidNameError("Invalid TabPage name: '%s'" % page_name)
    419         if page_name in self.pages:
    420             raise AlreadyExistsError(
    421                 "TabPage named '%s' already exists" % page_name)
    422 
    423         self.pages[page_name] = self.page_class(self.pages_frame)
    424         self._pages_order.append(page_name)
    425         self._tab_set.add_tab(page_name)
    426 
    427         if len(self.pages) == 1: # adding first page
    428             self._default_page = page_name
    429             self.change_page(page_name)
    430 
    431     def remove_page(self, page_name):
    432         """Destroy the page whose name is given in page_name."""
    433         if not page_name in self.pages:
    434             raise KeyError("No such TabPage: '%s" % page_name)
    435 
    436         self._pages_order.remove(page_name)
    437 
    438         # handle removing last remaining, default, or currently shown page
    439         if len(self._pages_order) > 0:
    440             if page_name == self._default_page:
    441                 # set a new default page
    442                 self._default_page = self._pages_order[0]
    443         else:
    444             self._default_page = None
    445 
    446         if page_name == self._current_page:
    447             self.change_page(self._default_page)
    448 
    449         self._tab_set.remove_tab(page_name)
    450         page = self.pages.pop(page_name)
    451         page.frame.destroy()
    452 
    453     def change_page(self, page_name):
    454         """Show the page whose name is given in page_name."""
    455         if self._current_page == page_name:
    456             return
    457         if page_name is not None and page_name not in self.pages:
    458             raise KeyError("No such TabPage: '%s'" % page_name)
    459 
    460         if self._current_page is not None:
    461             self.pages[self._current_page]._hide()
    462         self._current_page = None
    463 
    464         if page_name is not None:
    465             self._current_page = page_name
    466             self.pages[page_name]._show()
    467 
    468         self._tab_set.set_selected_tab(page_name)
    469 
    470 def _tabbed_pages(parent):
    471     # test dialog
    472     root=Tk()
    473     width, height, x, y = list(map(int, re.split('[x+]', parent.geometry())))
    474     root.geometry("+%d+%d"%(x, y + 175))
    475     root.title("Test tabbed pages")
    476     tabPage=TabbedPageSet(root, page_names=['Foobar','Baz'], n_rows=0,
    477                           expand_tabs=False,
    478                           )
    479     tabPage.pack(side=TOP, expand=TRUE, fill=BOTH)
    480     Label(tabPage.pages['Foobar'].frame, text='Foo', pady=20).pack()
    481     Label(tabPage.pages['Foobar'].frame, text='Bar', pady=20).pack()
    482     Label(tabPage.pages['Baz'].frame, text='Baz').pack()
    483     entryPgName=Entry(root)
    484     buttonAdd=Button(root, text='Add Page',
    485             command=lambda:tabPage.add_page(entryPgName.get()))
    486     buttonRemove=Button(root, text='Remove Page',
    487             command=lambda:tabPage.remove_page(entryPgName.get()))
    488     labelPgName=Label(root, text='name of page to add/remove:')
    489     buttonAdd.pack(padx=5, pady=5)
    490     buttonRemove.pack(padx=5, pady=5)
    491     labelPgName.pack(padx=5)
    492     entryPgName.pack(padx=5)
    493     root.mainloop()
    494 
    495 
    496 if __name__ == '__main__':
    497     from idlelib.idle_test.htest import run
    498     run(_tabbed_pages)
    499