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 __init__(self, column, row, on_update_state_action, parent_viewport, logger): def on_check(button): self.on_change_state(TRUE) self.on_update_state_action(TRUE) def on_uncheck(button): self.on_change_state(FALSE) self.on_update_state_action(FALSE) self.logger = logger self.column, self.row = column, row self.parent_viewport = parent_viewport self.viewport = Viewport() USER_DB_CURSOR.execute('SELECT current_locale FROM i18n') self.current_locale = USER_DB_CURSOR.fetchone()[0] self.on_update_state_action = on_update_state_action self.checked_checkbox_button, self.unchecked_checkbox_button = create_two_state_button( CheckedCheckboxButton(on_click_action=on_uncheck, parent_viewport=self.viewport), UncheckedCheckboxButton(on_click_action=on_check, parent_viewport=self.viewport)) self.buttons = [ self.checked_checkbox_button, self.unchecked_checkbox_button ] self.screen_resolution = (0, 0) self.description_label = None self.is_activated = False self.opacity = 0 self.on_window_resize_handlers = [ self.on_window_resize, ]
def __init__(self, stage_number, on_buy_stage_action, parent_viewport): def on_buy_shop_stage(button): button.on_deactivate(instant=True) self.on_buy_stage_action(self.stage_number) self.logger = getLogger( f'root.app.game.map.shop.view.cell.{stage_number}') self.is_activated = False self.stage_number = stage_number USER_DB_CURSOR.execute('SELECT current_locale FROM i18n') self.current_locale = USER_DB_CURSOR.fetchone()[0] self.on_buy_stage_action = on_buy_stage_action self.data = [] self.screen_resolution = (0, 0) self.parent_viewport = parent_viewport self.viewport = Viewport() self.locked_label = ShopStageLockedLabel(parent_viewport=self.viewport) self.level_placeholder_label = ShopStageLevelPlaceholderLabel( parent_viewport=self.viewport) self.previous_stage_placeholder_label = ShopStagePreviousStagePlaceholderLabel( parent_viewport=self.viewport) self.hourly_profit_description_label = ShopStageHourlyProfitDescriptionLabel( parent_viewport=self.viewport) self.hourly_profit_value_label = ShopStageHourlyProfitValueLabel( parent_viewport=self.viewport) self.storage_capacity_description_label = ShopStageStorageCapacityDescriptionLabel( parent_viewport=self.viewport) self.storage_capacity_value_label = ShopStageStorageCapacityValueLabel( parent_viewport=self.viewport) self.exp_bonus_description_label = ShopStageExpBonusDescriptionLabel( parent_viewport=self.viewport) self.exp_bonus_value_label = ShopStageExpBonusValueLabel( parent_viewport=self.viewport) self.price_label = ShopStagePriceLabel(parent_viewport=self.viewport) self.under_construction_label = ShopStageUnderConstructionLabel( parent_viewport=self.viewport) self.build_button = BuildShopStageButton( on_click_action=on_buy_shop_stage, parent_viewport=self.viewport) self.buttons = [ self.build_button, ] USER_DB_CURSOR.execute('''SELECT money FROM game_progress''') self.money = USER_DB_CURSOR.fetchone()[0] self.opacity = 0 self.on_window_resize_handlers = [ self.on_window_resize, self.locked_label.on_window_resize, self.level_placeholder_label.on_window_resize, self.previous_stage_placeholder_label.on_window_resize, self.hourly_profit_description_label.on_window_resize, self.hourly_profit_value_label.on_window_resize, self.storage_capacity_description_label.on_window_resize, self.storage_capacity_value_label.on_window_resize, self.exp_bonus_description_label.on_window_resize, self.exp_bonus_value_label.on_window_resize, self.price_label.on_window_resize, self.under_construction_label.on_window_resize ]
def __init__(self, column, row, possible_values_list, on_update_state_action, parent_viewport, logger): def on_increment(button): self.choice_state += 1 self.temp_value_label.on_update_args( self.possible_values_list[self.choice_state]) self.on_update_state_action(self.choice_state) if self.choice_state >= len(self.possible_values_list) - 1: button.on_disable() button.state = 'normal' if self.choice_state > 0: button.paired_button.on_activate(instant=True) def on_decrement(button): self.choice_state -= 1 self.temp_value_label.on_update_args( self.possible_values_list[self.choice_state]) self.on_update_state_action(self.choice_state) if self.choice_state <= 0: button.on_disable() button.state = 'normal' if self.choice_state < len(self.possible_values_list) - 1: button.paired_button.on_activate(instant=True) self.logger = logger self.column, self.row = column, row self.parent_viewport = parent_viewport self.viewport = Viewport() USER_DB_CURSOR.execute('SELECT current_locale FROM i18n') self.current_locale = USER_DB_CURSOR.fetchone()[0] self.on_update_state_action = on_update_state_action self.screen_resolution = (0, 0) self.description_label = None self.temp_value_label = None self.choice_state = None self.possible_values_list = possible_values_list self.increment_button, self.decrement_button = create_two_state_button( IncrementButton(on_click_action=on_increment, parent_viewport=self.viewport), DecrementButton(on_click_action=on_decrement, parent_viewport=self.viewport)) self.buttons = [self.increment_button, self.decrement_button] self.is_activated = False self.opacity = 0 self.on_window_resize_handlers = [ self.on_window_resize, ]
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 __init__( self, map_id, on_buy_map_action, on_switch_map_action, on_set_money_target_action, on_reset_money_target_action, parent_viewport, logger ): def on_set_money_target(button): pass def on_reset_money_target(button): pass def on_buy_map(button): button.on_deactivate() self.on_buy_map_action(self.map_id) def on_switch_map(button): self.on_switch_map_action(self.map_id) self.logger = logger self.is_activated = False self.map_id = map_id self.parent_viewport = parent_viewport self.viewport = Viewport() USER_DB_CURSOR.execute('SELECT current_locale FROM i18n') self.current_locale = USER_DB_CURSOR.fetchone()[0] self.on_buy_map_action = on_buy_map_action self.on_switch_map_action = on_switch_map_action self.on_set_money_target_action = on_set_money_target_action self.on_reset_money_target_action = on_reset_money_target_action self.screen_resolution = (0, 0) USER_DB_CURSOR.execute('''SELECT level, money FROM game_progress''') self.level, self.money = USER_DB_CURSOR.fetchone() self.money_target_activated = False self.opacity = 0 self.build_map_button = BuildMapButton(on_click_action=on_buy_map, parent_viewport=self.viewport) self.switch_map_button = SwitchMapButton(on_click_action=on_switch_map, parent_viewport=self.viewport) self.buttons = [self.build_map_button, self.switch_map_button] self.data = MAP_SWITCHER_STATE_MATRIX[self.map_id] self.locked_label = MapSwitcherCellLockedLabel(parent_viewport=self.viewport) self.level_placeholder_label = MapSwitcherLevelPlaceholderLabel(parent_viewport=self.viewport) self.level_placeholder_label.on_update_args((self.data[MAP_LEVEL_REQUIRED], )) self.unlock_available_label = MapSwitcherUnlockAvailableLabel(parent_viewport=self.viewport) self.unlock_available_label.on_update_args((self.data[MAP_PRICE], )) self.title_label = None self.icon_labels = [] self.on_window_resize_handlers = [ self.on_window_resize, self.locked_label.on_window_resize, self.level_placeholder_label.on_window_resize, self.unlock_available_label.on_window_resize ]
def __init__(self, column, row, parent_viewport, logger): self.logger = logger self.column, self.row = column, row self.parent_viewport = parent_viewport self.viewport = Viewport() USER_DB_CURSOR.execute('SELECT current_locale FROM i18n') self.current_locale = USER_DB_CURSOR.fetchone()[0] self.description_label = None self.checkboxes = [] self.buttons = [] self.is_activated = False self.screen_resolution = (0, 0) self.opacity = 0 self.on_window_resize_handlers = [ self.on_window_resize, ]
def __init__(self, controller, logger, child_window=False): self.logger = logger self.child_window = child_window self.is_activated = False self.controller = controller self.viewport = Viewport() self.opacity = 0 self.buttons = [] self.game_progress_notifications = [] self.malfunction_notifications = [] self.on_mouse_press_handlers = [] self.on_mouse_release_handlers = [] self.on_mouse_motion_handlers = [] self.on_mouse_drag_handlers = [] self.on_mouse_leave_handlers = [] self.on_mouse_scroll_handlers = [] self.on_key_press_handlers = [] self.on_text_handlers = [] self.on_window_resize_handlers = [ self.on_window_resize, ] self.on_window_activate_handlers = [ self.on_window_activate, ] self.on_window_show_handlers = [ self.on_window_show, ] self.on_window_deactivate_handlers = [ self.on_window_deactivate, ] self.on_window_hide_handlers = [ self.on_window_hide, ] self.screen_resolution = (0, 0) USER_DB_CURSOR.execute('SELECT current_locale, clock_24h FROM i18n') self.current_locale, self.clock_24h_enabled = USER_DB_CURSOR.fetchone() self.game_progress_notifications_enabled = False self.shader_sprite = None USER_DB_CURSOR.execute('SELECT * FROM notification_settings') self.level_up_notification_enabled, self.feature_unlocked_notification_enabled, \ self.construction_completed_notification_enabled, self.enough_money_notification_enabled, \ self.bonus_expired_notification_enabled, self.shop_storage_notification_enabled,\ self.voice_not_found_notification_enabled = USER_DB_CURSOR.fetchone()
def __init__(self, column, row, on_update_state_action, parent_viewport, logger): self.logger = logger self.column, self.row = column, row self.parent_viewport = parent_viewport self.viewport = Viewport() USER_DB_CURSOR.execute('SELECT current_locale FROM i18n') self.current_locale = USER_DB_CURSOR.fetchone()[0] self.on_update_state_action = on_update_state_action self.screen_resolution = (0, 0) self.description_label = None self.knob = None self.is_activated = False self.opacity = 0 self.on_window_resize_handlers = [ self.on_window_resize, ] self.on_mouse_motion_handlers = [] self.on_mouse_press_handlers = [] self.on_mouse_release_handlers = [] self.on_mouse_drag_handlers = []
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
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 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")
def __init__(self, text="", draw_border = True, valid_chars = None, validate_on_type = True, single_paragraph = True, text_formatter = None, alignment = None, font_desc = None, **kwargs): Bin.__init__(self, **kwargs) self.display_label = graphics.Label(color=self.color) self.viewport = Viewport(self.display_label) self.viewport.connect("on-render", self.__on_viewport_render) self.add_child(self.viewport) self.can_focus = True self.interactive = True self.editable = True #: current cursor position self.cursor_position = None #: start position of the selection self.selection_start = 0 #: end position of the selection self.selection_end = 0 if font_desc is not None: self.font_desc = font_desc self.display_label.font_desc = self.font_desc #: text alignment in the entry self.alignment = alignment #: if True, a border will be drawn around the input element self.draw_border = draw_border #self.connect("on-key-press", self.__on_key_press) self.connect("on-mouse-down", self.__on_mouse_down) self.connect("on-double-click", self.__on_double_click) self.connect("on-triple-click", self.__on_triple_click) self.connect("on-blur", self.__on_blur) self.connect("on-focus", self.__on_focus) self.connect_after("on-render", self.__on_render) self._scene_mouse_move = None self._scene_mouse_up = None self._selection_start_position = None self._letter_positions = [] #: a string, function or regexp or valid chars for the input #: in case of function, it will receive the string to be tested #: as input and expects to receive back a boolean of whether the string #: is valid or not self.valid_chars = valid_chars #: should the content be validate right when typing and invalid version prohibited self.validate_on_type = validate_on_type #: function to style the entry text - change color and such #: the function receives one param - the text, and must return #: processed text back. will be using original text if the function #: does not return anything. #: Note: this function can change only the style, not the actual content #: as the latter will mess up text selection because of off-sync between #: the label value and what is displayed self.text_formatter = text_formatter if text_formatter else self.text_formatter #: should the text input support multiple lines self.single_paragraph = single_paragraph self.update_text(text) self._last_good_value = text # last known good value of the input
def __init__(self, construction_type, row, on_buy_construction_action, on_set_money_target_action, on_reset_money_target_action, parent_viewport): def on_set_money_target(button): button.paired_button.opacity = button.opacity button.on_deactivate(instant=True) button.paired_button.on_activate() self.on_set_money_target_action(self.construction_type, self.row, self.entity_number) def on_reset_money_target(button): button.paired_button.opacity = button.opacity button.on_deactivate(instant=True) button.paired_button.on_activate() self.on_reset_money_target_action() def on_buy_construction(button): button.on_deactivate(instant=True) self.enable_money_target_button.on_deactivate(instant=True) self.disable_money_target_button.on_deactivate(instant=True) self.on_buy_construction_action(self.construction_type, self.row, self.entity_number) self.logger = getLogger( f'root.app.game.map.constructor.view.cell.{construction_type}.{row}' ) self.is_activated = False self.parent_viewport = parent_viewport self.viewport = Viewport() self.construction_type, self.row = construction_type, row USER_DB_CURSOR.execute('SELECT current_locale FROM i18n') self.current_locale = USER_DB_CURSOR.fetchone()[0] self.on_buy_construction_action = on_buy_construction_action self.on_set_money_target_action = on_set_money_target_action self.on_reset_money_target_action = on_reset_money_target_action self.entity_number = None self.data = [] self.screen_resolution = (0, 0) self.locked_label = ConstructorLockedLabel( parent_viewport=self.viewport) self.title_label = None self.level_required_label = ConstructorLevelPlaceholderLabel( parent_viewport=self.viewport) self.previous_entity_required_label = None self.environment_required_label = None self.unlock_available_label = None self.under_construction_description_label = UnderConstructionDescriptionLabel( parent_viewport=self.viewport) self.enable_money_target_button, self.disable_money_target_button = create_two_state_button( SetMoneyTargetButton(on_click_action=on_set_money_target, parent_viewport=self.viewport), ResetMoneyTargetButton(on_click_action=on_reset_money_target, parent_viewport=self.viewport)) self.build_button = BuildConstructionButton( on_click_action=on_buy_construction, parent_viewport=self.viewport) self.buttons = [ self.enable_money_target_button, self.disable_money_target_button, self.build_button ] USER_DB_CURSOR.execute('''SELECT money FROM game_progress''') self.money = USER_DB_CURSOR.fetchone()[0] self.money_target_activated = False self.opacity = 0 self.on_window_resize_handlers = [ self.on_window_resize, self.locked_label.on_window_resize, self.level_required_label.on_window_resize, self.under_construction_description_label.on_window_resize ]
class Entry(Bin): """A text entry field""" __gsignals__ = { "on-change": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT, )), "on-position-change": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT, )), } padding = 5 mouse_cursor = gdk.CursorType.XTERM font_desc = "Sans Serif 10" #: pango.FontDescription to use for the label color = "#000" #: font color cursor_color = "#000" #: cursor color selection_color = "#A8C2E0" #: fill color of the selection region def __init__(self, text="", draw_border=True, valid_chars=None, validate_on_type=True, single_paragraph=True, text_formatter=None, alignment=None, font_desc=None, **kwargs): Bin.__init__(self, **kwargs) self.display_label = graphics.Label(color=self.color) self.viewport = Viewport(self.display_label) self.viewport.connect("on-render", self.__on_viewport_render) self.add_child(self.viewport) self.can_focus = True self.interactive = True self.editable = True #: current cursor position self.cursor_position = None #: start position of the selection self.selection_start = 0 #: end position of the selection self.selection_end = 0 if font_desc is not None: self.font_desc = font_desc self.display_label.font_desc = self.font_desc #: text alignment in the entry self.alignment = alignment #: if True, a border will be drawn around the input element self.draw_border = draw_border #self.connect("on-key-press", self.__on_key_press) self.connect("on-mouse-down", self.__on_mouse_down) self.connect("on-double-click", self.__on_double_click) self.connect("on-triple-click", self.__on_triple_click) self.connect("on-blur", self.__on_blur) self.connect("on-focus", self.__on_focus) self.connect_after("on-render", self.__on_render) self._scene_mouse_move = None self._scene_mouse_up = None self._selection_start_position = None self._letter_positions = [] #: a string, function or regexp or valid chars for the input #: in case of function, it will receive the string to be tested #: as input and expects to receive back a boolean of whether the string #: is valid or not self.valid_chars = valid_chars #: should the content be validate right when typing and invalid version prohibited self.validate_on_type = validate_on_type #: function to style the entry text - change color and such #: the function receives one param - the text, and must return #: processed text back. will be using original text if the function #: does not return anything. #: Note: this function can change only the style, not the actual content #: as the latter will mess up text selection because of off-sync between #: the label value and what is displayed self.text_formatter = text_formatter if text_formatter else self.text_formatter #: should the text input support multiple lines self.single_paragraph = single_paragraph self.update_text(text) self._last_good_value = text # last known good value of the input def __setattr__(self, name, val): if name == "cursor_position" and not self.editable: val = None if self.__dict__.get(name, "hamster_graphics_no_value_really") == val: return if name == "text": val = val or "" if getattr(self, "text_formatter", None): markup = self.text_formatter( val.replace("&", "&").replace("<", "<").replace(">", ">")) if markup: self.display_label.markup = markup else: self.display_label.text = val Bin.__setattr__(self, name, val) if name == "text": self.emit("on-change", val) if name in ("font_desc", "alignment", "single_paragraph", "color"): setattr(self.display_label, name, val) elif name == "alloc_w" and getattr(self, "overflow", False) != False and hasattr( self, "display_label"): self.display_label.width = val - self.horizontal_padding elif name == "overflow" and val != False and hasattr( self, "display_label"): if val in (pango.WrapMode.WORD, pango.WrapMode.WORD_CHAR, pango.WrapMode.CHAR): self.display_label.wrap = val self.display_label.ellipsize = None elif val in (pango.EllipsizeMode.START, pango.EllipsizeMode.END): self.display_label.wrap = None self.display_label.ellipsize = val if name == "cursor_position": self.emit("on-position-change", val) def get_min_size(self): return self.min_width or 0, max( self.min_height, self.display_label.height + self.vertical_padding) def text_formatter(self, text): return None def test_value(self, text): if not self.valid_chars: return True elif isinstance(self.valid_chars, basestring): return set(text) - set(self.valid_chars) == set([]) elif hasattr(self.valid_chars, '__call__'): return self.valid_chars(text) else: return False def get_height_for_width_size(self): return self.get_min_size() def update_text(self, text): """updates the text field value and the last good known value, respecting the valid_chars and validate_on_type flags""" text = text or "" if self.test_value(text): self.text = text if self.validate_on_type: self._last_good_value = text elif not self.validate_on_type: self.text = text else: return False self.viewport.height = self.display_label.height if self.cursor_position is None: self.cursor_position = len(text) return True def _index_to_pos(self, index): """give coordinates for the position in text. maps to the display_label's pango function""" ext = self.display_label._test_layout.index_to_pos(index) extents = [ e / pango.SCALE for e in (ext.x, ext.y, ext.width, ext.height) ] return extents def _xy_to_index(self, x, y): """from coordinates caluculate position in text. maps to the display_label's pango function""" x = x - self.display_label.x - self.viewport.x index = self.display_label._test_layout.xy_to_index( int(x * pango.SCALE), int(y * pango.SCALE)) return index[0] + index[1] def __on_focus(self, sprite): self._last_good_value = self.text def __on_blur(self, sprite): self._edit_done() def _edit_done(self): if self.test_value(self.text): self._last_good_value = self.text else: self.update_text(self._last_good_value) def __on_mouse_down(self, sprite, event): i = self._xy_to_index(event.x, event.y) self.selection_start = self.selection_end = self.cursor_position = i self._selection_start_position = self.selection_start scene = self.get_scene() if not self._scene_mouse_up: self._scene_mouse_up = scene.connect("on-mouse-up", self._on_scene_mouse_up) self._scene_mouse_move = scene.connect("on-mouse-move", self._on_scene_mouse_move) def __on_double_click(self, sprite, event): # find the word cursor = self.cursor_position self.selection_start = self.text.rfind(" ", 0, cursor) + 1 end = self.text.find(" ", cursor) self.cursor_position = self.selection_end = end if end > 0 else len( self.text) def __on_triple_click(self, sprite, event): self.selection_start = 0 self.cursor_position = self.selection_end = len(self.text) def _on_scene_mouse_up(self, scene, event): scene.disconnect(self._scene_mouse_up) scene.disconnect(self._scene_mouse_move) self._scene_mouse_up = self._scene_mouse_move = None def _on_scene_mouse_move(self, scene, event): if self.focused == False: return # now try to derive cursor position x, y = self.display_label.from_scene_coords(event.x, event.y) x = x + self.display_label.x + self.viewport.x i = self._xy_to_index(x, y) self.cursor_position = i if self.cursor_position < self._selection_start_position: self.selection_start = self.cursor_position self.selection_end = self._selection_start_position else: self.selection_start = self._selection_start_position self.selection_end = self.cursor_position def _get_iter(self, index=0): """returns iterator that has been run till the specified index""" iter = self.display_label._test_layout.get_iter() for i in range(index): iter.next_char() return iter def _do_key_press(self, event): """responding to key events""" key = event.keyval shift = event.state & gdk.ModifierType.SHIFT_MASK control = event.state & gdk.ModifierType.CONTROL_MASK if not self.editable: return def emit_and_return(): self.emit("on-key-press", event) return if self.single_paragraph and key == gdk.KEY_Return: self._edit_done() return emit_and_return() self._letter_positions = [] if key == gdk.KEY_Left: if shift and self.cursor_position == 0: return emit_and_return() if control: self.cursor_position = self.text[:self.cursor_position].rstrip( ).rfind(" ") + 1 else: self.cursor_position -= 1 if shift: if self.cursor_position < self.selection_start: self.selection_start = self.cursor_position else: self.selection_end = self.cursor_position elif self.selection_start != self.selection_end: self.cursor_position = self.selection_start elif key == gdk.KEY_Right: if shift and self.cursor_position == len(self.text): return emit_and_return() if control: prev_pos = self.cursor_position self.cursor_position = self.text[self.cursor_position:].lstrip( ).find(" ") if self.cursor_position == -1: self.cursor_position = len(self.text) else: self.cursor_position += prev_pos + 1 else: self.cursor_position += 1 if shift: if self.cursor_position > self.selection_end: self.selection_end = self.cursor_position else: self.selection_start = self.cursor_position elif self.selection_start != self.selection_end: self.cursor_position = self.selection_end elif key == gdk.KEY_Up and self.single_paragraph == False: iter = self._get_iter(self.cursor_position) if str(iter.get_line_readonly()) != str( self.display_label._test_layout.get_line_readonly(0)): char_pos = iter.get_char_extents().x char_line = str(iter.get_line_readonly()) # now we run again to run until previous line prev_iter, iter = self._get_iter(), self._get_iter() prev_line = None while str(iter.get_line_readonly()) != char_line: if str(prev_line) != str(iter.get_line_readonly()): prev_iter = iter.copy() prev_line = iter.get_line_readonly() iter.next_char() index = prev_iter.get_line_readonly().x_to_index( char_pos - prev_iter.get_char_extents().x) index = index[1] + index[2] self.cursor_position = index if shift: if self.cursor_position < self.selection_start: self.selection_start = self.cursor_position else: self.selection_end = self.cursor_position elif self.selection_start != self.selection_end: self.cursor_position = self.selection_start elif key == gdk.KEY_Down and self.single_paragraph == False: iter = self._get_iter(self.cursor_position) char_pos = iter.get_char_extents().x if iter.next_line(): index = iter.get_line_readonly().x_to_index( char_pos - iter.get_char_extents().x) index = index[1] + index[2] self.cursor_position = index if shift: if self.cursor_position > self.selection_end: self.selection_end = self.cursor_position else: self.selection_start = self.cursor_position elif self.selection_start != self.selection_end: self.cursor_position = self.selection_end elif key == gdk.KEY_Home: if self.single_paragraph or control: self.cursor_position = 0 if shift: self.selection_end = self.selection_start self.selection_start = self.cursor_position else: iter = self._get_iter(self.cursor_position) line = str(iter.get_line_readonly()) # find the start of the line iter = self._get_iter() while str(iter.get_line_readonly()) != line: iter.next_char() self.cursor_position = iter.get_index() if shift: start_iter = self._get_iter(self.selection_start) end_iter = self._get_iter(self.selection_end) if str(start_iter.get_line_readonly()) == str( end_iter.get_line_readonly()): self.selection_end = self.selection_start self.selection_start = self.cursor_position else: if self.cursor_position < self.selection_start: self.selection_start = self.cursor_position else: self.selection_end = self.cursor_position elif key == gdk.KEY_End: if self.single_paragraph or control: self.cursor_position = len(self.text) if shift: self.selection_start = self.selection_end self.selection_end = self.cursor_position else: iter = self._get_iter(self.cursor_position) #find the end of the line line = str(iter.get_line_readonly()) prev_iter = None while str(iter.get_line_readonly()) == line: prev_iter = iter.copy() moved = iter.next_char() if not moved: prev_iter = iter break self.cursor_position = prev_iter.get_index() if shift: start_iter = self._get_iter(self.selection_start) end_iter = self._get_iter(self.selection_end) if str(start_iter.get_line_readonly()) == str( end_iter.get_line_readonly()): self.selection_start = self.selection_end self.selection_end = self.cursor_position else: if self.cursor_position > self.selection_end: self.selection_end = self.cursor_position else: self.selection_start = self.cursor_position elif key == gdk.KEY_BackSpace: if self.selection_start != self.selection_end: if not self.update_text(self.text[:self.selection_start] + self.text[self.selection_end:]): return emit_and_return() elif self.cursor_position > 0: if not self.update_text(self.text[:self.cursor_position - 1] + self.text[self.cursor_position:]): return emit_and_return() self.cursor_position -= 1 elif key == gdk.KEY_Delete: if self.selection_start != self.selection_end: if not self.update_text(self.text[:self.selection_start] + self.text[self.selection_end:]): return emit_and_return() elif self.cursor_position < len(self.text): if not self.update_text(self.text[:self.cursor_position] + self.text[self.cursor_position + 1:]): return emit_and_return() elif key == gdk.KEY_Escape: return emit_and_return() #prevent garbage from common save file mneumonic elif control and key in (gdk.KEY_s, gdk.KEY_S): return emit_and_return() # copying and pasting elif control and key in (gdk.KEY_c, gdk.KEY_C): # copy clipboard = gtk.Clipboard() clipboard.set_text( self.text[self.selection_start:self.selection_end]) return emit_and_return() elif control and key in (gdk.KEY_x, gdk.KEY_X): # cut text = self.text[self.selection_start:self.selection_end] if self.update_text(self.text[:self.selection_start] + self.text[self.selection_end:]): clipboard = gtk.Clipboard() clipboard.set_text(text) elif control and key in (gdk.KEY_v, gdk.KEY_V): # paste clipboard = gtk.Clipboard() clipboard.request_text(self._on_clipboard_text) return emit_and_return() elif control and key in (gdk.KEY_a, gdk.KEY_A): # select all self.selection_start = 0 self.cursor_position = self.selection_end = len(self.text) return emit_and_return() # normal letters elif event.string: if self.update_text(self.text[:self.selection_start] + event.string + self.text[self.selection_end:]): self.cursor_position = self.selection_start + 1 self.selection_start = self.selection_end = self.cursor_position return emit_and_return() else: # in case of anything else just go home return emit_and_return() self.cursor_position = min(max(0, self.cursor_position), len(self.text)) self.selection_start = min(max(0, self.selection_start), len(self.text)) self.selection_end = min(max(0, self.selection_end), len(self.text)) if shift == False: self.selection_start = self.selection_end = self.cursor_position return emit_and_return() def _on_clipboard_text(self, clipboard, text, data): if self.update_text(self.text[:self.selection_start] + text + self.text[self.selection_end:]): self.selection_start = self.selection_end = self.cursor_position = self.selection_start + len( text) def do_render(self): self.graphics.set_line_style(width=1) self.graphics.rectangle(0.5, -1.5, self.width, self.height + 2, 3) if self.draw_border: self.graphics.fill_preserve("#fff") self.graphics.stroke("#aaa") def __on_render(self, sprite): self.graphics.rectangle(0, 0, self.width, self.height) self.graphics.new_path() if self.cursor_position is not None: cur_x, cur_y, cur_w, cur_h = self._index_to_pos( self.cursor_position) if self.display_label.x + cur_x > self.viewport.width: self.display_label.x = min( 0, self.viewport.width - cur_x) # cursor visible at the right side elif self.display_label.x + cur_x < 0: self.display_label.x -= (self.display_label.x + cur_x ) # cursor visible at the left side elif self.display_label.x < 0 and self.display_label.x + self.display_label.width < self.viewport.width: self.display_label.x = min( self.viewport.width - self.display_label.width, 0) # align the label within the entry if self.display_label.width < self.viewport.width: if self.alignment == pango.Alignment.RIGHT: self.display_label.x = self.viewport.width - self.display_label.width elif self.alignment == pango.Alignment.CENTER: self.display_label.x = (self.viewport.width - self.display_label.width) / 2 #if self.single_paragraph: # self.display_label.y = (self.viewport.height - self.display_label.height) / 2.0 self.viewport._sprite_dirty = True # so that we get the cursor drawn def __on_viewport_render(self, viewport): if self.focused == False: return if self.cursor_position is None: return cur_x, cur_y, cur_w, cur_h = self._index_to_pos(self.cursor_position) cur_x = cur_x + self.display_label.x cur_y += self.display_label.y viewport.graphics.move_to(cur_x + 0.5, cur_y) viewport.graphics.line_to(cur_x + 0.5, cur_y + cur_h) viewport.graphics.stroke(self.cursor_color) if self.selection_start == self.selection_end: return # all done! start_x, start_y, start_w, start_h = self._index_to_pos( self.selection_start) end_x, end_y, end_w, end_h = self._index_to_pos(self.selection_end) iter = self._get_iter(self.selection_start) char_exts = iter.get_char_extents() cur_x, cur_y = char_exts.x / pango.SCALE, char_exts.y / pango.SCALE + self.display_label.y cur_line = None for i in range(self.selection_end - self.selection_start): prev_iter = pango.LayoutIter.copy(iter) iter.next_char() line = iter.get_line_readonly() if str(cur_line) != str( line): # can't compare layout lines for some reason exts = prev_iter.get_char_extents() char_exts = [ ext / pango.SCALE for ext in (exts.x, exts.y, exts.width, exts.height) ] viewport.graphics.rectangle( cur_x + self.display_label.x, cur_y, char_exts[0] + char_exts[2] - cur_x, char_exts[3]) char_exts = iter.get_char_extents() cur_x, cur_y = char_exts.x / pango.SCALE, char_exts.y / pango.SCALE + self.display_label.y cur_line = line exts = iter.get_char_extents() char_exts = [ ext / pango.SCALE for ext in (exts.x, exts.y, exts.width, exts.height) ] viewport.graphics.rectangle( cur_x + self.display_label.x, cur_y, char_exts[0] - cur_x, self.display_label.y + char_exts[1] - cur_y + char_exts[3]) viewport.graphics.fill(self.selection_color)
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")
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
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")
def __init__(self, text="", draw_border=True, valid_chars=None, validate_on_type=True, single_paragraph=True, text_formatter=None, alignment=None, font_desc=None, **kwargs): Bin.__init__(self, **kwargs) self.display_label = graphics.Label(color=self.color) self.viewport = Viewport(self.display_label) self.viewport.connect("on-render", self.__on_viewport_render) self.add_child(self.viewport) self.can_focus = True self.interactive = True self.editable = True #: current cursor position self.cursor_position = None #: start position of the selection self.selection_start = 0 #: end position of the selection self.selection_end = 0 if font_desc is not None: self.font_desc = font_desc self.display_label.font_desc = self.font_desc #: text alignment in the entry self.alignment = alignment #: if True, a border will be drawn around the input element self.draw_border = draw_border #self.connect("on-key-press", self.__on_key_press) self.connect("on-mouse-down", self.__on_mouse_down) self.connect("on-double-click", self.__on_double_click) self.connect("on-triple-click", self.__on_triple_click) self.connect("on-blur", self.__on_blur) self.connect("on-focus", self.__on_focus) self.connect_after("on-render", self.__on_render) self._scene_mouse_move = None self._scene_mouse_up = None self._selection_start_position = None self._letter_positions = [] #: a string, function or regexp or valid chars for the input #: in case of function, it will receive the string to be tested #: as input and expects to receive back a boolean of whether the string #: is valid or not self.valid_chars = valid_chars #: should the content be validate right when typing and invalid version prohibited self.validate_on_type = validate_on_type #: function to style the entry text - change color and such #: the function receives one param - the text, and must return #: processed text back. will be using original text if the function #: does not return anything. #: Note: this function can change only the style, not the actual content #: as the latter will mess up text selection because of off-sync between #: the label value and what is displayed self.text_formatter = text_formatter if text_formatter else self.text_formatter #: should the text input support multiple lines self.single_paragraph = single_paragraph self.update_text(text) self._last_good_value = text # last known good value of the input
class Entry(Bin): """A text entry field""" __gsignals__ = { "on-change": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)), "on-position-change": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)), } padding = 5 mouse_cursor = gdk.CursorType.XTERM font_desc = "Sans Serif 10" #: pango.FontDescription to use for the label color = "#000" #: font color cursor_color = "#000" #: cursor color selection_color = "#A8C2E0" #: fill color of the selection region def __init__(self, text="", draw_border = True, valid_chars = None, validate_on_type = True, single_paragraph = True, text_formatter = None, alignment = None, font_desc = None, **kwargs): Bin.__init__(self, **kwargs) self.display_label = graphics.Label(color=self.color) self.viewport = Viewport(self.display_label) self.viewport.connect("on-render", self.__on_viewport_render) self.add_child(self.viewport) self.can_focus = True self.interactive = True self.editable = True #: current cursor position self.cursor_position = None #: start position of the selection self.selection_start = 0 #: end position of the selection self.selection_end = 0 if font_desc is not None: self.font_desc = font_desc self.display_label.font_desc = self.font_desc #: text alignment in the entry self.alignment = alignment #: if True, a border will be drawn around the input element self.draw_border = draw_border #self.connect("on-key-press", self.__on_key_press) self.connect("on-mouse-down", self.__on_mouse_down) self.connect("on-double-click", self.__on_double_click) self.connect("on-triple-click", self.__on_triple_click) self.connect("on-blur", self.__on_blur) self.connect("on-focus", self.__on_focus) self.connect_after("on-render", self.__on_render) self._scene_mouse_move = None self._scene_mouse_up = None self._selection_start_position = None self._letter_positions = [] #: a string, function or regexp or valid chars for the input #: in case of function, it will receive the string to be tested #: as input and expects to receive back a boolean of whether the string #: is valid or not self.valid_chars = valid_chars #: should the content be validate right when typing and invalid version prohibited self.validate_on_type = validate_on_type #: function to style the entry text - change color and such #: the function receives one param - the text, and must return #: processed text back. will be using original text if the function #: does not return anything. #: Note: this function can change only the style, not the actual content #: as the latter will mess up text selection because of off-sync between #: the label value and what is displayed self.text_formatter = text_formatter if text_formatter else self.text_formatter #: should the text input support multiple lines self.single_paragraph = single_paragraph self.update_text(text) self._last_good_value = text # last known good value of the input def __setattr__(self, name, val): if name == "cursor_position" and not self.editable: val = None if self.__dict__.get(name, "hamster_graphics_no_value_really") == val: return if name == "text": val = val or "" if getattr(self, "text_formatter", None): markup = self.text_formatter(val.replace("&", "&").replace("<", "<").replace(">", ">")) if markup: self.display_label.markup = markup else: self.display_label.text = val Bin.__setattr__(self, name, val) if name == "text": self.emit("on-change", val) if name in("font_desc", "alignment", "single_paragraph", "color"): setattr(self.display_label, name, val) elif name == "alloc_w" and getattr(self, "overflow", False) != False and hasattr(self, "display_label"): self.display_label.width = val - self.horizontal_padding elif name == "overflow" and val != False and hasattr(self, "display_label"): if val in (pango.WrapMode.WORD, pango.WrapMode.WORD_CHAR, pango.WrapMode.CHAR): self.display_label.wrap = val self.display_label.ellipsize = None elif val in (pango.EllipsizeMode.START, pango.EllipsizeMode.END): self.display_label.wrap = None self.display_label.ellipsize = val if name == "cursor_position": self.emit("on-position-change", val) def get_min_size(self): return self.min_width or 0, max(self.min_height, self.display_label.height + self.vertical_padding) def text_formatter(self, text): return None def test_value(self, text): if not self.valid_chars: return True elif isinstance(self.valid_chars, basestring): return set(text) - set(self.valid_chars) == set([]) elif hasattr(self.valid_chars, '__call__'): return self.valid_chars(text) else: return False def get_height_for_width_size(self): return self.get_min_size() def update_text(self, text): """updates the text field value and the last good known value, respecting the valid_chars and validate_on_type flags""" text = text or "" if self.test_value(text): self.text = text if self.validate_on_type: self._last_good_value = text elif not self.validate_on_type: self.text = text else: return False self.viewport.height = self.display_label.height if self.cursor_position is None: self.cursor_position = len(text) return True def _index_to_pos(self, index): """give coordinates for the position in text. maps to the display_label's pango function""" ext = self.display_label._test_layout.index_to_pos(index) extents = [e / pango.SCALE for e in (ext.x, ext.y, ext.width, ext.height)] return extents def _xy_to_index(self, x, y): """from coordinates caluculate position in text. maps to the display_label's pango function""" x = x - self.display_label.x - self.viewport.x index = self.display_label._test_layout.xy_to_index(int(x*pango.SCALE), int(y*pango.SCALE)) return index[0] + index[1] def __on_focus(self, sprite): self._last_good_value = self.text def __on_blur(self, sprite): self._edit_done() def _edit_done(self): if self.test_value(self.text): self._last_good_value = self.text else: self.update_text(self._last_good_value) def __on_mouse_down(self, sprite, event): i = self._xy_to_index(event.x, event.y) self.selection_start = self.selection_end = self.cursor_position = i self._selection_start_position = self.selection_start scene = self.get_scene() if not self._scene_mouse_up: self._scene_mouse_up = scene.connect("on-mouse-up", self._on_scene_mouse_up) self._scene_mouse_move = scene.connect("on-mouse-move", self._on_scene_mouse_move) def __on_double_click(self, sprite, event): # find the word cursor = self.cursor_position self.selection_start = self.text.rfind(" ", 0, cursor) + 1 end = self.text.find(" ", cursor) self.cursor_position = self.selection_end = end if end > 0 else len(self.text) def __on_triple_click(self, sprite, event): self.selection_start = 0 self.cursor_position = self.selection_end = len(self.text) def _on_scene_mouse_up(self, scene, event): scene.disconnect(self._scene_mouse_up) scene.disconnect(self._scene_mouse_move) self._scene_mouse_up = self._scene_mouse_move = None def _on_scene_mouse_move(self, scene, event): if self.focused == False: return # now try to derive cursor position x, y = self.display_label.from_scene_coords(event.x, event.y) x = x + self.display_label.x + self.viewport.x i = self._xy_to_index(x, y) self.cursor_position = i if self.cursor_position < self._selection_start_position: self.selection_start = self.cursor_position self.selection_end = self._selection_start_position else: self.selection_start = self._selection_start_position self.selection_end = self.cursor_position def _get_iter(self, index = 0): """returns iterator that has been run till the specified index""" iter = self.display_label._test_layout.get_iter() for i in range(index): iter.next_char() return iter def _do_key_press(self, event): """responding to key events""" key = event.keyval shift = event.state & gdk.ModifierType.SHIFT_MASK control = event.state & gdk.ModifierType.CONTROL_MASK if not self.editable: return def emit_and_return(): self.emit("on-key-press", event) return if self.single_paragraph and key == gdk.KEY_Return: self._edit_done() return emit_and_return() self._letter_positions = [] if key == gdk.KEY_Left: if shift and self.cursor_position == 0: return emit_and_return() if control: self.cursor_position = self.text[:self.cursor_position].rstrip().rfind(" ") + 1 else: self.cursor_position -= 1 if shift: if self.cursor_position < self.selection_start: self.selection_start = self.cursor_position else: self.selection_end = self.cursor_position elif self.selection_start != self.selection_end: self.cursor_position = self.selection_start elif key == gdk.KEY_Right: if shift and self.cursor_position == len(self.text): return emit_and_return() if control: prev_pos = self.cursor_position self.cursor_position = self.text[self.cursor_position:].lstrip().find(" ") if self.cursor_position == -1: self.cursor_position = len(self.text) else: self.cursor_position += prev_pos + 1 else: self.cursor_position += 1 if shift: if self.cursor_position > self.selection_end: self.selection_end = self.cursor_position else: self.selection_start = self.cursor_position elif self.selection_start != self.selection_end: self.cursor_position = self.selection_end elif key == gdk.KEY_Up and self.single_paragraph == False: iter = self._get_iter(self.cursor_position) if str(iter.get_line_readonly()) != str(self.display_label._test_layout.get_line_readonly(0)): char_pos = iter.get_char_extents().x char_line = str(iter.get_line_readonly()) # now we run again to run until previous line prev_iter, iter = self._get_iter(), self._get_iter() prev_line = None while str(iter.get_line_readonly()) != char_line: if str(prev_line) != str(iter.get_line_readonly()): prev_iter = iter.copy() prev_line = iter.get_line_readonly() iter.next_char() index = prev_iter.get_line_readonly().x_to_index(char_pos - prev_iter.get_char_extents().x) index = index[1] + index[2] self.cursor_position = index if shift: if self.cursor_position < self.selection_start: self.selection_start = self.cursor_position else: self.selection_end = self.cursor_position elif self.selection_start != self.selection_end: self.cursor_position = self.selection_start elif key == gdk.KEY_Down and self.single_paragraph == False: iter = self._get_iter(self.cursor_position) char_pos = iter.get_char_extents().x if iter.next_line(): index = iter.get_line_readonly().x_to_index(char_pos - iter.get_char_extents().x) index = index[1] + index[2] self.cursor_position = index if shift: if self.cursor_position > self.selection_end: self.selection_end = self.cursor_position else: self.selection_start = self.cursor_position elif self.selection_start != self.selection_end: self.cursor_position = self.selection_end elif key == gdk.KEY_Home: if self.single_paragraph or control: self.cursor_position = 0 if shift: self.selection_end = self.selection_start self.selection_start = self.cursor_position else: iter = self._get_iter(self.cursor_position) line = str(iter.get_line_readonly()) # find the start of the line iter = self._get_iter() while str(iter.get_line_readonly()) != line: iter.next_char() self.cursor_position = iter.get_index() if shift: start_iter = self._get_iter(self.selection_start) end_iter = self._get_iter(self.selection_end) if str(start_iter.get_line_readonly()) == str(end_iter.get_line_readonly()): self.selection_end = self.selection_start self.selection_start = self.cursor_position else: if self.cursor_position < self.selection_start: self.selection_start = self.cursor_position else: self.selection_end = self.cursor_position elif key == gdk.KEY_End: if self.single_paragraph or control: self.cursor_position = len(self.text) if shift: self.selection_start = self.selection_end self.selection_end = self.cursor_position else: iter = self._get_iter(self.cursor_position) #find the end of the line line = str(iter.get_line_readonly()) prev_iter = None while str(iter.get_line_readonly()) == line: prev_iter = iter.copy() moved = iter.next_char() if not moved: prev_iter = iter break self.cursor_position = prev_iter.get_index() if shift: start_iter = self._get_iter(self.selection_start) end_iter = self._get_iter(self.selection_end) if str(start_iter.get_line_readonly()) == str(end_iter.get_line_readonly()): self.selection_start = self.selection_end self.selection_end = self.cursor_position else: if self.cursor_position > self.selection_end: self.selection_end = self.cursor_position else: self.selection_start = self.cursor_position elif key == gdk.KEY_BackSpace: if self.selection_start != self.selection_end: if not self.update_text(self.text[:self.selection_start] + self.text[self.selection_end:]): return emit_and_return() elif self.cursor_position > 0: if not self.update_text(self.text[:self.cursor_position-1] + self.text[self.cursor_position:]): return emit_and_return() self.cursor_position -= 1 elif key == gdk.KEY_Delete: if self.selection_start != self.selection_end: if not self.update_text(self.text[:self.selection_start] + self.text[self.selection_end:]): return emit_and_return() elif self.cursor_position < len(self.text): if not self.update_text(self.text[:self.cursor_position] + self.text[self.cursor_position+1:]): return emit_and_return() elif key == gdk.KEY_Escape: return emit_and_return() #prevent garbage from common save file mneumonic elif control and key in (gdk.KEY_s, gdk.KEY_S): return emit_and_return() # copying and pasting elif control and key in (gdk.KEY_c, gdk.KEY_C): # copy clipboard = gtk.Clipboard() clipboard.set_text(self.text[self.selection_start:self.selection_end]) return emit_and_return() elif control and key in (gdk.KEY_x, gdk.KEY_X): # cut text = self.text[self.selection_start:self.selection_end] if self.update_text(self.text[:self.selection_start] + self.text[self.selection_end:]): clipboard = gtk.Clipboard() clipboard.set_text(text) elif control and key in (gdk.KEY_v, gdk.KEY_V): # paste clipboard = gtk.Clipboard() clipboard.request_text(self._on_clipboard_text) return emit_and_return() elif control and key in (gdk.KEY_a, gdk.KEY_A): # select all self.selection_start = 0 self.cursor_position = self.selection_end = len(self.text) return emit_and_return() # normal letters elif event.string: if self.update_text(self.text[:self.selection_start] + event.string + self.text[self.selection_end:]): self.cursor_position = self.selection_start + 1 self.selection_start = self.selection_end = self.cursor_position return emit_and_return() else: # in case of anything else just go home return emit_and_return() self.cursor_position = min(max(0, self.cursor_position), len(self.text)) self.selection_start = min(max(0, self.selection_start), len(self.text)) self.selection_end = min(max(0, self.selection_end), len(self.text)) if shift == False: self.selection_start = self.selection_end = self.cursor_position return emit_and_return() def _on_clipboard_text(self, clipboard, text, data): if self.update_text(self.text[:self.selection_start] + text + self.text[self.selection_end:]): self.selection_start = self.selection_end = self.cursor_position = self.selection_start + len(text) def do_render(self): self.graphics.set_line_style(width=1) self.graphics.rectangle(0.5, -1.5, self.width, self.height + 2, 3) if self.draw_border: self.graphics.fill_preserve("#fff") self.graphics.stroke("#aaa") def __on_render(self, sprite): self.graphics.rectangle(0, 0, self.width, self.height) self.graphics.new_path() if self.cursor_position is not None: cur_x, cur_y, cur_w, cur_h = self._index_to_pos(self.cursor_position) if self.display_label.x + cur_x > self.viewport.width: self.display_label.x = min(0, self.viewport.width - cur_x) # cursor visible at the right side elif self.display_label.x + cur_x < 0: self.display_label.x -= (self.display_label.x + cur_x) # cursor visible at the left side elif self.display_label.x < 0 and self.display_label.x + self.display_label.width < self.viewport.width: self.display_label.x = min(self.viewport.width - self.display_label.width, 0) # align the label within the entry if self.display_label.width < self.viewport.width: if self.alignment == pango.Alignment.RIGHT: self.display_label.x = self.viewport.width - self.display_label.width elif self.alignment == pango.Alignment.CENTER: self.display_label.x = (self.viewport.width - self.display_label.width) / 2 #if self.single_paragraph: # self.display_label.y = (self.viewport.height - self.display_label.height) / 2.0 self.viewport._sprite_dirty = True # so that we get the cursor drawn def __on_viewport_render(self, viewport): if self.focused == False: return if self.cursor_position is None: return cur_x, cur_y, cur_w, cur_h = self._index_to_pos(self.cursor_position) cur_x = cur_x + self.display_label.x cur_y += self.display_label.y viewport.graphics.move_to(cur_x + 0.5, cur_y) viewport.graphics.line_to(cur_x + 0.5, cur_y + cur_h) viewport.graphics.stroke(self.cursor_color) if self.selection_start == self.selection_end: return # all done! start_x, start_y, start_w, start_h = self._index_to_pos(self.selection_start) end_x, end_y, end_w, end_h = self._index_to_pos(self.selection_end) iter = self._get_iter(self.selection_start) char_exts = iter.get_char_extents() cur_x, cur_y = char_exts.x / pango.SCALE, char_exts.y / pango.SCALE + self.display_label.y cur_line = None for i in range(self.selection_end - self.selection_start): prev_iter = pango.LayoutIter.copy(iter) iter.next_char() line = iter.get_line_readonly() if str(cur_line) != str(line): # can't compare layout lines for some reason exts = prev_iter.get_char_extents() char_exts = [ext / pango.SCALE for ext in (exts.x, exts.y, exts.width, exts.height)] viewport.graphics.rectangle(cur_x + self.display_label.x, cur_y, char_exts[0] + char_exts[2] - cur_x, char_exts[3]) char_exts = iter.get_char_extents() cur_x, cur_y = char_exts.x / pango.SCALE, char_exts.y / pango.SCALE + self.display_label.y cur_line = line exts = iter.get_char_extents() char_exts = [ext / pango.SCALE for ext in (exts.x, exts.y, exts.width, exts.height)] viewport.graphics.rectangle(cur_x + self.display_label.x, cur_y, char_exts[0] - cur_x, self.display_label.y + char_exts[1] - cur_y + char_exts[3]) viewport.graphics.fill(self.selection_color)