Skillversitas

Notebooks and Tabbed UIs

The notebook is the tabbed container: a widget with a row of tabs, each tab a page, and the selected page's content shown in the body. It is the standard structure for the multi-view UI - the settings dialog with its category tabs, the IDE with its editor tabs, the browser with its page tabs - and it is built from the gtk.Notebook with its append_page (the page content plus the tab label) and the switch-page signal (the notification that the visible page changed). This chapter covers the notebook's construction, the tab labels (the plain, the labeled, the closable), the page management (the reorder, the remove, the position), the switch-page signal and its uses, and the patterns - the settings tabs, the document tabs.

Building the Notebook

import gtk

def build_notebook():
    nb = gtk.Notebook()

    # the pages: content widget + tab label
    page1 = build_general_page()      # a container of settings
    page2 = build_appearance_page()
    page3 = build_advanced_page()

    # append_page(child, tab_label) - the label is a widget
    nb.append_page(page1, gtk.Label("General"))
    nb.append_page(page2, gtk.Label("Appearance"))
    nb.append_page(page3, gtk.Label("Advanced"))

    # the tab label can be any widget - the icon + text:
    tab = gtk.HBox()
    tab.pack_start(gtk.Image.new_from_icon_name(
        "preferences-system", gtk.ICON_SIZE_MENU), False, False, 0)
    tab.pack_start(gtk.Label("General"), False, False, 0)
    # nb.append_page(page, tab)   # the rich tab label

    # the current page:
    nb.set_current_page(1)            # zero-based index
    print(nb.get_current_page())
    print(nb.get_n_pages())

    # the switch signal - the visible page changed:
    def on_switch(nb, page, page_num):
        print("now showing page", page_num)
    nb.connect("switch-page", on_switch)
    return nb

# the notebook in the window: it is the growing child
# (True, True) of the layout - the tabs take their height,
# the current page fills the body

The Notebook is the tabbed container, and its construction is the append_page(child, tab_label) - the content widget for the page and the tab label (a widget, most often a Label). The pages are the views, the tabs are their selectors, and the notebook shows the current page's content in its body. The set_current_page (zero-based) and get_current_page read and set the visible page, and the get_n_pages counts them. The tab label being a widget is the flexibility: the plain Label is the standard, and the HBox with an image plus a label is the icon-and-text tab the settings dialogs use. The switch-page signal fires when the visible page changes (the user clicks a tab, or the code calls set_current_page), carrying the page widget and its index - the hook for the "when the user switches to the Appearance tab, refresh the preview" behavior, and the one that keeps the per-page state in sync with the visibility. In the layout, the notebook is the growing child - the tabs take their natural height and the current page fills the body, which is the True, True pack the containers chapter's rules describe.

Tab Labels: Closable and the Position

import gtk

# the tab position: where the tabs sit around the body
nb.set_tab_pos(gtk.POS_TOP)     # TOP (default), BOTTOM, LEFT, RIGHT

# the closable tab - the label with a close button:
def make_closable_tab(label_text, on_close):
    tab = gtk.HBox()
    tab.pack_start(gtk.Label(label_text), False, False, 4)
    close = gtk.Button()
    close.set_relief(gtk.RELIEF_NONE)
    close.set_image(gtk.Image.new_from_icon_name(
        "window-close", gtk.ICON_SIZE_MENU))
    close.connect("clicked", on_close)
    tab.pack_start(close, False, False, 0)
    return tab

# the document-tab pattern: each open file is a page, the
# tab is the filename + the close button, and the close
# removes the page:
def close_tab(nb, page_index, close_button):
    nb.remove_page(page_index)
    if nb.get_n_pages() == 0:
        # the last tab closed: the app can quit or show
        # the empty state
        pass

# the tab reordering: the pages can be rearranged
nb.reorder_page(page_widget, new_position)

# the movable tabs: the user drags tabs to reorder them
nb.set_tab_reorderable(page_widget, True)

