Source code for pygame_menu.widgets.widget.scrollbar

# coding=utf-8
"""
pygame-menu
https://github.com/ppizarror/pygame-menu

SCROLLBAR
ScrollBar class, manage the selection in a range of values.

License:
-------------------------------------------------------------------------------
The MIT License (MIT)
Copyright 2017-2020 Pablo Pizarro R. @ppizarror

Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-------------------------------------------------------------------------------
"""

import pygame
import pygame_menu.locals as _locals
from pygame_menu.utils import make_surface, assert_orientation
from pygame_menu.widgets.core import Widget


[docs]class ScrollBar(Widget): """ A scroll bar include 3 separate controls: a slider, scroll arrows, and a page control: a. The slider provides a way to quickly go to any part of the document b. The scroll arrows are push buttons which can be used to accurately navigate to a particular place in a document. c. The page control is the area over which the slider is dragged (the scroll bar's background). Clicking here moves the scroll bar towards the click by one "page". .. warning:: Arrows are not yet implemented :param length: Length of the page control :type length: int :param values_range: Min and max values :type values_range: tuple, list :param scrollbar_id: Bar identifier :type scrollbar_id: str :param orientation: Bar orientation ORIENTATION_HORIZONTAL/ORIENTATION_VERTICAL :type orientation: str :param slider_pad: Space between slider and page control :type slider_pad: int, float :param slider_color: Color of the slider :type slider_color: tuple, list :param page_ctrl_thick: Page control thickness :type page_ctrl_thick: int, float :param page_ctrl_color: Page control color :type page_ctrl_color: tuple, list :param onchange: Callback when changing the selector :type onchange: callable, None :param onreturn: Callback when pressing return button :type onreturn: callable, None """ def __init__(self, length, values_range, scrollbar_id='', orientation=_locals.ORIENTATION_HORIZONTAL, slider_pad=0, slider_color=(200, 200, 200), page_ctrl_thick=20, page_ctrl_color=(235, 235, 235), onchange=None, onreturn=None, *args, **kwargs): assert isinstance(length, (int, float)) assert isinstance(values_range, (tuple, list)) assert values_range[1] > values_range[0], 'minimum value first is expected' assert isinstance(slider_pad, (int, float)) assert isinstance(page_ctrl_thick, (int, float)) assert page_ctrl_thick - 2 * slider_pad >= 2, 'slider shall be visible' super(ScrollBar, self).__init__(widget_id=scrollbar_id, onchange=onchange, onreturn=onreturn, args=args, kwargs=kwargs) self._values_range = list(values_range) self.scrolling = False # type: bool self._orientation = 0 # type: int self._opp_orientation = int(not self._orientation) # type: int self._page_ctrl_length = length self._page_ctrl_thick = page_ctrl_thick self._page_ctrl_color = page_ctrl_color self._slider_rect = None # type: (pygame.Rect,None) self._slider_pad = slider_pad self._slider_color = slider_color self._slider_position = 0 # type: int self._single_step = 20 # type: int self._page_step = None # type: (int,None) if values_range[1] - values_range[0] > length: self.set_page_step(length) else: self.set_page_step((values_range[1] - values_range[0]) / 5.0) # Arbitrary self.set_orientation(orientation) def _apply_font(self): pass def _apply_size_changes(self): """ Apply scrollbar changes. :return: None """ dims = ('width', 'height') setattr(self._rect, dims[self._orientation], self._page_ctrl_length) setattr(self._rect, dims[self._opp_orientation], self._page_ctrl_thick) self._slider_rect = pygame.Rect(0, 0, self._rect.width, self._rect.height) setattr(self._slider_rect, dims[self._orientation], self._page_step) setattr(self._slider_rect, dims[self._opp_orientation], self._page_ctrl_thick) # Update slider position according to the current one pos = ('x', 'y') setattr(self._slider_rect, pos[self._orientation], self._slider_position) self._slider_rect = self._slider_rect.inflate(-2 * self._slider_pad, -2 * self._slider_pad) # noinspection PyMissingOrEmptyDocstring
[docs] def draw(self, surface): self._render() self._fill_background_color(surface) surface.blit(self._surface, self._rect.topleft)
[docs] def get_maximum(self): """ :return: Return the greatest acceptable value :rtype: int """ return self._values_range[1]
[docs] def get_minimum(self): """ :return: Return the smallest acceptable value :rtype: int """ return self._values_range[0]
[docs] def get_orientation(self): """ :return: Return the scroll bar orientation :rtype: str """ if self._orientation == 0: return _locals.ORIENTATION_HORIZONTAL else: return _locals.ORIENTATION_VERTICAL
[docs] def get_page_step(self): """ :return: Return amount that the value changes by when the user click on the page control surface :rtype: int """ return self._page_step * (self._values_range[1] - self._values_range[0]) / \ self._page_ctrl_length
[docs] def get_value(self): """ Return the value according to the slider position. :return: Position in pixels :rtype: int """ value = self._values_range[0] + self._slider_position * \ (self._values_range[1] - self._values_range[0]) / (self._page_ctrl_length - self._page_step) # Correction due to value scaling value = max(self._values_range[0], value) value = min(self._values_range[1], value) return value
def _render(self): if not self._render_hash_changed(self._rect.size, self._slider_rect.x, self._slider_rect.y, self._slider_rect.width, self._slider_rect.height): return self._surface = make_surface(*self._rect.size) self._surface.fill(self._page_ctrl_color) # Render slider if self._shadow: lit_rect = pygame.Rect(self._slider_rect) slider_rect = lit_rect.inflate(-self._shadow_offset * 2, -self._shadow_offset * 2) shadow_rect = lit_rect.inflate(-self._shadow_offset, -self._shadow_offset) shadow_rect = shadow_rect.move(self._shadow_tuple[0] / 2, self._shadow_tuple[1] / 2) pygame.draw.rect(self._surface, self._font_selected_color, lit_rect) pygame.draw.rect(self._surface, self._shadow_color, shadow_rect) pygame.draw.rect(self._surface, self._slider_color, slider_rect) else: pygame.draw.rect(self._surface, self._slider_color, self._slider_rect) def _scroll(self, pixels): """ Moves the slider based on mouse events relative to change along axis. The slider travel is limited to page control length. :param pixels: Number of pixels to scroll :type pixels: int :return: True is scroll position has changed :rtype: bool """ if not pixels: return False axis = self._orientation space_before = self._rect.topleft[axis] - \ self._slider_rect.move(*self._rect.topleft).topleft[axis] + self._slider_pad move = max(round(pixels), space_before) space_after = self._rect.bottomright[axis] - \ self._slider_rect.move(*self._rect.topleft).bottomright[axis] - self._slider_pad move = min(move, space_after) if not move: return False move_pos = [0, 0] move_pos[axis] = move self._slider_rect.move_ip(*move_pos) self._slider_position += move return True
[docs] def set_length(self, value): """ Set the length of the page control area. :param value: Length of the area :type value: int, float :return: None """ assert isinstance(value, (int, float)) assert 0 < value self._page_ctrl_length = value self._slider_position = min(self._slider_position, self._page_ctrl_length - self._page_step) self._apply_size_changes()
[docs] def set_maximum(self, value): """ Set the greatest acceptable value. :param value: Maximum value :type value: int, float :return: None """ assert isinstance(value, (int, float)) assert value > self._values_range[0], 'Maximum value shall greater than {}'.format(self._values_range[0]) self._values_range[1] = value
[docs] def set_minimum(self, value): """ Set the smallest acceptable value. :param value: Minimum value :type value: int, float :return: None """ assert isinstance(value, (int, float)) assert 0 <= value < self._values_range[1], "Minimum value shall lower than {}".format(self._values_range[1]) self._values_range[0] = value
[docs] def set_orientation(self, orientation): """ Set the scroll bar orientation to vertical or horizontal. :param orientation: Widget orientation, could be ORIENTATION_HORIZONTAL/ORIENTATION_VERTICAL :type orientation: str :return: None """ assert_orientation(orientation) if orientation == _locals.ORIENTATION_HORIZONTAL: self._orientation = 0 elif orientation == _locals.ORIENTATION_VERTICAL: self._orientation = 1 self._opp_orientation = int(not self._orientation) self._apply_size_changes()
[docs] def set_page_step(self, value): """ Set the amount that the value changes by when the user click on the page control surface. The length of the slider is related to this value, and typically represents the proportion of the document area shown in a scrolling view. :param value: Page step :type value: int :return: None """ assert 0 < value, 'Page step shall be > 0' # Slider length shall represent the same ratio self._page_step = round(1.0 * self._page_ctrl_length * value / (self._values_range[1] - self._values_range[0])) if self._single_step >= self._page_step: self._single_step = self._page_step // 2 # Arbitrary to be lower than page step self._apply_size_changes()
[docs] def set_value(self, value): """ Set the position of the scrollbar. :param value: Position :type value: int, float :return: None """ assert self._values_range[0] <= value <= self._values_range[1], \ '{} < {} < {}'.format(self._values_range[0], value, self._values_range[1]) pixels = 1.0 * (value - self._values_range[0]) * (self._page_ctrl_length - self._page_step) / \ (self._values_range[1] - self._values_range[0]) # Correction due to value scaling pixels = max(0, pixels) pixels = min(self._page_ctrl_length - self._page_step, pixels) self._scroll(pixels - self._slider_position)
# noinspection PyMissingOrEmptyDocstring
[docs] def update(self, events): updated = False for event in events: # type: pygame.event.Event if event.type == pygame.KEYDOWN and self._orientation == 1 \ and event.key in (pygame.K_PAGEUP, pygame.K_PAGEDOWN): direction = 1 if event.key == pygame.K_PAGEDOWN else -1 if self._scroll(direction * self._page_step): self.change() updated = True elif self.mouse_enabled and event.type is pygame.MOUSEMOTION and self.scrolling: if self._scroll(event.rel[self._orientation]): self.change() updated = True elif self.mouse_enabled and event.type is pygame.MOUSEBUTTONDOWN: if event.button in (4, 5) and self._orientation == 1: # Vertical bar: scroll down (4) or up (5) direction = -1 if event.button == 4 else 1 if self._scroll(direction * self._single_step): self.change() updated = True else: # The _slider_rect origin is related to the widget surface if self._slider_rect.move(*self._rect.topleft).collidepoint(event.pos): # Initialize scrolling self.scrolling = True elif self._rect.collidepoint(*event.pos): # Moves towards the click by one "page" (= slider length without pad) srect = self._slider_rect.move(*self._rect.topleft) pos = (srect.x, srect.y) direction = 1 if event.pos[self._orientation] > pos[self._orientation] else -1 if self._scroll(direction * self._page_step): self.change() updated = True elif self.mouse_enabled and event.type is pygame.MOUSEBUTTONUP: if self.scrolling: self.scrolling = False updated = True return updated