コード例 #1
0
    def test_set_dimensions(self, _init_pygame, default_ui_manager):
        button = UIButton(relative_rect=pygame.Rect(0, 0, 150, 30),
                          text="Test Button",
                          tool_tip_text="This is a test of the button's tool tip functionality.",
                          manager=default_ui_manager)

        button.set_dimensions(pygame.Vector2(250.0, 60.0))

        assert button.drawable_shape.containing_rect.width == 250 and button.drawable_shape.containing_rect.height == 60
コード例 #2
0
class UIClosedDropDownState:
    """
    The closed state of the drop down just displays the currently chosen option and a button that
    will switch the menu to the expanded state.

    :param drop_down_menu_ui: The UIDropDownElement this state belongs to.
    :param selected_option: The currently selected option.
    :param base_position_rect: Position and dimensions rectangle.
    :param open_button_width: Width of open button.
    :param expand_direction: Direction of expansion, 'up' or 'down'.
    :param manager: The UI Manager for the whole UI.
    :param container: The container the element is within.
    :param object_ids: The object IDs for the drop down UI element.
    :param element_ids: The element IDs for the drop down UI element.
    :param visible: Whether the element is visible by default. Warning -
                    container visibility may override this.
    """
    def __init__(self,
                 drop_down_menu_ui: 'UIDropDownMenu',
                 selected_option: str,
                 base_position_rect: Union[pygame.Rect, None],
                 open_button_width: int,
                 expand_direction: Union[str, None],
                 manager: IUIManagerInterface,
                 container: IContainerLikeInterface,
                 object_ids: Union[List[Union[str, None]], None],
                 element_ids: Union[List[str], None],
                 visible: int = 1):

        self.drop_down_menu_ui = drop_down_menu_ui
        self.selected_option_button = None
        self.open_button = None
        self.selected_option = selected_option
        self.base_position_rect = base_position_rect
        self.expand_direction = expand_direction
        self.ui_manager = manager
        self.ui_container = container
        self.element_ids = element_ids
        self.object_ids = object_ids

        self.open_button_width = open_button_width

        self.should_transition = False
        self.target_state = 'expanded'
        self.visible = visible

        self.active_buttons = []

    def disable(self):
        """
        Disables the closed state so that it is no longer interactive.
        """
        self.selected_option_button.disable()
        if self.open_button is not None:
            self.open_button.disable()
        self.drop_down_menu_ui.drawable_shape.set_active_state('disabled')

    def enable(self):
        """
        Re-enables the closed state so we can once again interact with it.
        """
        self.selected_option_button.enable()
        if self.open_button is not None:
            self.open_button.enable()
        self.drop_down_menu_ui.drawable_shape.set_active_state('normal')

    def rebuild(self):
        """
        Rebuild the closed state from theming parameters and dimensions.

        """
        theming_parameters = {
            'normal_bg': self.drop_down_menu_ui.background_colour,
            'normal_border': self.drop_down_menu_ui.border_colour,
            'disabled_bg': self.drop_down_menu_ui.disabled_background_colour,
            'disabled_border': self.drop_down_menu_ui.disabled_border_colour,
            'border_width': self.drop_down_menu_ui.border_width,
            'shadow_width': self.drop_down_menu_ui.shadow_width,
            'shape_corner_radius': self.drop_down_menu_ui.shape_corner_radius
        }

        if self.drop_down_menu_ui.shape == 'rectangle':
            self.drop_down_menu_ui.drawable_shape = RectDrawableShape(
                self.drop_down_menu_ui.rect, theming_parameters,
                ['normal', 'disabled'], self.ui_manager)
        elif self.drop_down_menu_ui.shape == 'rounded_rectangle':
            shape_rect = self.drop_down_menu_ui.rect
            self.drop_down_menu_ui.drawable_shape = RoundedRectangleShape(
                shape_rect, theming_parameters, ['normal', 'disabled'],
                self.ui_manager)

        self.drop_down_menu_ui.set_image(
            self.drop_down_menu_ui.drawable_shape.get_fresh_surface())

        # extra
        if self.open_button is not None:
            expand_button_symbol = '▼'
            if self.expand_direction is not None:
                if self.expand_direction == 'up':
                    expand_button_symbol = '▲'
                elif self.expand_direction == 'down':
                    expand_button_symbol = '▼'
            self.open_button.set_text(expand_button_symbol)

    def start(self, should_rebuild: bool = True):
        """
        Called each time we enter the closed state. It creates the necessary elements, the
        selected option and the open button.
        """
        if should_rebuild:
            self.rebuild()

        self.should_transition = False

        border_and_shadow = (self.drop_down_menu_ui.shadow_width +
                             self.drop_down_menu_ui.border_width)
        self.active_buttons = []
        self.selected_option_button = UIButton(
            pygame.Rect(
                (border_and_shadow, border_and_shadow),
                (self.base_position_rect.width - self.open_button_width,
                 self.base_position_rect.height)),
            self.selected_option,
            self.ui_manager,
            self.ui_container,
            starting_height=2,
            parent_element=self.drop_down_menu_ui,
            object_id='#selected_option',
            visible=self.visible)
        self.drop_down_menu_ui.join_focus_sets(self.selected_option_button)
        self.active_buttons.append(self.selected_option_button)

        if self.open_button_width > 0:
            open_button_x = (border_and_shadow +
                             self.base_position_rect.width -
                             self.open_button_width)
            expand_button_symbol = '▼'
            if self.expand_direction is not None:
                if self.expand_direction == 'up':
                    expand_button_symbol = '▲'
                elif self.expand_direction == 'down':
                    expand_button_symbol = '▼'
            self.open_button = UIButton(pygame.Rect(
                (open_button_x, border_and_shadow),
                (self.open_button_width, self.base_position_rect.height)),
                                        expand_button_symbol,
                                        self.ui_manager,
                                        self.ui_container,
                                        starting_height=2,
                                        parent_element=self.drop_down_menu_ui,
                                        object_id='#expand_button',
                                        visible=self.visible)
            self.drop_down_menu_ui.join_focus_sets(self.open_button)
            self.active_buttons.append(self.open_button)

    def finish(self):
        """
        Called when we leave the closed state. Kills the open button and the selected option button.
        """
        self.selected_option_button.kill()
        if self.open_button is not None:
            self.open_button.kill()

    def process_event(self, event: pygame.event.Event) -> bool:
        """
        Processes events for the closed state of the drop down.

        :param event: The event to process.

        :return: Return True if we want to consume this event so it is not passed on to the
                 rest of the UI.
        """
        if event.type == UI_BUTTON_PRESSED and event.ui_element in self.active_buttons:
            self.should_transition = True

        return False

    def update_position(self):
        """
        Update the position of all the button elements in the closed drop down state.

        Used when the position of the  drop down has been altered directly, rather than when it has
        been moved as a consequence of it's container being moved.
        """

        # update the base position rect
        border_and_shadow = (self.drop_down_menu_ui.shadow_width +
                             self.drop_down_menu_ui.border_width)
        self.base_position_rect.x = self.drop_down_menu_ui.relative_rect.x + border_and_shadow
        self.base_position_rect.y = self.drop_down_menu_ui.relative_rect.y + border_and_shadow

        # update all the ui elements that depend on the base position
        self.selected_option_button.set_relative_position(
            (border_and_shadow, border_and_shadow))

        if self.open_button is not None:
            open_button_x = (border_and_shadow +
                             self.base_position_rect.width -
                             self.open_button_width)
            self.open_button.set_relative_position(
                (open_button_x, self.base_position_rect.y))

    def update_dimensions(self):
        """
        Update the dimensions of all the button elements in the closed drop down state.

        Used when the dimensions of the drop down have been altered.
        """

        # update the base position rect
        border_and_shadow = (self.drop_down_menu_ui.shadow_width +
                             self.drop_down_menu_ui.border_width)
        self.base_position_rect.width = (
            self.drop_down_menu_ui.relative_rect.width -
            (2 * border_and_shadow))
        self.base_position_rect.height = (
            self.drop_down_menu_ui.relative_rect.height -
            (2 * border_and_shadow))

        # update all the ui elements that depend on the base position rect
        self.selected_option_button.set_dimensions(
            (self.base_position_rect.width - self.open_button_width,
             self.base_position_rect.height))
        if self.open_button is not None:
            open_button_x = (border_and_shadow +
                             self.base_position_rect.width -
                             self.open_button_width)
            self.open_button.set_dimensions(
                (self.open_button_width, self.base_position_rect.height))
            self.open_button.set_relative_position(
                (open_button_x, border_and_shadow))

    def on_fresh_drawable_shape_ready(self):
        """
        Called by an element's drawable shape when it has a new image surface ready for use,
        normally after a rebuilding/redrawing of some kind.

        In this case the result is to set the UI element's image to the new surface.
        """
        self.drop_down_menu_ui.set_image(
            self.drop_down_menu_ui.drawable_shape.get_fresh_surface())

    def show(self):
        """
        Show selected_option_button and open_button.
        """
        self.visible = 1

        if self.open_button is not None:
            self.open_button.show()
        if self.selected_option_button is not None:
            self.selected_option_button.show()

    def hide(self):
        """
        Hide selected_option_button and open_button.
        """
        self.visible = 0

        if self.open_button is not None:
            self.open_button.hide()
        if self.selected_option_button is not None:
            self.selected_option_button.hide()