The tab label and the page management. The set_tab_pos places the tabs on an edge (the default top, or the left for the vertical-tab layout). The closable tab is the composition - the label plus a small flat close button in the tab's box, the button's clicked wired to the handler that removes the page - and it is the document UI's tab: each open file is a page, the tab shows the filename and the close affordance, and the close removes the page (the remove_page by index, with the last-tab-closed case handled by the app). The page management beyond append: the reorder_page moves a page to a new position (the programmatic reorder), and the set_tab_reorderable lets the user drag the tabs to reorder them (the interactive case, the editor's tab drag). The discipline for the document tabs: the page index is not stable across removals (removing page 2 shifts the later indices), so the close handler resolves the page by the widget or by a stored id, not by a cached index - the index-drift bug is the one that closes the wrong tab.

The switch-page Signal and Per-Page State

import gtk

# the per-page state: each page has data the switch must
# reconcile (the settings page's unsaved changes, the editor
# page's buffer)
def on_switch(nb, page, page_num):
    # the page being SHOWN: activate its state
    pages = {0: "general", 1: "appearance", 2: "advanced"}
    name = pages.get(page_num)
    if name == "appearance":
        refresh_preview()     # the appearance preview redraws
    # the page being HIDDEN: its state is left as-is (the
    # notebook keeps every page realized; the hidden pages
    # are not destroyed, just not visible)

# the pattern: the switch is the synchronization point -
# the shown page's state is refreshed, the hidden page's
# state is preserved (the notebook does not destroy the
# hidden pages, so their widgets and their data survive the
# switch, and the round-trip to a tab shows the same state)

# the guard: a page that is expensive to build is built on
# first switch (the lazy page) - the append_page with a
# placeholder, the real content built in the first switch
def on_first_switch(nb, page, page_num):
    if page_num == 2 and not built[2]:
        page.add(build_heavy_advanced_content())
        built[2] = True
        nb.emit_by_name("switch-page")   # re-emit for the
                                          # now-real content

The switch-page signal is the synchronization point for the per-page state. The notebook keeps every page realized - the hidden pages are not destroyed, their widgets and their data survive the switch - so the round-trip to a tab shows the same state, and the switch handler's job is the refresh of the page being shown (the appearance preview that redraws, the list that reloads) and, where the page is expensive, the lazy build on first switch. The lazy-page pattern is the optimization for the heavy page: the append_page with a lightweight placeholder, the real content built in the first switch-page for that index, and the flag that builds it once. The signal's arguments - the page widget and the zero-based index - are what the handler uses to identify the page, and the discipline is the same as the event signals: the handler reacts to the visibility change, the state is the data, and the shown page is brought current by the handler. The notebook is the container that makes the multi-view UI a tab switch instead of a window switch, and the switch-page signal is where the per-view logic lives.

The Working Rules

  • The notebook is the growing child. The tabs take their natural size, the current page fills the body - the True, True pack, the same rule as the editor's main area.
  • Resolve the page by the widget or a stored id, not a cached index. The remove_page shifts the later indices, and the cached index is the wrong-tab bug in the closable-tab UI.
  • The hidden pages are not destroyed. The notebook keeps them realized, so the per-page state survives the switch - the round-trip shows the same data, and the switch handler's job is the refresh, not the rebuild.
  • Build the heavy page lazily, on first switch. The placeholder plus the first-switch build is the pattern for the expensive page, and the flag ensures the build happens once.
  • The tab label is a widget. The plain label is the standard; the icon-plus-label box is the settings pattern, and the closable tab is the label plus the close button that removes the page.

Summary

The notebook is the tabbed container: the append_page(child, tab_label) adds a page with its tab, the set_current_page/get_current_page read and set the visible page, and the switch-page signal fires on the change (the user's tab click or the code's set) carrying the page and its index. The tab label is a widget - the plain label, the icon-plus-label box, the closable tab with its close button that removes the page - and the page management includes the remove_page, the reorder_page, and the user-drag set_tab_reorderable. The switch-page signal is the synchronization point: the shown page's state is refreshed, the hidden pages are kept realized (their data survives the switch), and the heavy page is built lazily on its first switch. The rules keep the tabbed UI correct - the notebook as the growing child, the page resolved by identity not a drifting index, the per-page state preserved across the switches - and the patterns (the settings tabs, the document tabs) are the two compositions the structure supports.