Source code for pygame_menu.widgets.widget.menubar
"""
pygame-menu
https://github.com/ppizarror/pygame-menu
MENUBAR
MenuBar class to display the Menu title.
"""
# File constants no. 1000
from __future__ import annotations
__all__ = [
# Main class
"MenuBar",
# Menubar styles
"MENUBAR_STYLE_ADAPTIVE",
"MENUBAR_STYLE_SIMPLE",
"MENUBAR_STYLE_TITLE_ONLY",
"MENUBAR_STYLE_TITLE_ONLY_DIAGONAL",
"MENUBAR_STYLE_NONE",
"MENUBAR_STYLE_UNDERLINE",
"MENUBAR_STYLE_UNDERLINE_TITLE",
# Custom types
"MenuBarStyleModeType",
]
from typing import Any
import pygame
import pygame.gfxdraw as gfxdraw
from pygame_menu._types import (
CallbackType,
ColorInputType,
ColorType,
EventVectorType,
NumberInstance,
NumberType,
Tuple2IntType,
VectorInstance,
)
from pygame_menu.locals import (
FINGERUP,
POSITION_EAST,
POSITION_NORTH,
POSITION_SOUTH,
POSITION_WEST,
)
from pygame_menu.utils import assert_color, get_finger_pos, warn
from pygame_menu.widgets.core.widget import Widget, WidgetTransformationNotImplemented
# Menubar styles
MENUBAR_STYLE_ADAPTIVE = 1000
MENUBAR_STYLE_SIMPLE = 1001
MENUBAR_STYLE_TITLE_ONLY = 1002
MENUBAR_STYLE_TITLE_ONLY_DIAGONAL = 1003
MENUBAR_STYLE_NONE = 1004
MENUBAR_STYLE_UNDERLINE = 1005
MENUBAR_STYLE_UNDERLINE_TITLE = 1006
# Menubar operation modes
_MODE_CLOSE = 1020
_MODE_BACK = 1021
# Custom types
MenuBarStyleModeType = int
[docs]
class MenuBar(Widget):
"""
MenuBar widget.
.. note::
MenuBar only accepts translation transformation.
:param title: Title of the menubar
:param width: Width of the widget, generally the same as the width of the menu
:param background_color: Background color
:param menubar_id: ID of the MenuBar
:param back_box: Draw a back-box button on header
:param back_box_background_color: Back-box button color
:param mode: Mode of drawing the bar
:param modify_scrollarea: If ``True`` it modifies the scrollbars of the scrollarea depending on the bar mode
:param offsetx: Offset x-position of title in px
:param offsety: Offset y-position of title in px
:param onreturn: Callback when pressing the back-box button
:param args: Optional arguments for callbacks
:param kwargs: Optional keyword arguments for callbacks
"""
_backbox: bool
_backbox_background_color: ColorType
_backbox_border_width: int
_backbox_pos: Any
_backbox_rect: pygame.Rect | None
_box_mode: int
_modify_scrollarea: bool
_offsetx: NumberType
_offsety: NumberType
_polygon_pos: Any
_scrollbar_deltas: list[tuple[int, Tuple2IntType]]
_style: int
_width: int
fixed: bool
def __init__(
self,
title: Any,
width: NumberType,
background_color: ColorInputType,
menubar_id: str = "",
back_box: bool = False,
back_box_background_color: ColorInputType = (0, 0, 0),
mode: MenuBarStyleModeType = MENUBAR_STYLE_ADAPTIVE,
modify_scrollarea: bool = True,
offsetx: NumberType = 0,
offsety: NumberType = 0,
onreturn: CallbackType = None,
*args,
**kwargs,
) -> None:
assert isinstance(width, NumberInstance)
assert isinstance(back_box, bool)
assert width > 0, "width must be greater or equal than zero"
background_color = assert_color(background_color)
back_box_background_color = assert_color(back_box_background_color)
# MenuBar has no ID
super().__init__(
args=args,
kwargs=kwargs,
onreturn=onreturn,
title=title,
widget_id=menubar_id,
selectable=False,
)
self._backbox = back_box
self._backbox_background_color = back_box_background_color
self._backbox_border_width = 1 # px
self._backbox_pos = None
self._backbox_rect = None
self._background_color = background_color
self._box_mode = 0
self._modify_scrollarea = modify_scrollarea
self._mouseover_check_rect = lambda: self._backbox_rect
self._offsetx = 0
self._offsety = 0
self._polygon_pos = None
# north, east, south, west
self._scrollbar_deltas = [(0, (0, 0)), (0, (0, 0)), (0, (0, 0)), (0, (0, 0))]
self._style = mode
self._width = int(width)
self.set_title(title, offsetx, offsety)
# Public's
self.fixed = True
def _apply_font(self) -> None:
pass
def set_padding(self, *args, **kwargs) -> MenuBar:
return self
def scale(self, *args, **kwargs) -> MenuBar:
raise WidgetTransformationNotImplemented()
def resize(self, *args, **kwargs) -> MenuBar:
raise WidgetTransformationNotImplemented()
def set_max_width(self, *args, **kwargs) -> MenuBar:
raise WidgetTransformationNotImplemented()
def set_max_height(self, *args, **kwargs) -> MenuBar:
raise WidgetTransformationNotImplemented()
def rotate(self, *args, **kwargs) -> MenuBar:
raise WidgetTransformationNotImplemented()
def flip(self, *args, **kwargs) -> MenuBar:
raise WidgetTransformationNotImplemented()
def set_selection_effect(self, *args, **kwargs) -> MenuBar:
return self
def set_border(self, *args, **kwargs) -> MenuBar:
return self
def _check_title_color(self, background_menu: bool) -> None:
"""
Performs title color and prints a warning if the color is similar to the
background.
"""
if background_menu and self._menu is not None:
c_back = self._menu.get_theme().background_color
else:
c_back = self._background_color
if not isinstance(c_back, VectorInstance): # If is color
return
tol = 5
c_dif_1 = abs(c_back[0] - self._font_color[0])
c_dif_2 = abs(c_back[1] - self._font_color[1])
c_dif_3 = abs(c_back[2] - self._font_color[2])
if self._verbose and c_dif_1 < tol and c_dif_2 < tol and c_dif_3 < tol:
status = "equal" if c_dif_1 == c_dif_2 == c_dif_3 == 0 else "similar"
bg_type = "menu" if background_menu else "title"
warn(
f"title font color {self._font_color} is {status} to the {bg_type} "
f"background color {c_back}, consider editing your Theme"
)
[docs]
def get_title_offset(self) -> Tuple2IntType:
"""
Return the title offset on x-axis and y-axis (x, y) in px.
:return: Title offset
"""
return int(self._offsetx), int(self._offsety)
[docs]
def set_backbox_border_width(self, width: int) -> None:
"""
Set backbox border width in px.
:param width: Width in px
"""
assert isinstance(width, int)
assert width > 0
self._backbox_border_width = width
def _draw_background_color(self, *args, **kwargs) -> None:
pass
def _draw_border(self, *args, **kwargs) -> None:
pass
def _backbox_visible(self) -> bool:
"""
Return ``True`` if backbox is visible.
:return: Bool
"""
# The following check belongs to the case if the Menu displays a "x" button
# to close the Menu, but onclose Menu method is None (Nothing is executed),
# then the button will not be displayed
# noinspection PyProtectedMember
return (
(self._mouse_enabled or self._touchscreen_enabled)
and self._backbox
and not (
self._box_mode == _MODE_CLOSE
and self._menu is not None
and self._menu._onclose is None
)
)
def _draw(self, surface: pygame.Surface) -> None:
if len(self._polygon_pos) > 2:
gfxdraw.filled_polygon(surface, self._polygon_pos, self._background_color)
# Draw backbox if enabled
if self._backbox_visible():
pygame.draw.rect(
surface,
self._backbox_background_color,
self._backbox_rect,
self._backbox_border_width,
)
pygame.draw.polygon(
surface, self._backbox_background_color, self._backbox_pos
)
surface.blit(
self._surface,
(
self._rect.topleft[0] + self._offsetx,
self._rect.topleft[1] + self._offsety,
),
)
[docs]
def get_scrollbar_style_change(self, position: str) -> tuple[int, Tuple2IntType]:
"""
Return scrollbar change (width, position) depending on the style of the
menubar.
:param position: Position of the scrollbar
:return: Change in length and position in px
"""
self._render()
if not self._modify_scrollarea or not self.is_visible():
return 0, (0, 0)
elif not self.fixed or self.is_floating():
if self._style == MENUBAR_STYLE_ADAPTIVE:
if position == POSITION_EAST:
t = self._polygon_pos[4][1] - self._polygon_pos[2][1]
return t, (0, -t)
return 0, (0, 0)
elif position == POSITION_NORTH:
return self._scrollbar_deltas[0]
elif position == POSITION_EAST:
return self._scrollbar_deltas[1]
elif position == POSITION_SOUTH:
return self._scrollbar_deltas[2]
elif position == POSITION_WEST:
return self._scrollbar_deltas[3]
return 0, (0, 0)
def _render(self) -> bool | None:
if self._menu is None:
return None
# noinspection PyProtectedMember
menu_prev_condition = (
not self._menu or not self._menu._top or not self._menu._top._prev
)
if not self._render_hash_changed(
self._menu.get_id(),
self._rect.x,
self._rect.y,
self._title,
self._width,
self._visible,
self._font_selected_color,
menu_prev_condition,
):
return True
# Update box mode
elif menu_prev_condition:
self._box_mode = _MODE_CLOSE
else:
self._box_mode = _MODE_BACK
self._surface = self._render_string(self._title, self._font_selected_color)
self._rect.width, self._rect.height = self._surface.get_size()
self._apply_transforms() # Rotation does not affect rect size
dy = 0
if self._style == MENUBAR_STYLE_ADAPTIVE:
"""
A-------------------B D-E: 25 dx
|**** x | *0,6 height
| D------------C
F----E/
"""
a = self._rect.x, self._rect.y
b = self._rect.x + self._width - 1, self._rect.y
c = self._rect.x + self._width - 1, self._rect.y + self._rect.height * 0.6
d = (
self._rect.x + self._rect.width + 25 + self._offsetx,
self._rect.y + self._rect.height * 0.6,
)
e = (
self._rect.x + self._rect.width + 5 + self._offsetx,
self._rect.y + self._rect.height,
)
f = self._rect.x, self._rect.y + self._rect.height
self._polygon_pos = a, b, c, d, e, f
cross_size = int(self._rect.height * 0.6)
self._scrollbar_deltas = [
(0, (0, self._rect.height)),
(-cross_size, (0, cross_size)),
(0, (0, 0)),
(-self._rect.height, (0, self._rect.height)),
]
self._check_title_color(background_menu=False)
elif self._style == MENUBAR_STYLE_SIMPLE:
"""
A-------------------B
|**** x | *1,0 height
D-------------------C
"""
a = self._rect.x, self._rect.y
b = self._rect.x + self._width - 1, self._rect.y
c = self._rect.x + self._width - 1, self._rect.y + self._rect.height
d = self._rect.x, self._rect.y + self._rect.height
self._polygon_pos = a, b, c, d
cross_size = int(self._rect.height * self._backbox_visible())
self._scrollbar_deltas = [
(0, (0, self._rect.height)),
(-self._rect.height, (0, self._rect.height)),
(0, (0, 0)),
(-self._rect.height, (0, self._rect.height)),
]
self._check_title_color(background_menu=False)
elif self._style == MENUBAR_STYLE_TITLE_ONLY:
"""
A-----B
| *** | x *0,6 height
D-----C
"""
a = self._rect.x, self._rect.y
b = self._rect.x + self._rect.width + 5 + self._offsetx, self._rect.y
c = (
self._rect.x + self._rect.width + 5 + self._offsetx,
self._rect.y + self._rect.height,
)
d = self._rect.x, self._rect.y + self._rect.height
self._polygon_pos = a, b, c, d
cross_size = int(self._rect.height * 0.6 * self._backbox_visible())
self._scrollbar_deltas = [
(0, (0, self._rect.height)),
(-cross_size, (0, cross_size)),
(0, (0, 0)),
(-self._rect.height, (0, self._rect.height)),
]
self._check_title_color(background_menu=False)
elif self._style == MENUBAR_STYLE_TITLE_ONLY_DIAGONAL:
"""
A--------B
| **** / x *0,6 height
D-----C
"""
a = self._rect.x, self._rect.y
b = self._rect.x + self._rect.width + 25 + self._offsetx, self._rect.y
c = (
self._rect.x + self._rect.width + 5 + self._offsetx,
self._rect.y + self._rect.height,
)
d = self._rect.x, self._rect.y + self._rect.height
self._polygon_pos = a, b, c, d
cross_size = int(self._rect.height * 0.6 * self._backbox_visible())
self._scrollbar_deltas = [
(0, (0, self._rect.height)),
(-cross_size, (0, cross_size)),
(0, (0, 0)),
(-self._rect.height, (0, self._rect.height)),
]
self._check_title_color(background_menu=False)
elif self._style == MENUBAR_STYLE_NONE:
"""
A------------------B
**** x *0,6 height
"""
a = self._rect.x, self._rect.y
b = self._rect.x + self._width - 1, self._rect.y
self._polygon_pos = a, b
cross_size = int(self._rect.height * 0.6 * self._backbox_visible())
self._scrollbar_deltas = [
(0, (0, self._rect.height)),
(-cross_size, (0, cross_size)),
(0, (0, 0)),
(-self._rect.height, (0, self._rect.height)),
]
self._check_title_color(background_menu=True)
elif self._style == MENUBAR_STYLE_UNDERLINE:
"""
**** x
A-------------------B *0,09 height
D-------------------C
"""
# dy = 0
a = self._rect.x, self._rect.y + 0.91 * self._rect.height + dy
b = (
self._rect.x + self._width - 1,
self._rect.y + 0.91 * self._rect.height + dy,
)
c = self._rect.x + self._width - 1, self._rect.y + self._rect.height + dy
d = self._rect.x, self._rect.y + self._rect.height + dy
self._polygon_pos = a, b, c, d
cross_size = int(0.6 * self._rect.height * self._backbox_visible())
self._scrollbar_deltas = [
(0, (0, self._rect.height)),
(-self._rect.height, (0, self._rect.height)),
(0, (0, 0)),
(-self._rect.height, (0, self._rect.height)),
]
self._check_title_color(background_menu=True)
elif self._style == MENUBAR_STYLE_UNDERLINE_TITLE:
"""
**** x
A----B *0,09 height
D----C
"""
# dy = 3
a = self._rect.x, self._rect.y + 0.91 * self._rect.height + dy
b = (
self._rect.x + self._rect.width + 5 + self._offsetx,
self._rect.y + 0.91 * self._rect.height + dy,
)
c = (
self._rect.x + self._rect.width + 5 + self._offsetx,
self._rect.y + self._rect.height + dy,
)
d = self._rect.x, self._rect.y + self._rect.height + dy
self._polygon_pos = a, b, c, d
cross_size = int(0.6 * self._rect.height * self._backbox_visible())
self._scrollbar_deltas = [
(0, (0, self._rect.height)),
(-cross_size, (0, cross_size)),
(0, (0, 0)),
(-self._rect.height, (0, self._rect.height)),
]
self._check_title_color(background_menu=True)
else:
raise ValueError(f"invalid menubar mode {self._style}")
self._rect.height += dy
# Create the back box
if self._backbox:
backbox_margin = 4
# Subtract the scrollarea thickness if float and enabled
scroll_delta = 0
if self._floating and self._menu is not None:
scroll_delta = self._menu.get_width() - self._menu.get_width(inner=True)
self._backbox_rect = pygame.Rect(
int(
self._rect.x
+ self._width
- cross_size
+ backbox_margin
- scroll_delta
),
int(self._rect.y + backbox_margin),
int(cross_size - 2 * backbox_margin),
int(cross_size - 2 * backbox_margin),
)
if self._box_mode == _MODE_CLOSE:
# Make a cross for top Menu
self._backbox_pos = (
(self._backbox_rect.left + 4, self._backbox_rect.top + 4),
(self._backbox_rect.centerx, self._backbox_rect.centery),
(self._backbox_rect.right - 4, self._backbox_rect.top + 4),
(self._backbox_rect.centerx, self._backbox_rect.centery),
(self._backbox_rect.right - 4, self._backbox_rect.bottom - 4),
(self._backbox_rect.centerx, self._backbox_rect.centery),
(self._backbox_rect.left + 4, self._backbox_rect.bottom - 4),
(self._backbox_rect.centerx, self._backbox_rect.centery),
(self._backbox_rect.left + 4, self._backbox_rect.top + 4),
)
elif self._box_mode == _MODE_BACK:
# Make a back arrow for sub-menus
self._backbox_pos = (
(self._backbox_rect.left + 5, self._backbox_rect.centery),
(self._backbox_rect.centerx, self._backbox_rect.top + 5),
(self._backbox_rect.centerx, self._backbox_rect.centery - 2),
(self._backbox_rect.right - 5, self._backbox_rect.centery - 2),
(self._backbox_rect.right - 5, self._backbox_rect.centery + 2),
(self._backbox_rect.centerx, self._backbox_rect.centery + 2),
(self._backbox_rect.centerx, self._backbox_rect.bottom - 5),
(self._backbox_rect.left + 5, self._backbox_rect.centery),
)
return True
[docs]
def set_title(
self, title: Any, offsetx: NumberType = 0, offsety: NumberType = 0
) -> MenuBar:
"""
Set the menubar title.
:param title: Menu title
:param offsetx: Offset x-position of title in px
:param offsety: Offset y-position of title in px
:return: Self reference
"""
assert isinstance(offsetx, NumberInstance)
assert isinstance(offsety, NumberInstance)
self._title = str(title)
self._offsety = offsety
self._offsetx = offsetx
if self._menu is not None:
self._render()
return self
[docs]
def get_height(
self, apply_padding: bool = True, apply_selection: bool = False
) -> int:
if self._floating or not self.is_visible():
return 0
return super().get_height(apply_padding, apply_selection)
[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
for event in events:
# Check mouse over
if self._backbox_visible():
self._check_mouseover(event)
# User clicks/touches the backbox rect; don't consider the mouse wheel (button 4 & 5)
if (
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
):
event_pos = get_finger_pos(self._menu, event)
if self._backbox_visible() and self._backbox_rect.collidepoint(
*event_pos
):
if event.type == pygame.MOUSEBUTTONUP:
self._sound.play_click_mouse()
else:
self._sound.play_click_touch()
self.apply()
return True
# User applies joy back button
elif event.type == pygame.JOYBUTTONDOWN and self._joystick_enabled:
if self._ctrl.joy_back(event, self):
self._sound.play_key_del()
self.apply()
return True
return False