"""
pygame-menu
https://github.com/ppizarror/pygame-menu
DROPSELECT
Drop select widget. This is similar to HTML selects, it can contain many items
(options) to select. The selection is unique.
"""
from __future__ import annotations
__all__ = ["DropSelect", "DropSelectManager"]
import math
from abc import ABC
from typing import TYPE_CHECKING, Any
import pygame
from pygame_menu._types import (
CallbackType,
ColorInputType,
ColorType,
CursorInputType,
CursorType,
EventVectorType,
NumberInstance,
NumberType,
PaddingInstance,
PaddingType,
Tuple2IntType,
Tuple2NumberType,
Tuple3IntType,
Tuple4IntType,
)
from pygame_menu.font import FontType, assert_font, get_font
from pygame_menu.locals import (
FINGERDOWN,
FINGERUP,
ORIENTATION_VERTICAL,
POSITION_NORTHWEST,
POSITION_SOUTHEAST,
)
from pygame_menu.utils import (
assert_color,
assert_cursor,
assert_position,
assert_vector,
check_key_pressed_valid,
get_finger_pos,
make_surface,
parse_padding,
uuid4,
)
from pygame_menu.widgets.core.widget import (
AbstractWidgetManager,
Widget,
WidgetTransformationNotImplemented,
)
from pygame_menu.widgets.widget.button import Button
from pygame_menu.widgets.widget.frame import Frame
from pygame_menu.widgets.widget.selector import check_selector_items
if TYPE_CHECKING:
from collections.abc import Callable
import pygame_menu
# noinspection PyProtectedMember
[docs]
class DropSelect(Widget):
"""
Drop select is a selector within a Frame. This drops a vertical frame if
requested. Drop select can contain selectable items (options), only 1 can be
selected.
The items of the DropSelect are:
.. code-block:: python
items = [('Item1', a, b, c...), ('Item2', d, e, f...)]
The callbacks receive the current selected item, its index in the list, the
associated arguments, and all unknown keyword arguments, where
``selected_item=widget.get_value()`` and ``selected_index=widget.get_index()``:
.. code-block:: python
onchange((selected_item, selected_index), a, b, c..., **kwargs)
onreturn((selected_item, selected_index), a, b, c..., **kwargs)
For example, if ``selected_index=0`` then ``selected_item=('Item1', a, b, c...)``.
.. note::
DropSelect only accepts translation transformation.
:param title: Drop select title
:param items: Items of the drop select
:param dropselect_id: ID of the drop select
:param default: Index of default item to display. If ``None`` no item is selected
:param onchange: Callback when changing the drop select item
:param onreturn: Callback when pressing return (apply) on the selected item
:param onselect: Function when selecting the widget
:param open_middle: If ``True`` the selection box is opened in the middle of the menu
:param placeholder: Text shown if no option is selected yet
:param placeholder_add_to_selection_box: If ``True`` adds the placeholder button to the selection box
:param scrollbar_color: Scrollbar color
:param scrollbar_cursor: Cursor of the scrollbars if mouse is placed over. By default, is ``None``
:param scrollbar_shadow: Indicate if a shadow is drawn on each scrollbar
:param scrollbar_shadow_color: Color of the shadow of each scrollbar
:param scrollbar_shadow_offset: Offset of the scrollbar shadow in px
:param scrollbar_shadow_position: Position of the scrollbar shadow. See :py:mod:`pygame_menu.locals`
:param scrollbar_slider_color: Color of the sliders
:param scrollbar_slider_hover_color: Color of the slider if hovered or clicked
:param scrollbar_slider_pad: Space between slider and scrollbars borders in px
:param scrollbar_thick: Scrollbar thickness in px
:param scrollbars: Scrollbar position. See :py:mod:`pygame_menu.locals`
:param selection_box_arrow_color: Selection box arrow color
:param selection_box_arrow_margin: Selection box arrow margin (left, right, vertical) in px
:param selection_box_bgcolor: Selection box background color
:param selection_box_border_color: Selection box border color
:param selection_box_border_width: Selection box border width
:param selection_box_height: Selection box height, counted as how many options are packed before showing scroll
:param selection_box_inflate: Selection box inflate on x-axis and y-axis (x, y) in px
:param selection_box_margin: Selection box on x-axis and y-axis (x, y) margin from title in px
:param selection_box_text_margin: Selection box text margin (left) in px
:param selection_box_width: Selection box width in px. If ``0`` compute automatically to fit placeholder
:param selection_infinite: If ``True`` selection can rotate through bottom/top
:param selection_option_border_color: Option border color
:param selection_option_border_width: Option border width
:param selection_option_cursor: Option cursor. If ``None`` use the same cursor as the widget
:param selection_option_font: Option font. If ``None`` use the same font as the widget
:param selection_option_font_color: Option font color
:param selection_option_font_size: Option font size. If ``None`` use the 100% of the widget font size
:param selection_option_padding: Selection padding. See padding styling
:param selection_option_selected_bgcolor: Selected option background color
:param selection_option_selected_font_color: Selected option font color
:param kwargs: Optional keyword arguments
"""
_drop_frame: Frame | None
_index: int
_items: list[tuple[Any, ...]] | list[str]
_open_bottom: bool
_open_middle: bool
_opened: bool
_option_buttons: list[Button]
_option_font: pygame.font.Font | None
_placeholder: str
_placeholder_add_to_selection_box: bool
_scrollbar_color: ColorType
_scrollbar_cursor: CursorType # type: ignore
_scrollbar_shadow: bool
_scrollbar_shadow_color: ColorType
_scrollbar_shadow_offset: int
_scrollbar_shadow_position: str
_scrollbar_slider_color: ColorType
_scrollbar_slider_hover_color: ColorType
_scrollbar_slider_pad: NumberType
_scrollbar_thick: int
_scrollbars: str
_selection_box_arrow_color: ColorType
_selection_box_arrow_margin: Tuple3IntType
_selection_box_bgcolor: ColorType
_selection_box_border_color: ColorType
_selection_box_border_width: int
_selection_box_height: int
_selection_box_inflate: Tuple2IntType
_selection_box_margin: Tuple2IntType
_selection_box_text_margin: int
_selection_box_width: int
_selection_infinite: bool
_selection_option_border_color: ColorType
_selection_option_border_width: int
_selection_option_cursor: CursorType # type: ignore
_selection_option_font_style: dict[str, Any]
_selection_option_left_space: bool
_selection_option_left_space_height_factor: float
_selection_option_left_space_margin: Tuple3IntType
_selection_option_padding: Tuple4IntType
_selection_option_selected_bgcolor: ColorType
_title_size: Tuple2IntType
def __init__(
self,
title: Any,
items: list[tuple[Any, ...]] | list[str],
close_on_apply: bool = True,
dropselect_id: str = "",
default: int | None = None,
onchange: CallbackType = None,
onreturn: CallbackType = None,
onselect: CallbackType = None,
open_middle: bool = False,
placeholder: str = "Select an option",
placeholder_add_to_selection_box: bool = True,
scrollbar_color: ColorInputType = (235, 235, 235),
scrollbar_cursor: CursorInputType = None, # type: ignore
scrollbar_shadow: bool = False,
scrollbar_shadow_color: ColorInputType = (0, 0, 0),
scrollbar_shadow_offset: int = 2,
scrollbar_shadow_position: str = POSITION_NORTHWEST,
scrollbar_slider_color: ColorInputType = (200, 200, 200),
scrollbar_slider_hover_color: ColorInputType = (170, 170, 170),
scrollbar_slider_pad: NumberType = 0,
scrollbar_thick: int = 20,
scrollbars: str = POSITION_SOUTHEAST,
selection_box_arrow_color: ColorInputType = (150, 150, 150),
selection_box_arrow_margin: Tuple3IntType = (5, 5, 0),
selection_box_bgcolor: ColorInputType = (255, 255, 255),
selection_box_border_color: ColorInputType = (150, 150, 150),
selection_box_border_width: int = 1,
selection_box_height: int = 3,
selection_box_inflate: Tuple2IntType = (0, 0),
selection_box_margin: Tuple2NumberType = (25, 0),
selection_box_text_margin: int = 5,
selection_box_width: int = 0,
selection_infinite: bool = False,
selection_option_border_color: ColorInputType = (220, 220, 220),
selection_option_border_width: int = 1,
selection_option_cursor: CursorInputType = None, # type: ignore
selection_option_font: FontType | None = None,
selection_option_font_color: ColorInputType = (0, 0, 0),
selection_option_font_size: int | None = None,
selection_option_left_space: bool = False,
selection_option_left_space_height_factor: float = 1.0,
selection_option_padding: PaddingType = 5,
selection_option_selected_bgcolor: ColorInputType = (188, 227, 244),
selection_option_selected_font_color: ColorInputType = (0, 0, 0),
*args,
**kwargs,
) -> None:
assert isinstance(default, (int, type(None)))
assert isinstance(dropselect_id, str)
assert isinstance(items, list)
assert isinstance(open_middle, bool)
assert isinstance(placeholder, str)
assert isinstance(placeholder_add_to_selection_box, bool)
# Check items list
check_selector_items(items)
if default is None:
default = -1
assert default >= -1, "default position must be equal or greater than zero"
assert default < len(items), (
"default position should be lower than number of values"
)
assert isinstance(dropselect_id, str), "id must be a string"
assert isinstance(default, int), "default must be an integer"
# Check styling
assert isinstance(scrollbar_shadow, bool)
assert isinstance(scrollbar_shadow_offset, int) and scrollbar_shadow_offset >= 0
assert (
isinstance(scrollbar_slider_pad, NumberInstance)
and scrollbar_slider_pad >= 0
)
assert isinstance(scrollbar_thick, int) and scrollbar_thick >= 0
assert (
isinstance(selection_box_border_width, int)
and selection_box_border_width >= 0
)
assert isinstance(selection_box_height, int) and selection_box_height >= 1
assert (
isinstance(selection_box_text_margin, int)
and selection_box_text_margin >= 0
)
assert isinstance(selection_box_width, int) and selection_box_width >= 0
assert isinstance(selection_infinite, bool)
assert (
isinstance(selection_option_border_width, int)
and selection_option_border_width >= 0
)
assert isinstance(selection_option_padding, PaddingInstance)
assert_cursor(scrollbar_cursor)
assert_cursor(selection_option_cursor)
assert_position(scrollbar_shadow_position)
assert_position(scrollbars)
assert_vector(selection_box_arrow_margin, 3, int)
assert_vector(selection_box_inflate, 2, int)
assert_vector(selection_box_margin, 2)
scrollbar_color = assert_color(scrollbar_color)
scrollbar_shadow_color = assert_color(scrollbar_shadow_color)
scrollbar_slider_color = assert_color(scrollbar_slider_color)
scrollbar_slider_hover_color = assert_color(scrollbar_slider_hover_color)
selection_box_arrow_color = assert_color(selection_box_arrow_color)
selection_box_bgcolor = assert_color(selection_box_bgcolor)
selection_box_border_color = assert_color(selection_box_border_color)
selection_option_border_color = assert_color(selection_option_border_color)
selection_option_font_color = assert_color(selection_option_font_color)
selection_option_selected_bgcolor = assert_color(
selection_option_selected_bgcolor
)
selection_option_selected_font_color = assert_color(
selection_option_selected_font_color
)
if selection_option_font is not None:
assert_font(selection_option_font)
if selection_option_font_size is not None:
assert (
isinstance(selection_option_font_size, int)
and selection_option_font_size > 0
)
super().__init__(
onchange=onchange,
onreturn=onreturn,
onselect=onselect,
title=title,
widget_id=dropselect_id,
args=args,
kwargs=kwargs,
accept_events=True,
selection_effect_draw_post=False,
)
self._close_on_apply = close_on_apply
self._default_value = default
self._drop_frame = None
self._index = default
self._items = items.copy()
self._open_bottom = True
self._open_middle = open_middle
self._placeholder = placeholder
self._placeholder_add_to_selection_box = placeholder_add_to_selection_box
self._title_size = (0, 0)
# If True adds a space equals to the height of the option at left, used for
# drawing some options (for example, ticks, boxes, etc.)
self._selection_option_left_space = selection_option_left_space
self._selection_option_left_space_height_factor = (
selection_option_left_space_height_factor
)
self._selection_option_left_space_margin = (0, 0, 0) # left, right, top
# Style
self._option_font = None
self._scrollbar_color = scrollbar_color
self._scrollbar_cursor = scrollbar_cursor
self._scrollbar_shadow = scrollbar_shadow
self._scrollbar_shadow_color = scrollbar_shadow_color
self._scrollbar_shadow_offset = scrollbar_shadow_offset
self._scrollbar_shadow_position = scrollbar_shadow_position
self._scrollbar_slider_color = scrollbar_slider_color
self._scrollbar_slider_hover_color = scrollbar_slider_hover_color
self._scrollbar_slider_pad = scrollbar_slider_pad
self._scrollbar_thick = scrollbar_thick
self._scrollbars = scrollbars
self._selection_box_arrow_color = selection_box_arrow_color
self._selection_box_arrow_margin = selection_box_arrow_margin
self._selection_box_bgcolor = selection_box_bgcolor
self._selection_box_border_color = selection_box_border_color
self._selection_box_border_width = selection_box_border_width
self._selection_box_height = selection_box_height
self._selection_box_inflate = selection_box_inflate
self._selection_box_margin = (
int(selection_box_margin[0]),
int(selection_box_margin[1]),
)
self._selection_box_text_margin = selection_box_text_margin
self._selection_box_width = selection_box_width
self._selection_infinite = selection_infinite
self._selection_option_border_color = selection_option_border_color
self._selection_option_border_width = selection_option_border_width
self._selection_option_cursor = selection_option_cursor
self._selection_option_padding = parse_padding(selection_option_padding)
self._selection_option_selected_bgcolor = selection_option_selected_bgcolor
self._selection_option_font_style = {
"color": selection_option_font_color,
"color_selected": selection_option_selected_font_color,
"name": selection_option_font,
"size": selection_option_font_size,
}
[docs]
def set_default_value(self, index: int) -> DropSelect:
self._default_value = index
return self
def _apply_font(self) -> None:
# Compute title size
title_render = self._font.size(self._title)
w = int(
title_render[0]
+ self._selection_box_margin[0]
- self._selection_box_inflate[0] / 2
)
h = int(
title_render[1]
+ self._selection_box_inflate[1] / 2
- self._selection_box_border_width
)
self._title_size = (w, h)
# Load option font
if self._selection_option_font_style["name"] is None:
self._selection_option_font_style["name"] = self._font_name
if self._selection_option_font_style["size"] is None:
self._selection_option_font_style["size"] = int(self._font_size)
self._option_font = get_font(
self._selection_option_font_style["name"],
self._selection_option_font_style["size"],
)
if self._selection_box_width == 0:
f = self._render_option_string(self._placeholder)
h = self._render_string(
self._title, self.get_font_color_status()
).get_height()
self._selection_box_width = int(
f.get_width()
+ self._selection_box_arrow_margin[0]
+ self._selection_box_arrow_margin[1]
+ h
- h / 4
+ 2 * self._selection_box_border_width
)
def _make_selection_drop(self) -> DropSelect:
"""
Make the selection drop box. This function creates the box UI and adds
the selection options. Should be called after the widget is configured.
:return: Self reference
"""
from pygame_menu._scrollarea import get_scrollbars_from_position
if not self.configured:
raise RuntimeError(
f"{self.get_class_id()} must be configured before creating selection drop"
)
# Create options buttons
total_height: int = 0
max_height: int = 0
frame_width: int = self._selection_box_width + self._selection_box_inflate[0]
self._option_buttons = []
# Add placeholder button
if self._placeholder_add_to_selection_box:
self._items.insert(0, (self._placeholder, -1))
for opt_id in range(len(self._items)):
option = self._items[opt_id]
btn = Button(
option[0],
onreturn=self._click_option,
index=opt_id - (1 if self._placeholder_add_to_selection_box else 0),
button_id=self._id + "+option-" + uuid4(short=True),
)
btn.set_background_color(color=self._selection_box_bgcolor)
btn.set_border(
width=self._selection_option_border_width,
color=self._selection_option_border_color,
)
btn.set_controls(
joystick=False, # Only drop select controls the joystick behavior
mouse=self._mouse_enabled,
touchscreen=self._touchscreen_enabled,
keyboard=False, # Only drop select controls the keyboard behavior
)
btn.set_cursor( # This feature does not work properly
cursor=self._selection_option_cursor
)
font_c = self._selection_option_font_style["color"]
if self._placeholder_add_to_selection_box:
font_color = font_c if opt_id != 0 else self._font_readonly_color
else:
font_color = font_c
btn.set_font(
antialias=self._font_antialias,
background_color=None,
color=font_color,
font=self._selection_option_font_style["name"],
font_size=self._selection_option_font_style["size"],
readonly_color=self._font_readonly_color,
readonly_selected_color=self._font_readonly_selected_color,
selected_color=self._font_selected_color,
)
btn.set_padding(padding=self._selection_option_padding)
btn.add_self_to_kwargs("btn")
btn.set_tab_size(self._tab_size)
btn.configured = True
btn.set_menu(self._menu)
btn._update__repr___(self)
self._option_buttons.append(btn)
bh = btn.get_height() - self._selection_option_border_width
if self._selection_option_left_space and not (
self._placeholder_add_to_selection_box and opt_id == 0
):
prev_pad = btn._padding # top, right, bottom, left
prev_pad_t: Tuple4IntType = btn._padding_transform
dh = int(
btn.get_height(apply_padding=False)
* self._selection_option_left_space_height_factor
)
btn.set_attribute("left_space_height", dh)
m = self._selection_option_left_space_margin
btn._padding = (
prev_pad[0],
prev_pad[1],
prev_pad[2],
prev_pad[3] + dh + m[0] + m[1],
)
btn._padding_transform = (
prev_pad_t[0],
prev_pad_t[1],
prev_pad_t[2],
prev_pad_t[3] + dh + m[0] + m[1],
)
total_height += bh
if opt_id + 1 <= self._selection_box_height:
max_height += bh
max_width = frame_width
if total_height != max_height:
max_width -= self._scrollbar_thick
frame_width -= self._scrollbar_thick
total_height -= self._selection_box_border_width
max_height -= self._selection_box_border_width
elif total_height > 0:
total_height += self._selection_box_border_width
max_height += self._selection_box_border_width
for btn in self._option_buttons:
max_width = max(
max_width, btn.get_width() - self._selection_option_border_width
)
# Update options rect delta width
for btn in self._option_buttons:
btn._rect_size_delta = (max_width - btn.get_width(), 0)
# Pop placeholder
if self._placeholder_add_to_selection_box:
self._items.pop(0)
placeholder_button = self._option_buttons.pop(0)
else:
placeholder_button = None
# Unpack previous frame (if exists)
if self._drop_frame is not None:
self._drop_frame.set_menu(None)
# Create frame
self._drop_frame = Frame(
max_width,
max(total_height, 1),
ORIENTATION_VERTICAL,
frame_id=self._id + "+frame-" + uuid4(short=True),
)
self._drop_frame._accepts_title = False
self._drop_frame._menu_can_be_none_pack = True
self._drop_frame.hide()
self._drop_frame.set_background_color(color=self._selection_box_bgcolor)
self._drop_frame.set_border(
width=self._selection_box_border_width,
color=self._selection_box_border_color,
)
self._drop_frame.set_scrollarea(self._scrollarea)
self._drop_frame.relax()
self._drop_frame.configured = True
self._drop_frame.set_tab_size(self._tab_size)
self._drop_frame._update__repr___(self)
self._drop_frame.set_controls(
joystick=self._joystick_enabled,
mouse=self._mouse_enabled,
touchscreen=self._touchscreen_enabled,
keyboard=self._keyboard_enabled,
)
if total_height > 0:
# Menu is needed while creating the scrollarea, as that reference
# is later passed to the scrollbars, which need menu for touchscreen
# events
self._drop_frame._menu = self._menu
self._drop_frame.make_scrollarea(
max_width=frame_width,
max_height=max_height,
scrollarea_color=self._selection_box_arrow_color,
scrollbar_color=self._scrollbar_color,
scrollbar_cursor=self._scrollbar_cursor,
scrollbar_shadow_color=self._scrollbar_shadow_color,
scrollbar_shadow_offset=self._scrollbar_shadow_offset,
scrollbar_shadow_position=self._scrollbar_shadow_position,
scrollbar_shadow=self._scrollbar_shadow,
scrollbar_slider_color=self._scrollbar_slider_color,
scrollbar_slider_hover_color=self._scrollbar_slider_hover_color,
scrollbar_slider_pad=self._scrollbar_slider_pad,
scrollbar_thick=self._scrollbar_thick,
scrollbars=get_scrollbars_from_position(self._scrollbars),
)
# Menu is formerly set in frame. That method adds the frame to update frames
# depending on if frame is scrollable or not
self._drop_frame.set_menu(self._menu)
self._drop_frame.set_scrollarea(self._scrollarea)
if self._frame is not None:
self._drop_frame.set_frame(self._frame)
# Set sizing properties
if total_height > 0:
add_scrollbar = self._scrollbar_thick if max_width != frame_width else 0
border_w = (
self._selection_box_border_width if total_height != max_height else 0
)
self._drop_frame.set_attribute(
"height", max_height + add_scrollbar - border_w
)
self._drop_frame.set_attribute("width", frame_width)
else:
self._drop_frame.set_attribute("height", 0)
self._drop_frame.set_attribute("width", 0)
if self._placeholder_add_to_selection_box:
placeholder_button.hide()
margin_width = (
self._selection_box_border_width if total_height == max_height else 0
)
self._drop_frame.set_attribute("extra_margin", margin_width)
self._drop_frame.set_attribute("placeholder_button", placeholder_button)
# Pack options
if self._placeholder_add_to_selection_box:
self._drop_frame.pack(placeholder_button)
for opt in self._option_buttons:
self._drop_frame.pack(opt, margin=(0, -self._selection_option_border_width))
# Update options if index is defined
if self._index != -1:
self.set_value(self._index)
return self
def _append_to_menu(self) -> None:
self._make_selection_drop()
[docs]
def hide(self) -> DropSelect:
super().hide()
if self._drop_frame is not None:
self._drop_frame.hide()
return self
[docs]
def show(self) -> DropSelect:
super().show()
if self.active:
self._toggle_drop()
return self
[docs]
def set_frame(self, frame: pygame_menu.widgets.Frame) -> DropSelect:
super().set_frame(frame)
if self._drop_frame is not None:
self._drop_frame.set_frame(frame)
return self
def _click_option(self, index: int, btn: Button) -> None:
"""
Function triggered after option has been selected or clicked.
:param index: Option index within list
"""
btn.set_attribute("ignore_scroll_to_widget")
prev_index = self._index
self.set_value(index)
if self._index != prev_index and self._index != -1:
self.change(*self._items[self._index][1:])
if self._close_on_apply:
self.active = False
if self._drop_frame is not None:
self._drop_frame.hide()
btn.remove_attribute("ignore_scroll_to_widget")
[docs]
def set_position(self, x: NumberType, y: NumberType) -> DropSelect:
super().set_position(x, y)
if self._drop_frame is not None:
x = self._rect.x
y = self._rect.y
if not self._open_middle:
if self._open_bottom:
self._drop_frame.set_position(
x + self._title_size[0],
y
+ self._title_size[1]
+ self.get_attribute("delta_title_height", 0),
)
else:
self._drop_frame.set_position(
x + self._title_size[0],
y
- self._drop_frame.get_attribute("height")
+ self._drop_frame.get_attribute("extra_margin"),
)
else:
self._drop_frame.set_position(*self._compute_position_middle())
for w in self._option_buttons:
w._set_position_relative_to_frame()
if self._placeholder_add_to_selection_box:
placeholder_button: Button = self._drop_frame.get_attribute(
"placeholder_button"
)
placeholder_button._set_position_relative_to_frame()
self._drop_frame.update_position()
return self
def scale(self, *args, **kwargs) -> DropSelect:
raise WidgetTransformationNotImplemented()
def resize(self, *args, **kwargs) -> DropSelect:
raise WidgetTransformationNotImplemented()
def set_max_width(self, *args, **kwargs) -> DropSelect:
raise WidgetTransformationNotImplemented()
def set_max_height(self, *args, **kwargs) -> DropSelect:
raise WidgetTransformationNotImplemented()
def rotate(self, *args, **kwargs) -> DropSelect:
raise WidgetTransformationNotImplemented()
def flip(self, *args, **kwargs) -> DropSelect:
raise WidgetTransformationNotImplemented()
def _draw(self, surface: pygame.Surface) -> None:
surface.blit(self._surface, self._rect.topleft)
[docs]
def draw_after_if_selected(self, surface: pygame.Surface | None) -> DropSelect:
super().draw_after_if_selected(surface)
if self.active and self.is_visible():
self._check_drop_made()
if self._open_middle:
# Draw drop frame in menu widgets surface
assert self._menu is not None, "middle position need menu reference"
surface = self._menu._widgets_surface
self._drop_frame.draw(surface)
self.last_surface = surface
return self
def _render_option_string(self, text: str) -> pygame.Surface:
"""
Render option string surface.
:param text: Text to render
:return: Option string surface
"""
color = self._selection_option_font_style["color"]
if self.readonly or self._index == -1:
color = self._font_readonly_color
text = text.replace("\t", " " * self._tab_size)
return self._option_font.render(text, self._font_antialias, color)
def _get_current_selected_text(self) -> str:
"""
Return the current selected text.
:return: Text
"""
if self._index == -1:
current_selected = self._placeholder
else:
current_selected = self.get_value()[0][0]
return current_selected
def _render(self) -> bool | None:
if self._option_font is None:
return None
scroll_v: int = 0
menu_height: int = (
0 if self._menu is None else self._menu.get_height(widget=True)
)
current_selected = self._get_current_selected_text()
if not self._render_hash_changed(
current_selected,
self._selected,
self._visible,
self._index,
self.readonly,
self.active,
self._open_bottom,
scroll_v,
menu_height,
self._open_middle,
len(self._items),
self._rect.x,
self._rect.y,
):
return True
title = self._render_string(self._title, self.get_font_color_status())
current = self._render_option_string(current_selected)
# Compute virtual inflate to be applied to current rect
vi = 0 # Virtual inflate
if current.get_height() < title.get_height():
vi = (title.get_height() - current.get_height()) / 2
current_rect_bg = current.get_rect()
current_rect_bg.x += title.get_width() + self._selection_box_margin[0]
current_rect_bg.y += int(
(self._selection_box_inflate[1]) / 2 + vi + self._selection_box_margin[1]
)
current_rect_bg.width = self._selection_box_width
current_rect_bg = current_rect_bg.inflate(
(
int(self._selection_box_inflate[0]),
int(self._selection_box_inflate[1] + 2 * vi),
)
)
# Compute delta title if height is lower than selection box
h = title.get_height()
delta_title_height = max(int(math.floor((current_rect_bg.height - h) / 2)), 0)
# Create arrows
arrow = pygame.Rect(
int(
title.get_width()
+ self._selection_box_margin[0]
+ self._selection_box_width
- h
),
int(
self._selection_box_arrow_margin[2]
+ (self._selection_box_inflate[1] + vi) / 4
+ delta_title_height
+ self._selection_box_margin[1]
),
h,
h,
)
w = h + self._selection_box_arrow_margin[1]
# Check which direction it should open (top, bottom)
self._open_bottom = True
if self._drop_frame is not None and self._scrollarea is not None:
assert self.get_scrollarea().get_world() is not None, (
"scrollarea world is None. The widget have been appended to the Menu?"
)
rect = self._rect.copy()
rect.width = self._selection_box_width
rect.y += delta_title_height
rect.x += self._title_size[0]
rect.height += self._drop_frame.get_attribute("height")
if rect.width != 0 and rect.height != 0:
rect_clipped = self._scrollarea.get_world_rect().clip(rect)
if rect.height != rect_clipped.height:
self._open_bottom = False
# Create frame
if (
self._drop_frame is not None
and self._open_middle
and self._menu is not None
):
self._drop_frame.set_scrollarea(self._menu.get_scrollarea())
self._drop_frame._frame = None
if not self._menu._mouse_motion_selection:
self.force_menu_draw_focus = True
else:
self.force_menu_draw_focus = False
delta_title_height_dh = (
math.ceil((current_rect_bg.height - h) / 2)
+ 2 * self._selection_box_border_width
)
if delta_title_height != 0:
self.set_attribute("delta_title_height", delta_title_height_dh)
arrow_up = (
(
int(arrow.right - w + h / 2 - h / 16),
int(arrow.centery - h / 6 - h / 20),
),
(
int(arrow.right - w + h / 2 + h / 4 - h / 16),
int(arrow.centery + h / 4 - h / 20),
),
(int(arrow.right - w + h - h / 16), int(arrow.centery - h / 6 - h / 20)),
)
arrow_down = (
(
int(arrow.right - w + h / 2 - h / 16),
int(arrow.centery + h / 4 - h / 20),
),
(
int(arrow.right - w + h / 2 + h / 4 - h / 16),
int(arrow.centery - h / 6 - h / 20),
),
(int(arrow.right - w + h - h / 16), int(arrow.centery + h / 4 - h / 20)),
)
if not self._open_bottom:
if not self.active:
arrow_right_pos = arrow_down
else:
arrow_right_pos = arrow_up
else:
if not self.active:
arrow_right_pos = arrow_up
else:
arrow_right_pos = arrow_down
self._surface = make_surface(
title.get_width()
+ self._selection_box_margin[0]
+ self._selection_box_width
+ self._selection_box_inflate[0] / 2
+ self._selection_box_border_width,
max(
title.get_height() + self._selection_box_inflate[1],
current_rect_bg.height - self.active * self._selection_box_border_width,
),
)
self._surface.blit(
title, (0, int(self._selection_box_inflate[1] / 2 + delta_title_height))
)
pygame.draw.rect(self._surface, self._selection_box_bgcolor, current_rect_bg)
pygame.draw.rect(
self._surface,
self._selection_box_border_color,
current_rect_bg,
self._selection_box_border_width,
)
# Crop current max width
cropped_current_w = (
self._selection_box_width
- self._selection_box_arrow_margin[0]
- self._selection_box_arrow_margin[1]
- h / 2
- h / 16
- self._selection_box_text_margin
)
assert cropped_current_w > 0, (
"there is no left space for text width, try increasing selection_box_width size"
)
new_current = make_surface(cropped_current_w, current.get_height())
new_current.blit(current, (0, 0))
# new_current.fill((0, 0, 0))
self._surface.blit(
new_current,
(
int(
title.get_width()
+ self._selection_box_margin[0]
+ self._selection_box_text_margin
),
int(
self._selection_box_inflate[1] / 2
+ vi
- 1
+ self._selection_box_margin[1]
),
),
)
if len(self._items) > 0:
pygame.draw.polygon(
self._surface, self._selection_box_arrow_color, arrow_right_pos
)
self._rect.width, self._rect.height = self._surface.get_size()
self.force_menu_surface_update()
return True
[docs]
def get_index(self) -> int:
"""
Get selected index.
:return: Selected index
"""
return self._index
[docs]
def get_value(self) -> tuple[tuple[Any, ...] | str, int]:
"""
Return the current value of the selected index.
:return: Value and index as a tuple, (value, index)
"""
if self._index == -1:
raise ValueError("any item has been selected yet as index is -1")
return self._items[self._index], self._index
[docs]
def value_changed(self) -> bool:
return self._index != self._default_value
def _down(self) -> None:
"""
Move current selection down.
"""
if self.readonly:
return None
elif not self._items:
return None
elif not self.active:
return self._toggle_drop()
elif self._index == -1:
self.set_value(len(self._items) - 1)
else:
if self._selection_infinite:
self.set_value((self._index - 1) % len(self._items))
else:
prev = self._index
new = max(0, self._index - 1)
if prev == new:
return None
self.set_value(new)
self.change(*self._items[self._index][1:])
self._sound.play_key_add()
return None
def _up(self) -> None:
"""
Move current selection up.
"""
if self.readonly:
return None
elif not self._items:
return None
elif not self.active:
return self._toggle_drop()
elif self._index == -1:
self.set_value(0)
else:
if self._selection_infinite:
self.set_value((self._index + 1) % len(self._items))
else:
prev = self._index
new = min(self._index + 1, len(self._items) - 1)
if prev == new:
return None
self.set_value(new)
self.change(*self._items[self._index][1:])
self._sound.play_key_add()
return None
[docs]
def set_value(self, item: str | int, process_index: bool = False) -> None:
"""
Set the current value of the widget, selecting the item that matches the
text if ``item`` is a string, or the index if ``item`` is an integer.
This method raises ``ValueError`` if no item found.
For example, if widget item list is ``[['a', 0], ['b', 1], ['a', 2]]``:
- *widget*.set_value('a') -> Widget selects the first item (index 0)
- *widget*.set_value(2) -> Widget selects the third item (index 2)
.. note::
This method does not trigger any event (change).
:param item: The item to select, either a string or an integer index.
:param process_index: Ignored in this class; used by subclasses.
"""
if isinstance(item, str):
found = False
for i in self._items:
if i[0] == item:
self._index = self._items.index(i)
found = True
break
if not found:
raise ValueError(f'no value "{item}" found in drop select')
elif isinstance(item, int):
assert -1 <= item < len(self._items), (
"item index must be greater than zero and lower than the number "
"of items on the drop select"
)
self._index = item
# Base class ignores process_index
# (DropSelectMultiple overrides and uses it)
# Update options background selection
for b_ind_x in range(len(self._option_buttons)):
btn = self._option_buttons[b_ind_x]
if b_ind_x == self._index:
btn.set_background_color(self._selection_option_selected_bgcolor)
btn.update_font(
{"color": self._selection_option_font_style["color_selected"]}
)
if not self._drop_frame.has_attribute("ignorescroll"):
btn.scroll_to_widget(scroll_parent=False)
else:
btn.set_background_color(self._selection_box_bgcolor)
btn.update_font({"color": self._selection_option_font_style["color"]})
# Force render
self._render()
[docs]
def update_items(self, items: list[tuple[Any, ...]] | list[str]) -> None:
"""
Update drop select items.
.. note::
If the length of the list is different from the previous one, the new
index of the select will be the ``-1``, that is, same as the unselected
status.
:param items: New drop select items; format ``[('Item1', a, b, c...), ('Item2', d, e, f...)]``
"""
assert isinstance(items, list)
if len(items) > 0:
check_selector_items(items)
if (
self._index != -1
and len(items) != len(self._items)
or (self._index + 1) > len(items)
):
self._index = -1
self._default_value = -1
self._items = items.copy()
if self._drop_frame is not None:
self._drop_frame.set_menu(None)
self._drop_frame = None
self.active = False
self._make_selection_drop()
def _check_drop_made(self) -> None:
"""
Checks if drop selection has been made.
"""
if self._drop_frame is None:
raise _SelectionDropNotMadeException(
f"selection drop has not been made yet. Call {self.get_class_id()}"
f"._make_selection_drop() for avoiding this exception"
)
[docs]
def get_items(self) -> list[tuple[Any, ...]] | list[str]:
"""
Return a copy of the select items.
:return: Select items list
"""
return self._items.copy()
def _toggle_drop(self) -> None:
"""
Open drop selection.
"""
self._check_drop_made()
if not self._selected:
return
elif not self._items:
return
self.active = not self.active
if self.active:
if self._drop_frame is not None:
self._drop_frame.show()
self.scroll_to_widget(scroll_parent=False)
else:
if self._drop_frame is not None:
self._drop_frame.hide()
if self.active and self._index != -1:
self.set_value(self._index)
def _compute_position_middle(self, add_offset: bool = True) -> Tuple2IntType:
"""
Compute box position if position is in the middle.
:param add_offset: Adds offset
:return: Position
"""
assert self._menu is not None, (
"menu cannot be none if the position is in middle (open_middle)"
)
self._check_drop_made()
if add_offset:
offx, offy = self._menu.get_scrollarea().get_offsets()
else:
offx, offy = 0, 0
w, h = self._menu.get_size()
h -= self._menu.get_menubar().get_height()
bw, bh = self._selection_box_width, self._drop_frame.get_attribute("height")
assert w >= bw, (
f"selection box width ({bw}) cannot be greater than menu width ({w})"
)
assert h >= bh, (
f"selection box height ({bh}) cannot be greater than menu height ({h})"
)
x = (w - bw) / 2 + offx
y = (h - bh) / 2 + offy
return x, y
[docs]
def get_focus_rect(self) -> pygame.Rect:
self._check_drop_made()
rect = self.get_rect(apply_padding=False, to_real_position=True)
if self.active:
rect.width = self._selection_box_width
rect.x += self._title_size[0]
rect.height += self._drop_frame.get_attribute(
"height"
) - self._drop_frame.get_attribute("extra_margin")
if not self._open_bottom:
rect.y -= self._drop_frame.get_attribute(
"height"
) - self._drop_frame.get_attribute("extra_margin")
if self._open_middle:
x, y = self._compute_position_middle(add_offset=False)
rect.x = int(x + self._menu.get_position()[0])
rect.y = int(y + self._menu.get_scrollarea().get_position()[1])
rect.height -= self._rect.height
else:
rect.width -= self._selection_box_border_width
return rect
def _drop_frame_scrolling(self) -> bool:
"""
Checks if drop frame is scrolling.
:return: ``True`` if scrolling
"""
if (
self._drop_frame is None
or self._drop_frame.get_scrollarea(inner=True) is None
):
return False
return self._drop_frame.get_scrollarea(inner=True).is_scrolling()
[docs]
def update(self, events: EventVectorType) -> bool:
self.apply_update_callbacks(events)
if self.readonly or not self.is_visible():
self._readonly_check_mouseover(events)
return False
# Check scroll
self._check_drop_made()
updated = self._drop_frame.update(events)
if updated:
return True
for event in events:
if event.type == pygame.KEYDOWN: # Check key is valid
if self._ignores_keyboard_nonphysical() and not check_key_pressed_valid(
event
):
continue
# Check mouse over
self._check_mouseover(event)
# Events
keydown = self._keyboard_enabled and event.type == pygame.KEYDOWN
joy_hatmotion = self._joystick_enabled and event.type == pygame.JOYHATMOTION
joy_axismotion = (
self._joystick_enabled and event.type == pygame.JOYAXISMOTION
)
joy_button_down = (
self._joystick_enabled and event.type == pygame.JOYBUTTONDOWN
)
# Left button
if (
keydown
and self._ctrl.move_down(event, self)
or joy_hatmotion
and self._ctrl.joy_left(event, self)
or joy_axismotion
and self._ctrl.joy_axis_x_left(event, self)
):
if not self.active:
continue
self._down()
return True
# Right button
elif (
keydown
and self._ctrl.move_up(event, self)
or joy_hatmotion
and self._ctrl.joy_right(event, self)
or joy_axismotion
and self._ctrl.joy_axis_x_right(event, self)
):
if not self.active:
continue
self._up()
return True
# Press enter
elif (
keydown
and self._ctrl.apply(event, self)
or joy_button_down
and self._ctrl.joy_select(event, self)
):
if self.active and self._index >= 0:
self._sound.play_key_add()
self.apply(*self._items[self._index][1:])
if not (
self.active and (not self._close_on_apply and self._index != -1)
):
self._toggle_drop()
return True
# Press keys which active the drop but not apply
elif keydown and self._ctrl.tab(event, self):
self._toggle_drop()
return True
# Close the selection
elif keydown and (
self._ctrl.escape(event, self) or self._ctrl.back(event, self)
):
if self.active:
self._toggle_drop()
return True
# Click on dropselect; don't consider the mouse wheel (button 4 & 5)
elif self.active and (
event.type == pygame.MOUSEBUTTONDOWN
and self._mouse_enabled
and event.button in (1, 2, 3)
or event.type == FINGERDOWN
and self._touchscreen_enabled
and self._menu is not None
and not self._drop_frame_scrolling()
):
event_pos = get_finger_pos(self._menu, event)
if self._drop_frame.get_rect(
apply_padding=False, to_real_position=True
).collidepoint(*event_pos):
return True
# Click on dropselect; don't consider the mouse wheel (button 4 & 5)
elif (
event.type == pygame.MOUSEBUTTONUP
and self._mouse_enabled
and event.button in (1, 2, 3)
or event.type == FINGERUP
and self._touchscreen_enabled
and self._menu is not None
and not self._drop_frame_scrolling()
):
# Check for mouse clicks within
if self.active:
for btn in self._option_buttons:
btn.set_attribute("ignore_scroll_to_widget")
updated = btn.update([event])
try:
btn.remove_attribute("ignore_scroll_to_widget")
except IndexError:
pass
if updated:
return True
# Get event position based on input type
event_pos = get_finger_pos(self._menu, event)
# If collides
rect = self.get_rect(to_real_position=True, apply_padding=False)
if rect.collidepoint(*event_pos):
# Check if mouse collides left or right as percentage, use only X coordinate
mouse_x, _ = event_pos
topleft, _ = rect.topleft
dist = mouse_x - (
topleft + self._title_size[0]
) # Distance from title
if dist > 0: # User clicked the options, not title
self._toggle_drop()
return True
else:
if self.active and not self.get_focus_rect().collidepoint(
*event_pos
):
self._toggle_drop()
return True
# Check mousemove
# elif event.type == pygame.MOUSEMOTION:
# for btn in self._option_buttons:
# btn._check_mouseover(event)
return False
class _SelectionDropNotMadeException(Exception):
"""
Exception thrown if drop selection has not been made.
"""
pass
class DropSelectManager(AbstractWidgetManager, ABC):
"""
DropSelect manager.
"""
def dropselect(
self,
title: Any,
items: list[tuple[Any, ...]] | list[str],
default: int | None = None,
dropselect_id: str = "",
onchange: CallbackType = None,
onreturn: CallbackType = None,
onselect: Callable[[bool, Widget, pygame_menu.Menu], Any] | None = None,
open_middle: bool = False,
placeholder: str = "Select an option",
placeholder_add_to_selection_box: bool = True,
**kwargs,
) -> pygame_menu.widgets.DropSelect:
"""
Add a dropselect to the Menu: Drop select is a selector within a Frame.
This drops a vertical frame if requested.
Drop select can contain selectable items (options), but only one can be
selected.
The items of the DropSelect are:
.. code-block:: python
items = [('Item1', a, b, c...), ('Item2', d, e, f...)]
The callbacks receive the current selected item, its index in the list,
the associated arguments, and all unknown keyword arguments, where
``selected_item=widget.get_value()`` and ``selected_index=widget.get_index()``:
.. code-block:: python
onchange((selected_item, selected_index), a, b, c..., **kwargs)
onreturn((selected_item, selected_index), a, b, c..., **kwargs)
For example, if ``selected_index=0`` then ``selected_item=('Item1', a, b, c...)``.
If ``onselect`` is defined, the callback is executed as follows, where
``selected`` is a boolean representing the selected status:
.. code-block:: python
onselect(selected, widget, menu)
kwargs (Optional)
- ``align`` (str) – Widget `alignment <https://pygame-menu.readthedocs.io/en/latest/_source/themes.html#alignment>`_
- ``background_color`` (tuple, list, str, int, :py:class:`pygame.Color`, :py:class:`pygame_menu.baseimage.BaseImage`) – Color of the background. ``None`` for no-color
- ``background_inflate`` (tuple, list) – Inflate background on x-axis and y-axis (x, y) in px
- ``border_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Widget border color. ``None`` for no-color
- ``border_inflate`` (tuple, list) – Widget border inflate on x-axis and y-axis (x, y) in px
- ``border_position`` (str, tuple, list) – Widget border positioning. It can be a single position, or a tuple/list of positions. Only are accepted: north, south, east, and west. See :py:mod:`pygame_menu.locals`
- ``border_width`` (int) – Border width in px. If ``0`` disables the border
- ``cursor`` (int, :py:class:`pygame.cursors.Cursor`, None) – Cursor of the widget if the mouse is placed over
- ``float`` (bool) - If ``True`` the widget don't contribute width/height to the Menu widget positioning computation, and don't add one unit to the rows
- ``float_origin_position`` (bool) - If ``True`` the widget position is set to the top-left position of the Menu if the widget is floating
- ``font_background_color`` (tuple, list, str, int, :py:class:`pygame.Color`, None) – Widget font background color
- ``font_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Widget font color
- ``font_name`` (str, :py:class:`pathlib.Path`, :py:class:`pygame.font.Font`) – Widget font path
- ``font_shadow_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Font shadow color
- ``font_shadow_offset`` (int) – Font shadow offset in px
- ``font_shadow_position`` (str) – Font shadow position, see locals for position
- ``font_shadow`` (bool) – Font shadow is enabled or disabled
- ``font_size`` (int) – Font size of the widget
- ``margin`` (tuple, list) – Widget (left, bottom) margin in px
- ``padding`` (int, float, tuple, list) – Widget padding according to CSS rules. General shape: (top, right, bottom, left)
- ``readonly_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Color of the widget if readonly mode
- ``readonly_selected_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Color of the widget if readonly mode and is selected
- ``selection_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Color of the selected widget; only affects the font color
- ``selection_effect`` (:py:class:`pygame_menu.widgets.core.Selection`) – Widget selection effect
- ``shadow_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Color of the widget shadow
- ``shadow_radius`` (int) - Border radius of the shadow
- ``shadow_type`` (str) - Shadow type, it can be ``'rectangular'`` or ``'ellipse'``
- ``shadow_width`` (int) - Width of the shadow. If ``0`` the shadow is disabled
- ``tab_size`` (int) – Width of a tab character
kwargs for modifying selection box/option style (Optional)
- ``scrollbar_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Scrollbar color
- ``scrollbar_cursor`` (int, :py:class:`pygame.cursors.Cursor`, None) – Cursor of the scrollbars if the mouse is placed over
- ``scrollbar_shadow_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Color of the shadow of each scrollbar
- ``scrollbar_shadow_offset`` (int) – Offset of the scrollbar shadow in px
- ``scrollbar_shadow_position`` (str) – Position of the scrollbar shadow. See :py:mod:`pygame_menu.locals`
- ``scrollbar_shadow`` (bool) – Indicate if a shadow is drawn on each scrollbar
- ``scrollbar_slider_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Color of the sliders
- ``scrollbar_slider_hover_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Color of the slider if hovered or clicked
- ``scrollbar_slider_pad`` (int, float) – Space between slider and scrollbars borders in px
- ``scrollbar_thick`` (int) – Scrollbar thickness in px
- ``scrollbars`` (str) – Scrollbar position. See :py:mod:`pygame_menu.locals`
- ``selection_box_arrow_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Selection box arrow color
- ``selection_box_arrow_margin`` (tuple) – Selection box arrow margin (left, right, vertical) in px
- ``selection_box_bgcolor`` (tuple, list, str, int, :py:class:`pygame.Color`) – Selection box background color
- ``selection_box_border_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Selection box border color
- ``selection_box_border_width`` (int) – Selection box border width
- ``selection_box_height`` (int) – Number of the options which can be packed before showing scroll
- ``selection_box_inflate`` (tuple) – Selection box inflate on x-axis and y-axis (x, y) in px
- ``selection_box_margin`` (tuple, list) – Selection box on x-axis and y-axis (x, y) margin from title in px
- ``selection_box_text_margin`` (int) – Left margin of the text inside the selection box in px
- ``selection_box_width`` (int) – Selection box width in px. If ``0`` compute automatically to fit placeholder
- ``selection_infinite`` (bool) – If ``True`` selection can rotate through bottom/top
- ``selection_option_border_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Option border color
- ``selection_option_border_width`` (int) – Option border width
- ``selection_option_font_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Option font color
- ``selection_option_font_size`` (int, None) – Option font size. If ``None`` use the 75% of the widget font size
- ``selection_option_font`` (str, :py:class:`pathlib.Path`, :py:class:`pygame.font.Font`) – Option font. If ``None`` use the same font as the widget
- ``selection_option_padding`` (int, float, tuple, list ) – Selection padding. See padding styling
- ``selection_option_selected_bgcolor`` (tuple, list, str, int, :py:class:`pygame.Color`) – Selected option background color
- ``selection_option_selected_font_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Selected option font color
.. note::
All theme-related optional kwargs use the default Menu theme if not
defined.
.. note::
This is applied only to the base Menu (not the currently displayed,
stored in ``_current`` pointer); for such behavior apply to
:py:meth:`pygame_menu.menu.Menu.get_current` object.
.. warning::
Be careful with kwargs collision. Consider that all optional documented
kwargs keys are removed from the object.
:param title: Drop select title
:param items: Item list of the drop select; format ``[('Item1', a, b, c...), ('Item2', d, e, f...)]``
:param default: Index of default item to display. If ``None`` no item is selected
:param dropselect_id: ID of the dropselect
:param onchange: Callback when changing the drop select item
:param onreturn: Callback when pressing return (apply) on the selected item
:param onselect: Function when selecting the widget
:param open_middle: If ``True`` the selection box is opened in the middle of the menu
:param placeholder: Text shown if no option is selected yet
:param placeholder_add_to_selection_box: If ``True`` adds the placeholder button to the selection box
:param kwargs: Optional keyword arguments
:return: Widget object
:rtype: :py:class:`pygame_menu.widgets.DropSelect`
"""
# Filter widget attributes to avoid passing them to the callbacks
attributes = self._filter_widget_attributes(kwargs)
# Get selection box properties
selection_box_arrow_color = kwargs.pop(
"selection_box_arrow_color", self._theme.widget_box_arrow_color
)
selection_box_arrow_margin = kwargs.pop(
"selection_box_arrow_margin", self._theme.widget_box_arrow_margin
)
selection_box_bgcolor = kwargs.pop(
"selection_box_bgcolor", self._theme.widget_box_background_color
)
selection_box_border_color = kwargs.pop(
"selection_box_border_color", self._theme.widget_box_border_color
)
selection_box_border_width = kwargs.pop(
"selection_box_border_width", self._theme.widget_box_border_width
)
selection_box_height = kwargs.pop("selection_box_height", 3)
selection_box_inflate = kwargs.pop(
"selection_box_inflate", self._theme.widget_border_inflate
)
selection_box_margin = kwargs.pop(
"selection_box_margin", self._theme.widget_box_margin
)
selection_box_text_margin = kwargs.pop(
"selection_box_text_margin", self._theme.widget_box_arrow_margin[0]
)
selection_box_width = kwargs.pop("selection_box_width", 0)
selection_infinite = kwargs.pop("selection_infinite", False)
selection_option_border_color = kwargs.pop(
"selection_option_border_color", self._theme.scrollbar_color
)
selection_option_border_width = kwargs.pop(
"selection_option_border_width", self._theme.widget_box_border_width
)
# selection_option_cursor = kwargs.pop('selection_option_cursor', None)
selection_option_font = kwargs.pop("selection_option_font", None)
selection_option_font_color = kwargs.pop(
"selection_option_font_color", (0, 0, 0)
)
selection_option_font_size = kwargs.pop("selection_option_font_size", None)
selection_option_padding = kwargs.pop("selection_option_padding", (2, 5))
selection_option_selected_bgcolor = kwargs.pop(
"selection_option_selected_bgcolor", (188, 227, 244)
)
selection_option_selected_font_color = kwargs.pop(
"selection_option_selected_font_color", (0, 0, 0)
)
# Get selection box scrollbar properties
scrollbar_color = kwargs.pop("scrollbar_color", self._theme.scrollbar_color)
scrollbar_cursor = kwargs.pop("scrollbar_cursor", self._theme.scrollbar_cursor)
scrollbar_shadow_color = kwargs.pop(
"scrollbar_shadow_color", self._theme.scrollbar_shadow_color
)
scrollbar_shadow_offset = kwargs.pop(
"scrollbar_shadow_offset", self._theme.scrollbar_shadow_offset
)
scrollbar_shadow_position = kwargs.pop(
"scrollbar_shadow_position", self._theme.scrollbar_shadow_position
)
scrollbar_shadow = kwargs.pop("scrollbar_shadow", self._theme.scrollbar_shadow)
scrollbar_slider_color = kwargs.pop(
"scrollbar_slider_color", self._theme.scrollbar_slider_color
)
scrollbar_slider_hover_color = kwargs.pop(
"scrollbar_slider_hover_color", self._theme.scrollbar_slider_hover_color
)
scrollbar_slider_pad = kwargs.pop(
"scrollbar_slider_pad", self._theme.scrollbar_slider_pad
)
scrollbar_thick = kwargs.pop("scrollbar_thick", self._theme.scrollbar_thick)
scrollbars = kwargs.pop("scrollbars", self._theme.scrollarea_position)
# Convert to tuple if enabled
if isinstance(selection_box_margin, NumberInstance):
selection_box_margin = (selection_box_margin, selection_box_margin)
widget = DropSelect(
default=default,
dropselect_id=dropselect_id,
items=items,
onchange=onchange,
onreturn=onreturn,
onselect=onselect,
open_middle=open_middle,
placeholder=placeholder,
placeholder_add_to_selection_box=placeholder_add_to_selection_box,
scrollbar_color=scrollbar_color,
scrollbar_cursor=scrollbar_cursor,
scrollbar_shadow=scrollbar_shadow,
scrollbar_shadow_color=scrollbar_shadow_color,
scrollbar_shadow_offset=scrollbar_shadow_offset,
scrollbar_shadow_position=scrollbar_shadow_position,
scrollbar_slider_color=scrollbar_slider_color,
scrollbar_slider_hover_color=scrollbar_slider_hover_color,
scrollbar_slider_pad=scrollbar_slider_pad,
scrollbar_thick=scrollbar_thick,
scrollbars=scrollbars,
selection_box_arrow_color=selection_box_arrow_color,
selection_box_arrow_margin=selection_box_arrow_margin,
selection_box_bgcolor=selection_box_bgcolor,
selection_box_border_color=selection_box_border_color,
selection_box_border_width=selection_box_border_width,
selection_box_height=selection_box_height,
selection_box_inflate=selection_box_inflate,
selection_box_margin=selection_box_margin,
selection_box_text_margin=selection_box_text_margin,
selection_box_width=selection_box_width,
selection_infinite=selection_infinite,
selection_option_border_color=selection_option_border_color,
selection_option_border_width=selection_option_border_width,
# selection_option_cursor=selection_option_cursor,
selection_option_font=selection_option_font,
selection_option_font_color=selection_option_font_color,
selection_option_font_size=selection_option_font_size,
selection_option_padding=selection_option_padding,
selection_option_selected_bgcolor=selection_option_selected_bgcolor,
selection_option_selected_font_color=selection_option_selected_font_color,
title=title,
**kwargs,
)
self._configure_widget(widget=widget, **attributes)
self._append_widget(widget)
return widget