Exemplo n.º 1
0
    def rebuild(self):
        """
        A complete rebuild of the drawable shape used by this element.

        """
        theming_parameters = {'normal_bg': self.background_colour,
                              'normal_border': self.border_colour,
                              'normal_image': self.background_image,
                              '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'], self.ui_manager)
        elif self.shape == 'rounded_rectangle':
            self.drawable_shape = RoundedRectangleShape(self.rect, theming_parameters,
                                                        ['normal'], self.ui_manager)

        self.on_fresh_drawable_shape_ready()

        if self.list_and_scroll_bar_container is None:
            self.list_and_scroll_bar_container = UIContainer(
                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) -
                            (2 * self.border_width),
                            self.relative_rect.height -
                            (2 * self.shadow_width) -
                            (2 * self.border_width)),
                manager=self.ui_manager,
                starting_height=self.starting_height,
                container=self.ui_container,
                parent_element=self._parent_element,
                object_id='#selection_list_container',
                anchors=self.anchors,
                visible=self.visible
            )
            self.join_focus_sets(self.list_and_scroll_bar_container)
        else:
            self.list_and_scroll_bar_container.set_dimensions((self.relative_rect.width -
                                                               (2 * self.shadow_width) -
                                                               (2 * self.border_width),
                                                               self.relative_rect.height -
                                                               (2 * self.shadow_width) -
                                                               (2 * self.border_width)))
            self.list_and_scroll_bar_container.set_relative_position((self.relative_rect.left +
                                                                      self.shadow_width +
                                                                      self.border_width,
                                                                      self.relative_rect.top +
                                                                      self.shadow_width +
                                                                      self.border_width))

        self.set_item_list(self._raw_item_list)
Exemplo n.º 2
0
    def __init__(self,
                 relative_rect: pygame.Rect,
                 starting_layer_height: int,
                 manager: IUIManagerInterface,
                 *,
                 element_id: str = 'panel',
                 margins: Dict[str, int] = None,
                 container: Union[IContainerLikeInterface, None] = None,
                 parent_element: UIElement = None,
                 object_id: Union[str, None] = None,
                 anchors: Dict[str, str] = None):

        super().__init__(relative_rect,
                         manager,
                         container,
                         starting_height=starting_layer_height,
                         layer_thickness=1,
                         anchors=anchors)

        self._create_valid_ids(container=container,
                               parent_element=parent_element,
                               object_id=object_id,
                               element_id=element_id)

        self.background_colour = None
        self.border_colour = None
        self.background_image = None
        self.border_width = 1
        self.shadow_width = 2
        self.shape_corner_radius = 0
        self.shape = 'rectangle'

        self.rebuild_from_changed_theme_data()

        if margins is None:
            self.container_margins = {
                'left': self.shadow_width + self.border_width,
                'right': self.shadow_width + self.border_width,
                'top': self.shadow_width + self.border_width,
                'bottom': self.shadow_width + self.border_width
            }
        else:
            self.container_margins = margins

        container_rect = pygame.Rect(
            self.relative_rect.left + self.container_margins['left'],
            self.relative_rect.top + self.container_margins['top'],
            self.relative_rect.width -
            (self.container_margins['left'] + self.container_margins['right']),
            self.relative_rect.height -
            (self.container_margins['top'] + self.container_margins['bottom']))

        self.panel_container = UIContainer(
            container_rect,
            manager,
            starting_height=starting_layer_height,
            container=container,
            parent_element=self,
            object_id='#panel_container',
            anchors=anchors)
Exemplo n.º 3
0
    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)
Exemplo n.º 4
0
    def rebuild(self):
        """
        Rebuilds the window when the theme has changed.

        """
        if self._window_root_container is None:
            self._window_root_container = UIContainer(pygame.Rect(self.relative_rect.x +
                                                                  self.shadow_width,
                                                                  self.relative_rect.y +
                                                                  self.shadow_width,
                                                                  self.relative_rect.width -
                                                                  (2 * self.shadow_width),
                                                                  self.relative_rect.height -
                                                                  (2 * self.shadow_width)),
                                                      manager=self.ui_manager,
                                                      starting_height=1,
                                                      is_window_root_container=True,
                                                      container=None,
                                                      parent_element=self,
                                                      object_id="#window_root_container")
        if self.window_element_container is None:
            window_container_rect = pygame.Rect(self.border_width,
                                                self.title_bar_height,
                                                (self._window_root_container.relative_rect.width -
                                                 (2 * self.border_width)),
                                                (self._window_root_container.relative_rect.height -
                                                 self.title_bar_height - self.border_width))
            self.window_element_container = UIContainer(window_container_rect,
                                                        self.ui_manager,
                                                        starting_height=0,
                                                        container=self._window_root_container,
                                                        parent_element=self,
                                                        object_id="#window_element_container",
                                                        anchors={'top': 'top', 'bottom': 'bottom',
                                                                 'left': 'left', 'right': 'right'})

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

        self.set_dimensions(self.relative_rect.size)

        if self.window_element_container is not None:
            element_container_width = (self._window_root_container.relative_rect.width -
                                       (2 * self.border_width))
            element_container_height = (self._window_root_container.relative_rect.height -
                                        self.title_bar_height)
            self.window_element_container.set_dimensions((element_container_width,
                                                          element_container_height))
            self.window_element_container.set_relative_position((self.border_width,
                                                                 self.title_bar_height))

            if self.enable_title_bar:
                if self.title_bar is not None:
                    self.title_bar.set_dimensions((self._window_root_container.relative_rect.width -
                                                   self.title_bar_button_width,
                                                   self.title_bar_height))
                else:
                    title_bar_width = (self._window_root_container.relative_rect.width -
                                       self.title_bar_button_width)
                    self.title_bar = UIButton(relative_rect=pygame.Rect(0, 0,
                                                                        title_bar_width,
                                                                        self.title_bar_height),
                                              text=self.window_display_title,
                                              manager=self.ui_manager,
                                              container=self._window_root_container,
                                              parent_element=self,
                                              object_id='#title_bar',
                                              anchors={'top': 'top', 'bottom': 'top',
                                                       'left': 'left', 'right': 'right'}
                                              )
                    self.title_bar.set_hold_range((100, 100))

                if self.enable_close_button:
                    if self.close_window_button is not None:
                        close_button_pos = (-self.title_bar_button_width, 0)
                        self.close_window_button.set_dimensions((self.title_bar_button_width,
                                                                 self.title_bar_height))
                        self.close_window_button.set_relative_position(close_button_pos)
                    else:
                        close_rect = pygame.Rect((-self.title_bar_button_width, 0),
                                                 (self.title_bar_button_width,
                                                  self.title_bar_height))
                        self.close_window_button = UIButton(relative_rect=close_rect,
                                                            text='╳',
                                                            manager=self.ui_manager,
                                                            container=self._window_root_container,
                                                            parent_element=self,
                                                            object_id='#close_button',
                                                            anchors={'top': 'top',
                                                                     'bottom': 'top',
                                                                     'left': 'right',
                                                                     'right': 'right'}
                                                            )

                else:
                    if self.close_window_button is not None:
                        self.close_window_button.kill()
                        self.close_window_button = None
            else:
                if self.title_bar is not None:
                    self.title_bar.kill()
                    self.title_bar = None
                if self.close_window_button is not None:
                    self.close_window_button.kill()
                    self.close_window_button = None