コード例 #3
0
class UIExpandedDropDownState:
    """
    The expanded state of the drop down  displays the currently chosen option, all the available
    options and a button to close the menu and return to the closed state.

    Picking an option will also close the menu.

    :param drop_down_menu_ui: The UIDropDownElement this state belongs to.
    :param options_list: The list of options in this drop down.
    :param selected_option: The currently selected option.
    :param base_position_rect: Position and dimensions rectangle.
    :param close_button_width: Width of close button.
    :param expand_direction: Direction of expansion, 'up' or 'down'.
    :param manager: The UI Manager for the whole UI.
    :param container: The container the element is within.
    :param object_ids: The object IDs for the drop down UI element.
    :param element_ids: The element IDs for the drop down UI element.
    """
    def __init__(self, drop_down_menu_ui: 'UIDropDownMenu',
                 options_list: List[str], selected_option: str,
                 base_position_rect: Union[pygame.Rect,
                                           None], close_button_width: int,
                 expand_direction: Union[str,
                                         None], manager: IUIManagerInterface,
                 container: IContainerLikeInterface,
                 object_ids: Union[List[Union[str, None]],
                                   None], element_ids: Union[List[str], None]):

        self.drop_down_menu_ui = drop_down_menu_ui
        self.options_list = options_list
        self.selected_option = selected_option
        self.base_position_rect = base_position_rect

        self.expand_direction = expand_direction
        self.ui_manager = manager
        self.ui_container = container
        self.element_ids = element_ids
        self.object_ids = object_ids

        # sizing variables
        self.options_list_height = 0
        self.option_list_y_pos = 0
        self.close_button_width = close_button_width

        # UI elements
        self.selected_option_button = None
        self.close_button = None
        self.options_selection_list = None

        # state transitioning
        self.should_transition = False
        self.target_state = 'closed'

        self.active_buttons = []

    def rebuild(self):
        """
        Rebuild the state from theming parameters and dimensions.

        """
        theming_parameters = {
            'normal_bg': self.drop_down_menu_ui.background_colour,
            'normal_border': self.drop_down_menu_ui.border_colour,
            'border_width': self.drop_down_menu_ui.border_width,
            'shadow_width': self.drop_down_menu_ui.shadow_width,
            'shape_corner_radius': self.drop_down_menu_ui.shape_corner_radius
        }

        shape_rect = self.drop_down_menu_ui.relative_rect
        if self.drop_down_menu_ui.shape == 'rectangle':
            self.drop_down_menu_ui.drawable_shape = RectDrawableShape(
                shape_rect, theming_parameters, ['normal'], self.ui_manager)

        elif self.drop_down_menu_ui.shape == 'rounded_rectangle':
            self.drop_down_menu_ui.drawable_shape = RoundedRectangleShape(
                shape_rect, theming_parameters, ['normal'], self.ui_manager)

        self.on_fresh_drawable_shape_ready()

        # extra
        if self.close_button is not None:
            expand_button_symbol = '▼'
            if self.expand_direction is not None:
                if self.expand_direction == 'up':
                    expand_button_symbol = '▲'
                elif self.expand_direction == 'down':
                    expand_button_symbol = '▼'
            self.close_button.set_text(expand_button_symbol)

    def start(self, should_rebuild: bool = True):
        """
        Called each time we enter the expanded state. It creates the necessary elements, the
        selected option, all the other available options and the close button.

        """
        self.should_transition = False

        border_and_shadow = (self.drop_down_menu_ui.shadow_width +
                             self.drop_down_menu_ui.border_width)
        self.active_buttons = []
        self.selected_option_button = UIButton(
            pygame.Rect(
                (border_and_shadow, border_and_shadow),
                (self.base_position_rect.width - self.close_button_width,
                 self.base_position_rect.height)),
            self.selected_option,
            self.ui_manager,
            self.ui_container,
            starting_height=2,
            parent_element=self.drop_down_menu_ui,
            object_id=ObjectID('#selected_option', None))
        self.drop_down_menu_ui.join_focus_sets(self.selected_option_button)
        self.active_buttons.append(self.selected_option_button)

        expand_button_symbol = '▼'

        list_object_id = '#drop_down_options_list'
        list_object_ids = self.drop_down_menu_ui.object_ids[:]
        list_object_ids.append(list_object_id)
        list_class_ids = self.drop_down_menu_ui.class_ids[:]
        list_class_ids.append(None)
        list_element_ids = self.drop_down_menu_ui.element_ids[:]
        list_element_ids.append('selection_list')

        final_ids = self.ui_manager.get_theme().build_all_combined_ids(
            list_element_ids, list_class_ids, list_object_ids)

        self._calculate_options_list_sizes(final_ids)
        if self.expand_direction is not None:
            if self.expand_direction == 'up':
                expand_button_symbol = '▲'

                if self.drop_down_menu_ui.expansion_height_limit is None:
                    self.drop_down_menu_ui.expansion_height_limit = self.base_position_rect.top

                self.options_list_height = min(
                    self.options_list_height,
                    self.drop_down_menu_ui.expansion_height_limit)

                self.option_list_y_pos = self.base_position_rect.top - self.options_list_height

            elif self.expand_direction == 'down':
                expand_button_symbol = '▼'

                if self.drop_down_menu_ui.expansion_height_limit is None:
                    height_limit = (self.drop_down_menu_ui.ui_container.
                                    relative_rect.height -
                                    self.base_position_rect.bottom)
                    self.drop_down_menu_ui.expansion_height_limit = height_limit

                self.options_list_height = min(
                    self.options_list_height,
                    self.drop_down_menu_ui.expansion_height_limit)

                self.option_list_y_pos = self.base_position_rect.bottom

        if self.close_button_width > 0:
            close_button_x = (border_and_shadow +
                              self.base_position_rect.width -
                              self.close_button_width)

            self.close_button = UIButton(pygame.Rect(
                (close_button_x, border_and_shadow),
                (self.close_button_width, self.base_position_rect.height)),
                                         expand_button_symbol,
                                         self.ui_manager,
                                         self.ui_container,
                                         starting_height=2,
                                         parent_element=self.drop_down_menu_ui,
                                         object_id='#expand_button')
            self.drop_down_menu_ui.join_focus_sets(self.close_button)
            self.active_buttons.append(self.close_button)
        list_rect = pygame.Rect(self.drop_down_menu_ui.relative_rect.left,
                                self.option_list_y_pos,
                                (self.drop_down_menu_ui.relative_rect.width -
                                 self.close_button_width),
                                self.options_list_height)
        self.options_selection_list = UISelectionList(
            list_rect,
            starting_height=3,
            item_list=self.options_list,
            allow_double_clicks=False,
            manager=self.ui_manager,
            parent_element=self.drop_down_menu_ui,
            container=self.drop_down_menu_ui.ui_container,
            anchors=self.drop_down_menu_ui.anchors,
            object_id='#drop_down_options_list')
        self.drop_down_menu_ui.join_focus_sets(self.options_selection_list)

        if should_rebuild:
            self.rebuild()

    def _calculate_options_list_sizes(self, final_ids):
        try:
            list_shadow_width = int(self.ui_manager.get_theme().get_misc_data(
                'shadow_width', final_ids))
        except (LookupError, ValueError):
            list_shadow_width = 2
        try:
            list_border_width = int(self.ui_manager.get_theme().get_misc_data(
                'border_width', final_ids))
        except (LookupError, ValueError):
            list_border_width = 1
        try:
            list_item_height = int(self.ui_manager.get_theme().get_misc_data(
                'list_item_height', final_ids))
        except (LookupError, ValueError):
            list_item_height = 20
        options_list_border_and_shadow = list_shadow_width + list_border_width
        self.options_list_height = (
            (list_item_height * len(self.options_list)) +
            (2 * options_list_border_and_shadow))
        self.option_list_y_pos = 0

    def finish(self):
        """
        cleans everything up upon exiting the expanded menu state.
        """
        self.options_selection_list.kill()
        self.selected_option_button.kill()
        if self.close_button is not None:
            self.close_button.kill()

    def process_event(self, event: pygame.event.Event) -> bool:
        """
        Processes events for the closed state of the drop down.

        :param event: The event to process.

        :return: Return True if we want to consume this event so it is not passed on to the
                 rest of the UI.

        """
        if event.type == UI_BUTTON_PRESSED and event.ui_element in self.active_buttons:
            self.should_transition = True

        if (event.type == UI_SELECTION_LIST_NEW_SELECTION
                and event.ui_element == self.options_selection_list):
            selection = self.options_selection_list.get_single_selection()
            self.drop_down_menu_ui.selected_option = selection
            self.should_transition = True

            # old event - to be removed in 0.8.0
            event_data = {
                'user_type': OldType(UI_DROP_DOWN_MENU_CHANGED),
                'text': self.drop_down_menu_ui.selected_option,
                'ui_element': self.drop_down_menu_ui,
                'ui_object_id':
                self.drop_down_menu_ui.most_specific_combined_id
            }
            pygame.event.post(pygame.event.Event(pygame.USEREVENT, event_data))

            # new event
            event_data = {
                'text': self.drop_down_menu_ui.selected_option,
                'ui_element': self.drop_down_menu_ui,
                'ui_object_id':
                self.drop_down_menu_ui.most_specific_combined_id
            }
            pygame.event.post(
                pygame.event.Event(UI_DROP_DOWN_MENU_CHANGED, event_data))

        return False  # don't consume any events

    def update_position(self):
        """
        Update the position of all the button elements in the open drop down state.

        Used when the position of the  drop down has been altered directly, rather than when it
        has been moved as a consequence of it's container being moved.
        """

        # update the base position rect
        border_and_shadow = (self.drop_down_menu_ui.shadow_width +
                             self.drop_down_menu_ui.border_width)
        self.base_position_rect.x = self.drop_down_menu_ui.relative_rect.x + border_and_shadow
        self.base_position_rect.y = self.drop_down_menu_ui.relative_rect.y + border_and_shadow

        # update all the ui elements that depend on the base position
        self.selected_option_button.set_relative_position(
            (border_and_shadow, border_and_shadow))
        list_post = (self.drop_down_menu_ui.relative_rect.left,
                     self.option_list_y_pos)
        self.options_selection_list.set_relative_position(list_post)

        if self.close_button is not None:
            close_button_x = (border_and_shadow +
                              self.base_position_rect.width -
                              self.close_button_width)
            self.close_button.set_relative_position(
                [close_button_x, border_and_shadow])

    def update_dimensions(self):
        """
        Update the dimensions of all the button elements in the closed drop down state.

        Used when the dimensions of the drop down have been altered.
        """

        # update the base position rect
        border_and_shadow = (self.drop_down_menu_ui.shadow_width +
                             self.drop_down_menu_ui.border_width)
        self.base_position_rect.width = (
            self.drop_down_menu_ui.relative_rect.width -
            (2 * border_and_shadow))
        self.base_position_rect.height = (
            self.drop_down_menu_ui.relative_rect.height -
            (2 * border_and_shadow))

        if self.expand_direction is not None:
            if self.expand_direction == 'up':
                self.options_list_height = min(
                    self.options_list_height,
                    self.drop_down_menu_ui.expansion_height_limit)
                self.option_list_y_pos = self.base_position_rect.top - self.options_list_height

            elif self.expand_direction == 'down':
                self.options_list_height = min(
                    self.options_list_height,
                    self.drop_down_menu_ui.expansion_height_limit)
                self.option_list_y_pos = self.base_position_rect.bottom

        # update all the ui elements that depend on the base position rect
        self.selected_option_button.set_dimensions(
            (self.base_position_rect.width - self.close_button_width,
             self.base_position_rect.height))

        self.options_selection_list.set_dimensions(
            ((self.drop_down_menu_ui.relative_rect.width -
              self.close_button_width), self.options_list_height))

        list_pos = (self.drop_down_menu_ui.relative_rect.left,
                    self.option_list_y_pos)
        self.options_selection_list.set_relative_position(list_pos)
        if self.close_button is not None:
            close_button_x = (border_and_shadow +
                              self.base_position_rect.width -
                              self.close_button_width)
            self.close_button.set_dimensions(
                (self.close_button_width, self.base_position_rect.height))
            self.close_button.set_relative_position(
                (close_button_x, border_and_shadow))

    def on_fresh_drawable_shape_ready(self):
        """
        Called by an element's drawable shape when it has a new image surface ready for use,
        normally after a rebuilding/redrawing of some kind.

        In this case the result is to set the UI element's image to the new surface.
        """
        self.drop_down_menu_ui.set_image(
            self.drop_down_menu_ui.drawable_shape.get_fresh_surface())

    def hide(self):
        """
        Transition from expanded state to closed state.
        """
        self.should_transition = True
