class UIHorizontalSlider(UIElement): """ A horizontal slider is intended to help users adjust values within a range, for example a volume control. :param relative_rect: A rectangle describing the position and dimensions of the element. :param start_value: The value to start the slider at. :param value_range: The full range of values. :param manager: The UIManager that manages this element. :param container: The container that this element is within. If set to None will be the root window's container. :param parent_element: The element this element 'belongs to' in the theming hierarchy. :param object_id: A custom defined ID for fine tuning of theming. :param anchors: A dictionary describing what this element's relative_rect is relative to. """ def __init__(self, relative_rect: pygame.Rect, start_value: Union[float, int], value_range: Tuple[Union[float, int], Union[float, int]], manager: IUIManagerInterface, container: Union[IContainerLikeInterface, None] = None, parent_element: UIElement = None, object_id: Union[str, None] = None, anchors: Dict[str, str] = None ): new_element_ids, new_object_ids = self.create_valid_ids(container=container, parent_element=parent_element, object_id=object_id, element_id='horizontal_slider') super().__init__(relative_rect, manager, container, layer_thickness=2, starting_height=1, element_ids=new_element_ids, object_ids=new_object_ids, anchors=anchors) self.default_button_width = 20 self.arrow_button_width = self.default_button_width self.sliding_button_width = self.default_button_width self.current_percentage = 0.5 self.left_limit_position = 0.0 self.starting_grab_x_difference = 0 self.value_range = value_range value_range_length = self.value_range[1] - self.value_range[0] self.current_value = int(self.value_range[0] + (self.current_percentage * value_range_length)) self.grabbed_slider = False self.has_moved_recently = False self.has_been_moved_by_user_recently = False self.background_colour = None self.border_colour = None self.border_width = None self.shadow_width = None self.drawable_shape = None self.shape_type = 'rectangle' self.shape_corner_radius = None self.background_rect = None self.scrollable_width = None self.right_limit_position = None self.scroll_position = None self.left_button = None self.right_button = None self.sliding_button = None self.arrow_buttons_enabled = True self.button_container = None self.rebuild_from_changed_theme_data() sliding_x_pos = int(self.background_rect.width / 2 - self.sliding_button_width / 2) self.sliding_button = UIButton(pygame.Rect((sliding_x_pos, 0), (self.sliding_button_width, self.background_rect.height)), '', self.ui_manager, container=self.button_container, starting_height=1, parent_element=self, object_id='#sliding_button', anchors={'left': 'left', 'right': 'left', 'top': 'top', 'bottom': 'bottom'} ) self.sliding_button.set_hold_range((self.background_rect.width, 100)) self.set_current_value(start_value) def rebuild(self): """ Rebuild anything that might need rebuilding. """ border_and_shadow = self.border_width + self.shadow_width self.background_rect = pygame.Rect((border_and_shadow + self.relative_rect.x, border_and_shadow + self.relative_rect.y), (self.relative_rect.width - (2 * border_and_shadow), self.relative_rect.height - (2 * border_and_shadow))) theming_parameters = {'normal_bg': self.background_colour, 'normal_border': self.border_colour, 'border_width': self.border_width, 'shadow_width': self.shadow_width, 'shape_corner_radius': self.shape_corner_radius} if self.shape_type == 'rectangle': self.drawable_shape = RectDrawableShape(self.rect, theming_parameters, ['normal'], self.ui_manager) elif self.shape_type == 'rounded_rectangle': self.drawable_shape = RoundedRectangleShape(self.rect, theming_parameters, ['normal'], self.ui_manager) self.set_image(self.drawable_shape.get_surface('normal')) if self.button_container is None: self.button_container = UIContainer(self.background_rect, manager=self.ui_manager, container=self.ui_container, anchors=self.anchors, object_id='#horiz_scrollbar_buttons_container') else: self.button_container.set_dimensions(self.background_rect.size) self.button_container.set_relative_position(self.background_rect.topleft) # Things below here depend on theme data so need to be updated on a rebuild if self.arrow_buttons_enabled: self.arrow_button_width = self.default_button_width if self.left_button is None: self.left_button = UIButton(pygame.Rect((0, 0), (self.arrow_button_width, self.background_rect.height)), '◀', self.ui_manager, container=self.button_container, starting_height=1, parent_element=self, object_id='#left_button', anchors={'left': 'left', 'right': 'left', 'top': 'top', 'bottom': 'bottom'} ) if self.right_button is None: self.right_button = UIButton(pygame.Rect((-self.arrow_button_width, 0), (self.arrow_button_width, self.background_rect.height)), '▶', self.ui_manager, container=self.button_container, starting_height=1, parent_element=self, object_id='#right_button', anchors={'left': 'right', 'right': 'right', 'top': 'top', 'bottom': 'bottom'}) else: self.arrow_button_width = 0 if self.left_button is not None: self.left_button.kill() self.left_button = None if self.right_button is not None: self.right_button.kill() self.right_button = None self.scrollable_width = (self.background_rect.width - self.sliding_button_width - (2 * self.arrow_button_width)) self.right_limit_position = self.scrollable_width self.scroll_position = self.scrollable_width / 2 if self.sliding_button is not None: sliding_x_pos = int((self.background_rect.width / 2) - (self.sliding_button_width / 2)) self.sliding_button.set_relative_position((sliding_x_pos, 0)) self.sliding_button.set_dimensions((self.sliding_button_width, self.background_rect.height)) self.sliding_button.set_hold_range((self.background_rect.width, 100)) self.set_current_value(self.current_value) def kill(self): """ Overrides the normal sprite kill() method to also kill the button elements that help make up the slider. """ self.button_container.kill() super().kill() def update(self, time_delta: float): """ Takes care of actually moving the slider based on interactions reported by the buttons or based on movement of the mouse if we are gripping the slider itself. :param time_delta: the time in seconds between calls to update. """ super().update(time_delta) if not self.alive(): return moved_this_frame = False if self.left_button is not None and (self.left_button.held and self.scroll_position > self.left_limit_position): self.scroll_position -= (250.0 * time_delta) self.scroll_position = max(self.scroll_position, self.left_limit_position) x_pos = (self.scroll_position + self.arrow_button_width) y_pos = 0 self.sliding_button.set_relative_position((x_pos, y_pos)) moved_this_frame = True elif self.right_button is not None and (self.right_button.held and self.scroll_position < self.right_limit_position): self.scroll_position += (250.0 * time_delta) self.scroll_position = min(self.scroll_position, self.right_limit_position) x_pos = (self.scroll_position + self.arrow_button_width) y_pos = 0 self.sliding_button.set_relative_position((x_pos, y_pos)) moved_this_frame = True mouse_x, mouse_y = self.ui_manager.get_mouse_position() if self.sliding_button.held and self.sliding_button.in_hold_range((mouse_x, mouse_y)): if not self.grabbed_slider: self.grabbed_slider = True real_scroll_pos = self.sliding_button.rect.left self.starting_grab_x_difference = mouse_x - real_scroll_pos real_scroll_pos = self.sliding_button.rect.left current_grab_difference = mouse_x - real_scroll_pos adjustment_required = current_grab_difference - self.starting_grab_x_difference self.scroll_position = self.scroll_position + adjustment_required self.scroll_position = min(max(self.scroll_position, self.left_limit_position), self.right_limit_position) x_pos = (self.scroll_position + self.arrow_button_width) y_pos = 0 self.sliding_button.set_relative_position((x_pos, y_pos)) moved_this_frame = True elif not self.sliding_button.held: self.grabbed_slider = False if moved_this_frame: self.current_percentage = self.scroll_position / self.scrollable_width self.current_value = self.value_range[0] + (self.current_percentage * (self.value_range[1] - self.value_range[0])) if not self.has_moved_recently: self.has_moved_recently = True if not self.has_been_moved_by_user_recently: self.has_been_moved_by_user_recently = True event_data = {'user_type': UI_HORIZONTAL_SLIDER_MOVED, 'value': self.current_value, 'ui_element': self, 'ui_object_id': self.most_specific_combined_id} slider_moved_event = pygame.event.Event(pygame.USEREVENT, event_data) pygame.event.post(slider_moved_event) def get_current_value(self) -> Union[float, int]: """ Gets the current value the slider is set to. :return: The current value recorded by the slider. """ self.has_moved_recently = False self.has_been_moved_by_user_recently = False return self.current_value def set_current_value(self, value: Union[float, int]): """ Sets the value of the slider, which will move the position of the slider to match. Will issue a warning if the value set is not in the value range. :param value: The value to set. """ if min(self.value_range[0], self.value_range[1]) <= value <= max(self.value_range[0], self.value_range[1]): self.current_value = float(value) value_range_size = (self.value_range[1] - self.value_range[0]) if value_range_size != 0: self.current_percentage = (self.current_value - self.value_range[0])/value_range_size self.scroll_position = self.scrollable_width * self.current_percentage x_pos = (self.scroll_position + self.arrow_button_width) y_pos = 0 self.sliding_button.set_relative_position((x_pos, y_pos)) self.has_moved_recently = True else: warnings.warn('value not in range', UserWarning) def rebuild_from_changed_theme_data(self): """ Called by the UIManager to check the theming data and rebuild whatever needs rebuilding for this element when the theme data has changed. """ has_any_changed = False shape_type = 'rectangle' shape_type_string = self.ui_theme.get_misc_data(self.object_ids, self.element_ids, 'shape') if shape_type_string is not None and shape_type_string in ['rectangle', 'rounded_rectangle']: shape_type = shape_type_string if shape_type != self.shape_type: self.shape_type = shape_type has_any_changed = True if self._check_shape_theming_changed(defaults={'border_width': 1, 'shadow_width': 2, 'shape_corner_radius': 2}): has_any_changed = True background_colour = self.ui_theme.get_colour_or_gradient(self.object_ids, self.element_ids, 'dark_bg') if background_colour != self.background_colour: self.background_colour = background_colour has_any_changed = True border_colour = self.ui_theme.get_colour_or_gradient(self.object_ids, self.element_ids, 'normal_border') if border_colour != self.border_colour: self.border_colour = border_colour has_any_changed = True buttons_enable_param = self.ui_theme.get_misc_data(self.object_ids, self.element_ids, 'enable_arrow_buttons') if buttons_enable_param is not None: try: buttons_enable = bool(int(buttons_enable_param)) except ValueError: buttons_enable = True if buttons_enable != self.arrow_buttons_enabled: self.arrow_buttons_enabled = buttons_enable has_any_changed = True sliding_button_width = self.default_button_width sliding_button_width_string = self.ui_theme.get_misc_data(self.object_ids, self.element_ids, 'sliding_button_width') if sliding_button_width_string is not None: try: sliding_button_width = int(sliding_button_width_string) except ValueError: sliding_button_width = self.default_button_width if sliding_button_width != self.sliding_button_width: self.sliding_button_width = sliding_button_width has_any_changed = True if has_any_changed: self.rebuild() def set_position(self, position: Union[pygame.math.Vector2, Tuple[int, int], Tuple[float, float]]): """ Sets the absolute screen position of this slider, updating all subordinate button elements at the same time. :param position: The absolute screen position to set. """ super().set_position(position) border_and_shadow = self.border_width + self.shadow_width self.background_rect.x = border_and_shadow + self.relative_rect.x self.background_rect.y = border_and_shadow + self.relative_rect.y self.button_container.set_relative_position(self.background_rect.topleft) def set_relative_position(self, position: Union[pygame.math.Vector2, Tuple[int, int], Tuple[float, float]]): """ Sets the relative screen position of this slider, updating all subordinate button elements at the same time. :param position: The relative screen position to set. """ super().set_relative_position(position) border_and_shadow = self.border_width + self.shadow_width self.background_rect.x = border_and_shadow + self.relative_rect.x self.background_rect.y = border_and_shadow + self.relative_rect.y self.button_container.set_relative_position(self.background_rect.topleft) def set_dimensions(self, dimensions: Union[pygame.math.Vector2, Tuple[int, int], Tuple[float, float]]): """ Method to directly set the dimensions of an element. :param dimensions: The new dimensions to set. """ super().set_dimensions(dimensions) border_and_shadow = self.border_width + self.shadow_width self.background_rect.width = self.relative_rect.width - (2 * border_and_shadow) self.background_rect.height = self.relative_rect.height - (2 * border_and_shadow) self.button_container.set_dimensions(self.background_rect.size) # sort out sliding button parameters self.scrollable_width = (self.background_rect.width - self.sliding_button_width - (2 * self.arrow_button_width)) self.right_limit_position = self.scrollable_width self.scroll_position = self.scrollable_width * self.current_percentage slider_x_pos = self.scroll_position + self.arrow_button_width slider_y_pos = 0 self.sliding_button.set_dimensions((self.sliding_button_width, self.background_rect.height)) self.sliding_button.set_relative_position((slider_x_pos, slider_y_pos))
class UIScrollingContainer(UIElement, IContainerLikeInterface): """ A container like UI element that lets users scroll around a larger container of content with scroll bars. :param relative_rect: The size and relative position of the container. This will also be the starting size of the scrolling area. :param manager: The UI manager for this element. :param starting_height: The starting layer height of this container above it's container. Defaults to 1. :param container: The container this container is within. Defaults to None (which is the root container for the UI) :param parent_element: A parent element for this container. Defaults to None, or the container if you've set that. :param object_id: An object ID for this element. :param anchors: Layout anchors in a dictionary. :param visible: Whether the element is visible by default. Warning - container visibility may override this. """ def __init__(self, relative_rect: pygame.Rect, manager: IUIManagerInterface, *, starting_height: int = 1, container: Union[IContainerLikeInterface, None] = None, parent_element: Union[UIElement, None] = None, object_id: Union[ObjectID, str, None] = None, anchors: Union[Dict[str, str], None] = None, visible: int = 1): super().__init__(relative_rect, manager, container, starting_height=starting_height, layer_thickness=2, anchors=anchors, visible=visible) self._create_valid_ids(container=container, parent_element=parent_element, object_id=object_id, element_id='scrolling_container') # self.parent_element = parent_element self.scroll_bar_width = 0 self.scroll_bar_height = 0 self.need_to_sort_out_scrollbars = False self.vert_scroll_bar = None # type: Union[UIVerticalScrollBar, None] self.horiz_scroll_bar = None # type: Union[UIHorizontalScrollBar, None] self.set_image(self.ui_manager.get_universal_empty_surface()) # this contains the scroll bars and the 'view' container self._root_container = UIContainer(relative_rect=relative_rect, manager=manager, starting_height=starting_height, container=container, parent_element=parent_element, object_id=ObjectID(object_id='#root_container', class_id=None), anchors=anchors, visible=self.visible) # This container is the view on to the scrollable container it's size is determined by # the size of the root container and whether there are any scroll bars or not. view_rect = pygame.Rect(0, 0, relative_rect.width, relative_rect.height) self._view_container = UIContainer(relative_rect=view_rect, manager=manager, starting_height=0, container=self._root_container, parent_element=parent_element, object_id=ObjectID(object_id='#view_container', class_id=None), anchors={'left': 'left', 'right': 'right', 'top': 'top', 'bottom': 'bottom'}) # This container is what we actually put other stuff in. # It is aligned to the top left corner but that isn't that important for a container that # can be much larger than it's view scrollable_rect = pygame.Rect(0, 0, relative_rect.width, relative_rect.height) self.scrollable_container = UIContainer(relative_rect=scrollable_rect, manager=manager, starting_height=0, container=self._view_container, parent_element=parent_element, object_id=ObjectID( object_id='#scrollable_container', class_id=None), anchors={'left': 'left', 'right': 'left', 'top': 'top', 'bottom': 'top'}) self.scrolling_height = 0 self.scrolling_width = 0 self.scrolling_bottom = 0 self.scrolling_right = 0 self._calculate_scrolling_dimensions() def get_container(self) -> IUIContainerInterface: """ Gets the scrollable container area (the one that moves around with the scrollbars) from this container-like UI element. :return: the scrolling container. """ return self.scrollable_container def kill(self): """ Overrides the basic kill() method of a pygame sprite so that we also kill all the UI elements in this panel. """ self._root_container.kill() super().kill() def set_position(self, position: Union[pygame.math.Vector2, Tuple[int, int], Tuple[float, float]]): """ Method to directly set the absolute screen rect position of an element. :param position: The new position to set. """ super().set_position(position) self._root_container.set_dimensions(position) def set_relative_position(self, position: Union[pygame.math.Vector2, Tuple[int, int], Tuple[float, float]]): """ Method to directly set the relative rect position of an element. :param position: The new position to set. """ super().set_relative_position(position) self._root_container.set_relative_position(position) def set_dimensions(self, dimensions: Union[pygame.math.Vector2, Tuple[int, int], Tuple[float, float]]): """ Method to directly set the dimensions of an element. NOTE: Using this on elements inside containers with non-default anchoring arrangements may make a mess of them. :param dimensions: The new dimensions to set. """ super().set_dimensions(dimensions) self._root_container.set_dimensions(dimensions) self._calculate_scrolling_dimensions() self._sort_out_element_container_scroll_bars() def set_scrollable_area_dimensions(self, dimensions: Union[pygame.math.Vector2, Tuple[int, int], Tuple[float, float]]): """ Set the size of the scrollable area container. It starts the same size as the view container but often you want to expand it, or why have a scrollable container? :param dimensions: The new dimensions. """ self.scrollable_container.set_dimensions(dimensions) self._calculate_scrolling_dimensions() self._sort_out_element_container_scroll_bars() def update(self, time_delta: float): """ Updates the scrolling container's position based upon the scroll bars and updates the scrollbar's visible percentage as well if that has changed. :param time_delta: The time passed between frames, measured in seconds. """ super().update(time_delta) if (self.vert_scroll_bar is not None and self.vert_scroll_bar.check_has_moved_recently()): self._calculate_scrolling_dimensions() vis_percent = self._view_container.rect.height / self.scrolling_height if self.vert_scroll_bar.start_percentage <= 0.5: start_height = int(self.vert_scroll_bar.start_percentage * self.scrolling_height) else: button_percent_height = (self.vert_scroll_bar.sliding_button.rect.height / self.vert_scroll_bar.scrollable_height) button_bottom_percent = (self.vert_scroll_bar.start_percentage + button_percent_height) start_height = (int(button_bottom_percent * self.scrolling_height) - self._view_container.rect.height) if vis_percent < 1.0: self.vert_scroll_bar.set_visible_percentage(vis_percent) else: self._remove_vert_scrollbar() if self.scrolling_bottom < self._view_container.rect.bottom: start_height = min(start_height, self._view_container.rect.height) new_pos = (self.scrollable_container.relative_rect.x, -start_height) self.scrollable_container.set_relative_position(new_pos) if (self.horiz_scroll_bar is not None and self.horiz_scroll_bar.check_has_moved_recently()): self._calculate_scrolling_dimensions() vis_percent = self._view_container.rect.width / self.scrolling_width if self.horiz_scroll_bar.start_percentage <= 0.5: start_width = int(self.horiz_scroll_bar.start_percentage * self.scrolling_width) else: button_percent_width = (self.horiz_scroll_bar.sliding_button.rect.width / self.horiz_scroll_bar.scrollable_width) button_right_percent = (self.horiz_scroll_bar.start_percentage + button_percent_width) start_width = (int(button_right_percent * self.scrolling_width) - self._view_container.rect.width) if vis_percent < 1.0: self.horiz_scroll_bar.set_visible_percentage(vis_percent) else: self._remove_horiz_scrollbar() if self.scrolling_right < self._view_container.rect.right: start_width = min(start_width, self._view_container.rect.width) new_pos = (-start_width, self.scrollable_container.relative_rect.y) self.scrollable_container.set_relative_position(new_pos) def _calculate_scrolling_dimensions(self): """ Calculate all the variables we need to scroll the container correctly. This is a bit of a fiddly process since we can resize our viewing area, the scrollable area and we generally don't want to yank the area you are looking at too much either. Plus, the scrollbars only have somewhat limited accuracy so need clamping... """ scrolling_top = min(self.scrollable_container.rect.top, self._view_container.rect.top) scrolling_left = min(self.scrollable_container.rect.left, self._view_container.rect.left) # used for clamping self.scrolling_bottom = max(self.scrollable_container.rect.bottom, self._view_container.rect.bottom) self.scrolling_right = max(self.scrollable_container.rect.right, self._view_container.rect.right) self.scrolling_height = self.scrolling_bottom - scrolling_top self.scrolling_width = self.scrolling_right - scrolling_left def _sort_out_element_container_scroll_bars(self): """ This creates, re-sizes or removes the scrollbars after resizing, but not after the scroll bar has been moved. Instead it tries to keep the scrollbars in the same approximate position they were in before resizing """ self._check_scroll_bars_and_adjust() need_horiz_scroll_bar, need_vert_scroll_bar = self._check_scroll_bars_and_adjust() if need_vert_scroll_bar: vis_percent = self._view_container.rect.height / self.scrolling_height if self.vert_scroll_bar is None: self.scroll_bar_width = 20 scroll_bar_rect = pygame.Rect(-self.scroll_bar_width, 0, self.scroll_bar_width, self._view_container.rect.height) self.vert_scroll_bar = UIVerticalScrollBar(relative_rect=scroll_bar_rect, visible_percentage=vis_percent, manager=self.ui_manager, container=self._root_container, parent_element=self, anchors={'left': 'right', 'right': 'right', 'top': 'top', 'bottom': 'bottom'}) else: start_percent = ((self._view_container.rect.top - self.scrollable_container.rect.top) / self.scrolling_height) self.vert_scroll_bar.start_percentage = start_percent self.vert_scroll_bar.set_visible_percentage(vis_percent) self.vert_scroll_bar.set_dimensions((self.scroll_bar_width, self._view_container.rect.height)) else: self._remove_vert_scrollbar() if need_horiz_scroll_bar: vis_percent = self._view_container.rect.width / self.scrolling_width if self.horiz_scroll_bar is None: self.scroll_bar_height = 20 scroll_bar_rect = pygame.Rect(0, -self.scroll_bar_height, self._view_container.rect.width, self.scroll_bar_height) self.horiz_scroll_bar = UIHorizontalScrollBar(relative_rect=scroll_bar_rect, visible_percentage=vis_percent, manager=self.ui_manager, container=self._root_container, parent_element=self, anchors={'left': 'left', 'right': 'right', 'top': 'bottom', 'bottom': 'bottom'}) else: start_percent = ((self._view_container.rect.left - self.scrollable_container.rect.left) / self.scrolling_width) self.horiz_scroll_bar.start_percentage = start_percent self.horiz_scroll_bar.set_visible_percentage(vis_percent) self.horiz_scroll_bar.set_dimensions((self._view_container.rect.width, self.scroll_bar_height)) else: self._remove_horiz_scrollbar() def _check_scroll_bars_and_adjust(self): """ Check if we need a horizontal or vertical scrollbar and adjust the containers if we do. Adjusting the containers for a scrollbar, may mean we now need a scrollbar in the other dimension so we need to call this twice. """ self.scroll_bar_width = 0 self.scroll_bar_height = 0 need_horiz_scroll_bar = False need_vert_scroll_bar = False if (self.scrolling_height > self._view_container.rect.height or self.scrollable_container.relative_rect.top != 0): need_vert_scroll_bar = True self.scroll_bar_width = 20 if (self.scrolling_width > self._view_container.rect.width or self.scrollable_container.relative_rect.left != 0): need_horiz_scroll_bar = True self.scroll_bar_height = 20 if need_vert_scroll_bar or need_horiz_scroll_bar: new_width = (self._root_container.rect.width - self.scroll_bar_width) new_height = (self._root_container.rect.height - self.scroll_bar_height) new_dimensions = (new_width, new_height) self._view_container.set_dimensions(new_dimensions) self._calculate_scrolling_dimensions() return need_horiz_scroll_bar, need_vert_scroll_bar def _remove_vert_scrollbar(self): """ Get rid of the vertical scroll bar and resize the containers appropriately. """ if self.vert_scroll_bar is not None: self.vert_scroll_bar.kill() self.vert_scroll_bar = None self.scroll_bar_width = 0 new_width = (self._root_container.rect.width - self.scroll_bar_width) old_height = self._view_container.rect.height new_dimensions = (new_width, old_height) self._view_container.set_dimensions(new_dimensions) self._calculate_scrolling_dimensions() if self.horiz_scroll_bar is not None: self.horiz_scroll_bar.set_dimensions((self._view_container.rect.width, self.scroll_bar_height)) def _remove_horiz_scrollbar(self): """ Get rid of the horiz scroll bar and resize the containers appropriately. """ if self.horiz_scroll_bar is not None: self.horiz_scroll_bar.kill() self.horiz_scroll_bar = None self.scroll_bar_height = 0 new_height = (self._root_container.rect.height - self.scroll_bar_height) old_width = self._view_container.rect.width new_dimensions = (old_width, new_height) self._view_container.set_dimensions(new_dimensions) self._calculate_scrolling_dimensions() if self.vert_scroll_bar is not None: self.vert_scroll_bar.set_dimensions((self.scroll_bar_width, self._view_container.rect.height)) def disable(self): """ Disables all elements in the container so they are no longer interactive. """ if self.is_enabled: self.is_enabled = False self._root_container.disable() def enable(self): """ Enables all elements in the container so they are interactive again. """ if not self.is_enabled: self.is_enabled = True self._root_container.enable() def show(self): """ In addition to the base UIElement.show() - call show() of owned container - _root_container. All other subelements (view_container, scrollbars) are children of _root_container, so it's visibility will propagate to them - there is no need to call their show() methods separately. """ super().show() self._root_container.show() def hide(self): """ In addition to the base UIElement.hide() - call hide() of owned container - _root_container. All other subelements (view_container, scrollbars) are children of _root_container, so it's visibility will propagate to them - there is no need to call their hide() methods separately. """ self._root_container.hide() super().hide()
class UIVerticalScrollBar(UIElement): """ A vertical scroll bar allows users to position a smaller visible area within a vertically larger area. :param relative_rect: The size and position of the scroll bar. :param visible_percentage: The vertical percentage of the larger area that is visible, between 0.0 and 1.0. :param manager: The UIManager that manages this element. :param container: The container that this element is within. If set to None will be the root window's container. :param parent_element: The element this element 'belongs to' in the theming hierarchy. :param object_id: A custom defined ID for fine tuning of theming. :param anchors: A dictionary describing what this element's relative_rect is relative to. :param visible: Whether the element is visible by default. Warning - container visibility may override this. """ def __init__(self, relative_rect: pygame.Rect, visible_percentage: float, manager: IUIManagerInterface, container: Union[IContainerLikeInterface, None] = None, parent_element: UIElement = None, object_id: Union[ObjectID, str, None] = None, anchors: Dict[str, str] = None, visible: int = 1): super().__init__(relative_rect, manager, container, layer_thickness=2, starting_height=1, anchors=anchors, visible=visible) self._create_valid_ids(container=container, parent_element=parent_element, object_id=object_id, element_id='vertical_scroll_bar') self.button_height = 20 self.arrow_button_height = self.button_height self.scroll_position = 0.0 self.top_limit = 0.0 self.starting_grab_y_difference = 0 self.visible_percentage = max(0.0, min(visible_percentage, 1.0)) self.start_percentage = 0.0 self.grabbed_slider = False self.has_moved_recently = False self.scroll_wheel_moved = False self.scroll_wheel_amount = 0 self.background_colour = None self.border_colour = None self.disabled_border_colour = None self.disabled_background_colour = None self.border_width = None self.shadow_width = None self.drawable_shape = None self.shape = 'rectangle' self.shape_corner_radius = None self.background_rect = None # type: Union[None, pygame.Rect] self.scrollable_height = None # type: Union[None, int, float] self.bottom_limit = None self.sliding_rect_position = None # type: Union[None, pygame.math.Vector2] self.top_button = None self.bottom_button = None self.sliding_button = None self.enable_arrow_buttons = True self.button_container = None self.rebuild_from_changed_theme_data() scroll_bar_height = max( 5, int(self.scrollable_height * self.visible_percentage)) self.sliding_button = UIButton(pygame.Rect( (int(self.sliding_rect_position[0]), int(self.sliding_rect_position[1])), (self.background_rect.width, scroll_bar_height)), '', self.ui_manager, container=self.button_container, starting_height=1, parent_element=self, object_id="#sliding_button", anchors={ 'left': 'left', 'right': 'right', 'top': 'top', 'bottom': 'top' }) self.join_focus_sets(self.sliding_button) self.sliding_button.set_hold_range((100, self.background_rect.height)) def rebuild(self): """ Rebuild anything that might need rebuilding. """ border_and_shadow = self.border_width + self.shadow_width self.background_rect = pygame.Rect( (border_and_shadow + self.relative_rect.x, border_and_shadow + self.relative_rect.y), (self.relative_rect.width - (2 * border_and_shadow), self.relative_rect.height - (2 * border_and_shadow))) theming_parameters = { 'normal_bg': self.background_colour, 'normal_border': self.border_colour, 'disabled_bg': self.disabled_background_colour, 'disabled_border': self.disabled_border_colour, 'border_width': self.border_width, 'shadow_width': self.shadow_width, 'shape_corner_radius': self.shape_corner_radius } if self.shape == 'rectangle': self.drawable_shape = RectDrawableShape(self.rect, theming_parameters, ['normal', 'disabled'], self.ui_manager) elif self.shape == 'rounded_rectangle': self.drawable_shape = RoundedRectangleShape( self.rect, theming_parameters, ['normal', 'disabled'], self.ui_manager) self.set_image(self.drawable_shape.get_fresh_surface()) if self.button_container is None: self.button_container = UIContainer( self.background_rect, manager=self.ui_manager, container=self.ui_container, anchors=self.anchors, object_id='#vert_scrollbar_buttons_container', visible=self.visible) self.join_focus_sets(self.button_container) else: self.button_container.set_dimensions(self.background_rect.size) self.button_container.set_relative_position( self.background_rect.topleft) if self.enable_arrow_buttons: self.arrow_button_height = self.button_height if self.top_button is None: self.top_button = UIButton(pygame.Rect( (0, 0), (self.background_rect.width, self.arrow_button_height)), '▲', self.ui_manager, container=self.button_container, starting_height=1, parent_element=self, object_id=ObjectID( "#top_button", "@arrow_button"), anchors={ 'left': 'left', 'right': 'right', 'top': 'top', 'bottom': 'top' }) self.join_focus_sets(self.top_button) if self.bottom_button is None: self.bottom_button = UIButton(pygame.Rect( (0, -self.arrow_button_height), (self.background_rect.width, self.arrow_button_height)), '▼', self.ui_manager, container=self.button_container, starting_height=1, parent_element=self, object_id=ObjectID( "#bottom_button", "@arrow_button"), anchors={ 'left': 'left', 'right': 'right', 'top': 'bottom', 'bottom': 'bottom' }) self.join_focus_sets(self.bottom_button) else: self.arrow_button_height = 0 if self.top_button is not None: self.top_button.kill() self.top_button = None if self.bottom_button is not None: self.bottom_button.kill() self.bottom_button = None self.scrollable_height = self.background_rect.height - ( 2 * self.arrow_button_height) self.bottom_limit = self.scrollable_height scroll_bar_height = max( 5, int(self.scrollable_height * self.visible_percentage)) self.scroll_position = min(max(self.scroll_position, self.top_limit), self.bottom_limit - scroll_bar_height) x_pos = 0 y_pos = (self.scroll_position + self.arrow_button_height) self.sliding_rect_position = pygame.math.Vector2(x_pos, y_pos) if self.sliding_button is not None: self.sliding_button.set_relative_position( self.sliding_rect_position) self.sliding_button.set_dimensions( (self.background_rect.width, scroll_bar_height)) self.sliding_button.set_hold_range( (100, self.background_rect.height)) def check_has_moved_recently(self) -> bool: """ Returns True if the scroll bar was moved in the last call to the update function. :return: True if we've recently moved the scroll bar, False otherwise. """ return self.has_moved_recently def kill(self): """ Overrides the kill() method of the UI element class to kill all the buttons in the scroll bar and clear any of the parts of the scroll bar that are currently recorded as the 'last focused vertical scroll bar element' on the ui manager. NOTE: the 'last focused' state on the UI manager is used so that the mouse wheel will move whichever scrollbar we last fiddled with even if we've been doing other stuff. This seems to be consistent with the most common mousewheel/scrollbar interactions used elsewhere. """ self.button_container.kill() super().kill() def process_event(self, event: pygame.event.Event) -> bool: """ Checks an event from pygame's event queue to see if the scroll bar needs to react to it. In this case it is just mousewheel events, mainly because the buttons that make up the scroll bar will handle the required mouse click events. :param event: The event to process. :return: Returns True if we've done something with the input event. """ consumed_event = False if (self.is_enabled and self._check_is_focus_set_hovered() and event.type == pygame.MOUSEWHEEL): self.scroll_wheel_moved = True self.scroll_wheel_amount = event.y consumed_event = True return consumed_event def _check_is_focus_set_hovered(self) -> bool: """ Check if this scroll bar's focus set is currently hovered in the UI. :return: True if it was. """ return any(element.hovered for element in self.get_focus_set()) def update(self, time_delta: float): """ Called once per update loop of our UI manager. Deals largely with moving the scroll bar and updating the resulting 'start_percentage' variable that is then used by other 'scrollable' UI elements to control the point they start drawing. Reacts to presses of the up and down arrow buttons, movement of the mouse wheel and dragging of the scroll bar itself. :param time_delta: A float, roughly representing the time in seconds between calls to this method. """ super().update(time_delta) self.has_moved_recently = False if self.alive(): moved_this_frame = False if self.scroll_wheel_moved and ( self.scroll_position > self.top_limit or self.scroll_position < self.bottom_limit): self.scroll_wheel_moved = False self.scroll_position -= self.scroll_wheel_amount * (750.0 * time_delta) self.scroll_position = min( max(self.scroll_position, self.top_limit), self.bottom_limit - self.sliding_button.relative_rect.height) x_pos = 0 y_pos = (self.scroll_position + self.arrow_button_height) self.sliding_button.set_relative_position((x_pos, y_pos)) moved_this_frame = True elif self.top_button is not None and self.top_button.held: self.scroll_position -= (250.0 * time_delta) self.scroll_position = max(self.scroll_position, self.top_limit) x_pos = 0 y_pos = (self.scroll_position + self.arrow_button_height) self.sliding_button.set_relative_position((x_pos, y_pos)) moved_this_frame = True elif self.bottom_button is not None and self.bottom_button.held: self.scroll_position += (250.0 * time_delta) self.scroll_position = min( self.scroll_position, self.bottom_limit - self.sliding_button.relative_rect.height) x_pos = 0 y_pos = (self.scroll_position + self.arrow_button_height) self.sliding_button.set_relative_position((x_pos, y_pos)) moved_this_frame = True mouse_x, mouse_y = self.ui_manager.get_mouse_position() if self.sliding_button.held and self.sliding_button.in_hold_range( (mouse_x, mouse_y)): if not self.grabbed_slider: self.grabbed_slider = True real_scroll_pos = self.sliding_button.rect.top self.starting_grab_y_difference = mouse_y - real_scroll_pos real_scroll_pos = self.sliding_button.rect.top current_grab_difference = mouse_y - real_scroll_pos adjustment_required = current_grab_difference - self.starting_grab_y_difference self.scroll_position = self.scroll_position + adjustment_required self.scroll_position = min( max(self.scroll_position, self.top_limit), self.bottom_limit - self.sliding_button.rect.height) x_pos = 0 y_pos = (self.scroll_position + self.arrow_button_height) self.sliding_button.set_relative_position((x_pos, y_pos)) moved_this_frame = True elif not self.sliding_button.held: self.grabbed_slider = False if moved_this_frame: self.start_percentage = self.scroll_position / self.scrollable_height if not self.has_moved_recently: self.has_moved_recently = True def redraw_scrollbar(self): """ Redraws the 'scrollbar' portion of the whole UI element. Called when we change the visible percentage. """ self.scrollable_height = self.background_rect.height - ( 2 * self.arrow_button_height) self.bottom_limit = self.scrollable_height scroll_bar_height = max( 5, int(self.scrollable_height * self.visible_percentage)) x_pos = 0 y_pos = (self.scroll_position + self.arrow_button_height) self.sliding_rect_position = pygame.math.Vector2(x_pos, y_pos) if self.sliding_button is None: self.sliding_button = UIButton(pygame.Rect( int(x_pos), int(y_pos), self.background_rect.width, scroll_bar_height), '', self.ui_manager, container=self.button_container, starting_height=1, parent_element=self, object_id="#sliding_button", anchors={ 'left': 'left', 'right': 'right', 'top': 'top', 'bottom': 'top' }) self.join_focus_sets(self.sliding_button) else: self.sliding_button.set_relative_position( self.sliding_rect_position) self.sliding_button.set_dimensions( (self.background_rect.width, scroll_bar_height)) self.sliding_button.set_hold_range((100, self.background_rect.height)) def set_visible_percentage(self, percentage: float): """ Sets the percentage of the total 'scrollable area' that is currently visible. This will affect the size of the scrollbar and should be called if the vertical size of the 'scrollable area' or the vertical size of the visible area change. :param percentage: A float between 0.0 and 1.0 representing the percentage that is visible. """ self.visible_percentage = max(0.0, min(1.0, percentage)) if 1.0 - self.start_percentage < self.visible_percentage: self.start_percentage = 1.0 - self.visible_percentage self.redraw_scrollbar() def reset_scroll_position(self): """ Reset the current scroll position back to the top. """ self.scroll_position = 0.0 self.start_percentage = 0.0 def rebuild_from_changed_theme_data(self): """ Called by the UIManager to check the theming data and rebuild whatever needs rebuilding for this element when the theme data has changed. """ super().rebuild_from_changed_theme_data() has_any_changed = False if self._check_misc_theme_data_changed( attribute_name='shape', default_value='rectangle', casting_func=str, allowed_values=['rectangle', 'rounded_rectangle']): has_any_changed = True if self._check_shape_theming_changed(defaults={ 'border_width': 1, 'shadow_width': 2, 'shape_corner_radius': 2 }): has_any_changed = True background_colour = self.ui_theme.get_colour_or_gradient( 'dark_bg', self.combined_element_ids) if background_colour != self.background_colour: self.background_colour = background_colour has_any_changed = True border_colour = self.ui_theme.get_colour_or_gradient( 'normal_border', self.combined_element_ids) if border_colour != self.border_colour: self.border_colour = border_colour has_any_changed = True disabled_background_colour = self.ui_theme.get_colour_or_gradient( 'disabled_dark_bg', self.combined_element_ids) if disabled_background_colour != self.disabled_background_colour: self.disabled_background_colour = disabled_background_colour has_any_changed = True disabled_border_colour = self.ui_theme.get_colour_or_gradient( 'disabled_border', self.combined_element_ids) if disabled_border_colour != self.disabled_border_colour: self.disabled_border_colour = disabled_border_colour has_any_changed = True def parse_to_bool(str_data: str): return bool(int(str_data)) if self._check_misc_theme_data_changed( attribute_name='enable_arrow_buttons', default_value=True, casting_func=parse_to_bool): has_any_changed = True if has_any_changed: self.rebuild() def set_position(self, position: Union[pygame.math.Vector2, Tuple[int, int], Tuple[float, float]]): """ Sets the absolute screen position of this scroll bar, updating all subordinate button elements at the same time. :param position: The absolute screen position to set. """ super().set_position(position) border_and_shadow = self.border_width + self.shadow_width self.background_rect.x = border_and_shadow + self.relative_rect.x self.background_rect.y = border_and_shadow + self.relative_rect.y self.button_container.set_relative_position( self.background_rect.topleft) def set_relative_position(self, position: Union[pygame.math.Vector2, Tuple[int, int], Tuple[float, float]]): """ Sets the relative screen position of this scroll bar, updating all subordinate button elements at the same time. :param position: The relative screen position to set. """ super().set_relative_position(position) border_and_shadow = self.border_width + self.shadow_width self.background_rect.x = border_and_shadow + self.relative_rect.x self.background_rect.y = border_and_shadow + self.relative_rect.y self.button_container.set_relative_position( self.background_rect.topleft) def set_dimensions(self, dimensions: Union[pygame.math.Vector2, Tuple[int, int], Tuple[float, float]]): """ Method to directly set the dimensions of an element. :param dimensions: The new dimensions to set. """ super().set_dimensions(dimensions) border_and_shadow = self.border_width + self.shadow_width self.background_rect.width = self.relative_rect.width - ( 2 * border_and_shadow) self.background_rect.height = self.relative_rect.height - ( 2 * border_and_shadow) self.button_container.set_dimensions(self.background_rect.size) # sort out scroll bar parameters self.scrollable_height = self.background_rect.height - ( 2 * self.arrow_button_height) self.bottom_limit = self.scrollable_height scroll_bar_height = max( 5, int(self.scrollable_height * self.visible_percentage)) base_scroll_bar_y = self.arrow_button_height max_scroll_bar_y = base_scroll_bar_y + (self.scrollable_height - scroll_bar_height) self.sliding_rect_position.y = max( base_scroll_bar_y, min((base_scroll_bar_y + int(self.start_percentage * self.scrollable_height)), max_scroll_bar_y)) self.scroll_position = self.sliding_rect_position.y - base_scroll_bar_y self.sliding_button.set_dimensions( (self.background_rect.width, scroll_bar_height)) self.sliding_button.set_relative_position(self.sliding_rect_position) def disable(self): """ Disables the scroll bar so it is no longer interactive. """ if self.is_enabled: self.is_enabled = False self.button_container.disable() self.drawable_shape.set_active_state('disabled') def enable(self): """ Enables the scroll bar so it is interactive once again. """ if not self.is_enabled: self.is_enabled = True self.button_container.enable() self.drawable_shape.set_active_state('normal') def show(self): """ In addition to the base UIElement.show() - show the self.button_container which will propagate and show all the buttons. """ super().show() self.button_container.show() def hide(self): """ In addition to the base UIElement.hide() - hide the self.button_container which will propagate and hide all the buttons. """ super().hide() self.button_container.hide()
class UIHorizontalSlider(UIElement): """ A horizontal slider is intended to help users adjust values within a range, for example a volume control. :param relative_rect: A rectangle describing the position and dimensions of the element. :param start_value: The value to start the slider at. :param value_range: The full range of values. :param manager: The UIManager that manages this element. :param container: The container that this element is within. If set to None will be the root window's container. :param parent_element: The element this element 'belongs to' in the theming hierarchy. :param object_id: A custom defined ID for fine tuning of theming. :param anchors: A dictionary describing what this element's relative_rect is relative to. :param visible: Whether the element is visible by default. Warning - container visibility may override this. :param click_increment: the amount to increment by when clicking one of the arrow buttons. """ def __init__(self, relative_rect: pygame.Rect, start_value: Union[float, int], value_range: Tuple[Union[float, int], Union[float, int]], manager: IUIManagerInterface, container: Union[IContainerLikeInterface, None] = None, parent_element: UIElement = None, object_id: Union[ObjectID, str, None] = None, anchors: Dict[str, str] = None, visible: int = 1, click_increment: Union[float, int] = 1): super().__init__(relative_rect, manager, container, layer_thickness=2, starting_height=1, anchors=anchors, visible=visible) self._create_valid_ids(container=container, parent_element=parent_element, object_id=object_id, element_id='horizontal_slider') self.default_button_width = 20 self.arrow_button_width = self.default_button_width self.sliding_button_width = self.default_button_width self.current_percentage = 0.5 self.left_limit_position = 0.0 self.starting_grab_x_difference = 0 if (isinstance(start_value, int) and isinstance(value_range[0], int) and isinstance(value_range[1], int)): self.use_integers_for_value = True else: self.use_integers_for_value = False self.value_range = value_range value_range_length = self.value_range[1] - self.value_range[0] self.current_value = self.value_range[0] + (self.current_percentage * value_range_length) if self.use_integers_for_value: self.current_value = int(self.current_value) self.grabbed_slider = False self.has_moved_recently = False self.has_been_moved_by_user_recently = False self.background_colour = None self.border_colour = None self.disabled_border_colour = None self.disabled_background_colour = None self.border_width = None self.shadow_width = None self.drawable_shape = None self.shape = 'rectangle' self.shape_corner_radius = None self.background_rect = None # type: Optional[pygame.Rect] self.scrollable_width = None self.right_limit_position = None self.scroll_position = None self.left_button = None self.right_button = None self.sliding_button = None self.enable_arrow_buttons = True self.button_container = None self.button_held_repeat_time = 0.2 self.button_held_repeat_acc = 0.0 self.increment = click_increment self.rebuild_from_changed_theme_data() sliding_x_pos = int(self.background_rect.width / 2 - self.sliding_button_width / 2) self.sliding_button = UIButton(pygame.Rect( (sliding_x_pos, 0), (self.sliding_button_width, self.background_rect.height)), '', self.ui_manager, container=self.button_container, starting_height=1, parent_element=self, object_id=ObjectID( object_id='#sliding_button', class_id='None'), anchors={ 'left': 'left', 'right': 'left', 'top': 'top', 'bottom': 'bottom' }, visible=self.visible) self.sliding_button.set_hold_range((self.background_rect.width, 100)) self.set_current_value(start_value) def rebuild(self): """ Rebuild anything that might need rebuilding. """ border_and_shadow = self.border_width + self.shadow_width self.background_rect = pygame.Rect( (border_and_shadow + self.relative_rect.x, border_and_shadow + self.relative_rect.y), (self.relative_rect.width - (2 * border_and_shadow), self.relative_rect.height - (2 * border_and_shadow))) theming_parameters = { 'normal_bg': self.background_colour, 'normal_border': self.border_colour, 'disabled_bg': self.disabled_background_colour, 'disabled_border': self.disabled_border_colour, 'border_width': self.border_width, 'shadow_width': self.shadow_width, 'shape_corner_radius': self.shape_corner_radius } if self.shape == 'rectangle': self.drawable_shape = RectDrawableShape(self.rect, theming_parameters, ['normal', 'disabled'], self.ui_manager) elif self.shape == 'rounded_rectangle': self.drawable_shape = RoundedRectangleShape( self.rect, theming_parameters, ['normal', 'disabled'], self.ui_manager) self.set_image(self.drawable_shape.get_fresh_surface()) if self.button_container is None: self.button_container = UIContainer( self.background_rect, manager=self.ui_manager, container=self.ui_container, anchors=self.anchors, object_id='#horiz_scrollbar_buttons_container', visible=self.visible) else: self.button_container.set_dimensions(self.background_rect.size) self.button_container.set_relative_position( self.background_rect.topleft) # Things below here depend on theme data so need to be updated on a rebuild if self.enable_arrow_buttons: self.arrow_button_width = self.default_button_width if self.left_button is None: self.left_button = UIButton(pygame.Rect( (0, 0), (self.arrow_button_width, self.background_rect.height)), '◀', self.ui_manager, container=self.button_container, starting_height=1, parent_element=self, object_id=ObjectID( "#left_button", "@arrow_button"), anchors={ 'left': 'left', 'right': 'left', 'top': 'top', 'bottom': 'bottom' }, visible=self.visible) if self.right_button is None: self.right_button = UIButton(pygame.Rect( (-self.arrow_button_width, 0), (self.arrow_button_width, self.background_rect.height)), '▶', self.ui_manager, container=self.button_container, starting_height=1, parent_element=self, object_id=ObjectID( "#right_button", "@arrow_button"), anchors={ 'left': 'right', 'right': 'right', 'top': 'top', 'bottom': 'bottom' }, visible=self.visible) else: self.arrow_button_width = 0 if self.left_button is not None: self.left_button.kill() self.left_button = None if self.right_button is not None: self.right_button.kill() self.right_button = None self.scrollable_width = (self.background_rect.width - self.sliding_button_width - (2 * self.arrow_button_width)) self.right_limit_position = self.scrollable_width self.scroll_position = self.scrollable_width / 2 if self.sliding_button is not None: sliding_x_pos = int((self.background_rect.width / 2) - (self.sliding_button_width / 2)) self.sliding_button.set_relative_position((sliding_x_pos, 0)) self.sliding_button.set_dimensions( (self.sliding_button_width, self.background_rect.height)) self.sliding_button.set_hold_range( (self.background_rect.width, 100)) self.set_current_value(self.current_value, False) def kill(self): """ Overrides the normal sprite kill() method to also kill the button elements that help make up the slider. """ self.button_container.kill() super().kill() def update(self, time_delta: float): """ Takes care of actually moving the slider based on interactions reported by the buttons or based on movement of the mouse if we are gripping the slider itself. :param time_delta: the time in seconds between calls to update. """ super().update(time_delta) if not (self.alive() and self.is_enabled): return moved_this_frame = False moved_this_frame = self._update_arrow_buttons(moved_this_frame, time_delta) mouse_x, mouse_y = self.ui_manager.get_mouse_position() if self.sliding_button.held and self.sliding_button.in_hold_range( (mouse_x, mouse_y)): if not self.grabbed_slider: self.grabbed_slider = True real_scroll_pos = self.sliding_button.rect.left self.starting_grab_x_difference = mouse_x - real_scroll_pos real_scroll_pos = self.sliding_button.rect.left current_grab_difference = mouse_x - real_scroll_pos adjustment_required = current_grab_difference - self.starting_grab_x_difference self.scroll_position = self.scroll_position + adjustment_required self.scroll_position = min( max(self.scroll_position, self.left_limit_position), self.right_limit_position) x_pos = (self.scroll_position + self.arrow_button_width) y_pos = 0 self.sliding_button.set_relative_position((x_pos, y_pos)) moved_this_frame = True elif not self.sliding_button.held: self.grabbed_slider = False if moved_this_frame: self.current_percentage = self.scroll_position / self.scrollable_width self.current_value = self.value_range[0] + ( self.current_percentage * (self.value_range[1] - self.value_range[0])) if self.use_integers_for_value: self.current_value = int(self.current_value) if not self.has_moved_recently: self.has_moved_recently = True if not self.has_been_moved_by_user_recently: self.has_been_moved_by_user_recently = True # old event - to be removed in 0.8.0 event_data = { 'user_type': OldType(UI_HORIZONTAL_SLIDER_MOVED), 'value': self.current_value, 'ui_element': self, 'ui_object_id': self.most_specific_combined_id } pygame.event.post(pygame.event.Event(pygame.USEREVENT, event_data)) # new event event_data = { 'value': self.current_value, 'ui_element': self, 'ui_object_id': self.most_specific_combined_id } pygame.event.post( pygame.event.Event(UI_HORIZONTAL_SLIDER_MOVED, event_data)) def _update_arrow_buttons(self, moved_this_frame, time_delta): if self.left_button is not None and ( self.left_button.held and self.scroll_position > self.left_limit_position): if self.button_held_repeat_acc > self.button_held_repeat_time: self.scroll_position -= (250.0 * time_delta) self.scroll_position = max(self.scroll_position, self.left_limit_position) x_pos = (self.scroll_position + self.arrow_button_width) y_pos = 0 self.sliding_button.set_relative_position((x_pos, y_pos)) moved_this_frame = True else: self.button_held_repeat_acc += time_delta elif self.right_button is not None and ( self.right_button.held and self.scroll_position < self.right_limit_position): if self.button_held_repeat_acc > self.button_held_repeat_time: self.scroll_position += (250.0 * time_delta) self.scroll_position = min(self.scroll_position, self.right_limit_position) x_pos = (self.scroll_position + self.arrow_button_width) y_pos = 0 self.sliding_button.set_relative_position((x_pos, y_pos)) moved_this_frame = True else: self.button_held_repeat_acc += time_delta else: self.button_held_repeat_acc = 0.0 return moved_this_frame def process_event(self, event: pygame.event.Event) -> bool: processed_event = False if event.type == UI_BUTTON_PRESSED: if (event.ui_element in [self.left_button, self.right_button] and self.button_held_repeat_acc < self.button_held_repeat_time and (self.value_range[0] <= self.get_current_value() <= self.value_range[1])): old_value = self.get_current_value() new_value = (old_value - self.increment if event.ui_element == self.left_button else old_value + self.increment) self.set_current_value(new_value, False) processed_event = True event_data = { 'value': self.current_value, 'ui_element': self, 'ui_object_id': self.most_specific_combined_id } pygame.event.post( pygame.event.Event(UI_HORIZONTAL_SLIDER_MOVED, event_data)) return processed_event def get_current_value(self) -> Union[float, int]: """ Gets the current value the slider is set to. :return: The current value recorded by the slider. """ self.has_moved_recently = False self.has_been_moved_by_user_recently = False return self.current_value def set_current_value(self, value: Union[float, int], warn: bool = True): """ Sets the value of the slider, which will move the position of the slider to match. Will issue a warning if the value set is not in the value range. :param value: The value to set. :param warn: set to false to suppress the default warning, instead the value will be clamped. """ if self.use_integers_for_value: value = int(value) min_value = min(self.value_range[0], self.value_range[1]) max_value = max(self.value_range[0], self.value_range[1]) if value < min_value or value > max_value: if warn: warnings.warn('value not in range', UserWarning) return else: self.current_value = max(min(value, max_value), min_value) else: self.current_value = value value_range_size = (self.value_range[1] - self.value_range[0]) if value_range_size != 0: self.current_percentage = (float(self.current_value) - self.value_range[0]) / value_range_size self.scroll_position = self.scrollable_width * self.current_percentage x_pos = (self.scroll_position + self.arrow_button_width) y_pos = 0 self.sliding_button.set_relative_position((x_pos, y_pos)) self.has_moved_recently = True def rebuild_from_changed_theme_data(self): """ Called by the UIManager to check the theming data and rebuild whatever needs rebuilding for this element when the theme data has changed. """ super().rebuild_from_changed_theme_data() has_any_changed = False if self._check_misc_theme_data_changed( attribute_name='shape', default_value='rectangle', casting_func=str, allowed_values=['rectangle', 'rounded_rectangle']): has_any_changed = True if self._check_shape_theming_changed(defaults={ 'border_width': 1, 'shadow_width': 2, 'shape_corner_radius': 2 }): has_any_changed = True background_colour = self.ui_theme.get_colour_or_gradient( 'dark_bg', self.combined_element_ids) if background_colour != self.background_colour: self.background_colour = background_colour has_any_changed = True border_colour = self.ui_theme.get_colour_or_gradient( 'normal_border', self.combined_element_ids) if border_colour != self.border_colour: self.border_colour = border_colour has_any_changed = True disabled_background_colour = self.ui_theme.get_colour_or_gradient( 'disabled_dark_bg', self.combined_element_ids) if disabled_background_colour != self.disabled_background_colour: self.disabled_background_colour = disabled_background_colour has_any_changed = True disabled_border_colour = self.ui_theme.get_colour_or_gradient( 'disabled_border', self.combined_element_ids) if disabled_border_colour != self.disabled_border_colour: self.disabled_border_colour = disabled_border_colour has_any_changed = True def parse_to_bool(str_data: str): return bool(int(str_data)) if self._check_misc_theme_data_changed( attribute_name='enable_arrow_buttons', default_value=True, casting_func=parse_to_bool): has_any_changed = True if self._check_misc_theme_data_changed( attribute_name='sliding_button_width', default_value=self.default_button_width, casting_func=int): has_any_changed = True if has_any_changed: self.rebuild() def set_position(self, position: Union[pygame.math.Vector2, Tuple[int, int], Tuple[float, float]]): """ Sets the absolute screen position of this slider, updating all subordinate button elements at the same time. :param position: The absolute screen position to set. """ super().set_position(position) border_and_shadow = self.border_width + self.shadow_width self.background_rect.x = border_and_shadow + self.relative_rect.x self.background_rect.y = border_and_shadow + self.relative_rect.y self.button_container.set_relative_position( self.background_rect.topleft) def set_relative_position(self, position: Union[pygame.math.Vector2, Tuple[int, int], Tuple[float, float]]): """ Sets the relative screen position of this slider, updating all subordinate button elements at the same time. :param position: The relative screen position to set. """ super().set_relative_position(position) border_and_shadow = self.border_width + self.shadow_width self.background_rect.x = border_and_shadow + self.relative_rect.x self.background_rect.y = border_and_shadow + self.relative_rect.y self.button_container.set_relative_position( self.background_rect.topleft) def set_dimensions(self, dimensions: Union[pygame.math.Vector2, Tuple[int, int], Tuple[float, float]]): """ Method to directly set the dimensions of an element. :param dimensions: The new dimensions to set. """ super().set_dimensions(dimensions) border_and_shadow = self.border_width + self.shadow_width self.background_rect.width = self.relative_rect.width - ( 2 * border_and_shadow) self.background_rect.height = self.relative_rect.height - ( 2 * border_and_shadow) self.button_container.set_dimensions(self.background_rect.size) # sort out sliding button parameters self.scrollable_width = (self.background_rect.width - self.sliding_button_width - (2 * self.arrow_button_width)) self.right_limit_position = self.scrollable_width self.scroll_position = self.scrollable_width * self.current_percentage slider_x_pos = self.scroll_position + self.arrow_button_width slider_y_pos = 0 self.sliding_button.set_dimensions( (self.sliding_button_width, self.background_rect.height)) self.sliding_button.set_relative_position((slider_x_pos, slider_y_pos)) def disable(self): """ Disable the slider. It should not be interactive and will use the disabled theme colours. """ if self.is_enabled: self.is_enabled = False self.sliding_button.disable() if self.left_button: self.left_button.disable() if self.right_button: self.right_button.disable() self.drawable_shape.set_active_state('disabled') def enable(self): """ Enable the slider. It should become interactive and will use the normal theme colours. """ if not self.is_enabled: self.is_enabled = True self.sliding_button.enable() if self.left_button: self.left_button.enable() if self.right_button: self.right_button.enable() self.drawable_shape.set_active_state('normal') def show(self): """ In addition to the base UIElement.show() - show the sliding button and show the button_container which will propagate and show the left and right buttons. """ super().show() self.sliding_button.show() if self.button_container is not None: self.button_container.show() def hide(self): """ In addition to the base UIElement.hide() - hide the sliding button and hide the button_container which will propagate and hide the left and right buttons. """ super().hide() self.sliding_button.hide() if self.button_container is not None: self.button_container.hide()
class UIHorizontalScrollBar(UIElement): """ A horizontal scroll bar allows users to position a smaller visible area within a horizontally larger area. :param relative_rect: The size and position of the scroll bar. :param visible_percentage: The horizontal percentage of the larger area that is visible, between 0.0 and 1.0. :param manager: The UIManager that manages this element. :param container: The container that this element is within. If set to None will be the root window's container. :param parent_element: The element this element 'belongs to' in the theming hierarchy. :param object_id: A custom defined ID for fine tuning of theming. :param anchors: A dictionary describing what this element's relative_rect is relative to. """ def __init__(self, relative_rect: pygame.Rect, visible_percentage: float, manager: IUIManagerInterface, container: Union[IContainerLikeInterface, None] = None, parent_element: UIElement = None, object_id: Union[str, None] = None, anchors: Dict[str, str] = None): new_element_ids, new_object_ids = self._create_valid_ids(container=container, parent_element=parent_element, object_id=object_id, element_id='horizontal_scroll_bar') super().__init__(relative_rect, manager, container, layer_thickness=2, starting_height=1, element_ids=new_element_ids, object_ids=new_object_ids, anchors=anchors) self.button_width = 20 self.arrow_button_width = self.button_width self.scroll_position = 0.0 self.left_limit = 0.0 self.starting_grab_x_difference = 0 self.visible_percentage = max(0.0, min(visible_percentage, 1.0)) self.start_percentage = 0.0 self.grabbed_slider = False self.has_moved_recently = False self.scroll_wheel_left = False self.scroll_wheel_right = False self.background_colour = None self.border_colour = None self.border_width = None self.shadow_width = None self.drawable_shape = None self.shape_type = 'rectangle' self.shape_corner_radius = None self.background_rect = None # type: Union[None, pygame.Rect] self.scrollable_width = None # type: Union[None, int, float] self.right_limit = None self.sliding_rect_position = None # type: Union[None, pygame.math.Vector2] self.left_button = None self.right_button = None self.sliding_button = None self.arrow_buttons_enabled = True self.button_container = None self.rebuild_from_changed_theme_data() scroll_bar_width = max(5, int(self.scrollable_width * self.visible_percentage)) self.sliding_button = UIButton(pygame.Rect((int(self.sliding_rect_position[0]), int(self.sliding_rect_position[1])), (scroll_bar_width, self.background_rect.height)), '', self.ui_manager, container=self.button_container, starting_height=1, parent_element=self, object_id="#sliding_button", anchors={'left': 'left', 'right': 'left', 'top': 'top', 'bottom': 'bottom'}) self.sliding_button.set_hold_range((self.background_rect.width, 100)) def rebuild(self): """ Rebuild anything that might need rebuilding. """ border_and_shadow = self.border_width + self.shadow_width self.background_rect = pygame.Rect((border_and_shadow + self.relative_rect.x, border_and_shadow + self.relative_rect.y), (self.relative_rect.width - (2 * border_and_shadow), self.relative_rect.height - (2 * border_and_shadow))) theming_parameters = {'normal_bg': self.background_colour, 'normal_border': self.border_colour, 'border_width': self.border_width, 'shadow_width': self.shadow_width, 'shape_corner_radius': self.shape_corner_radius} if self.shape_type == 'rectangle': self.drawable_shape = RectDrawableShape(self.rect, theming_parameters, ['normal'], self.ui_manager) elif self.shape_type == 'rounded_rectangle': self.drawable_shape = RoundedRectangleShape(self.rect, theming_parameters, ['normal'], self.ui_manager) self.set_image(self.drawable_shape.get_surface('normal')) if self.button_container is None: self.button_container = UIContainer(self.background_rect, manager=self.ui_manager, container=self.ui_container, anchors=self.anchors, object_id='#horiz_scrollbar_buttons_container') else: self.button_container.set_dimensions(self.background_rect.size) self.button_container.set_relative_position(self.background_rect.topleft) if self.arrow_buttons_enabled: self.arrow_button_width = self.button_width if self.left_button is None: self.left_button = UIButton(pygame.Rect((0, 0), (self.arrow_button_width, self.background_rect.height)), '◀', self.ui_manager, container=self.button_container, starting_height=1, parent_element=self, object_id="#left_button", anchors={'left': 'left', 'right': 'left', 'top': 'top', 'bottom': 'bottom'} ) if self.right_button is None: self.right_button = UIButton(pygame.Rect((-self.arrow_button_width, 0), (self.arrow_button_width, self.background_rect.height)), '▶', self.ui_manager, container=self.button_container, starting_height=1, parent_element=self, object_id="#right_button", anchors={'left': 'right', 'right': 'right', 'top': 'top', 'bottom': 'bottom'}) else: self.arrow_button_width = 0 if self.left_button is not None: self.left_button.kill() self.left_button = None if self.right_button is not None: self.right_button.kill() self.right_button = None self.scrollable_width = self.background_rect.width - (2 * self.arrow_button_width) self.right_limit = self.scrollable_width scroll_bar_width = max(5, int(self.scrollable_width * self.visible_percentage)) self.scroll_position = min(max(self.scroll_position, self.left_limit), self.right_limit - scroll_bar_width) x_pos = (self.scroll_position + self.arrow_button_width) y_pos = 0 self.sliding_rect_position = pygame.math.Vector2(x_pos, y_pos) if self.sliding_button is not None: self.sliding_button.set_relative_position(self.sliding_rect_position) self.sliding_button.set_dimensions((scroll_bar_width, self.background_rect.height)) self.sliding_button.set_hold_range((self.background_rect.width, 100)) def check_has_moved_recently(self) -> bool: """ Returns True if the scroll bar was moved in the last call to the update function. :return: True if we've recently moved the scroll bar, False otherwise. """ return self.has_moved_recently def kill(self): """ Overrides the kill() method of the UI element class to kill all the buttons in the scroll bar and clear any of the parts of the scroll bar that are currently recorded as the 'last focused horizontal scroll bar element' on the ui manager. NOTE: the 'last focused' state on the UI manager is used so that the mouse wheel will move whichever scrollbar we last fiddled with even if we've been doing other stuff. This seems to be consistent with the most common mousewheel/scrollbar interactions used elsewhere. """ self.ui_manager.clear_last_focused_from_horiz_scrollbar(self) self.ui_manager.clear_last_focused_from_horiz_scrollbar(self.sliding_button) self.ui_manager.clear_last_focused_from_horiz_scrollbar(self.left_button) self.ui_manager.clear_last_focused_from_horiz_scrollbar(self.right_button) self.button_container.kill() super().kill() def focus(self): """ When we focus the scroll bar as a whole for any reason we pass that status down to the 'bar' part of the scroll bar. """ if self.sliding_button is not None: self.ui_manager.set_focus_element(self.sliding_button) def process_event(self, event: pygame.event.Event) -> bool: """ Checks an event from pygame's event queue to see if the scroll bar needs to react to it. In this case it is just mousewheel events, mainly because the buttons that make up the scroll bar will handle the required mouse click events. :param event: The event to process. :return: Returns True if we've done something with the input event. """ # pygame.MOUSEWHEEL only defined after pygame 1.9 try: pygame.MOUSEWHEEL except AttributeError: pygame.MOUSEWHEEL = -1 consumed_event = False if self._check_was_last_focused() and event.type == pygame.MOUSEWHEEL: if event.x > 0: self.scroll_wheel_left = True consumed_event = True elif event.x < 0: self.scroll_wheel_right = True consumed_event = True return consumed_event def _check_was_last_focused(self) -> bool: """ Check if this scroll bar was the last one focused in the UI. :return: True if it was. """ last_focused_scrollbar_element = self.ui_manager.get_last_focused_horiz_scrollbar() return (last_focused_scrollbar_element is not None and ((last_focused_scrollbar_element is self) or (last_focused_scrollbar_element is self.sliding_button) or (last_focused_scrollbar_element is self.left_button) or (last_focused_scrollbar_element is self.right_button))) def update(self, time_delta: float): """ Called once per update loop of our UI manager. Deals largely with moving the scroll bar and updating the resulting 'start_percentage' variable that is then used by other 'scrollable' UI elements to control the point they start drawing. Reacts to presses of the up and down arrow buttons, movement of the mouse wheel and dragging of the scroll bar itself. :param time_delta: A float, roughly representing the time in seconds between calls to this method. """ super().update(time_delta) self.has_moved_recently = False if self.alive(): moved_this_frame = False if (self.left_button is not None and (self.left_button.held or self.scroll_wheel_left) and self.scroll_position > self.left_limit): self.scroll_wheel_left = False self.scroll_position -= (250.0 * time_delta) self.scroll_position = max(self.scroll_position, self.left_limit) x_pos = (self.scroll_position + self.arrow_button_width) y_pos = 0 self.sliding_button.set_relative_position((x_pos, y_pos)) moved_this_frame = True elif (self.right_button is not None and (self.right_button.held or self.scroll_wheel_right) and self.scroll_position < self.right_limit): self.scroll_wheel_right = False self.scroll_position += (250.0 * time_delta) self.scroll_position = min(self.scroll_position, self.right_limit - self.sliding_button.relative_rect.width) x_pos = (self.scroll_position + self.arrow_button_width) y_pos = 0 self.sliding_button.set_relative_position((x_pos, y_pos)) moved_this_frame = True mouse_x, mouse_y = self.ui_manager.get_mouse_position() if self.sliding_button.held and self.sliding_button.in_hold_range((mouse_x, mouse_y)): if not self.grabbed_slider: self.grabbed_slider = True real_scroll_pos = self.sliding_button.rect.left self.starting_grab_x_difference = mouse_x - real_scroll_pos real_scroll_pos = self.sliding_button.rect.left current_grab_difference = mouse_x - real_scroll_pos adjustment_required = current_grab_difference - self.starting_grab_x_difference self.scroll_position = self.scroll_position + adjustment_required self.scroll_position = min(max(self.scroll_position, self.left_limit), self.right_limit - self.sliding_button.rect.width) x_pos = (self.scroll_position + self.arrow_button_width) y_pos = 0 self.sliding_button.set_relative_position((x_pos, y_pos)) moved_this_frame = True elif not self.sliding_button.held: self.grabbed_slider = False if moved_this_frame: self.start_percentage = self.scroll_position / self.scrollable_width if not self.has_moved_recently: self.has_moved_recently = True def redraw_scrollbar(self): """ Redraws the 'scrollbar' portion of the whole UI element. Called when we change the visible percentage. """ self.scrollable_width = self.background_rect.width - (2 * self.arrow_button_width) self.right_limit = self.scrollable_width scroll_bar_width = max(5, int(self.scrollable_width * self.visible_percentage)) x_pos = (self.scroll_position + self.arrow_button_width) y_pos = 0 self.sliding_rect_position = pygame.math.Vector2(x_pos, y_pos) if self.sliding_button is None: self.sliding_button = UIButton(pygame.Rect(int(x_pos), int(y_pos), scroll_bar_width, self.background_rect.height), '', self.ui_manager, container=self.button_container, starting_height=1, parent_element=self, object_id="#sliding_button", anchors={'left': 'left', 'right': 'left', 'top': 'top', 'bottom': 'bottom'}) else: self.sliding_button.set_relative_position(self.sliding_rect_position) self.sliding_button.set_dimensions((scroll_bar_width, self.background_rect.height)) self.sliding_button.set_hold_range((self.background_rect.width, 100)) def set_visible_percentage(self, percentage: float): """ Sets the percentage of the total 'scrollable area' that is currently visible. This will affect the size of the scrollbar and should be called if the horizontal size of the 'scrollable area' or the horizontal size of the visible area change. :param percentage: A float between 0.0 and 1.0 representing the percentage that is visible. """ self.visible_percentage = max(0.0, min(1.0, percentage)) if 1.0 - self.start_percentage < self.visible_percentage: self.start_percentage = 1.0 - self.visible_percentage self.redraw_scrollbar() def reset_scroll_position(self): """ Reset the current scroll position back to the top. """ self.scroll_position = 0.0 self.start_percentage = 0.0 def rebuild_from_changed_theme_data(self): """ Called by the UIManager to check the theming data and rebuild whatever needs rebuilding for this element when the theme data has changed. """ has_any_changed = False shape_type = 'rectangle' shape_type_string = self.ui_theme.get_misc_data(self.object_ids, self.element_ids, 'shape') if shape_type_string is not None and shape_type_string in ['rectangle', 'rounded_rectangle']: shape_type = shape_type_string if shape_type != self.shape_type: self.shape_type = shape_type has_any_changed = True if self._check_shape_theming_changed(defaults={'border_width': 1, 'shadow_width': 2, 'shape_corner_radius': 2}): has_any_changed = True background_colour = self.ui_theme.get_colour_or_gradient(self.object_ids, self.element_ids, 'dark_bg') if background_colour != self.background_colour: self.background_colour = background_colour has_any_changed = True border_colour = self.ui_theme.get_colour_or_gradient(self.object_ids, self.element_ids, 'normal_border') if border_colour != self.border_colour: self.border_colour = border_colour has_any_changed = True buttons_enable_param = self.ui_theme.get_misc_data(self.object_ids, self.element_ids, 'enable_arrow_buttons') if buttons_enable_param is not None: try: buttons_enable = bool(int(buttons_enable_param)) except ValueError: buttons_enable = True if buttons_enable != self.arrow_buttons_enabled: self.arrow_buttons_enabled = buttons_enable has_any_changed = True if has_any_changed: self.rebuild() def set_position(self, position: Union[pygame.math.Vector2, Tuple[int, int], Tuple[float, float]]): """ Sets the absolute screen position of this scroll bar, updating all subordinate button elements at the same time. :param position: The absolute screen position to set. """ super().set_position(position) border_and_shadow = self.border_width + self.shadow_width self.background_rect.x = border_and_shadow + self.relative_rect.x self.background_rect.y = border_and_shadow + self.relative_rect.y self.button_container.set_relative_position(self.background_rect.topleft) def set_relative_position(self, position: Union[pygame.math.Vector2, Tuple[int, int], Tuple[float, float]]): """ Sets the relative screen position of this scroll bar, updating all subordinate button elements at the same time. :param position: The relative screen position to set. """ super().set_relative_position(position) border_and_shadow = self.border_width + self.shadow_width self.background_rect.x = border_and_shadow + self.relative_rect.x self.background_rect.y = border_and_shadow + self.relative_rect.y self.button_container.set_relative_position(self.background_rect.topleft) def set_dimensions(self, dimensions: Union[pygame.math.Vector2, Tuple[int, int], Tuple[float, float]]): """ Method to directly set the dimensions of an element. :param dimensions: The new dimensions to set. """ super().set_dimensions(dimensions) border_and_shadow = self.border_width + self.shadow_width self.background_rect.width = self.relative_rect.width - (2 * border_and_shadow) self.background_rect.height = self.relative_rect.height - (2 * border_and_shadow) self.button_container.set_dimensions(self.background_rect.size) # sort out scroll bar parameters self.scrollable_width = self.background_rect.width - (2 * self.arrow_button_width) self.right_limit = self.scrollable_width scroll_bar_width = max(5, int(self.scrollable_width * self.visible_percentage)) base_scroll_bar_x = self.arrow_button_width max_scroll_bar_x = base_scroll_bar_x + (self.scrollable_width - scroll_bar_width) self.sliding_rect_position.x = max(base_scroll_bar_x, min((base_scroll_bar_x + int(self.start_percentage * self.scrollable_width)), max_scroll_bar_x)) self.scroll_position = self.sliding_rect_position.x - base_scroll_bar_x self.sliding_button.set_dimensions((scroll_bar_width, self.background_rect.height)) self.sliding_button.set_relative_position(self.sliding_rect_position)
class MenuBar(UIElement): """ Menubar sınıfı. İçinde menuler ve alt menuler içeren ui elemanı :param rect: elemanın pygame.Rect'i :param manager: ui yöneticisi :param container: bu elemanın neyin içinde olduğu :param parent: bu objenin 'sahibi' olan eleman :param object_id: objeye atanacak id :param anchors: objenin rect koordinatının nereye göre olduğunu bildiren dict :param menu_bar_data: bardaki menulerin ve alt menülerin bulunduğu dict """ def __init__(self, rect: pygame.Rect, manager, container: Union[IContainerLikeInterface, None] = None, parent: Union[UIElement, None] = None, object_id: Union[str, None] = None, anchors: Union[Dict[str, str], None] = None, menu_bar_data: Dict[str, Dict[str, Union[Dict, str]]] = MENU_BAR_DATA_DICT): super().__init__(rect, manager, container, starting_height=1, layer_thickness=1, anchors=anchors) self._create_valid_ids(container, parent, object_id, 'menu_bar') self.menu_data = menu_bar_data self.bg_color = None self.border_color = None self.shape_type = 'rectangle' self.rgb_tuple = None # type: Union[Tuple[int, int, int], None] self.open_menu = None self._selected_menu = None self._close_opened_menu = False self.rebuild_from_changed_theme_data() self.container_rect = pygame.Rect( self.relative_rect.left + (self.shadow_width + self.border_width), self.relative_rect.top + (self.shadow_width + self.border_width), self.relative_rect.width - 2 * (self.shadow_width + self.border_width), self.relative_rect.height - 2 * (self.shadow_width + self.border_width)) self.menu_bar_container = UIContainer(self.container_rect, manager, starting_height=1, parent_element=self, object_id='#menu_bar_container') # menü butonları için for loop current_y_pos = 0 for menu_key, menu_item in self.menu_data.items(): if menu_key == '#icon': icon_surf = pygame.image.load( os.path.join('litevision', 'res', 'image_examples', 'icon.png')).convert_alpha() self.icon = UIImage( pygame.Rect( (0 + GUI_OFFSET_VALUE + ((48 - 32) // 2), current_y_pos + GUI_OFFSET_VALUE), (32, 32)), icon_surf, self.ui_manager, self.menu_bar_container, self, menu_key) current_y_pos += (48 + GUI_OFFSET_VALUE) continue elif menu_key == '#empty_1' or menu_key == '#empty_2': empty_surf = pygame.Surface((48, 48), pygame.SRCALPHA, masks=pygame.Color('#2F4F4F')) UIImage( pygame.Rect((0 + GUI_OFFSET_VALUE, current_y_pos), (48, 48)), empty_surf, self.ui_manager, self.menu_bar_container, self, menu_key) current_y_pos += (48 + GUI_OFFSET_VALUE) continue elif menu_key == '#start_pause': sp_surf = pygame.Surface((48, 48), pygame.SRCALPHA, masks=pygame.Color('#2F4F4F')) sp_icon = pygame.image.load( os.path.join('litevision', 'res', 'image_examples', 'start_icon', 'start.png')).convert_alpha() sp_surf.blit(sp_icon, (0, 0)) self.start_button = UIImage( pygame.Rect((0 + GUI_OFFSET_VALUE, current_y_pos), (48, 48)), sp_surf, self.ui_manager, self.menu_bar_container, self, '#start_pause') current_y_pos += (48 + GUI_OFFSET_VALUE) continue elif menu_key == '#rgb_button': rgb_surf = pygame.Surface((48, 48), pygame.SRCALPHA, masks=pygame.Color('#2F4F4F')) rgb_icon = pygame.image.load( os.path.join('litevision', 'res', 'image_examples', 'rgb.png')).convert_alpha() rgb_surf.blit(rgb_icon, (0, 0)) self.rgb_button = UIImage( pygame.Rect((0 + GUI_OFFSET_VALUE, current_y_pos), (48, 48)), rgb_surf, self.ui_manager, self.menu_bar_container, self, '#rgb_button') current_y_pos += (48 + GUI_OFFSET_VALUE) continue UIButton(pygame.Rect((0 + GUI_OFFSET_VALUE, current_y_pos), (48, 48)), menu_item['display_name'], self.ui_manager, self.menu_bar_container, object_id=menu_key, parent_element=self) current_y_pos += (48 + GUI_OFFSET_VALUE) def unfocus(self): if self.open_menu is not None: self.open_menu.kill() self.open_menu = None if self._selected_menu is not None: self._selected_menu.unselect() self._selected_menu = None def kill(self): self.menu_bar_container.kill() super().kill() def _open_menu(self, event): if self.open_menu is not None: self.open_menu.kill() if event.ui_object_id != '#menu_bar.#start_processing': menu_key = event.ui_object_id.split('.')[-1] menu_size = ((len(self.menu_data[menu_key]['items']) * 20) + 3) item_data = [(item_data['display_name'], item_key) for item_key, item_data in self.menu_data[menu_key] ['items'].items()] menu_rect = pygame.Rect((0, 0), (200, menu_size)) menu_rect.topleft = event.ui_element.rect.topright top_ui_layer = self.ui_manager.get_sprite_group().get_top_layer() self.open_menu = UISelectionList(menu_rect, item_data, self.ui_manager, starting_height=top_ui_layer, parent_element=self, object_id=menu_key + '_items') self.ui_manager.set_focus_set(self) def rebuild_from_changed_theme_data(self): has_any_changed = False bg_color = self.ui_theme.get_colour_or_gradient( 'normal_bg', self.combined_element_ids) if bg_color != self.bg_color: self.bg_color = bg_color has_any_changed = True border_color = self.ui_theme.get_colour_or_gradient( 'normal_border', self.combined_element_ids) if border_color != self.border_color: self.border_color = border_color has_any_changed = True # misc shape_type_str = self.ui_theme.get_misc_data('shape', self.combined_element_ids) if (shape_type_str is not None and shape_type_str in ['rectangle'] and shape_type_str != self.shape_type): self.shape_type = shape_type_str has_any_changed = True if self._check_shape_theming_changed(defaults={ 'border_width': 1, 'shadow_width': 0, 'shape_corner_radius': 0 }): has_any_changed = True if has_any_changed: self.rebuild() def rebuild(self): theming_parameters = { 'normal_bg': self.bg_color, 'normal_border': self.border_color, 'border_width': self.border_width, 'shadow_width': self.shadow_width } if self.shape_type == 'rectangle': self.drawable_shape = RectDrawableShape(self.rect, theming_parameters, ['normal'], self.ui_manager) self.on_fresh_drawable_shape_ready() def process_event(self, event: pygame.event.Event) -> bool: consumed_event = False if (self is not None and event.type == pygame.MOUSEBUTTONDOWN and event.button in [pygame.BUTTON_LEFT, pygame.BUTTON_RIGHT]): scaled_mouse_pos = (int(event.pos[0] * self.ui_manager.mouse_pos_scale_factor[0]), int(event.pos[1] * self.ui_manager.mouse_pos_scale_factor[1])) if self.hover_point(scaled_mouse_pos[0], scaled_mouse_pos[1]): consumed_event = True if event.type == pygame.MOUSEBUTTONUP and event.button == 1: scaled_mouse_pos = self.start_button.ui_manager.calculate_scaled_mouse_position( event.pos) x = scaled_mouse_pos[0] y = scaled_mouse_pos[1] if self.start_button.hover_point(x, y): event_data = { 'user_type': pygame_gui.UI_BUTTON_START_PRESS, 'ui_element': self.start_button, 'ui_object_id': self.start_button.most_specific_combined_id } pygame.event.post( pygame.event.Event(pygame.USEREVENT, event_data)) elif self.rgb_button.hover_point(x, y): event_data = { 'user_type': pygame_gui.UI_BUTTON_START_PRESS, 'ui_element': self.rgb_button, 'ui_object_id': self.rgb_button.most_specific_combined_id } pygame.event.post( pygame.event.Event(pygame.USEREVENT, event_data)) if (event.type == pygame.USEREVENT and event.user_type == pygame_gui.UI_BUTTON_ON_HOVERED and event.ui_element in self.menu_bar_container.elements and self.open_menu != None): if self._selected_menu is not None: self._selected_menu.unselect() self._selected_menu = event.ui_element self._selected_menu.select() self._open_menu(event) if (event.type == pygame.USEREVENT and event.user_type == pygame_gui.UI_BUTTON_START_PRESS and event.ui_element in self.menu_bar_container.elements and event.ui_object_id != '#menu_bar.#start_pause' and event.ui_object_id != '#menu_bar.#rgb_button'): if self._selected_menu is not None: self._selected_menu.unselect() self._selected_menu = event.ui_element self._selected_menu.select() self._open_menu(event) return consumed_event def update(self, time_delta): super().update(time_delta) if self.menu_bar_container.layer_thickness != self.layer_thickness: self.layer_thickness = self.menu_bar_container.layer_thickness