def test_enable(self, _init_pygame: None, default_ui_manager: UIManager, _display_surface_return_none: None): button = UIButton(relative_rect=pygame.Rect(10, 10, 150, 30), text="Test Button", tool_tip_text="This is a test of the button's tool tip functionality.", manager=default_ui_manager) button.disable() button.enable() # process a mouse button down event button.process_event(pygame.event.Event(pygame.MOUSEBUTTONDOWN, {'button': 1, 'pos': (50, 25)})) # process a mouse button up event button.process_event(pygame.event.Event(pygame.MOUSEBUTTONUP, {'button': 1, 'pos': (50, 25)})) button.update(0.01) assert button.check_pressed() is True and button.is_enabled is True
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()