コード例 #4
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))
コード例 #5
0
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()
コード例 #6
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.
    """
    def __init__(self, relative_rect: pygame.Rect,
                 start_value: Union[float, int],
                 value_range: Tuple[Union[float, int], Union[float, int]],
                 manager: ui_manager.UIManager,
                 container: ui_container.UIContainer = None,
                 parent_element: UIElement = None,
                 object_id: Union[str, None] = None):

        new_element_ids, new_object_ids = self.create_valid_ids(parent_element=parent_element,
                                                                object_id=object_id,
                                                                element_id='horizontal_slider')
        super().__init__(relative_rect, manager, container,
                         object_ids=new_object_ids,
                         element_ids=new_element_ids,
                         starting_height=1,
                         layer_thickness=1)

        self.button_width = 20
        self.current_percentage = 0.5
        self.value_range = value_range
        self.grabbed_slider = False
        self.starting_grab_x_difference = 0
        self.has_moved_recently = False
        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.left_limit_position = 0.0

        self.border_width = None
        self.shadow_width = None
        self.border_colour = None
        self.background_colour = None
        self.background_rect = None
        self.sliding_button = 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.drawable_shape = None
        self.shape_type = 'rectangle'
        self.shape_corner_radius = None

        self.rebuild_from_changed_theme_data()

        # Things below here depend on theme data so need to be updated on a rebuild
        self.left_button = UIButton(pygame.Rect(self.background_rect.topleft,
                                                (self.button_width, self.background_rect.height)),
                                    '◀',
                                    self.ui_manager, self.ui_container, starting_height=2,
                                    parent_element=self,
                                    object_id='#left_button')
        self.right_button = UIButton(pygame.Rect((self.background_rect.x + self.background_rect.width -
                                                  self.button_width,
                                                  self.background_rect.y),
                                                 (self.button_width, self.background_rect.height)),
                                     '▶',
                                     self.ui_manager, self.ui_container, starting_height=2,
                                     parent_element=self,
                                     object_id='#right_button')

        sliding_x_pos = int(self.background_rect.x + self.background_rect.width/2 - self.button_width/2)
        self.sliding_button = UIButton(pygame.Rect((sliding_x_pos,
                                                    self.background_rect.y),
                                                   (self.button_width, self.background_rect.height)),
                                       '', self.ui_manager, self.ui_container, starting_height=2,
                                       parent_element=self,
                                       object_id='#sliding_button')

        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.

        """
        relative_background_rect = pygame.Rect((self.border_width + self.shadow_width,
                                                self.border_width + self.shadow_width),
                                               (self.rect.width - (2 * self.shadow_width) - (2 * self.border_width),
                                                self.rect.height - (2 * self.shadow_width) - (2 * self.border_width)))

        self.background_rect = pygame.Rect((relative_background_rect.x + self.relative_rect.x,
                                            relative_background_rect.y + self.relative_rect.y),
                                           relative_background_rect.size)

        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.image = self.drawable_shape.get_surface('normal')

        # Things below here depend on theme data so need to be updated on a rebuild
        self.scrollable_width = self.background_rect.width - (3 * self.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.x + self.background_rect.width / 2 - self.button_width / 2)
            self.sliding_button.set_relative_position((sliding_x_pos, self.background_rect.y))
            self.sliding_button.set_dimensions((self.button_width, self.background_rect.height))
            self.sliding_button.set_hold_range((self.background_rect.width, 100))
            self.set_current_value(self.current_value)

        if self.left_button is not None:
            self.left_button.set_relative_position(self.background_rect.topleft),
            self.left_button.set_dimensions((self.button_width, self.background_rect.height))

        if self.right_button is not None:
            self.right_button.set_relative_position((self.background_rect.x + self.background_rect.width -
                                                     self.button_width,
                                                     self.background_rect.y)),
            self.right_button.set_dimensions((self.button_width, self.background_rect.height))

    def kill(self):
        """
        Overrides the normal sprite kill() method to also kill the button elements that help make up the slider.

        """
        self.left_button.kill()
        self.right_button.kill()
        self.sliding_button.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.
        """
        if self.alive():
            moved_this_frame = False
            if 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.rect.x + self.shadow_width + self.border_width + self.button_width
                y_pos = self.rect.y + self.shadow_width + self.border_width
                self.sliding_button.set_position(pygame.math.Vector2(x_pos, y_pos))
                moved_this_frame = True
            elif 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.rect.x + self.shadow_width + self.border_width + self.button_width
                y_pos = self.rect.y + self.shadow_width + self.border_width
                self.sliding_button.set_position(pygame.math.Vector2(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.scroll_position + self.rect.x +
                                       self.shadow_width + self.border_width + self.button_width)
                    self.starting_grab_x_difference = mouse_x - real_scroll_pos

                real_scroll_pos = (self.scroll_position + self.rect.x +
                                   self.shadow_width + self.border_width + self.button_width)
                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.rect.x + self.shadow_width + self.border_width + self.button_width
                y_pos = self.rect.y + self.shadow_width + self.border_width
                self.sliding_button.set_position(pygame.math.Vector2(x_pos, y_pos))

                moved_this_frame = True
            elif not self.sliding_button.held:
                self.grabbed_slider = False

            if moved_this_frame:
                self.current_value = self.value_range[0] + (
                        (self.scroll_position / self.scrollable_width) * (self.value_range[1] - self.value_range[0]))
                if not self.has_moved_recently:
                    self.has_moved_recently = True

    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
        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:
                percentage = (self.current_value - self.value_range[0])/value_range_size
                self.scroll_position = self.scrollable_width * percentage

                x_pos = self.scroll_position + self.rect.x + self.shadow_width + self.border_width + self.button_width
                y_pos = self.rect.y + self.shadow_width + self.border_width
                self.sliding_button.set_position(pygame.math.Vector2(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:
            if 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

        corner_radius = 2
        shape_corner_radius_string = self.ui_theme.get_misc_data(self.object_ids,
                                                                 self.element_ids, 'shape_corner_radius')
        if shape_corner_radius_string is not None:
            try:
                corner_radius = int(shape_corner_radius_string)
            except ValueError:
                corner_radius = 2
        if corner_radius != self.shape_corner_radius:
            self.shape_corner_radius = corner_radius
            has_any_changed = True

        border_width = 1
        border_width_string = self.ui_theme.get_misc_data(self.object_ids, self.element_ids, 'border_width')
        if border_width_string is not None:
            try:
                border_width = int(border_width_string)
            except ValueError:
                border_width = 1

        if border_width != self.border_width:
            self.border_width = border_width
            has_any_changed = True

        shadow_width = 2
        shadow_width_string = self.ui_theme.get_misc_data(self.object_ids, self.element_ids, 'shadow_width')
        if shadow_width_string is not None:
            try:
                shadow_width = int(shadow_width_string)
            except ValueError:
                shadow_width = 2
        if shadow_width != self.shadow_width:
            self.shadow_width = shadow_width
            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

        if has_any_changed:
            self.rebuild()
コード例 #7
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()
コード例 #8
0
class UIMessageWindow(UIWindow):
    """
    A simple popup window for delivering text-only messages to users.

    :param message_window_rect: The size and position of the window, includes the menu bar across the top.
    :param message_title: The title of the message window.
    :param html_message: The message itself. Can make use of HTML (a subset of) to style the text.
    :param manager: The UIManager that manages this UIElement.
    :param object_id: A custom defined ID for fine tuning of theming.
    """
    def __init__(self,
                 message_window_rect: pygame.Rect,
                 message_title: str,
                 html_message: str,
                 manager: ui_manager.UIManager,
                 object_id: Union[str, None] = None):

        new_element_ids, new_object_ids = self.create_valid_ids(
            parent_element=None,
            object_id=object_id,
            element_id='message_window')
        super().__init__(message_window_rect, manager, new_element_ids,
                         new_object_ids)

        self.done_button_vertical_start = 30
        self.done_button_vertical_space = 40
        self.menu_bar_height = 20
        self.close_button_width = 20
        self.grabbed_window = False
        self.starting_grab_difference = (0, 0)

        self.shadow_width = None  # type: Union[None, int]
        self.border_width = None  # type: Union[None, int]
        self.background_colour = None
        self.border_colour = None

        self.border_rect = None
        self.background_rect = None
        self.text_block_rect = None  # type: Union[None, pygame.Rect]

        self.menu_bar = None
        self.close_window_button = None
        self.dismiss_button = None
        self.text_block = None

        self.drawable_shape = None
        self.shape_type = 'rectangle'
        self.shape_corner_radius = None

        self.rebuild_from_changed_theme_data()

        self.menu_bar = UIButton(relative_rect=pygame.Rect(
            (0, 0), ((self.rect.width - (self.shadow_width * 2)) -
                     self.close_button_width, self.menu_bar_height)),
                                 text=message_title,
                                 manager=manager,
                                 container=self.get_container(),
                                 parent_element=self,
                                 object_id='#message_window_title_bar')
        self.menu_bar.set_hold_range((100, 100))

        self.close_window_button = UIButton(relative_rect=pygame.Rect(
            ((self.rect.width - self.shadow_width * 2) -
             self.close_button_width, 0),
            (self.close_button_width, self.menu_bar_height)),
                                            text='╳',
                                            manager=manager,
                                            container=self.get_container(),
                                            parent_element=self,
                                            object_id='#close_button')

        self.dismiss_button = UIButton(
            relative_rect=pygame.Rect(
                (int(self.rect.width / 2) + 45,
                 (self.border_rect.height - self.done_button_vertical_start)),
                (70, 20)),
            text="Dismiss",
            manager=manager,
            container=self.get_container(),
            tool_tip_text="<font face=fira_code color=normal_text size=2>"
            "Click to get rid of this message.</font>",
            parent_element=self,
            object_id='#dismiss_button')

        self.text_block = UITextBox(html_message,
                                    self.text_block_rect,
                                    manager=manager,
                                    container=self.get_container(),
                                    parent_element=self)

    def rebuild(self):
        """

        """
        border_rect_width = self.rect.width - (self.shadow_width * 2)
        border_rect_height = self.rect.height - (self.shadow_width * 2)
        self.border_rect = pygame.Rect((self.shadow_width, self.shadow_width),
                                       (border_rect_width, border_rect_height))

        background_rect_width = border_rect_width - (self.border_width * 2)
        background_rect_height = border_rect_height - (self.border_width * 2)
        self.background_rect = pygame.Rect(
            (self.shadow_width + self.border_width,
             self.shadow_width + self.border_width),
            (background_rect_width, background_rect_height))

        self.text_block_rect = pygame.Rect(
            (self.border_width, self.menu_bar_height),
            (self.border_rect.width - self.border_width,
             (self.border_rect.height - self.menu_bar_height -
              self.done_button_vertical_space)))

        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.image = self.drawable_shape.get_surface('normal')

        self.get_container(
        ).relative_rect.width = self.rect.width - self.shadow_width * 2
        self.get_container(
        ).relative_rect.height = self.rect.height - self.shadow_width * 2
        self.get_container(
        ).relative_rect.x = self.relative_rect.x + self.shadow_width
        self.get_container(
        ).relative_rect.y = self.relative_rect.y + self.shadow_width
        self.get_container().update_containing_rect_position()

        if self.menu_bar is not None:
            self.menu_bar.set_dimensions(
                ((self.rect.width - (self.shadow_width * 2)) -
                 self.close_button_width, self.menu_bar_height))
        if self.close_window_button is not None:
            self.close_window_button.set_relative_position(
                ((self.rect.width - self.shadow_width * 2) -
                 self.close_button_width, 0))
        if self.dismiss_button is not None:
            self.dismiss_button.set_relative_position(
                ((self.rect.width / 2) + 45,
                 (self.border_rect.height - self.done_button_vertical_start)))
        if self.text_block is not None:
            self.text_block.set_relative_position(self.text_block_rect.topleft)
            self.text_block.set_dimensions(self.text_block_rect.size)

    def update(self, time_delta: float):
        """
        Called every update loop of our UI manager. Handles moving and closing the window.

        :param time_delta: The time in seconds between calls to this function.
        """
        if self.alive():

            if self.dismiss_button.check_pressed():
                self.kill()

            if self.menu_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.rect.x += adjustment_required[0]
                self.rect.y += adjustment_required[1]
                self.relative_rect.x = self.rect.x - self.ui_container.rect.x
                self.relative_rect.y = self.rect.y - self.ui_container.rect.y
                self.get_container().relative_rect.x += adjustment_required[0]
                self.get_container().relative_rect.y += adjustment_required[1]
                self.get_container().update_containing_rect_position()

            else:
                self.grabbed_window = False

            if self.close_window_button.check_pressed():
                self.kill()

        super().update(time_delta)

    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:
            if 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

        corner_radius = 2
        shape_corner_radius_string = self.ui_theme.get_misc_data(
            self.object_ids, self.element_ids, 'shape_corner_radius')
        if shape_corner_radius_string is not None:
            try:
                corner_radius = int(shape_corner_radius_string)
            except ValueError:
                corner_radius = 2
        if corner_radius != self.shape_corner_radius:
            self.shape_corner_radius = corner_radius
            has_any_changed = True

        border_width = 1
        border_width_string = self.ui_theme.get_misc_data(
            self.object_ids, self.element_ids, 'border_width')
        if border_width_string is not None:
            try:
                border_width = int(border_width_string)
            except ValueError:
                border_width = 1

        if border_width != self.border_width:
            self.border_width = border_width
            has_any_changed = True

        shadow_width = 2
        shadow_width_string = self.ui_theme.get_misc_data(
            self.object_ids, self.element_ids, 'shadow_width')
        if shadow_width_string is not None:
            try:
                shadow_width = int(shadow_width_string)
            except ValueError:
                shadow_width = 2
        if shadow_width != self.shadow_width:
            self.shadow_width = shadow_width
            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

        if has_any_changed:
            self.rebuild()
コード例 #9
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)