Пример #1
0
class ScrollArea(Table):
    """Container that will display scroll bars (either horizontal or vertical or
    both) if the space occupied by child is larger than the available.
    """

    #: scroll step size in pixels
    step_size = 30

    def __init__(self, contents=None, border=1, step_size=None,
                 scroll_horizontal="auto", scroll_vertical="auto",
                 **kwargs):
        Table.__init__(self, rows=2, cols=2, padding=[border, 0, 0, border], **kwargs)

        self.viewport = Viewport(x_align=0, y_align=0)
        self.interactive, self.can_focus = True, True

        if step_size:
            self.step_size = step_siz

        #: with of the surrounding border in pixels
        self.border = border

        #: visibility of the horizontal scroll bar. True for always, False for never and "auto" for auto
        self.scroll_horizontal = scroll_horizontal

        #: visibility of the vertical scroll bar. True for always, False for never and "auto" for auto
        self.scroll_vertical = scroll_vertical

        #even if we are don't need the scrollbar, do we reserve space for it?
        self.reserve_space_vertical = False
        self.reserve_space_horizontal = False


        #: vertical scroll bar widget
        self.vscroll = ScrollBar()

        #: horizontal scroll bar widget
        self.hscroll = ScrollBar(horizontal = True)

        self.attach(self.viewport, 0, 1, 0, 1)
        self.attach(self.vscroll, 1, 2, 0, 1)
        self.attach(self.hscroll, 0, 1, 1, 2)


        if contents:
            if isinstance(contents, graphics.Sprite):
                contents = [contents]

            for sprite in contents:
                self.add_child(sprite)

        self.connect("on-mouse-scroll", self.__on_mouse_scroll)
        for bar in (self.vscroll, self.hscroll):
            self.connect_child(bar, "on-scroll", self.on_scroll)
            self.connect_child(bar, "on-scroll-step", self.on_scroll_step)
            self.connect_child(bar, "on-scroll-page", self.on_scroll_page)


    def __setattr__(self, name, val):
        Table.__setattr__(self, name, val)
        if name in ("scroll_horizontal", "scroll_vertical"):
            self.queue_resize()

    def add_child(self, *sprites):
        for sprite in sprites:
            if sprite in (self.viewport, self.vscroll, self.hscroll):
                Table.add_child(self, sprite)
            else:
                self.viewport.add_child(*sprites)

    def get_min_size(self):
        return self.min_width or 0, self.min_height or 0

    def resize_children(self):
        # give viewport all our space
        w, h = self.viewport.alloc_w, self.viewport.alloc_w
        self.viewport.alloc_w = self.width - self.horizontal_padding
        self.viewport.alloc_h = self.height - self.vertical_padding

        # then check if it fits
        area_w, area_h = self.viewport.get_container_size()
        hvis = self.scroll_horizontal is True or (self.scroll_horizontal == "auto" and self.width < area_w)
        if hvis:
            if self.reserve_space_horizontal:
                self.hscroll.opacity = 1
            else:
                self.hscroll.visible = True
        else:
            if self.reserve_space_horizontal:
                self.hscroll.opacity = 0
            else:
                self.hscroll.visible = False
        vvis = self.scroll_vertical is True or (self.scroll_vertical == "auto" and self.height < area_h)
        if vvis:
            if self.reserve_space_vertical:
                self.vscroll.opacity = 1
            else:
                self.vscroll.visible = True
        else:
            if self.reserve_space_vertical:
                self.vscroll.opacity = 0
            else:
                self.vscroll.visible = False

        Table.resize_children(self)


        if self.viewport.child:
            self.scroll_x(self.viewport.child.x)
            self.scroll_y(self.viewport.child.y)


    def _scroll_y(self, y):
        # these are split into two to avoid echoes
        # check if we have anything to scroll
        area_h = self.viewport.get_container_size()[1]
        viewport_h = self.viewport.height

        if y < 0:
            y = max(y, viewport_h - area_h)
        y = min(y, 0)
        self.viewport.child.y = y

    def scroll_y(self, y):
        """scroll to y position"""
        self._scroll_y(y)
        self._update_sliders()

    def _scroll_x(self, x):
        area_w = self.viewport.get_container_size()[0]
        viewport_w = self.viewport.width
        if not viewport_w:
            return

        # when window grows pull in the viewport if it's out of the bounds
        if x < 0:
            x = max(x, viewport_w - area_w)
        x = min(x, 0)

        self.viewport.child.x = x

    def scroll_x(self, x):
        """scroll to x position"""
        self._scroll_x(x)
        self._update_sliders()


    def _update_sliders(self):
        area_w, area_h = self.viewport.get_container_size()
        area_w = area_w or 1 # avoid division by zero
        area_h = area_h or 1

        if self.vscroll.visible:
            v_aspect = min(float(self.viewport.height) / area_h, 1)
            self.vscroll.size = min(float(self.viewport.height) / area_h, 1)

            if v_aspect == 1:
                self.vscroll.offset = 0
            else:
                self.vscroll.offset = -1 * self.viewport.child.y / (area_h * (1 - v_aspect))

        if self.hscroll.visible:
            h_aspect = min(float(self.viewport.width) / area_w, 1)
            self.hscroll.size = min(float(self.viewport.width) / area_w, 1)
            if h_aspect == 1:
                self.hscroll.offset = 0
            else:
                self.hscroll.offset = -1 * self.viewport.child.x / (area_w * (1 - h_aspect))


    """events"""
    def __on_mouse_scroll(self, sprite, event):
        direction  = 1 if event.direction == gdk.ScrollDirection.DOWN else -1
        self.scroll_y(self.viewport.child.y - self.step_size * direction)

    def on_scroll(self, bar, offset):
        area_w, area_h = self.viewport.get_container_size()
        viewport_w, viewport_h = self.viewport.width, self.viewport.height

        if bar == self.vscroll:
            aspect = float(area_h - viewport_h) / area_h
            self._scroll_y(-1 * (area_h * aspect) * offset)
        else:
            aspect = float(area_w - viewport_w) / area_w
            self._scroll_x(-1 * (area_w * aspect) * offset)

    def on_scroll_step(self, bar, direction):
        if bar == self.vscroll:
            self.scroll_y(self.viewport.child.y - self.step_size * direction)
        else:
            self.scroll_x(self.viewport.child.x - self.step_size * direction)

    def on_scroll_page(self, bar, direction):
        if bar == self.vscroll:
            self.scroll_y(self.viewport.child.y - (self.viewport.height + self.step_size) * direction)
        else:
            self.scroll_x(self.viewport.child.y - (self.viewport.width + self.step_size) * direction)


    def do_render(self):
        if self.border:
            self.graphics.rectangle(0.5, 0.5, self.width, self.height)
            self.graphics.set_line_style(width=self.border)
            stroke_color = "#333" if self.focused else "#999"
            self.graphics.fill_stroke("#fff", stroke_color)
        else:
            self.graphics.rectangle(0, 0, self.width, self.height)
            self.graphics.fill("#fff")