Exemplo n.º 5
0
class UIWindow(UIElement, IContainerLikeInterface, IWindowInterface):
    """
    A base class for window GUI elements, any windows should inherit from this class.

    :param rect: A rectangle, representing size and position of the window (including title bar,
                 shadow and borders).
    :param manager: The UIManager that manages this UIWindow.
    :param window_display_title: A string that will appear in the windows title bar if it has one.
    :param element_id: An element ID for this window, if one is not supplied it defaults to
                       'window'.
    :param object_id: An optional object ID for this window, useful for distinguishing different
                      windows.
    :param resizable: Whether this window is resizable or not, defaults to False.

    """
    def __init__(self,
                 rect: pygame.Rect,
                 manager: IUIManagerInterface,
                 window_display_title: str = "",
                 element_id: Union[str, None] = None,
                 object_id: Union[str, None] = None,
                 resizable: bool = False):

        if element_id is None:
            element_id = 'window'

        new_element_ids, new_object_ids = self.create_valid_ids(container=None,
                                                                parent_element=None,
                                                                object_id=object_id,
                                                                element_id=element_id)

        self.window_display_title = window_display_title
        self._window_root_container = None
        self.resizable = resizable
        self.minimum_dimensions = (100, 100)
        self.edge_hovering = [False, False, False, False]

        super().__init__(rect, manager, container=None,
                         starting_height=1,
                         layer_thickness=1,
                         object_ids=new_object_ids,
                         element_ids=new_element_ids)

        self.set_image(self.ui_manager.get_universal_empty_surface())
        self.bring_to_front_on_focused = True

        self.is_blocking = False  # blocks all clicking events from interacting beyond this window

        self.resizing_mode_active = False
        self.start_resize_point = (0, 0)
        self.start_resize_rect = None

        self.grabbed_window = False
        self.starting_grab_difference = (0, 0)

        self.background_colour = None
        self.border_colour = None
        self.shape_type = 'rectangle'
        self.enable_title_bar = True
        self.enable_close_button = True
        self.title_bar_height = 28
        self.title_bar_button_width = self.title_bar_height

        # UI elements
        self.window_element_container = None
        self.title_bar = None
        self.close_window_button = None

        self.rebuild_from_changed_theme_data()

        self.window_stack = self.ui_manager.get_window_stack()
        self.window_stack.add_new_window(self)

    def set_blocking(self, state: bool):
        """
        Sets whether this window being open should block clicks to the rest of the UI or not.
        Defaults to False.

        :param state: True if this window should block mouse clicks.

        """
        self.is_blocking = state

    def set_minimum_dimensions(self, dimensions: Union[pygame.math.Vector2,
                                                       Tuple[int, int],
                                                       Tuple[float, float]]):
        """
        If this window is resizable, then the dimensions we set here will be the minimum that
        users can change the window to. They are also used as the minimum size when
        'set_dimensions' is called.

        :param dimensions: The new minimum dimension for the window.

        """
        self.minimum_dimensions = (min(self.ui_container.rect.width, int(dimensions[0])),
                                   min(self.ui_container.rect.height, int(dimensions[1])))

        if ((self.rect.width < self.minimum_dimensions[0]) or
                (self.rect.height < self.minimum_dimensions[1])):
            new_width = max(self.minimum_dimensions[0], self.rect.width)
            new_height = max(self.minimum_dimensions[1], self.rect.height)
            self.set_dimensions((new_width, new_height))

    def set_dimensions(self, dimensions: Union[pygame.math.Vector2,
                                               Tuple[int, int],
                                               Tuple[float, float]]):
        """
        Set the size of this window and then re-sizes and shifts the contents of the windows
        container to fit the new size.

        :param dimensions: The new dimensions to set.

        """
        # clamp to minimum dimensions and container size
        dimensions = (min(self.ui_container.rect.width,
                          max(self.minimum_dimensions[0],
                              int(dimensions[0]))),
                      min(self.ui_container.rect.height,
                          max(self.minimum_dimensions[1],
                              int(dimensions[1]))))

        # Don't use a basic gate on this set dimensions method because the container may be a
        # different size to the window
        super().set_dimensions(dimensions)

        if self._window_root_container is not None:
            new_container_dimensions = (self.relative_rect.width - (2 * self.shadow_width),
                                        self.relative_rect.height - (2 * self.shadow_width))
            if new_container_dimensions != self._window_root_container.relative_rect.size:
                self._window_root_container.set_dimensions(new_container_dimensions)
                container_pos = (self.relative_rect.x + self.shadow_width,
                                 self.relative_rect.y + self.shadow_width)
                self._window_root_container.set_relative_position(container_pos)

    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)

        if self._window_root_container is not None:
            container_pos = (self.relative_rect.x + self.shadow_width,
                             self.relative_rect.y + self.shadow_width)
            self._window_root_container.set_relative_position(container_pos)

    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)

        if self._window_root_container is not None:
            container_pos = (self.relative_rect.x + self.shadow_width,
                             self.relative_rect.y + self.shadow_width)
            self._window_root_container.set_relative_position(container_pos)

    def process_event(self, event: pygame.event.Event) -> bool:
        """
        Handles resizing & closing windows. Gives UI Windows access to pygame events. Derived
        windows should super() call this class if they implement their own process_event method.

        :param event: The event to process.

        :return bool: Return True if this element should consume this event and not pass it to the
                      rest of the UI.

        """
        consumed_event = False

        if self.is_blocking and event.type == pygame.MOUSEBUTTONDOWN:
            consumed_event = True

        if (self is not None and
                event.type == pygame.MOUSEBUTTONDOWN and
                event.button in [pygame.BUTTON_LEFT,
                                 pygame.BUTTON_MIDDLE,
                                 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 event.button == pygame.BUTTON_LEFT and (self.edge_hovering[0] or
                                                       self.edge_hovering[1] or
                                                       self.edge_hovering[2] or
                                                       self.edge_hovering[3]):
                self.resizing_mode_active = True
                self.start_resize_point = scaled_mouse_pos
                self.start_resize_rect = self.rect.copy()
                consumed_event = True
            elif self.hover_point(scaled_mouse_pos[0], scaled_mouse_pos[1]):
                consumed_event = True

        if (self is not None and event.type == pygame.MOUSEBUTTONUP and
                event.button == pygame.BUTTON_LEFT and self.resizing_mode_active):
            self.resizing_mode_active = False

        if (event.type == pygame.USEREVENT and event.user_type == UI_BUTTON_PRESSED
                and event.ui_element == self.close_window_button):
            self.kill()

        return consumed_event

    def check_clicked_inside_or_blocking(self, event: pygame.event.Event) -> bool:
        """
        A quick event check outside of the normal event processing so that this window is brought
        to the front of the window stack if we click on any of the elements contained within it.

        :param event: The event to check.

        :return: returns True if the event represents a click inside this window or the window
                 is blocking.

        """
        consumed_event = False
        if self.is_blocking and event.type == pygame.MOUSEBUTTONDOWN:
            consumed_event = True

        if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
            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]) or (self.edge_hovering[0] or
                                                         self.edge_hovering[1] or
                                                         self.edge_hovering[2] or
                                                         self.edge_hovering[3]):
                if self.bring_to_front_on_focused:
                    self.window_stack.move_window_to_front(self)
                consumed_event = True

        return consumed_event

    def update(self, time_delta: float):
        """
        A method called every update cycle of our application. Designed to be overridden by
        derived classes but also has a little functionality to make sure the window's layer
        'thickness' is accurate and to handle window resizing.

        :param time_delta: time passed in seconds between one call to this method and the next.

        """
        super().update(time_delta)

        # This is needed to keep the window in sync with the container after adding elements to it
        if self._window_root_container.layer_thickness != self.layer_thickness:
            self.layer_thickness = self._window_root_container.layer_thickness
        if self.title_bar is not None:
            if self.title_bar.held:
                mouse_x, mouse_y = self.ui_manager.get_mouse_position()
                if not self.grabbed_window:
                    self.window_stack.move_window_to_front(self)
                    self.grabbed_window = True
                    self.starting_grab_difference = (mouse_x - self.rect.x,
                                                     mouse_y - self.rect.y)

                current_grab_difference = (mouse_x - self.rect.x,
                                           mouse_y - self.rect.y)

                adjustment_required = (current_grab_difference[0] -
                                       self.starting_grab_difference[0],
                                       current_grab_difference[1] -
                                       self.starting_grab_difference[1])

                self.set_relative_position((self.relative_rect.x + adjustment_required[0],
                                            self.relative_rect.y + adjustment_required[1]))
            else:
                self.grabbed_window = False

        if self.resizing_mode_active:
            self._update_drag_resizing()

    def _update_drag_resizing(self):
        """
        Re-sizes a window that is being dragged around its the edges by the mouse.

        """
        x_pos = self.rect.left
        y_pos = self.rect.top
        x_dimension = self.rect.width
        y_dimension = self.rect.height
        mouse_x, mouse_y = self.ui_manager.get_mouse_position()
        x_diff = mouse_x - self.start_resize_point[0]
        y_diff = mouse_y - self.start_resize_point[1]
        if self.rect.height >= self.minimum_dimensions[1]:
            y_pos = self.start_resize_rect.y
            y_dimension = self.start_resize_rect.height
            if self.edge_hovering[1]:
                y_dimension = self.start_resize_rect.height - y_diff
                y_pos = self.start_resize_rect.y + y_diff
            elif self.edge_hovering[3]:
                y_dimension = self.start_resize_rect.height + y_diff

            if y_dimension < self.minimum_dimensions[1]:
                if y_diff > 0:
                    y_pos = self.rect.bottom - self.minimum_dimensions[1]
                else:
                    y_pos = self.rect.top
        if self.rect.width >= self.minimum_dimensions[0]:
            x_pos = self.start_resize_rect.x
            x_dimension = self.start_resize_rect.width
            if self.edge_hovering[0]:
                x_dimension = self.start_resize_rect.width - x_diff
                x_pos = self.start_resize_rect.x + x_diff
            elif self.edge_hovering[2]:
                x_dimension = self.start_resize_rect.width + x_diff

            if x_dimension < self.minimum_dimensions[0]:
                if x_diff > 0:
                    x_pos = self.rect.right - self.minimum_dimensions[0]
                else:
                    x_pos = self.rect.left
        x_dimension = max(self.minimum_dimensions[0],
                          min(self.ui_container.rect.width, x_dimension))
        y_dimension = max(self.minimum_dimensions[1],
                          min(self.ui_container.rect.height, y_dimension))
        self.set_position((x_pos, y_pos))
        self.set_dimensions((x_dimension, y_dimension))

    def get_container(self) -> UIContainer:
        """
        Returns the container that should contain all the UI elements in this window.

        :return UIContainer: The window's container.

        """
        return self.window_element_container

    def can_hover(self) -> bool:
        """
        Called to test if this window can be hovered.
        """
        return not (self.resizing_mode_active or (self.title_bar is not None and
                                                  self.title_bar.held))

    # noinspection PyUnusedLocal
    def check_hover(self, time_delta: float, hovered_higher_element: bool) -> bool:
        """
        For the window the only hovering we care about is the edges if this is a resizable window.

        :param time_delta: time passed in seconds between one call to this method and the next.
        :param hovered_higher_element: Have we already hovered an element/window above this one.

        """
        hovered = False
        if not self.resizing_mode_active:
            self.edge_hovering = [False, False, False, False]
        if self.alive() and self.can_hover() and not hovered_higher_element and self.resizable:
            mouse_x, mouse_y = self.ui_manager.get_mouse_position()

            # Build a temporary rect just a little bit larger than our container rect.
            resize_rect = pygame.Rect(self._window_root_container.rect.left - 4,
                                      self._window_root_container.rect.top - 4,
                                      self._window_root_container.rect.width + 8,
                                      self._window_root_container.rect.height + 8)
            if resize_rect.collidepoint(mouse_x, mouse_y):
                if resize_rect.right > mouse_x > resize_rect.right - 6:
                    self.edge_hovering[2] = True
                    hovered = True

                if resize_rect.left + 6 > mouse_x > resize_rect.left:
                    self.edge_hovering[0] = True
                    hovered = True

                if resize_rect.bottom > mouse_y > resize_rect.bottom - 6:
                    self.edge_hovering[3] = True
                    hovered = True

                if resize_rect.top + 6 > mouse_y > resize_rect.top:
                    self.edge_hovering[1] = True
                    hovered = True
        elif self.resizing_mode_active:
            hovered = True

        if self.is_blocking:
            hovered = True

        if hovered:
            hovered_higher_element = True
            self.hovered = True
        else:
            self.hovered = False

        return hovered_higher_element

    def get_top_layer(self) -> int:
        """
        Returns the 'highest' layer used by this window so that we can correctly place other
        windows on top of it.

        :return: The top layer for this window as a number (greater numbers are higher layers).

        """
        return self._layer + self.layer_thickness

    def change_layer(self, new_layer: int):
        """
        Move this window, and it's contents, to a new layer in the UI.

        :param new_layer: The layer to move to.

        """
        if new_layer != self._layer:
            super().change_layer(new_layer)
            if self._window_root_container is not None:
                self._window_root_container.change_layer(new_layer)

                if self._window_root_container.layer_thickness != self.layer_thickness:
                    self.layer_thickness = self._window_root_container.layer_thickness

    def kill(self):
        """
        Overrides the basic kill() method of a pygame sprite so that we also kill all the UI
        elements in this window, and remove if from the window stack.
        """
        window_close_event = pygame.event.Event(pygame.USEREVENT,
                                                {'user_type': UI_WINDOW_CLOSE,
                                                 'ui_element': self,
                                                 'ui_object_id': self.most_specific_combined_id})
        pygame.event.post(window_close_event)

        self.window_stack.remove_window(self)
        self._window_root_container.kill()
        super().kill()

    def rebuild(self):
        """
        Rebuilds the window when the theme has changed.

        """
        if self._window_root_container is None:
            self._window_root_container = UIContainer(pygame.Rect(self.relative_rect.x +
                                                                  self.shadow_width,
                                                                  self.relative_rect.y +
                                                                  self.shadow_width,
                                                                  self.relative_rect.width -
                                                                  (2 * self.shadow_width),
                                                                  self.relative_rect.height -
                                                                  (2 * self.shadow_width)),
                                                      manager=self.ui_manager,
                                                      starting_height=1,
                                                      is_window_root_container=True,
                                                      container=None,
                                                      parent_element=self,
                                                      object_id="#window_root_container")
        if self.window_element_container is None:
            window_container_rect = pygame.Rect(self.border_width,
                                                self.title_bar_height,
                                                (self._window_root_container.relative_rect.width -
                                                 (2 * self.border_width)),
                                                (self._window_root_container.relative_rect.height -
                                                 self.title_bar_height - self.border_width))
            self.window_element_container = UIContainer(window_container_rect,
                                                        self.ui_manager,
                                                        starting_height=0,
                                                        container=self._window_root_container,
                                                        parent_element=self,
                                                        object_id="#window_element_container",
                                                        anchors={'top': 'top', 'bottom': 'bottom',
                                                                 'left': 'left', 'right': 'right'})

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

        self.set_dimensions(self.relative_rect.size)

        if self.window_element_container is not None:
            element_container_width = (self._window_root_container.relative_rect.width -
                                       (2 * self.border_width))
            element_container_height = (self._window_root_container.relative_rect.height -
                                        self.title_bar_height)
            self.window_element_container.set_dimensions((element_container_width,
                                                          element_container_height))
            self.window_element_container.set_relative_position((self.border_width,
                                                                 self.title_bar_height))

            if self.enable_title_bar:
                if self.title_bar is not None:
                    self.title_bar.set_dimensions((self._window_root_container.relative_rect.width -
                                                   self.title_bar_button_width,
                                                   self.title_bar_height))
                else:
                    title_bar_width = (self._window_root_container.relative_rect.width -
                                       self.title_bar_button_width)
                    self.title_bar = UIButton(relative_rect=pygame.Rect(0, 0,
                                                                        title_bar_width,
                                                                        self.title_bar_height),
                                              text=self.window_display_title,
                                              manager=self.ui_manager,
                                              container=self._window_root_container,
                                              parent_element=self,
                                              object_id='#title_bar',
                                              anchors={'top': 'top', 'bottom': 'top',
                                                       'left': 'left', 'right': 'right'}
                                              )
                    self.title_bar.set_hold_range((100, 100))

                if self.enable_close_button:
                    if self.close_window_button is not None:
                        close_button_pos = (-self.title_bar_button_width, 0)
                        self.close_window_button.set_dimensions((self.title_bar_button_width,
                                                                 self.title_bar_height))
                        self.close_window_button.set_relative_position(close_button_pos)
                    else:
                        close_rect = pygame.Rect((-self.title_bar_button_width, 0),
                                                 (self.title_bar_button_width,
                                                  self.title_bar_height))
                        self.close_window_button = UIButton(relative_rect=close_rect,
                                                            text='╳',
                                                            manager=self.ui_manager,
                                                            container=self._window_root_container,
                                                            parent_element=self,
                                                            object_id='#close_button',
                                                            anchors={'top': 'top',
                                                                     'bottom': 'top',
                                                                     'left': 'right',
                                                                     'right': 'right'}
                                                            )

                else:
                    if self.close_window_button is not None:
                        self.close_window_button.kill()
                        self.close_window_button = None
            else:
                if self.title_bar is not None:
                    self.title_bar.kill()
                    self.title_bar = None
                if self.close_window_button is not None:
                    self.close_window_button.kill()
                    self.close_window_button = None

    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': 15,
                                                       '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

        enable_title_bar_param = self.ui_theme.get_misc_data(self.object_ids,
                                                             self.element_ids,
                                                             'enable_title_bar')
        if enable_title_bar_param is not None:
            try:
                enable_title_bar = bool(int(enable_title_bar_param))
            except ValueError:
                enable_title_bar = True
            if enable_title_bar != self.enable_title_bar:
                self.enable_title_bar = enable_title_bar
                has_any_changed = True

        if self.enable_title_bar:

            enable_close_button_param = self.ui_theme.get_misc_data(self.object_ids,
                                                                    self.element_ids,
                                                                    'enable_close_button')
            if enable_close_button_param is not None:
                try:
                    enable_close_button = bool(int(enable_close_button_param))
                except ValueError:
                    enable_close_button = True
                if enable_close_button != self.enable_close_button:
                    self.enable_close_button = enable_close_button
                    has_any_changed = True

            title_bar_height_string = self.ui_theme.get_misc_data(self.object_ids,
                                                                  self.element_ids,
                                                                  'title_bar_height')
            if title_bar_height_string is not None:
                try:
                    title_bar_height = int(title_bar_height_string)
                except ValueError:
                    title_bar_height = 28
                if title_bar_height != self.title_bar_height:
                    self.title_bar_height = title_bar_height
                    self.title_bar_button_width = title_bar_height
                    has_any_changed = True
        else:
            self.title_bar_height = 0

        if has_any_changed:
            self.rebuild()
    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()
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()
Exemplo n.º 8
0
    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)
