Example #1
0
    def __init__(self, labels = None, tab_position="top", tab_spacing = 0,
                 scroll_position = None, show_scroll = "auto", scroll_selects_tab = True, **kwargs):
        Box.__init__(self, horizontal=False, spacing=0, **kwargs)

        #: list of tabs in the order of appearance
        self.tabs = []

        #: list of pages in the order of appearance
        self.pages = []

        #: container of the pages
        self.pages_container = self.pages_container_class(padding=1)

        #: container of tabs. useful if you want to adjust padding/placement
        self.tabs_container = Group(fill=False, spacing=tab_spacing)
        self.tabs_container.on_mouse_over = lambda button: False  # ignore select-on-drag

        # viewport so that tabs don't go out of their area
        self._tabs_viewport = Viewport()
        self._tabs_viewport.get_min_size = self._tabs_viewport_get_min_size
        self._tabs_viewport.resize_children = self._tabs_viewport_resize_children
        self._tabs_viewport.add_child(self.tabs_container)

        #: wether scroll buttons should select next/previos tab or show the
        #: next/previos tab out of the view
        self.scroll_selects_tab = scroll_selects_tab

        #: container for custom content before tabs
        self.before_tabs = HBox(expand=False)

        #: container for custom content after tabs
        self.after_tabs = HBox(expand=False)

        #: button to scroll tabs back
        self.tabs_back = self.scroll_buttons_class("left",
                                                   expand=False,
                                                   visible=False,
                                                   enabled=False,
                                                   repeat_down_delay = 150)
        self.tabs_back.connect("on-mouse-down", self.on_back_press)

        #: button to scroll tabs forward
        self.tabs_forward = self.scroll_buttons_class("right",
                                                      expand=False,
                                                      visible=False,
                                                      enabled=False,
                                                      repeat_down_delay = 150)
        self.tabs_forward.connect("on-mouse-down", self.on_forward_press)


        #: the wrapping container that holds also the scroll buttons and everyting
        self.tabbox = self.tabbox_class(expand = False, expand_vert = False)
        self.tabbox.get_min_size = self.tabbox_get_min_size
        self.tabbox.get_height_for_width_size = self.tabbox_get_height_for_width_size


        self.tabbox.add_child(self.before_tabs, self.tabs_back,
                              self._tabs_viewport,
                              self.tabs_forward, self.after_tabs)

        #: current page
        self.current_page = 0

        #: tab position: top, right, bottom, left and combinations: "top-right", "left-bottom", etc.
        self.tab_position = tab_position


        for label in labels or []:
            self.add_page(label)

        #: where to place the scroll buttons on tab overflow. one of "start"
        #: (both at the start), "end" (both at the end) or "around" (on left
        #: and right of the tabs)
        self.scroll_position = scroll_position

        #: determines when to show scroll buttons. True for always, False for
        #: never, "auto" for auto appearing and disappearing, and
        #: "auto_invisible" for going transparent instead of disappearing
        #: (the latter avoids tab toggle)
        self.show_scroll = show_scroll
Example #2
0
    def __init__(self,
                 labels=None,
                 tab_position="top",
                 tab_spacing=0,
                 scroll_position=None,
                 show_scroll="auto",
                 scroll_selects_tab=True,
                 **kwargs):
        Box.__init__(self, horizontal=False, spacing=0, **kwargs)

        #: list of tabs in the order of appearance
        self.tabs = []

        #: list of pages in the order of appearance
        self.pages = []

        #: container of the pages
        self.pages_container = self.pages_container_class(padding=1)

        #: container of tabs. useful if you want to adjust padding/placement
        self.tabs_container = Group(fill=False, spacing=tab_spacing)
        self.tabs_container.on_mouse_over = lambda button: False  # ignore select-on-drag

        # viewport so that tabs don't go out of their area
        self._tabs_viewport = Viewport()
        self._tabs_viewport.get_min_size = self._tabs_viewport_get_min_size
        self._tabs_viewport.resize_children = self._tabs_viewport_resize_children
        self._tabs_viewport.add_child(self.tabs_container)

        #: wether scroll buttons should select next/previos tab or show the
        #: next/previos tab out of the view
        self.scroll_selects_tab = scroll_selects_tab

        #: container for custom content before tabs
        self.before_tabs = HBox(expand=False)

        #: container for custom content after tabs
        self.after_tabs = HBox(expand=False)

        #: button to scroll tabs back
        self.tabs_back = self.scroll_buttons_class("left",
                                                   expand=False,
                                                   visible=False,
                                                   enabled=False,
                                                   repeat_down_delay=150)
        self.tabs_back.connect("on-mouse-down", self.on_back_press)

        #: button to scroll tabs forward
        self.tabs_forward = self.scroll_buttons_class("right",
                                                      expand=False,
                                                      visible=False,
                                                      enabled=False,
                                                      repeat_down_delay=150)
        self.tabs_forward.connect("on-mouse-down", self.on_forward_press)

        #: the wrapping container that holds also the scroll buttons and everyting
        self.tabbox = self.tabbox_class(expand=False, expand_vert=False)
        self.tabbox.get_min_size = self.tabbox_get_min_size
        self.tabbox.get_height_for_width_size = self.tabbox_get_height_for_width_size

        self.tabbox.add_child(self.before_tabs, self.tabs_back,
                              self._tabs_viewport, self.tabs_forward,
                              self.after_tabs)

        #: current page
        self.current_page = 0

        #: tab position: top, right, bottom, left and combinations: "top-right", "left-bottom", etc.
        self.tab_position = tab_position

        for label in labels or []:
            self.add_page(label)

        #: where to place the scroll buttons on tab overflow. one of "start"
        #: (both at the start), "end" (both at the end) or "around" (on left
        #: and right of the tabs)
        self.scroll_position = scroll_position

        #: determines when to show scroll buttons. True for always, False for
        #: never, "auto" for auto appearing and disappearing, and
        #: "auto_invisible" for going transparent instead of disappearing
        #: (the latter avoids tab toggle)
        self.show_scroll = show_scroll