Пример #2
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")
Пример #3
0
class ScrollArea(Table):
    """Container that will display scroll bars (either horizontal or vertical or
    both) if the space occupied by child is larger than the available.
    """

    #: scroll step size in pixels
    step_size = 30

    def __init__(self,
                 contents=None,
                 border=1,
                 step_size=None,
                 scroll_horizontal="auto",
                 scroll_vertical="auto",
                 **kwargs):
        Table.__init__(self,
                       rows=2,
                       cols=2,
                       padding=[border, 0, 0, border],
                       **kwargs)

        self.viewport = Viewport(x_align=0, y_align=0)
        self.interactive, self.can_focus = True, True

        if step_size:
            self.step_size = step_siz

        #: with of the surrounding border in pixels
        self.border = border

        #: visibility of the horizontal scroll bar. True for always, False for never and "auto" for auto
        self.scroll_horizontal = scroll_horizontal

        #: visibility of the vertical scroll bar. True for always, False for never and "auto" for auto
        self.scroll_vertical = scroll_vertical

        #even if we are don't need the scrollbar, do we reserve space for it?
        self.reserve_space_vertical = False
        self.reserve_space_horizontal = False

        #: vertical scroll bar widget
        self.vscroll = ScrollBar()

        #: horizontal scroll bar widget
        self.hscroll = ScrollBar(horizontal=True)

        self.attach(self.viewport, 0, 1, 0, 1)
        self.attach(self.vscroll, 1, 2, 0, 1)
        self.attach(self.hscroll, 0, 1, 1, 2)

        if contents:
            if isinstance(contents, graphics.Sprite):
                contents = [contents]

            for sprite in contents:
                self.add_child(sprite)

        self.connect("on-mouse-scroll", self.__on_mouse_scroll)
        for bar in (self.vscroll, self.hscroll):
            self.connect_child(bar, "on-scroll", self.on_scroll)
            self.connect_child(bar, "on-scroll-step", self.on_scroll_step)
            self.connect_child(bar, "on-scroll-page", self.on_scroll_page)

    def __setattr__(self, name, val):
        Table.__setattr__(self, name, val)
        if name in ("scroll_horizontal", "scroll_vertical"):
            self.queue_resize()

    def add_child(self, *sprites):
        for sprite in sprites:
            if sprite in (self.viewport, self.vscroll, self.hscroll):
                Table.add_child(self, sprite)
            else:
                self.viewport.add_child(*sprites)

    def get_min_size(self):
        return self.min_width or 0, self.min_height or 0

    def resize_children(self):
        # give viewport all our space
        w, h = self.viewport.alloc_w, self.viewport.alloc_w
        self.viewport.alloc_w = self.width - self.horizontal_padding
        self.viewport.alloc_h = self.height - self.vertical_padding

        # then check if it fits
        area_w, area_h = self.viewport.get_container_size()
        hvis = self.scroll_horizontal is True or (
            self.scroll_horizontal == "auto" and self.width < area_w)
        if hvis:
            if self.reserve_space_horizontal:
                self.hscroll.opacity = 1
            else:
                self.hscroll.visible = True
        else:
            if self.reserve_space_horizontal:
                self.hscroll.opacity = 0
            else:
                self.hscroll.visible = False
        vvis = self.scroll_vertical is True or (self.scroll_vertical == "auto"
                                                and self.height < area_h)
        if vvis:
            if self.reserve_space_vertical:
                self.vscroll.opacity = 1
            else:
                self.vscroll.visible = True
        else:
            if self.reserve_space_vertical:
                self.vscroll.opacity = 0
            else:
                self.vscroll.visible = False

        Table.resize_children(self)

        if self.viewport.child:
            self.scroll_x(self.viewport.child.x)
            self.scroll_y(self.viewport.child.y)

    def _scroll_y(self, y):
        # these are split into two to avoid echoes
        # check if we have anything to scroll
        area_h = self.viewport.get_container_size()[1]
        viewport_h = self.viewport.height

        if y < 0:
            y = max(y, viewport_h - area_h)
        y = min(y, 0)
        self.viewport.child.y = y

    def scroll_y(self, y):
        """scroll to y position"""
        self._scroll_y(y)
        self._update_sliders()

    def _scroll_x(self, x):
        area_w = self.viewport.get_container_size()[0]
        viewport_w = self.viewport.width
        if not viewport_w:
            return

        # when window grows pull in the viewport if it's out of the bounds
        if x < 0:
            x = max(x, viewport_w - area_w)
        x = min(x, 0)

        self.viewport.child.x = x

    def scroll_x(self, x):
        """scroll to x position"""
        self._scroll_x(x)
        self._update_sliders()

    def _update_sliders(self):
        area_w, area_h = self.viewport.get_container_size()
        area_w = area_w or 1  # avoid division by zero
        area_h = area_h or 1

        if self.vscroll.visible:
            v_aspect = min(float(self.viewport.height) / area_h, 1)
            self.vscroll.size = min(float(self.viewport.height) / area_h, 1)

            if v_aspect == 1:
                self.vscroll.offset = 0
            else:
                self.vscroll.offset = -1 * self.viewport.child.y / (
                    area_h * (1 - v_aspect))

        if self.hscroll.visible:
            h_aspect = min(float(self.viewport.width) / area_w, 1)
            self.hscroll.size = min(float(self.viewport.width) / area_w, 1)
            if h_aspect == 1:
                self.hscroll.offset = 0
            else:
                self.hscroll.offset = -1 * self.viewport.child.x / (
                    area_w * (1 - h_aspect))

    """events"""

    def __on_mouse_scroll(self, sprite, event):
        direction = 1 if event.direction == gdk.ScrollDirection.DOWN else -1
        self.scroll_y(self.viewport.child.y - self.step_size * direction)

    def on_scroll(self, bar, offset):
        area_w, area_h = self.viewport.get_container_size()
        viewport_w, viewport_h = self.viewport.width, self.viewport.height

        if bar == self.vscroll:
            aspect = float(area_h - viewport_h) / area_h
            self._scroll_y(-1 * (area_h * aspect) * offset)
        else:
            aspect = float(area_w - viewport_w) / area_w
            self._scroll_x(-1 * (area_w * aspect) * offset)

    def on_scroll_step(self, bar, direction):
        if bar == self.vscroll:
            self.scroll_y(self.viewport.child.y - self.step_size * direction)
        else:
            self.scroll_x(self.viewport.child.x - self.step_size * direction)

    def on_scroll_page(self, bar, direction):
        if bar == self.vscroll:
            self.scroll_y(self.viewport.child.y -
                          (self.viewport.height + self.step_size) * direction)
        else:
            self.scroll_x(self.viewport.child.y -
                          (self.viewport.width + self.step_size) * direction)

    def do_render(self):
        if self.border:
            self.graphics.rectangle(0.5, 0.5, self.width, self.height)
            self.graphics.set_line_style(width=self.border)
            stroke_color = "#333" if self.focused else "#999"
            self.graphics.fill_stroke("#fff", stroke_color)
        else:
            self.graphics.rectangle(0, 0, self.width, self.height)
            self.graphics.fill("#fff")
Пример #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")