class UIColourChannelEditor(UIElement):
    """
    This colour picker specific element lets us edit a single colour channel (Red, Green, Blue,
    Hue etc). It's bundled along with the colour picker class because I don't see much use for it
    outside of a colour picker, but it still seemed sensible to make a class for a pattern in the
    colour picker that is repeated six times.

    :param relative_rect: The relative rectangle for sizing and positioning the element, relative
                          to the anchors.
    :param manager: The UI manager for the UI system.
    :param name: Name for this colour channel, (e.g 'R:' or 'B:'). Used for the label.
    :param channel_index: Index for the colour channel (e.g. red is 0, blue is 1, hue is also 0,
                          saturation is 1)
    :param value_range: Range of values for this channel (0 to 255 for R,G,B - 0 to 360 for hue, 0
                        to 100 for the rest)
    :param initial_value: Starting value for this colour channel.
    :param container: UI container for this element.
    :param parent_element: An element to parent this element, used for theming hierarchies and
                           events.
    :param object_id: A specific theming/event ID for this element.
    :param anchors: A dictionary of anchors used for setting up 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,
                 manager: IUIManagerInterface,
                 name: str,
                 channel_index: int,
                 value_range: Tuple[int, int],
                 initial_value: int,
                 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,
                         starting_height=1,
                         layer_thickness=1,
                         anchors=anchors,
                         visible=visible)

        self._create_valid_ids(container=container,
                               parent_element=parent_element,
                               object_id=object_id,
                               element_id='colour_channel_editor')

        self.range = value_range
        self.current_value = initial_value
        self.channel_index = channel_index

        self.set_image(self.ui_manager.get_universal_empty_surface())

        self.element_container = UIContainer(relative_rect,
                                             self.ui_manager,
                                             container=self.ui_container,
                                             parent_element=self,
                                             anchors=anchors,
                                             visible=self.visible)

        default_sizes = {
            'space_between': 3,
            'label_width': 17,
            'entry_width': 43,
            'line_height': 29,
            'slider_height': 21,
            'slider_vert_space': 4
        }

        self.label = UILabel(pygame.Rect(0, 0, -1,
                                         default_sizes['line_height']),
                             text=name,
                             manager=self.ui_manager,
                             container=self.element_container,
                             parent_element=self,
                             anchors={
                                 'left': 'left',
                                 'right': 'left',
                                 'top': 'top',
                                 'bottom': 'bottom'
                             })

        self.entry = UITextEntryLine(pygame.Rect(-default_sizes['entry_width'],
                                                 0,
                                                 default_sizes['entry_width'],
                                                 default_sizes['line_height']),
                                     manager=self.ui_manager,
                                     container=self.element_container,
                                     parent_element=self,
                                     anchors={
                                         'left': 'right',
                                         'right': 'right',
                                         'top': 'top',
                                         'bottom': 'bottom'
                                     })

        slider_width = (self.entry.rect.left - self.label.rect.right) - (
            2 * default_sizes['space_between'])

        self.slider = UIHorizontalSlider(pygame.Rect(
            (self.label.get_abs_rect().width + default_sizes['space_between']),
            default_sizes['slider_vert_space'], slider_width,
            default_sizes['slider_height']),
                                         start_value=initial_value,
                                         value_range=value_range,
                                         manager=self.ui_manager,
                                         container=self.element_container,
                                         parent_element=self,
                                         anchors={
                                             'left': 'left',
                                             'right': 'right',
                                             'top': 'top',
                                             'bottom': 'bottom'
                                         })

        self.entry.set_allowed_characters('numbers')
        self.entry.set_text(str(initial_value))
        self.entry.set_text_length_limit(3)

    def process_event(self, event: pygame.event.Event) -> bool:
        """
        Handles events that this UI element is interested in. In this case we are responding to the
        slider being moved and the user finishing entering text in the text entry element.

        :param event: The pygame Event to process.

        :return: True if event is consumed by this element and should not be passed on to other
                 elements.

        """
        consumed_event = super().process_event(event)
        if event.type == UI_TEXT_ENTRY_FINISHED and event.ui_element == self.entry:
            int_value = self.current_value
            try:
                int_value = int(self.entry.get_text())
            except ValueError:
                int_value = 0
            finally:
                self._set_value_from_entry(int_value)

        if event.type == UI_HORIZONTAL_SLIDER_MOVED and event.ui_element == self.slider:
            int_value = self.current_value
            try:
                int_value = int(self.slider.get_current_value())
            except ValueError:
                int_value = 0
            finally:
                self._set_value_from_slider(int_value)

        return consumed_event

    def _set_value_from_slider(self, new_value: int):
        """
        For updating the value in the text entry element when we've moved the slider. Also sends
        out an event for the color picker.

        :param new_value: The new value to set.

        """
        clipped_value = min(self.range[1], max(self.range[0], new_value))
        if clipped_value != self.current_value:
            self.current_value = clipped_value
            self.entry.set_text(str(self.current_value))
            # old event - to be removed in 0.8.0
            event_data = {
                'user_type': OldType(UI_COLOUR_PICKER_COLOUR_CHANNEL_CHANGED),
                'value': self.current_value,
                'channel_index': self.channel_index,
                '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,
                'channel_index': self.channel_index,
                'ui_element': self,
                'ui_object_id': self.most_specific_combined_id
            }
            pygame.event.post(
                pygame.event.Event(UI_COLOUR_PICKER_COLOUR_CHANNEL_CHANGED,
                                   event_data))

    def _set_value_from_entry(self, new_value: int):
        """
        For updating the value the slider element is set to when we've edited the text entry. The
        slider may have much less precision than the text entry depending on it's available width
        so we need to be careful to make the change one way. Also sends out an event for the color
        picker and clips the value to within the allowed value range.

        :param new_value: The new value to set.

        """
        clipped_value = min(self.range[1], max(self.range[0], new_value))
        if clipped_value != new_value:
            self.entry.set_text(str(clipped_value))
        if clipped_value != self.current_value:
            self.current_value = clipped_value
            self.slider.set_current_value(self.current_value)

            # old event - to be removed in 0.8.0
            event_data = {
                'user_type': OldType(UI_COLOUR_PICKER_COLOUR_CHANNEL_CHANGED),
                'value': self.current_value,
                'channel_index': self.channel_index,
                'ui_element': self,
                'ui_object_id': self.most_specific_combined_id
            }
            colour_channel_changed_event = pygame.event.Event(
                pygame.USEREVENT, event_data)
            pygame.event.post(colour_channel_changed_event)

            event_data = {
                'value': self.current_value,
                'channel_index': self.channel_index,
                'ui_element': self,
                'ui_object_id': self.most_specific_combined_id
            }
            colour_channel_changed_event = pygame.event.Event(
                UI_COLOUR_PICKER_COLOUR_CHANNEL_CHANGED, event_data)
            pygame.event.post(colour_channel_changed_event)

    def set_value(self, new_value: int):
        """
        For when we need to set the value of the colour channel from outside, usually from
        adjusting the colour elsewhere in the colour picker. Makes sure the new value is within the
        allowed range.

        :param new_value: Value to set.

        """
        clipped_value = min(self.range[1], max(self.range[0], new_value))
        if clipped_value != self.current_value:
            self.current_value = clipped_value
            self.entry.set_text(str(self.current_value))
            self.slider.set_current_value(self.current_value)

    def set_position(self, position: Union[pygame.math.Vector2,
                                           Tuple[int, int], Tuple[float,
                                                                  float]]):
        """
        Sets the absolute screen position of this channel, updating all subordinate elements at the
        same time.

        :param position: The absolute screen position to set.

        """
        super().set_position(position)
        self.element_container.set_relative_position(
            self.relative_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 channel, updating all subordinate elements at the
        same time.

        :param position: The relative screen position to set.

        """
        super().set_relative_position(position)
        self.element_container.set_relative_position(
            self.relative_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)
        self.element_container.set_dimensions(self.relative_rect.size)

    def show(self):
        """
        In addition to the base UIElement.show() - call show() of the element_container
        - which will propagate to the sub-elements - label, entry and slider.
        """
        super().show()

        self.element_container.show()

    def hide(self):
        """
        In addition to the base UIElement.hide() - call hide() of the element_container
        - which will propagate to the sub-elements - label, entry and slider.
        """
        super().hide()

        self.element_container.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()
    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))