Example #3
0
class Notebook(Box):
    """Container that allows grouping children in tab pages"""

    __gsignals__ = {
        "on-tab-change": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
    }

    #: class to use for constructing the overflow buttons that appear on overflow
    scroll_buttons_class = ScrollButton

    #: class for the wrapping container
    tabbox_class = HBox

    #: class for the pages container
    pages_container_class = Container


    def __init__(self, labels = None, tab_position="top", tab_spacing = 0,
                 scroll_position = None, show_scroll = "auto", scroll_selects_tab = True, **kwargs):
        Box.__init__(self, horizontal=False, spacing=0, **kwargs)

        #: list of tabs in the order of appearance
        self.tabs = []

        #: list of pages in the order of appearance
        self.pages = []

        #: container of the pages
        self.pages_container = self.pages_container_class(padding=1)

        #: container of tabs. useful if you want to adjust padding/placement
        self.tabs_container = Group(fill=False, spacing=tab_spacing)
        self.tabs_container.on_mouse_over = lambda button: False  # ignore select-on-drag

        # viewport so that tabs don't go out of their area
        self._tabs_viewport = Viewport()
        self._tabs_viewport.get_min_size = self._tabs_viewport_get_min_size
        self._tabs_viewport.resize_children = self._tabs_viewport_resize_children
        self._tabs_viewport.add_child(self.tabs_container)

        #: wether scroll buttons should select next/previos tab or show the
        #: next/previos tab out of the view
        self.scroll_selects_tab = scroll_selects_tab

        #: container for custom content before tabs
        self.before_tabs = HBox(expand=False)

        #: container for custom content after tabs
        self.after_tabs = HBox(expand=False)

        #: button to scroll tabs back
        self.tabs_back = self.scroll_buttons_class("left",
                                                   expand=False,
                                                   visible=False,
                                                   enabled=False,
                                                   repeat_down_delay = 150)
        self.tabs_back.connect("on-mouse-down", self.on_back_press)

        #: button to scroll tabs forward
        self.tabs_forward = self.scroll_buttons_class("right",
                                                      expand=False,
                                                      visible=False,
                                                      enabled=False,
                                                      repeat_down_delay = 150)
        self.tabs_forward.connect("on-mouse-down", self.on_forward_press)


        #: the wrapping container that holds also the scroll buttons and everyting
        self.tabbox = self.tabbox_class(expand = False, expand_vert = False)
        self.tabbox.get_min_size = self.tabbox_get_min_size
        self.tabbox.get_height_for_width_size = self.tabbox_get_height_for_width_size


        self.tabbox.add_child(self.before_tabs, self.tabs_back,
                              self._tabs_viewport,
                              self.tabs_forward, self.after_tabs)

        #: current page
        self.current_page = 0

        #: tab position: top, right, bottom, left and combinations: "top-right", "left-bottom", etc.
        self.tab_position = tab_position


        for label in labels or []:
            self.add_page(label)

        #: where to place the scroll buttons on tab overflow. one of "start"
        #: (both at the start), "end" (both at the end) or "around" (on left
        #: and right of the tabs)
        self.scroll_position = scroll_position

        #: determines when to show scroll buttons. True for always, False for
        #: never, "auto" for auto appearing and disappearing, and
        #: "auto_invisible" for going transparent instead of disappearing
        #: (the latter avoids tab toggle)
        self.show_scroll = show_scroll



    def __setattr__(self, name, val):
        if name == "tab_spacing":
            self.tabs_container.spacing = val
        else:
            if name == "current_page":
                val = self.find_page(val)

            if self.__dict__.get(name, "hamster_graphics_no_value_really") == val:
                return
            Box.__setattr__(self, name, val)

            if name == "tab_position" and hasattr(self, "tabs_container"):
                self.tabs_container.x = 0
                self._position_contents()

            elif name == "scroll_position":
                # reorder sprites based on scroll position
                if val == "start":
                    sprites = [self.before_tabs, self.tabs_back, self.tabs_forward, self._tabs_viewport, self.after_tabs]
                elif val == "end":
                    sprites = [self.before_tabs, self._tabs_viewport, self.tabs_back, self.tabs_forward, self.after_tabs]
                else:
                    sprites = [self.before_tabs, self.tabs_back, self._tabs_viewport, self.tabs_forward, self.after_tabs]
                self.tabbox.sprites = sprites
            elif name == "current_page":
                self._select_current_page()


    def add_page(self, tab, contents = None, index = None):
        """inserts a new page with the given label
        will perform insert if index is specified. otherwise will append the
        new tab to the end.
        tab can be either a string or a widget. if it is a string, a
        ui.NootebookTab will be created.

        Returns: added page and tab
        """
        if isinstance(tab, basestring):
            tab = NotebookTab(tab)

        tab.attachment = "bottom" if self.tab_position.startswith("bottom") else "top"
        self.tabs_container.connect_child(tab, "on-mouse-down", self.on_tab_down)

        page = Container(contents, visible=False)
        page.tab = tab
        self.pages_container.connect_child(page, "on-render", self.on_page_render)

        if index is None:
            self.tabs.append(tab)
            self.pages.append(page)
            self.tabs_container.add_child(tab)
            self.pages_container.add_child(page)
        else:
            self.tabs.insert(index, tab)
            self.pages.insert(index, page)
            self.tabs_container.insert(index, tab)
            self.pages_container.insert(index, tab)


        self.current_page = self.current_page or page
        self._position_contents()

        if self.get_scene():
            self.tabs_container.queue_resize()
            self.tabbox.resize_children()

        return page, tab


    def remove_page(self, page):
        """remove given page. can also pass in the page index"""
        page = self.find_page(page)
        if not page:
            return

        idx = self.pages.index(page)

        self.pages_container.remove_child(page)
        del self.pages[idx]

        self.tabs_container.remove_child(self.tabs[idx])
        del self.tabs[idx]

        if page == self.current_page:
            self.current_page = idx

        self.tabs_container.resize_children()
        self._position_contents()


    def find_page(self, page):
        """find page by index, tab label or tab object"""
        if not self.pages:
            return None

        if page in self.pages:
            return page
        elif isinstance(page, int):
            page = min(len(self.pages)-1, max(page, 0))
            return self.pages[page]
        elif isinstance(page, basestring) or isinstance(page, NotebookTab):
            for i, tab in enumerate(self.tabs):
                if tab == page or tab.label == page:
                    found_page = self.pages[i]
                    return found_page
        return None

    def _select_current_page(self):
        self.emit("on-tab-change", self.current_page)

        if not self.current_page:
            return

        self.tabs[self.pages.index(self.current_page)].toggle()
        for page in self.pages:
            page.visible = page == self.current_page

        self.current_page.grab_focus()


    def scroll_to_tab(self, tab):
        """scroll the tab list so that the specified tab is visible
        you can pass in the tab object, index or label"""
        if isinstance(tab, int):
            tab = self.tabs[tab]

        if isinstance(tab, basestring):
            for target_tab in self.tabs:
                if target_tab.label == tab:
                    tab = target_tab
                    break

        if self.tabs_container.x + tab.x < 0:
            self.tabs_container.x = -tab.x
        elif self.tabs_container.x + tab.x + tab.width > self._tabs_viewport.width:
            self.tabs_container.x = -(tab.x + tab.width - self._tabs_viewport.width) - 1
        self._position_tabs()


    """resizing and positioning"""
    def resize_children(self):
        Box.resize_children(self)

        pos = self.tab_position
        horizontal = pos.startswith("right") or pos.startswith("left")
        if horizontal:
            self.tabbox.alloc_w, self.tabbox.alloc_h = self.tabbox.alloc_h, self.tabbox.alloc_w

        if pos.startswith("right"):
            self.tabbox.x += self.tabbox.height
        elif pos.startswith("left"):
            self.tabbox.y += self.tabbox.width


        # show/hide thes croll buttons
        # doing it here to avoid recursion as changing visibility calls parent resize
        self.tabs_back.visible = self.tabs_forward.visible = self.show_scroll in (True, "auto_invisible")
        self.tabbox.resize_children()
        self.tabs_container.resize_children()


        if self.show_scroll == "auto_invisible":
            self.tabs_back.visible = self.tabs_forward.visible = True
            if self.tabs_container.width < self._tabs_viewport.width:
                self.tabs_back.opacity = self.tabs_forward.opacity = 0
            else:
                self.tabs_back.opacity = self.tabs_forward.opacity = 1

        else:
            self.tabs_back.opacity = self.tabs_forward.opacity = 1
            self.tabs_back.visible = self.tabs_forward.visible = self.show_scroll is True or \
                                                                (self.show_scroll == "auto" and \
                                                                 self.tabs_container.width > self._tabs_viewport.width)

        self.tabbox.resize_children()
        self._position_tabs()



    def tabbox_get_min_size(self):
        w, h = HBox.get_min_size(self.tabbox)
        return h, h

    def tabbox_get_height_for_width_size(self):
        w, h = HBox.get_min_size(self.tabbox)

        if self.tab_position.startswith("right") or self.tab_position.startswith("left"):
            w, h = h, w

        return w, h

    def _tabs_viewport_get_min_size(self):
        # viewport has no demands on size, so we ask the tabs container
        # when positioned on top, tell that we need at least the height
        # when on the side tell that we need at least the width
        w, h = self.tabs_container.get_min_size()
        return 50, h


    def _tabs_viewport_resize_children(self):
        # allow x_align to take effect only if tabs fit.
        x = max(self.tabs_container.x, self._tabs_viewport.width - self.tabs_container.width - 1)

        Bin.resize_children(self._tabs_viewport)

        if self.tabs_container.width > self._tabs_viewport.width:
            self.tabs_container.x = x

        self._position_tabs()


    """utilities"""
    def _position_tabs(self):
        if self.scroll_selects_tab and self.current_page:
            tab = self.current_page.tab
            if self.tabs_container.x + tab.x + tab.width > self._tabs_viewport.width:
                self.tabs_container.x = -(tab.x + tab.width - self._tabs_viewport.width)
            elif self.tabs_container.x + tab.x < 0:
                self.tabs_container.x = -tab.x


        # find first good tab if we all don't fit
        if self.tabs_container.width > self._tabs_viewport.width:
            for tab in self.tabs:
                if tab.x + self.tabs_container.x >= 0:
                    self.tabs_container.x = -tab.x
                    break

        # update opacity so we are not showing partial tabs
        for tab in self.tabs:
            if self.tabs_container.x + tab.x < 0 or self.tabs_container.x + tab.x + tab.width > self._tabs_viewport.width:
                tab.opacity = 0
            else:
                tab.opacity = 1


        # set scroll buttons clickable
        if self.scroll_selects_tab:
            self.tabs_back.enabled = self.current_page and self.pages.index(self.current_page) > 0
            self.tabs_forward.enabled = self.current_page and self.pages.index(self.current_page) < len(self.pages) - 1
        else:
            self.tabs_back.enabled = self.tabs_container.x  < -self.tabs_container.padding_left
            self.tabs_forward.enabled = self.tabs_container.x + self.tabs_container.width > self._tabs_viewport.width


    def _position_contents(self):
        attachment, alignment = self.tab_position or "top", "left"
        if "-" in self.tab_position:
            attachment, alignment = self.tab_position.split("-")

        self.orient_horizontal = attachment in ("right", "left")

        if alignment == "center":
            self.tabs_container.x_align = 0.5
        elif alignment in ("right", "bottom"):
            self.tabs_container.x_align = 1
        else:
            self.tabs_container.x_align = 0

        # on left side the rotation is upside down
        if attachment == "left":
            self.tabs_container.x_align = 1 - self.tabs_container.x_align

        if attachment == "bottom":
            self.tabs_container.y_align = 0
        else:
            self.tabs_container.y_align = 1

        for tab in self.tabs:
            tab.attachment = attachment

        self.clear()
        if attachment == "right":
            self.add_child(self.pages_container, self.tabbox)
            self.tabbox.rotation = math.pi / 2
        elif attachment == "left":
            self.add_child(self.tabbox, self.pages_container)
            self.tabbox.rotation = -math.pi / 2
        elif attachment == "bottom":
            self.add_child(self.pages_container, self.tabbox)
            self.tabbox.rotation = 0
        else: # defaults to top
            self.add_child(self.tabbox, self.pages_container)
            self.tabbox.rotation = 0


        for tab in self.tabs:
            tab.pivot_x = tab.width / 2
            tab.pivot_y = tab.height / 2

            tab.container.pivot_x = tab.container.width / 2
            tab.container.pivot_y = tab.container.height / 2
            if attachment == "bottom":
                tab.rotation = math.pi
                tab.container.rotation = math.pi
            else:
                tab.rotation = 0
                tab.container.rotation = 0


            if tab.force_vertical_image and tab.image:
                tab.image.pivot_x = tab.image.width / 2
                tab.image.pivot_y = tab.image.height / 2

                if attachment == "right":
                    tab.image.rotation = -math.pi / 2
                elif attachment == "left":
                    tab.image.rotation = math.pi / 2
                else:
                    tab.image.rotation = 0

        self.queue_resize()


    """mouse events"""
    def on_back_press(self, button, event):
        if self.scroll_selects_tab:
            if self.pages.index(self.current_page) > 0:
                self.current_page = self.pages.index(self.current_page) - 1
        else:
            # find the first elem before 0:
            for tab in reversed(self.tabs):
                if self.tabs_container.x + tab.x < 0:
                    self.tabs_container.x = -tab.x
                    break
        self._position_tabs()


    def on_forward_press(self, button, event):
        if self.scroll_selects_tab:
            if self.pages.index(self.current_page) < len(self.pages):
                self.current_page = self.pages.index(self.current_page) + 1
        else:
            if self.tabs_container.x + self.tabs_container.width > self._tabs_viewport.width:
                # find the first which doesn't fit:
                found = None
                for tab in self.tabs:
                    if self.tabs_container.x + tab.x + tab.width > self._tabs_viewport.width:
                        found = True
                        break

                if found:
                    self.tabs_container.x = -(tab.x + tab.width - self._tabs_viewport.width) - 1
            else:
                self.tabs_container.x = -(self.tabs_container.width - self._tabs_viewport.width)
        self._position_tabs()

    def on_tab_down(self, tab, event):
        self.current_page = tab


    """rendering"""
    def on_page_render(self, page):
        page.graphics.rectangle(0, 0, page.width, page.height)
        page.graphics.clip()

    def do_render(self):
        self.graphics.set_line_style(width = 1)

        x, y, w, h = (self.pages_container.x + 0.5,
                      self.pages_container.y + 0.5,
                      self.pages_container.width-1,
                      self.pages_container.height-1)

        self.graphics.rectangle(x, y, w, h)
        self.graphics.fill_stroke("#fafafa", "#999")
