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")
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")
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")
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")