Exemplo n.º 12
0
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()
Exemplo n.º 13
0
    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 == 'rectangle':
            self.drawable_shape = RectDrawableShape(self.rect,
                                                    theming_parameters,
                                                    ['normal'],
                                                    self.ui_manager)
        elif self.shape == '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.enable_arrow_buttons:
            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))
Exemplo n.º 14
0
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)
Exemplo n.º 15
0
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 UISelectionList(UIElement):
    """
    A rectangular element that holds any number of selectable text items displayed as a list.

    :param relative_rect: The positioning and sizing rectangle for the panel. See the layout guide
                          for details.
    :param item_list: A list of items as strings (item name only), or tuples of two strings (name,
                      theme_object_id).
    :param manager: The GUI manager that handles drawing and updating the UI and interactions
                    between elements.
    :param allow_multi_select: True if we are allowed to pick multiple things from the selection
                               list.
    :param allow_double_clicks: True if we can double click on items in the selection list.
    :param container: The container this element is inside of (by default the root container)
                      distinct from this panel's container.
    :param starting_height: The starting height up from it's container where this list is placed
                            into a layer.
    :param parent_element: A hierarchical 'parent' used for signifying belonging and used in
                           theming and events.
    :param object_id: An identifier that can be used to help distinguish this particular element
                      from others with the same hierarchy.
    :param anchors: Used to layout elements and dictate what the relative_rect is relative to.
                    Defaults to the top left.
    :param visible: Whether the element is visible by default. Warning - container visibility
                    may override this.
    """
    def __init__(self,
                 relative_rect: pygame.Rect,
                 item_list: Union[List[str], List[Tuple[str, str]]],
                 manager: IUIManagerInterface,
                 *,
                 allow_multi_select: bool = False,
                 allow_double_clicks: bool = True,
                 container: Union[IContainerLikeInterface, None] = None,
                 starting_height: int = 1,
                 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,
                         starting_height=starting_height,
                         layer_thickness=1,
                         anchors=anchors,
                         visible=visible)

        self._create_valid_ids(container=container,
                               parent_element=parent_element,
                               object_id=object_id,
                               element_id='selection_list')

        self._parent_element = parent_element
        self.list_and_scroll_bar_container = None
        self.item_list_container = None
        self._raw_item_list = item_list
        self.item_list = []
        self.allow_multi_select = allow_multi_select
        self.allow_double_clicks = allow_double_clicks

        self.background_colour = None
        self.border_colour = None
        self.background_image = None
        self.border_width = 1
        self.shadow_width = 2
        self.shape_corner_radius = 0
        self.shape = 'rectangle'

        self.scroll_bar = None  # type: Union[UIVerticalScrollBar, None]
        self.lowest_list_pos = 0
        self.total_height_of_list = 0
        self.list_item_height = 20
        self.scroll_bar_width = 20
        self.current_scroll_bar_width = 0

        self.rebuild_from_changed_theme_data()

    def get_single_selection(self) -> Union[str, None]:
        """
        Get the selected item in a list, if any. Only works if this is a single-selection list.

        :return: A single item name as a string or None.

        """
        if not self.allow_multi_select:
            selected_list = [
                item['text'] for item in self.item_list if item['selected']
            ]
            if len(selected_list) == 1:
                return selected_list[0]
            elif len(selected_list) == 0:
                return None
            else:
                raise RuntimeError(
                    'More than one item selected in single-selection,'
                    ' selection list')
        else:
            raise RuntimeError('Requesting single selection,'
                               ' from multi-selection list')

    def get_multi_selection(self) -> List[str]:
        """
        Get all the selected items in our selection list. Only works if this is a
        multi-selection list.

        :return: A list of the selected items in our selection list. May be empty if nothing
                 selected.

        """
        if self.allow_multi_select:
            return [
                item['text'] for item in self.item_list if item['selected']
            ]
        else:
            raise RuntimeError(
                'Requesting multi selection, from single-selection list')

    def update(self, time_delta: float):
        """
        A method called every update cycle of our application. Designed to be overridden by
        derived classes but also has a little functionality to make sure the panel's layer
        'thickness' is accurate and to handle window resizing.

        :param time_delta: time passed in seconds between one call to this method and the next.

        """
        super().update(time_delta)

        if self.scroll_bar is not None and self.scroll_bar.check_has_moved_recently(
        ):
            list_height_adjustment = min(
                self.scroll_bar.start_percentage * self.total_height_of_list,
                self.lowest_list_pos)
            for index, item in enumerate(self.item_list):
                new_height = int((index * self.list_item_height) -
                                 list_height_adjustment)
                if (-self.list_item_height <= new_height <=
                        self.item_list_container.relative_rect.height):
                    if item['button_element'] is not None:
                        item['button_element'].set_relative_position(
                            (0, new_height))
                    else:
                        button_rect = pygame.Rect(
                            0, new_height,
                            self.item_list_container.relative_rect.width,
                            self.list_item_height)
                        button = UIButton(
                            relative_rect=button_rect,
                            text=item['text'],
                            manager=self.ui_manager,
                            parent_element=self,
                            container=self.item_list_container,
                            object_id=ObjectID(
                                object_id=item['object_id'],
                                class_id='@selection_list_item'),
                            allow_double_clicks=self.allow_double_clicks,
                            anchors={
                                'left': 'left',
                                'right': 'right',
                                'top': 'top',
                                'bottom': 'top'
                            })
                        self.join_focus_sets(button)
                        item['button_element'] = button
                        if item['selected']:
                            item['button_element'].select()
                else:
                    if item['button_element'] is not None:
                        item['button_element'].kill()
                        item['button_element'] = None

    def set_item_list(self, new_item_list: Union[List[str], List[Tuple[str,
                                                                       str]]]):
        """
        Set a new string list (or tuple of strings & ids list) as the item list for this selection
        list. This will change what is displayed in the list.

        Tuples should be arranged like so:

         (list_text, object_ID)

         - list_text: displayed in the UI
         - object_ID: used for theming and events

        :param new_item_list: The new list to switch to. Can be a list of strings or tuples.

        """
        self._raw_item_list = new_item_list
        self.item_list = []  # type: List[Dict]
        for new_item in new_item_list:
            if isinstance(new_item, str):
                new_item_list_item = {
                    'text': new_item,
                    'button_element': None,
                    'selected': False,
                    'object_id': '#item_list_item'
                }
            elif isinstance(new_item, tuple):
                new_item_list_item = {
                    'text': new_item[0],
                    'button_element': None,
                    'selected': False,
                    'object_id': new_item[1]
                }
            else:
                raise ValueError('Invalid item list')

            self.item_list.append(new_item_list_item)

        self.total_height_of_list = self.list_item_height * len(self.item_list)
        self.lowest_list_pos = (
            self.total_height_of_list -
            self.list_and_scroll_bar_container.relative_rect.height)
        inner_visible_area_height = self.list_and_scroll_bar_container.relative_rect.height

        if self.total_height_of_list > inner_visible_area_height:
            # we need a scroll bar
            self.current_scroll_bar_width = self.scroll_bar_width
            percentage_visible = inner_visible_area_height / max(
                self.total_height_of_list, 1)

            if self.scroll_bar is not None:
                self.scroll_bar.reset_scroll_position()
                self.scroll_bar.set_visible_percentage(percentage_visible)
                self.scroll_bar.start_percentage = 0
            else:
                self.scroll_bar = UIVerticalScrollBar(
                    pygame.Rect(-self.scroll_bar_width, 0,
                                self.scroll_bar_width,
                                inner_visible_area_height),
                    visible_percentage=percentage_visible,
                    manager=self.ui_manager,
                    parent_element=self,
                    container=self.list_and_scroll_bar_container,
                    anchors={
                        'left': 'right',
                        'right': 'right',
                        'top': 'top',
                        'bottom': 'bottom'
                    })
                self.join_focus_sets(self.scroll_bar)
        else:
            if self.scroll_bar is not None:
                self.scroll_bar.kill()
                self.scroll_bar = None
            self.current_scroll_bar_width = 0

        # create button list container
        if self.item_list_container is not None:
            self.item_list_container.clear()
            if (self.item_list_container.relative_rect.width !=
                (self.list_and_scroll_bar_container.relative_rect.width -
                 self.current_scroll_bar_width)):
                container_dimensions = (
                    self.list_and_scroll_bar_container.relative_rect.width -
                    self.current_scroll_bar_width,
                    self.list_and_scroll_bar_container.relative_rect.height)
                self.item_list_container.set_dimensions(container_dimensions)
        else:
            self.item_list_container = UIContainer(
                pygame.Rect(
                    0, 0,
                    self.list_and_scroll_bar_container.relative_rect.width -
                    self.current_scroll_bar_width,
                    self.list_and_scroll_bar_container.relative_rect.height),
                manager=self.ui_manager,
                starting_height=0,
                parent_element=self,
                container=self.list_and_scroll_bar_container,
                object_id='#item_list_container',
                anchors={
                    'left': 'left',
                    'right': 'right',
                    'top': 'top',
                    'bottom': 'bottom'
                })
            self.join_focus_sets(self.item_list_container)
        item_y_height = 0
        for item in self.item_list:
            if item_y_height <= self.item_list_container.relative_rect.height:
                button_rect = pygame.Rect(
                    0, item_y_height,
                    self.item_list_container.relative_rect.width,
                    self.list_item_height)
                item['button_element'] = UIButton(
                    relative_rect=button_rect,
                    text=item['text'],
                    manager=self.ui_manager,
                    parent_element=self,
                    container=self.item_list_container,
                    object_id=ObjectID(object_id=item['object_id'],
                                       class_id='@selection_list_item'),
                    allow_double_clicks=self.allow_double_clicks,
                    anchors={
                        'left': 'left',
                        'right': 'right',
                        'top': 'top',
                        'bottom': 'top'
                    })
                self.join_focus_sets(item['button_element'])
                item_y_height += self.list_item_height
            else:
                break

    def process_event(self, event: pygame.event.Event) -> bool:
        """
        Can be overridden, also handle resizing windows. Gives UI Windows access to pygame events.
        Currently just blocks mouse click down events from passing through the panel.

        :param event: The event to process.

        :return: Should return True if this element makes use of this event.

        """
        if self.is_enabled and (
                event.type == pygame.USEREVENT and event.user_type
                in [UI_BUTTON_PRESSED, UI_BUTTON_DOUBLE_CLICKED]
                and event.ui_element in self.item_list_container.elements):
            for item in self.item_list:
                if item['button_element'] == event.ui_element:
                    if event.user_type == UI_BUTTON_DOUBLE_CLICKED:
                        event_data = {
                            'user_type':
                            UI_SELECTION_LIST_DOUBLE_CLICKED_SELECTION,
                            'text': event.ui_element.text,
                            'ui_element': self,
                            'ui_object_id': self.most_specific_combined_id
                        }
                    else:
                        if item['selected']:
                            item['selected'] = False
                            event.ui_element.unselect()

                            event_data = {
                                'user_type':
                                UI_SELECTION_LIST_DROPPED_SELECTION,
                                'text': event.ui_element.text,
                                'ui_element': self,
                                'ui_object_id': self.most_specific_combined_id
                            }

                        else:
                            item['selected'] = True
                            event.ui_element.select()

                            event_data = {
                                'user_type': UI_SELECTION_LIST_NEW_SELECTION,
                                'text': event.ui_element.text,
                                'ui_element': self,
                                'ui_object_id': self.most_specific_combined_id
                            }

                    selection_list_event = pygame.event.Event(
                        pygame.USEREVENT, event_data)
                    pygame.event.post(selection_list_event)
                elif not self.allow_multi_select:
                    if item['selected']:
                        item['selected'] = False
                        if item['button_element'] is not None:
                            item['button_element'].unselect()

                            event_data = {
                                'user_type':
                                UI_SELECTION_LIST_DROPPED_SELECTION,
                                'text': item['text'],
                                'ui_element': self,
                                'ui_object_id': self.most_specific_combined_id
                            }
                            drop_down_changed_event = pygame.event.Event(
                                pygame.USEREVENT, event_data)
                            pygame.event.post(drop_down_changed_event)

        return False  # Don't consume any events

    def set_dimensions(self, dimensions: Union[pygame.math.Vector2,
                                               Tuple[int, int], Tuple[float,
                                                                      float]]):
        """
        Set the size of this panel and then resizes and shifts the contents of the panel container
        to fit the new size.

        :param dimensions: The new dimensions to set.

        """
        # Don't use a basic gate on this set dimensions method because the container may be a
        # different size to the window
        super().set_dimensions(dimensions)

        border_and_shadow = self.border_width + self.shadow_width
        container_width = self.relative_rect.width - (2 * border_and_shadow)
        container_height = self.relative_rect.height - (2 * border_and_shadow)
        self.list_and_scroll_bar_container.set_dimensions(
            (container_width, container_height))

    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)
        border_and_shadow = self.border_width + self.shadow_width
        container_left = self.relative_rect.left + border_and_shadow
        container_top = self.relative_rect.top + border_and_shadow
        self.list_and_scroll_bar_container.set_relative_position(
            (container_left, container_top))

    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
        container_left = self.relative_rect.left + border_and_shadow
        container_top = self.relative_rect.top + border_and_shadow
        self.list_and_scroll_bar_container.set_relative_position(
            (container_left, container_top))

    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.list_and_scroll_bar_container.kill()
        super().kill()

    def rebuild_from_changed_theme_data(self):
        """
        Checks if any theming parameters have changed, and if so triggers a full rebuild of the
        button's drawable shape
        """
        super().rebuild_from_changed_theme_data()
        has_any_changed = False

        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

        # misc
        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

        if self._check_misc_theme_data_changed(
                attribute_name='list_item_height',
                default_value=20,
                casting_func=int):
            has_any_changed = True

        if has_any_changed:
            self.rebuild()

    def rebuild(self):
        """
        A complete rebuild of the drawable shape used by this element.

        """
        theming_parameters = {
            'normal_bg': self.background_colour,
            'normal_border': self.border_colour,
            'normal_image': self.background_image,
            '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'],
                                                    self.ui_manager)
        elif self.shape == 'rounded_rectangle':
            self.drawable_shape = RoundedRectangleShape(
                self.rect, theming_parameters, ['normal'], self.ui_manager)

        self.on_fresh_drawable_shape_ready()

        if self.list_and_scroll_bar_container is None:
            self.list_and_scroll_bar_container = UIContainer(
                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) -
                    (2 * self.border_width), self.relative_rect.height -
                    (2 * self.shadow_width) - (2 * self.border_width)),
                manager=self.ui_manager,
                starting_height=self.starting_height,
                container=self.ui_container,
                parent_element=self._parent_element,
                object_id='#selection_list_container',
                anchors=self.anchors,
                visible=self.visible)
            self.join_focus_sets(self.list_and_scroll_bar_container)
        else:
            self.list_and_scroll_bar_container.set_dimensions(
                (self.relative_rect.width - (2 * self.shadow_width) -
                 (2 * self.border_width), self.relative_rect.height -
                 (2 * self.shadow_width) - (2 * self.border_width)))
            self.list_and_scroll_bar_container.set_relative_position(
                (self.relative_rect.left + self.shadow_width +
                 self.border_width, self.relative_rect.top +
                 self.shadow_width + self.border_width))

        self.set_item_list(self._raw_item_list)

    def disable(self):
        """
        Disables all elements in the selection list so they are no longer interactive.
        """
        if self.is_enabled:
            self.is_enabled = False
            self.list_and_scroll_bar_container.disable()

            # clear selections
            for item in self.item_list:
                item['selected'] = False

    def enable(self):
        """
        Enables all elements in the selection list so they are interactive again.
        """
        if not self.is_enabled:
            self.is_enabled = True
            self.list_and_scroll_bar_container.enable()

    def show(self):
        """
        In addition to the base UIElement.show() - call show() of owned container -
        list_and_scroll_bar_container. All other subelements (item_list_container, scrollbar) are
        children of list_and_scroll_bar_container, so it's visibility will propagate to them -
        there is no need to call their show() methods separately.
        """
        super().show()

        self.list_and_scroll_bar_container.show()

    def hide(self):
        """
        In addition to the base UIElement.hide() - call hide() of owned container -
        list_and_scroll_bar_container. All other subelements (item_list_container, scrollbar) are
        children of list_and_scroll_bar_container, so it's visibility will propagate to them -
        there is no need to call their hide() methods separately.
        """
        super().hide()

        self.list_and_scroll_bar_container.hide()