Example #4
0
class Notebook(Box):
    """Container that allows grouping children in tab pages"""

    __gsignals__ = {
        "on-tab-change": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
                          (gobject.TYPE_PYOBJECT, )),
    }

    #: class to use for constructing the overflow buttons that appear on overflow
    scroll_buttons_class = ScrollButton

    #: class for the wrapping container
    tabbox_class = HBox

    #: class for the pages container
    pages_container_class = Container

    def __init__(self,
                 labels=None,
                 tab_position="top",
                 tab_spacing=0,
                 scroll_position=None,
                 show_scroll="auto",
                 scroll_selects_tab=True,
                 **kwargs):
        Box.__init__(self, horizontal=False, spacing=0, **kwargs)

        #: list of tabs in the order of appearance
        self.tabs = []

        #: list of pages in the order of appearance
        self.pages = []

        #: container of the pages
        self.pages_container = self.pages_container_class(padding=1)

        #: container of tabs. useful if you want to adjust padding/placement
        self.tabs_container = Group(fill=False, spacing=tab_spacing)
        self.tabs_container.on_mouse_over = lambda button: False  # ignore select-on-drag

        # viewport so that tabs don't go out of their area
        self._tabs_viewport = Viewport()
        self._tabs_viewport.get_min_size = self._tabs_viewport_get_min_size
        self._tabs_viewport.resize_children = self._tabs_viewport_resize_children
        self._tabs_viewport.add_child(self.tabs_container)

        #: wether scroll buttons should select next/previos tab or show the
        #: next/previos tab out of the view
        self.scroll_selects_tab = scroll_selects_tab

        #: container for custom content before tabs
        self.before_tabs = HBox(expand=False)

        #: container for custom content after tabs
        self.after_tabs = HBox(expand=False)

        #: button to scroll tabs back
        self.tabs_back = self.scroll_buttons_class("left",
                                                   expand=False,
                                                   visible=False,
                                                   enabled=False,
                                                   repeat_down_delay=150)
        self.tabs_back.connect("on-mouse-down", self.on_back_press)

        #: button to scroll tabs forward
        self.tabs_forward = self.scroll_buttons_class("right",
                                                      expand=False,
                                                      visible=False,
                                                      enabled=False,
                                                      repeat_down_delay=150)
        self.tabs_forward.connect("on-mouse-down", self.on_forward_press)

        #: the wrapping container that holds also the scroll buttons and everyting
        self.tabbox = self.tabbox_class(expand=False, expand_vert=False)
        self.tabbox.get_min_size = self.tabbox_get_min_size
        self.tabbox.get_height_for_width_size = self.tabbox_get_height_for_width_size

        self.tabbox.add_child(self.before_tabs, self.tabs_back,
                              self._tabs_viewport, self.tabs_forward,
                              self.after_tabs)

        #: current page
        self.current_page = 0

        #: tab position: top, right, bottom, left and combinations: "top-right", "left-bottom", etc.
        self.tab_position = tab_position

        for label in labels or []:
            self.add_page(label)

        #: where to place the scroll buttons on tab overflow. one of "start"
        #: (both at the start), "end" (both at the end) or "around" (on left
        #: and right of the tabs)
        self.scroll_position = scroll_position

        #: determines when to show scroll buttons. True for always, False for
        #: never, "auto" for auto appearing and disappearing, and
        #: "auto_invisible" for going transparent instead of disappearing
        #: (the latter avoids tab toggle)
        self.show_scroll = show_scroll

    def __setattr__(self, name, val):
        if name == "tab_spacing":
            self.tabs_container.spacing = val
        else:
            if name == "current_page":
                val = self.find_page(val)

            if self.__dict__.get(name,
                                 "hamster_graphics_no_value_really") == val:
                return
            Box.__setattr__(self, name, val)

            if name == "tab_position" and hasattr(self, "tabs_container"):
                self.tabs_container.x = 0
                self._position_contents()

            elif name == "scroll_position":
                # reorder sprites based on scroll position
                if val == "start":
                    sprites = [
                        self.before_tabs, self.tabs_back, self.tabs_forward,
                        self._tabs_viewport, self.after_tabs
                    ]
                elif val == "end":
                    sprites = [
                        self.before_tabs, self._tabs_viewport, self.tabs_back,
                        self.tabs_forward, self.after_tabs
                    ]
                else:
                    sprites = [
                        self.before_tabs, self.tabs_back, self._tabs_viewport,
                        self.tabs_forward, self.after_tabs
                    ]
                self.tabbox.sprites = sprites
            elif name == "current_page":
                self._select_current_page()

    def add_page(self, tab, contents=None, index=None):
        """inserts a new page with the given label
        will perform insert if index is specified. otherwise will append the
        new tab to the end.
        tab can be either a string or a widget. if it is a string, a
        ui.NootebookTab will be created.

        Returns: added page and tab
        """
        if isinstance(tab, basestring):
            tab = NotebookTab(tab)

        tab.attachment = "bottom" if self.tab_position.startswith(
            "bottom") else "top"
        self.tabs_container.connect_child(tab, "on-mouse-down",
                                          self.on_tab_down)

        page = Container(contents, visible=False)
        page.tab = tab
        self.pages_container.connect_child(page, "on-render",
                                           self.on_page_render)

        if index is None:
            self.tabs.append(tab)
            self.pages.append(page)
            self.tabs_container.add_child(tab)
            self.pages_container.add_child(page)
        else:
            self.tabs.insert(index, tab)
            self.pages.insert(index, page)
            self.tabs_container.insert(index, tab)
            self.pages_container.insert(index, tab)

        self.current_page = self.current_page or page
        self._position_contents()

        if self.get_scene():
            self.tabs_container.queue_resize()
            self.tabbox.resize_children()

        return page, tab

    def remove_page(self, page):
        """remove given page. can also pass in the page index"""
        page = self.find_page(page)
        if not page:
            return

        idx = self.pages.index(page)

        self.pages_container.remove_child(page)
        del self.pages[idx]

        self.tabs_container.remove_child(self.tabs[idx])
        del self.tabs[idx]

        if page == self.current_page:
            self.current_page = idx

        self.tabs_container.resize_children()
        self._position_contents()

    def find_page(self, page):
        """find page by index, tab label or tab object"""
        if not self.pages:
            return None

        if page in self.pages:
            return page
        elif isinstance(page, int):
            page = min(len(self.pages) - 1, max(page, 0))
            return self.pages[page]
        elif isinstance(page, basestring) or isinstance(page, NotebookTab):
            for i, tab in enumerate(self.tabs):
                if tab == page or tab.label == page:
                    found_page = self.pages[i]
                    return found_page
        return None

    def _select_current_page(self):
        self.emit("on-tab-change", self.current_page)

        if not self.current_page:
            return

        self.tabs[self.pages.index(self.current_page)].toggle()
        for page in self.pages:
            page.visible = page == self.current_page

        self.current_page.grab_focus()

    def scroll_to_tab(self, tab):
        """scroll the tab list so that the specified tab is visible
        you can pass in the tab object, index or label"""
        if isinstance(tab, int):
            tab = self.tabs[tab]

        if isinstance(tab, basestring):
            for target_tab in self.tabs:
                if target_tab.label == tab:
                    tab = target_tab
                    break

        if self.tabs_container.x + tab.x < 0:
            self.tabs_container.x = -tab.x
        elif self.tabs_container.x + tab.x + tab.width > self._tabs_viewport.width:
            self.tabs_container.x = -(tab.x + tab.width -
                                      self._tabs_viewport.width) - 1
        self._position_tabs()

    """resizing and positioning"""

    def resize_children(self):
        Box.resize_children(self)

        pos = self.tab_position
        horizontal = pos.startswith("right") or pos.startswith("left")
        if horizontal:
            self.tabbox.alloc_w, self.tabbox.alloc_h = self.tabbox.alloc_h, self.tabbox.alloc_w

        if pos.startswith("right"):
            self.tabbox.x += self.tabbox.height
        elif pos.startswith("left"):
            self.tabbox.y += self.tabbox.width

        # show/hide thes croll buttons
        # doing it here to avoid recursion as changing visibility calls parent resize
        self.tabs_back.visible = self.tabs_forward.visible = self.show_scroll in (
            True, "auto_invisible")
        self.tabbox.resize_children()
        self.tabs_container.resize_children()

        if self.show_scroll == "auto_invisible":
            self.tabs_back.visible = self.tabs_forward.visible = True
            if self.tabs_container.width < self._tabs_viewport.width:
                self.tabs_back.opacity = self.tabs_forward.opacity = 0
            else:
                self.tabs_back.opacity = self.tabs_forward.opacity = 1

        else:
            self.tabs_back.opacity = self.tabs_forward.opacity = 1
            self.tabs_back.visible = self.tabs_forward.visible = self.show_scroll is True or \
                                                                (self.show_scroll == "auto" and \
                                                                 self.tabs_container.width > self._tabs_viewport.width)

        self.tabbox.resize_children()
        self._position_tabs()

    def tabbox_get_min_size(self):
        w, h = HBox.get_min_size(self.tabbox)
        return h, h

    def tabbox_get_height_for_width_size(self):
        w, h = HBox.get_min_size(self.tabbox)

        if self.tab_position.startswith(
                "right") or self.tab_position.startswith("left"):
            w, h = h, w

        return w, h

    def _tabs_viewport_get_min_size(self):
        # viewport has no demands on size, so we ask the tabs container
        # when positioned on top, tell that we need at least the height
        # when on the side tell that we need at least the width
        w, h = self.tabs_container.get_min_size()
        return 50, h

    def _tabs_viewport_resize_children(self):
        # allow x_align to take effect only if tabs fit.
        x = max(self.tabs_container.x,
                self._tabs_viewport.width - self.tabs_container.width - 1)

        Bin.resize_children(self._tabs_viewport)

        if self.tabs_container.width > self._tabs_viewport.width:
            self.tabs_container.x = x

        self._position_tabs()

    """utilities"""

    def _position_tabs(self):
        if self.scroll_selects_tab and self.current_page:
            tab = self.current_page.tab
            if self.tabs_container.x + tab.x + tab.width > self._tabs_viewport.width:
                self.tabs_container.x = -(tab.x + tab.width -
                                          self._tabs_viewport.width)
            elif self.tabs_container.x + tab.x < 0:
                self.tabs_container.x = -tab.x

        # find first good tab if we all don't fit
        if self.tabs_container.width > self._tabs_viewport.width:
            for tab in self.tabs:
                if tab.x + self.tabs_container.x >= 0:
                    self.tabs_container.x = -tab.x
                    break

        # update opacity so we are not showing partial tabs
        for tab in self.tabs:
            if self.tabs_container.x + tab.x < 0 or self.tabs_container.x + tab.x + tab.width > self._tabs_viewport.width:
                tab.opacity = 0
            else:
                tab.opacity = 1

        # set scroll buttons clickable
        if self.scroll_selects_tab:
            self.tabs_back.enabled = self.current_page and self.pages.index(
                self.current_page) > 0
            self.tabs_forward.enabled = self.current_page and self.pages.index(
                self.current_page) < len(self.pages) - 1
        else:
            self.tabs_back.enabled = self.tabs_container.x < -self.tabs_container.padding_left
            self.tabs_forward.enabled = self.tabs_container.x + self.tabs_container.width > self._tabs_viewport.width

    def _position_contents(self):
        attachment, alignment = self.tab_position or "top", "left"
        if "-" in self.tab_position:
            attachment, alignment = self.tab_position.split("-")

        self.orient_horizontal = attachment in ("right", "left")

        if alignment == "center":
            self.tabs_container.x_align = 0.5
        elif alignment in ("right", "bottom"):
            self.tabs_container.x_align = 1
        else:
            self.tabs_container.x_align = 0

        # on left side the rotation is upside down
        if attachment == "left":
            self.tabs_container.x_align = 1 - self.tabs_container.x_align

        if attachment == "bottom":
            self.tabs_container.y_align = 0
        else:
            self.tabs_container.y_align = 1

        for tab in self.tabs:
            tab.attachment = attachment

        self.clear()
        if attachment == "right":
            self.add_child(self.pages_container, self.tabbox)
            self.tabbox.rotation = math.pi / 2
        elif attachment == "left":
            self.add_child(self.tabbox, self.pages_container)
            self.tabbox.rotation = -math.pi / 2
        elif attachment == "bottom":
            self.add_child(self.pages_container, self.tabbox)
            self.tabbox.rotation = 0
        else:  # defaults to top
            self.add_child(self.tabbox, self.pages_container)
            self.tabbox.rotation = 0

        for tab in self.tabs:
            tab.pivot_x = tab.width / 2
            tab.pivot_y = tab.height / 2

            tab.container.pivot_x = tab.container.width / 2
            tab.container.pivot_y = tab.container.height / 2
            if attachment == "bottom":
                tab.rotation = math.pi
                tab.container.rotation = math.pi
            else:
                tab.rotation = 0
                tab.container.rotation = 0

            if tab.force_vertical_image and tab.image:
                tab.image.pivot_x = tab.image.width / 2
                tab.image.pivot_y = tab.image.height / 2

                if attachment == "right":
                    tab.image.rotation = -math.pi / 2
                elif attachment == "left":
                    tab.image.rotation = math.pi / 2
                else:
                    tab.image.rotation = 0

        self.queue_resize()

    """mouse events"""

    def on_back_press(self, button, event):
        if self.scroll_selects_tab:
            if self.pages.index(self.current_page) > 0:
                self.current_page = self.pages.index(self.current_page) - 1
        else:
            # find the first elem before 0:
            for tab in reversed(self.tabs):
                if self.tabs_container.x + tab.x < 0:
                    self.tabs_container.x = -tab.x
                    break
        self._position_tabs()

    def on_forward_press(self, button, event):
        if self.scroll_selects_tab:
            if self.pages.index(self.current_page) < len(self.pages):
                self.current_page = self.pages.index(self.current_page) + 1
        else:
            if self.tabs_container.x + self.tabs_container.width > self._tabs_viewport.width:
                # find the first which doesn't fit:
                found = None
                for tab in self.tabs:
                    if self.tabs_container.x + tab.x + tab.width > self._tabs_viewport.width:
                        found = True
                        break

                if found:
                    self.tabs_container.x = -(tab.x + tab.width -
                                              self._tabs_viewport.width) - 1
            else:
                self.tabs_container.x = -(self.tabs_container.width -
                                          self._tabs_viewport.width)
        self._position_tabs()

    def on_tab_down(self, tab, event):
        self.current_page = tab

    """rendering"""

    def on_page_render(self, page):
        page.graphics.rectangle(0, 0, page.width, page.height)
        page.graphics.clip()

    def do_render(self):
        self.graphics.set_line_style(width=1)

        x, y, w, h = (self.pages_container.x + 0.5,
                      self.pages_container.y + 0.5,
                      self.pages_container.width - 1,
                      self.pages_container.height - 1)

        self.graphics.rectangle(x, y, w, h)
        self.graphics.fill_stroke("#fafafa", "#999")