mirror of
https://github.com/imayushsaini/Bombsquad-Ballistica-Modded-Server.git
synced 2025-11-14 17:46:03 +00:00
414 lines
14 KiB
Python
414 lines
14 KiB
Python
# Released under the MIT License. See LICENSE for details.
|
|
#
|
|
"""Popup window/menu related functionality."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import weakref
|
|
from typing import TYPE_CHECKING
|
|
|
|
import bauiv1 as bui
|
|
|
|
if TYPE_CHECKING:
|
|
from typing import Any, Sequence, Callable
|
|
|
|
|
|
class PopupWindow:
|
|
"""A transient window that positions and scales itself for visibility.
|
|
|
|
Category: UI Classes"""
|
|
|
|
def __init__(
|
|
self,
|
|
position: tuple[float, float],
|
|
size: tuple[float, float],
|
|
scale: float = 1.0,
|
|
offset: tuple[float, float] = (0, 0),
|
|
bg_color: tuple[float, float, float] = (0.35, 0.55, 0.15),
|
|
focus_position: tuple[float, float] = (0, 0),
|
|
focus_size: tuple[float, float] | None = None,
|
|
toolbar_visibility: str = 'menu_minimal_no_back',
|
|
):
|
|
# pylint: disable=too-many-locals
|
|
if focus_size is None:
|
|
focus_size = size
|
|
|
|
# In vr mode we can't have windows going outside the screen.
|
|
if bui.app.env.vr:
|
|
focus_size = size
|
|
focus_position = (0, 0)
|
|
|
|
width = focus_size[0]
|
|
height = focus_size[1]
|
|
|
|
# Ok, we've been given a desired width, height, and scale;
|
|
# we now need to ensure that we're all onscreen by scaling down if
|
|
# need be and clamping it to the UI bounds.
|
|
bounds = bui.uibounds()
|
|
edge_buffer = 15
|
|
bounds_width = bounds[1] - bounds[0] - edge_buffer * 2
|
|
bounds_height = bounds[3] - bounds[2] - edge_buffer * 2
|
|
|
|
fin_width = width * scale
|
|
fin_height = height * scale
|
|
if fin_width > bounds_width:
|
|
scale /= fin_width / bounds_width
|
|
fin_width = width * scale
|
|
fin_height = height * scale
|
|
if fin_height > bounds_height:
|
|
scale /= fin_height / bounds_height
|
|
fin_width = width * scale
|
|
fin_height = height * scale
|
|
|
|
x_min = bounds[0] + edge_buffer + fin_width * 0.5
|
|
y_min = bounds[2] + edge_buffer + fin_height * 0.5
|
|
x_max = bounds[1] - edge_buffer - fin_width * 0.5
|
|
y_max = bounds[3] - edge_buffer - fin_height * 0.5
|
|
|
|
x_fin = min(max(x_min, position[0] + offset[0]), x_max)
|
|
y_fin = min(max(y_min, position[1] + offset[1]), y_max)
|
|
|
|
# ok, we've calced a valid x/y position and a scale based on or
|
|
# focus area. ..now calc the difference between the center of our
|
|
# focus area and the center of our window to come up with the
|
|
# offset we'll need to plug in to the window
|
|
x_offs = (
|
|
(focus_position[0] + focus_size[0] * 0.5) - (size[0] * 0.5)
|
|
) * scale
|
|
y_offs = (
|
|
(focus_position[1] + focus_size[1] * 0.5) - (size[1] * 0.5)
|
|
) * scale
|
|
|
|
self.root_widget = bui.containerwidget(
|
|
transition='in_scale',
|
|
scale=scale,
|
|
toolbar_visibility=toolbar_visibility,
|
|
size=size,
|
|
parent=bui.get_special_widget('overlay_stack'),
|
|
stack_offset=(x_fin - x_offs, y_fin - y_offs),
|
|
scale_origin_stack_offset=(position[0], position[1]),
|
|
on_outside_click_call=self.on_popup_cancel,
|
|
claim_outside_clicks=True,
|
|
color=bg_color,
|
|
on_cancel_call=self.on_popup_cancel,
|
|
)
|
|
# complain if we outlive our root widget
|
|
bui.uicleanupcheck(self, self.root_widget)
|
|
|
|
def on_popup_cancel(self) -> None:
|
|
"""Called when the popup is canceled.
|
|
|
|
Cancels can occur due to clicking outside the window,
|
|
hitting escape, etc.
|
|
"""
|
|
|
|
|
|
class PopupMenuWindow(PopupWindow):
|
|
"""A menu built using popup-window functionality."""
|
|
|
|
def __init__(
|
|
self,
|
|
position: tuple[float, float],
|
|
choices: Sequence[str],
|
|
current_choice: str,
|
|
delegate: Any = None,
|
|
width: float = 230.0,
|
|
maxwidth: float | None = None,
|
|
scale: float = 1.0,
|
|
choices_disabled: Sequence[str] | None = None,
|
|
choices_display: Sequence[bui.Lstr] | None = None,
|
|
):
|
|
# FIXME: Clean up a bit.
|
|
# pylint: disable=too-many-branches
|
|
# pylint: disable=too-many-locals
|
|
# pylint: disable=too-many-statements
|
|
if choices_disabled is None:
|
|
choices_disabled = []
|
|
if choices_display is None:
|
|
choices_display = []
|
|
|
|
# FIXME: For the moment we base our width on these strings so
|
|
# we need to flatten them.
|
|
choices_display_fin: list[str] = []
|
|
for choice_display in choices_display:
|
|
choices_display_fin.append(choice_display.evaluate())
|
|
|
|
if maxwidth is None:
|
|
maxwidth = width * 1.5
|
|
|
|
self._transitioning_out = False
|
|
self._choices = list(choices)
|
|
self._choices_display = list(choices_display_fin)
|
|
self._current_choice = current_choice
|
|
self._choices_disabled = list(choices_disabled)
|
|
self._done_building = False
|
|
if not choices:
|
|
raise TypeError('Must pass at least one choice')
|
|
self._width = width
|
|
self._scale = scale
|
|
if len(choices) > 8:
|
|
self._height = 280
|
|
self._use_scroll = True
|
|
else:
|
|
self._height = 20 + len(choices) * 33
|
|
self._use_scroll = False
|
|
self._delegate = None # don't want this stuff called just yet..
|
|
|
|
# extend width to fit our longest string (or our max-width)
|
|
for index, choice in enumerate(choices):
|
|
if len(choices_display_fin) == len(choices):
|
|
choice_display_name = choices_display_fin[index]
|
|
else:
|
|
choice_display_name = choice
|
|
if self._use_scroll:
|
|
self._width = max(
|
|
self._width,
|
|
min(
|
|
maxwidth,
|
|
bui.get_string_width(
|
|
choice_display_name, suppress_warning=True
|
|
),
|
|
)
|
|
+ 75,
|
|
)
|
|
else:
|
|
self._width = max(
|
|
self._width,
|
|
min(
|
|
maxwidth,
|
|
bui.get_string_width(
|
|
choice_display_name, suppress_warning=True
|
|
),
|
|
)
|
|
+ 60,
|
|
)
|
|
|
|
# init parent class - this will rescale and reposition things as
|
|
# needed and create our root widget
|
|
super().__init__(
|
|
position, size=(self._width, self._height), scale=self._scale
|
|
)
|
|
|
|
if self._use_scroll:
|
|
self._scrollwidget = bui.scrollwidget(
|
|
parent=self.root_widget,
|
|
position=(20, 20),
|
|
highlight=False,
|
|
color=(0.35, 0.55, 0.15),
|
|
size=(self._width - 40, self._height - 40),
|
|
)
|
|
self._columnwidget = bui.columnwidget(
|
|
parent=self._scrollwidget, border=2, margin=0
|
|
)
|
|
else:
|
|
self._offset_widget = bui.containerwidget(
|
|
parent=self.root_widget,
|
|
position=(30, 15),
|
|
size=(self._width - 40, self._height),
|
|
background=False,
|
|
)
|
|
self._columnwidget = bui.columnwidget(
|
|
parent=self._offset_widget, border=2, margin=0
|
|
)
|
|
for index, choice in enumerate(choices):
|
|
if len(choices_display_fin) == len(choices):
|
|
choice_display_name = choices_display_fin[index]
|
|
else:
|
|
choice_display_name = choice
|
|
inactive = choice in self._choices_disabled
|
|
wdg = bui.textwidget(
|
|
parent=self._columnwidget,
|
|
size=(self._width - 40, 28),
|
|
on_select_call=bui.Call(self._select, index),
|
|
click_activate=True,
|
|
color=(0.5, 0.5, 0.5, 0.5)
|
|
if inactive
|
|
else (
|
|
(0.5, 1, 0.5, 1)
|
|
if choice == self._current_choice
|
|
else (0.8, 0.8, 0.8, 1.0)
|
|
),
|
|
padding=0,
|
|
maxwidth=maxwidth,
|
|
text=choice_display_name,
|
|
on_activate_call=self._activate,
|
|
v_align='center',
|
|
selectable=(not inactive),
|
|
)
|
|
if choice == self._current_choice:
|
|
bui.containerwidget(
|
|
edit=self._columnwidget,
|
|
selected_child=wdg,
|
|
visible_child=wdg,
|
|
)
|
|
|
|
# ok from now on our delegate can be called
|
|
self._delegate = weakref.ref(delegate)
|
|
self._done_building = True
|
|
|
|
def _select(self, index: int) -> None:
|
|
if self._done_building:
|
|
self._current_choice = self._choices[index]
|
|
|
|
def _activate(self) -> None:
|
|
bui.getsound('swish').play()
|
|
bui.apptimer(0.05, self._transition_out)
|
|
delegate = self._getdelegate()
|
|
if delegate is not None:
|
|
# Call this in a timer so it doesn't interfere with us killing
|
|
# our widgets and whatnot.
|
|
call = bui.Call(
|
|
delegate.popup_menu_selected_choice, self, self._current_choice
|
|
)
|
|
bui.apptimer(0, call)
|
|
|
|
def _getdelegate(self) -> Any:
|
|
return None if self._delegate is None else self._delegate()
|
|
|
|
def _transition_out(self) -> None:
|
|
if not self.root_widget:
|
|
return
|
|
if not self._transitioning_out:
|
|
self._transitioning_out = True
|
|
delegate = self._getdelegate()
|
|
if delegate is not None:
|
|
delegate.popup_menu_closing(self)
|
|
bui.containerwidget(edit=self.root_widget, transition='out_scale')
|
|
|
|
def on_popup_cancel(self) -> None:
|
|
if not self._transitioning_out:
|
|
bui.getsound('swish').play()
|
|
self._transition_out()
|
|
|
|
|
|
class PopupMenu:
|
|
"""A complete popup-menu control.
|
|
|
|
This creates a button and wrangles its pop-up menu.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
parent: bui.Widget,
|
|
position: tuple[float, float],
|
|
choices: Sequence[str],
|
|
current_choice: str | None = None,
|
|
on_value_change_call: Callable[[str], Any] | None = None,
|
|
opening_call: Callable[[], Any] | None = None,
|
|
closing_call: Callable[[], Any] | None = None,
|
|
width: float = 230.0,
|
|
maxwidth: float | None = None,
|
|
scale: float | None = None,
|
|
choices_disabled: Sequence[str] | None = None,
|
|
choices_display: Sequence[bui.Lstr] | None = None,
|
|
button_size: tuple[float, float] = (160.0, 50.0),
|
|
autoselect: bool = True,
|
|
):
|
|
# pylint: disable=too-many-locals
|
|
if choices_disabled is None:
|
|
choices_disabled = []
|
|
if choices_display is None:
|
|
choices_display = []
|
|
assert bui.app.classic is not None
|
|
uiscale = bui.app.ui_v1.uiscale
|
|
if scale is None:
|
|
scale = (
|
|
2.3
|
|
if uiscale is bui.UIScale.SMALL
|
|
else 1.65
|
|
if uiscale is bui.UIScale.MEDIUM
|
|
else 1.23
|
|
)
|
|
if current_choice not in choices:
|
|
current_choice = None
|
|
self._choices = list(choices)
|
|
if not choices:
|
|
raise TypeError('no choices given')
|
|
self._choices_display = list(choices_display)
|
|
self._choices_disabled = list(choices_disabled)
|
|
self._width = width
|
|
self._maxwidth = maxwidth
|
|
self._scale = scale
|
|
self._current_choice = (
|
|
current_choice if current_choice is not None else self._choices[0]
|
|
)
|
|
self._position = position
|
|
self._parent = parent
|
|
if not choices:
|
|
raise TypeError('Must pass at least one choice')
|
|
self._parent = parent
|
|
self._button_size = button_size
|
|
|
|
self._button = bui.buttonwidget(
|
|
parent=self._parent,
|
|
position=(self._position[0], self._position[1]),
|
|
autoselect=autoselect,
|
|
size=self._button_size,
|
|
scale=1.0,
|
|
label='',
|
|
on_activate_call=lambda: bui.apptimer(0, self._make_popup),
|
|
)
|
|
self._on_value_change_call = None # Don't wanna call for initial set.
|
|
self._opening_call = opening_call
|
|
self._autoselect = autoselect
|
|
self._closing_call = closing_call
|
|
self.set_choice(self._current_choice)
|
|
self._on_value_change_call = on_value_change_call
|
|
self._window_widget: bui.Widget | None = None
|
|
|
|
# Complain if we outlive our button.
|
|
bui.uicleanupcheck(self, self._button)
|
|
|
|
def _make_popup(self) -> None:
|
|
if not self._button:
|
|
return
|
|
if self._opening_call:
|
|
self._opening_call()
|
|
self._window_widget = PopupMenuWindow(
|
|
position=self._button.get_screen_space_center(),
|
|
delegate=self,
|
|
width=self._width,
|
|
maxwidth=self._maxwidth,
|
|
scale=self._scale,
|
|
choices=self._choices,
|
|
current_choice=self._current_choice,
|
|
choices_disabled=self._choices_disabled,
|
|
choices_display=self._choices_display,
|
|
).root_widget
|
|
|
|
def get_button(self) -> bui.Widget:
|
|
"""Return the menu's button widget."""
|
|
return self._button
|
|
|
|
def get_window_widget(self) -> bui.Widget | None:
|
|
"""Return the menu's window widget (or None if nonexistent)."""
|
|
return self._window_widget
|
|
|
|
def popup_menu_selected_choice(
|
|
self, popup_window: PopupWindow, choice: str
|
|
) -> None:
|
|
"""Called when a choice is selected."""
|
|
del popup_window # Unused here.
|
|
self.set_choice(choice)
|
|
if self._on_value_change_call:
|
|
self._on_value_change_call(choice)
|
|
|
|
def popup_menu_closing(self, popup_window: PopupWindow) -> None:
|
|
"""Called when the menu is closing."""
|
|
del popup_window # Unused here.
|
|
if self._button:
|
|
bui.containerwidget(edit=self._parent, selected_child=self._button)
|
|
self._window_widget = None
|
|
if self._closing_call:
|
|
self._closing_call()
|
|
|
|
def set_choice(self, choice: str) -> None:
|
|
"""Set the selected choice."""
|
|
self._current_choice = choice
|
|
displayname: str | bui.Lstr
|
|
if len(self._choices_display) == len(self._choices):
|
|
displayname = self._choices_display[self._choices.index(choice)]
|
|
else:
|
|
displayname = choice
|
|
if self._button:
|
|
bui.buttonwidget(edit=self._button, label=displayname)
|