Exemplo n.º 17
0
class UIPanel(UIElement, IContainerLikeInterface):
    """
    A rectangular panel that holds a UI container and is designed to overlap other elements. It
    acts a little like a window that is not shuffled about in a stack - instead remaining at the
    same layer distance from the container it was initially placed in.

    It's primary purpose is for things like involved HUDs in games that want to always sit on top
    of UI elements that may be present 'inside' the game world (e.g. player health bars). By
    creating a UI Panel at a height above the highest layer used by the game world's UI elements
    we can ensure that all elements added to the panel are always above the fray.

    :param relative_rect: The positioning and sizing rectangle for the panel. See the layout
                          guide for details.
    :param starting_layer_height: How many layers above its container to place this panel on.
    :param manager: The GUI manager that handles drawing and updating the UI and interactions
                    between elements.
    :param margins: Controls the distance between the edge of the panel and where it's
                    container should begin.
    :param container: The container this panel is inside of distinct from this panel's own
                      container.
    :param parent_element: A hierarchical 'parent' used for signifying belonging and used in
                           theming and events.
    :param object_id: An identifier that can be used to help distinguish this particular panel
                      from others.
    :param anchors: Used to layout elements and dictate what the relative_rect is relative to.
                    Defaults to the top left.
    :param visible: Whether the element is visible by default. Warning - container visibility
                    may override this.
    """
    def __init__(self,
                 relative_rect: pygame.Rect,
                 starting_layer_height: int,
                 manager: IUIManagerInterface,
                 *,
                 element_id: str = 'panel',
                 margins: Dict[str, int] = None,
                 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,
                         starting_height=starting_layer_height,
                         layer_thickness=1,
                         anchors=anchors,
                         visible=visible)

        self._create_valid_ids(container=container,
                               parent_element=parent_element,
                               object_id=object_id,
                               element_id=element_id)

        self.background_colour = None
        self.border_colour = None
        self.background_image = None
        self.border_width = 1
        self.shadow_width = 2
        self.shape_corner_radius = 0
        self.shape = 'rectangle'

        self.rebuild_from_changed_theme_data()

        if margins is None:
            self.container_margins = {
                'left': self.shadow_width + self.border_width,
                'right': self.shadow_width + self.border_width,
                'top': self.shadow_width + self.border_width,
                'bottom': self.shadow_width + self.border_width
            }
        else:
            self.container_margins = margins

        container_rect = pygame.Rect(
            self.relative_rect.left + self.container_margins['left'],
            self.relative_rect.top + self.container_margins['top'],
            self.relative_rect.width -
            (self.container_margins['left'] + self.container_margins['right']),
            self.relative_rect.height -
            (self.container_margins['top'] + self.container_margins['bottom']))

        self.panel_container = UIContainer(
            container_rect,
            manager,
            starting_height=starting_layer_height,
            container=container,
            parent_element=self,
            object_id=ObjectID(object_id='#panel_container', class_id=None),
            anchors=anchors,
            visible=self.visible)

    def update(self, time_delta: float):
        """
        A method called every update cycle of our application. Designed to be overridden by derived
        classes but also has a little functionality to make sure the panel's layer 'thickness' is
        accurate and to handle window resizing.

        :param time_delta: time passed in seconds between one call to this method and the next.

        """
        super().update(time_delta)
        if self.get_container().get_thickness() != self.layer_thickness:
            self.layer_thickness = self.get_container().get_thickness()

    def process_event(self, event: pygame.event.Event) -> bool:
        """
        Can be overridden, also handle resizing windows. Gives UI Windows access to pygame events.
        Currently just blocks mouse click down events from passing through the panel.

        :param event: The event to process.

        :return: Should return True if this element consumes this event.

        """
        consumed_event = False
        if (self is not None and event.type == pygame.MOUSEBUTTONDOWN
                and event.button in [
                    pygame.BUTTON_LEFT, pygame.BUTTON_RIGHT,
                    pygame.BUTTON_MIDDLE
                ]):
            scaled_mouse_pos = self.ui_manager.calculate_scaled_mouse_position(
                event.pos)
            if self.hover_point(scaled_mouse_pos[0], scaled_mouse_pos[1]):
                consumed_event = True

        return consumed_event

    def get_container(self) -> IUIContainerInterface:
        """
        Returns the container that should contain all the UI elements in this panel.

        :return UIContainer: The panel's container.

        """
        return self.panel_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.get_container().kill()
        super().kill()

    def set_dimensions(self, dimensions: Union[pygame.math.Vector2,
                                               Tuple[int, int], Tuple[float,
                                                                      float]]):
        """
        Set the size of this panel and then re-sizes and shifts the contents of the panel container
        to fit the new size.

        :param dimensions: The new dimensions to set.

        """
        # Don't use a basic gate on this set dimensions method because the container may be a
        # different size to the window
        super().set_dimensions(dimensions)

        new_container_dimensions = (
            self.relative_rect.width -
            (self.container_margins['left'] + self.container_margins['right']),
            self.relative_rect.height -
            (self.container_margins['top'] + self.container_margins['bottom']))
        if new_container_dimensions != self.get_container().get_size():
            container_rel_pos = (self.relative_rect.x +
                                 self.container_margins['left'],
                                 self.relative_rect.y +
                                 self.container_margins['top'])
            self.get_container().set_dimensions(new_container_dimensions)
            self.get_container().set_relative_position(container_rel_pos)

    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)
        container_rel_pos = (self.relative_rect.x +
                             self.container_margins['left'],
                             self.relative_rect.y +
                             self.container_margins['top'])
        self.get_container().set_relative_position(container_rel_pos)

    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)
        container_rel_pos = (self.relative_rect.x +
                             self.container_margins['left'],
                             self.relative_rect.y +
                             self.container_margins['top'])
        self.get_container().set_relative_position(container_rel_pos)

    def rebuild_from_changed_theme_data(self):
        """
        Checks if any theming parameters have changed, and if so triggers a full rebuild of the
        button's drawable shape.
        """
        super().rebuild_from_changed_theme_data()
        has_any_changed = False

        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

        background_image = None
        try:
            background_image = self.ui_theme.get_image(
                'background_image', self.combined_element_ids)
        except LookupError:
            background_image = None
        finally:
            if background_image != self.background_image:
                self.background_image = background_image
                has_any_changed = True

        # misc
        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

        if has_any_changed:
            self.rebuild()

    def rebuild(self):
        """
        A complete rebuild of the drawable shape used by this button.

        """
        theming_parameters = {
            'normal_bg': self.background_colour,
            'normal_border': self.border_colour,
            'normal_image': self.background_image,
            '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'],
                                                    self.ui_manager)
        elif self.shape == 'rounded_rectangle':
            self.drawable_shape = RoundedRectangleShape(
                self.rect, theming_parameters, ['normal'], self.ui_manager)

        self.on_fresh_drawable_shape_ready()

    def disable(self):
        """
        Disables all elements in the panel so they are no longer interactive.
        """
        if self.is_enabled:
            self.is_enabled = False
            self.panel_container.disable()

    def enable(self):
        """
        Enables all elements in the panel so they are interactive again.
        """
        if not self.is_enabled:
            self.is_enabled = True
            self.panel_container.enable()

    def show(self):
        """
        In addition to the base UIElement.show() - call show() of owned container - panel_container.
        """
        super().show()

        self.panel_container.show()

    def hide(self):
        """
        In addition to the base UIElement.hide() - call hide() of owned container - panel_container.
        """
        self.panel_container.hide()
        super().hide()
    def set_item_list(self, new_item_list: Union[List[str], List[Tuple[str,
                                                                       str]]]):
        """
        Set a new string list (or tuple of strings & ids list) as the item list for this selection
        list. This will change what is displayed in the list.

        Tuples should be arranged like so:

         (list_text, object_ID)

         - list_text: displayed in the UI
         - object_ID: used for theming and events

        :param new_item_list: The new list to switch to. Can be a list of strings or tuples.

        """
        self._raw_item_list = new_item_list
        self.item_list = []  # type: List[Dict]
        for new_item in new_item_list:
            if isinstance(new_item, str):
                new_item_list_item = {
                    'text': new_item,
                    'button_element': None,
                    'selected': False,
                    'object_id': '#item_list_item'
                }
            elif isinstance(new_item, tuple):
                new_item_list_item = {
                    'text': new_item[0],
                    'button_element': None,
                    'selected': False,
                    'object_id': new_item[1]
                }
            else:
                raise ValueError('Invalid item list')

            self.item_list.append(new_item_list_item)

        self.total_height_of_list = self.list_item_height * len(self.item_list)
        self.lowest_list_pos = (
            self.total_height_of_list -
            self.list_and_scroll_bar_container.relative_rect.height)
        inner_visible_area_height = self.list_and_scroll_bar_container.relative_rect.height

        if self.total_height_of_list > inner_visible_area_height:
            # we need a scroll bar
            self.current_scroll_bar_width = self.scroll_bar_width
            percentage_visible = inner_visible_area_height / max(
                self.total_height_of_list, 1)

            if self.scroll_bar is not None:
                self.scroll_bar.reset_scroll_position()
                self.scroll_bar.set_visible_percentage(percentage_visible)
                self.scroll_bar.start_percentage = 0
            else:
                self.scroll_bar = UIVerticalScrollBar(
                    pygame.Rect(-self.scroll_bar_width, 0,
                                self.scroll_bar_width,
                                inner_visible_area_height),
                    visible_percentage=percentage_visible,
                    manager=self.ui_manager,
                    parent_element=self,
                    container=self.list_and_scroll_bar_container,
                    anchors={
                        'left': 'right',
                        'right': 'right',
                        'top': 'top',
                        'bottom': 'bottom'
                    })
                self.join_focus_sets(self.scroll_bar)
        else:
            if self.scroll_bar is not None:
                self.scroll_bar.kill()
                self.scroll_bar = None
            self.current_scroll_bar_width = 0

        # create button list container
        if self.item_list_container is not None:
            self.item_list_container.clear()
            if (self.item_list_container.relative_rect.width !=
                (self.list_and_scroll_bar_container.relative_rect.width -
                 self.current_scroll_bar_width)):
                container_dimensions = (
                    self.list_and_scroll_bar_container.relative_rect.width -
                    self.current_scroll_bar_width,
                    self.list_and_scroll_bar_container.relative_rect.height)
                self.item_list_container.set_dimensions(container_dimensions)
        else:
            self.item_list_container = UIContainer(
                pygame.Rect(
                    0, 0,
                    self.list_and_scroll_bar_container.relative_rect.width -
                    self.current_scroll_bar_width,
                    self.list_and_scroll_bar_container.relative_rect.height),
                manager=self.ui_manager,
                starting_height=0,
                parent_element=self,
                container=self.list_and_scroll_bar_container,
                object_id='#item_list_container',
                anchors={
                    'left': 'left',
                    'right': 'right',
                    'top': 'top',
                    'bottom': 'bottom'
                })
            self.join_focus_sets(self.item_list_container)
        item_y_height = 0
        for item in self.item_list:
            if item_y_height <= self.item_list_container.relative_rect.height:
                button_rect = pygame.Rect(
                    0, item_y_height,
                    self.item_list_container.relative_rect.width,
                    self.list_item_height)
                item['button_element'] = UIButton(
                    relative_rect=button_rect,
                    text=item['text'],
                    manager=self.ui_manager,
                    parent_element=self,
                    container=self.item_list_container,
                    object_id=ObjectID(object_id=item['object_id'],
                                       class_id='@selection_list_item'),
                    allow_double_clicks=self.allow_double_clicks,
                    anchors={
                        'left': 'left',
                        'right': 'right',
                        'top': 'top',
                        'bottom': 'top'
                    })
                self.join_focus_sets(item['button_element'])
                item_y_height += self.list_item_height
            else:
                break
    def __init__(self,
                 relative_rect: pygame.Rect,
                 manager: IUIManagerInterface,
                 name: str,
                 channel_index: int,
                 value_range: Tuple[int, int],
                 initial_value: int,
                 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,
                         starting_height=1,
                         layer_thickness=1,
                         anchors=anchors,
                         visible=visible)

        self._create_valid_ids(container=container,
                               parent_element=parent_element,
                               object_id=object_id,
                               element_id='colour_channel_editor')

        self.range = value_range
        self.current_value = initial_value
        self.channel_index = channel_index

        self.set_image(self.ui_manager.get_universal_empty_surface())

        self.element_container = UIContainer(relative_rect,
                                             self.ui_manager,
                                             container=self.ui_container,
                                             parent_element=self,
                                             anchors=anchors,
                                             visible=self.visible)

        default_sizes = {
            'space_between': 3,
            'label_width': 17,
            'entry_width': 43,
            'line_height': 29,
            'slider_height': 21,
            'slider_vert_space': 4
        }

        self.label = UILabel(pygame.Rect(0, 0, -1,
                                         default_sizes['line_height']),
                             text=name,
                             manager=self.ui_manager,
                             container=self.element_container,
                             parent_element=self,
                             anchors={
                                 'left': 'left',
                                 'right': 'left',
                                 'top': 'top',
                                 'bottom': 'bottom'
                             })

        self.entry = UITextEntryLine(pygame.Rect(-default_sizes['entry_width'],
                                                 0,
                                                 default_sizes['entry_width'],
                                                 default_sizes['line_height']),
                                     manager=self.ui_manager,
                                     container=self.element_container,
                                     parent_element=self,
                                     anchors={
                                         'left': 'right',
                                         'right': 'right',
                                         'top': 'top',
                                         'bottom': 'bottom'
                                     })

        slider_width = (self.entry.rect.left - self.label.rect.right) - (
            2 * default_sizes['space_between'])

        self.slider = UIHorizontalSlider(pygame.Rect(
            (self.label.get_abs_rect().width + default_sizes['space_between']),
            default_sizes['slider_vert_space'], slider_width,
            default_sizes['slider_height']),
                                         start_value=initial_value,
                                         value_range=value_range,
                                         manager=self.ui_manager,
                                         container=self.element_container,
                                         parent_element=self,
                                         anchors={
                                             'left': 'left',
                                             'right': 'right',
                                             'top': 'top',
                                             'bottom': 'bottom'
                                         })

        self.entry.set_allowed_characters('numbers')
        self.entry.set_text(str(initial_value))
        self.entry.set_text_length_limit(3)
Exemplo n.º 20
0
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