# Released under the MIT License. See LICENSE for details.
#
"""Misc UI related types."""
from __future__ import annotations # Docs-generation hack.
import os
import time
import inspect
import warnings
from typing import TYPE_CHECKING, override
import babase
import _bauiv1
if TYPE_CHECKING:
from typing import Any, Type, Literal, Callable
import bauiv1
[docs]
def snap_slider_value(
value: float, *, min_value: float, max_value: float, increment: float
) -> float:
"""A :func:`bauiv1.sliderwidget` value, cleaned up for storing.
The widget works in single precision and snaps to its increment
grid in that arithmetic, so it reports things like
``0.800000011920929`` for the ``0.8`` a user picked. This returns
the value at the same grid point in the decimal form the user
would write, clamped to the range -- what config files and page
state should hold. Every consumer of slider values goes through
here so they agree on it.
An ``increment`` of zero or less means no grid; the value is
only clamped.
"""
if increment <= 0.0:
snapped = value
else:
steps = round((value - min_value) / increment)
# (Nine decimals: past single precision's own noise but well
# short of double's, so the grid value comes out exact.)
snapped = round(min_value + steps * increment, 9)
return float(min(max_value, max(min_value, snapped)))
# REMOVE WHEN API 9 SUPPORT ENDS
[docs]
def uicleanupcheck(obj: Any, widget: bauiv1.Widget) -> None:
"""
.. deprecated:: 1.7.51
Use :meth:`UIV1AppSubsystem.add_ui_cleanup_check()`.
Will be removed when api 9 support ends.
"""
warnings.warn(
'bauiv1.uicleanupcheck() will be removed when api 9 support ends;'
' use ba*.app.ui_v1.add_ui_cleanup_check() instead.',
DeprecationWarning,
stacklevel=2,
)
babase.app.ui_v1.add_ui_cleanup_check(obj, widget)
class TextWidgetStringEditAdapter(babase.StringEditAdapter):
"""A StringEditAdapter subclass for editing our text widgets."""
def __init__(self, text_widget: bauiv1.Widget) -> None:
self.widget = text_widget
# Ugly hacks to pull values from widgets. Really need to clean
# up that api.
description: Any = _bauiv1.textwidget(query_description=text_widget)
assert isinstance(description, str)
initial_text: Any = _bauiv1.textwidget(query=text_widget)
assert isinstance(initial_text, str)
max_length: Any = _bauiv1.textwidget(query_max_chars=text_widget)
assert isinstance(max_length, int)
is_password: Any = _bauiv1.textwidget(query_password=text_widget)
assert isinstance(is_password, bool)
kind_val: Any = _bauiv1.textwidget(query_string_edit_kind=text_widget)
assert isinstance(kind_val, str)
screen_space_center = text_widget.get_screen_space_center()
super().__init__(
description,
initial_text,
max_length,
screen_space_center,
is_password=is_password,
kind=babase.StringEditKind(kind_val),
)
@override
def _do_apply(self, new_text: str) -> None:
if self.widget:
_bauiv1.textwidget(
edit=self.widget, text=new_text, adapter_finished=True
)
@override
def _do_cancel(self) -> None:
if self.widget:
_bauiv1.textwidget(edit=self.widget, adapter_finished=True)
@override
def _do_submit(self) -> None:
if self.widget:
_bauiv1.textwidget(edit=self.widget, invoke_submit=True)
[docs]
class RootUIUpdatePause:
"""Pauses updates to the root-ui while in existence.
Instances are expected to be short-lived (covering an animation or
a server round-trip). Long holds get logged as warnings naming the
creation site; a common cause is an instance caught in a reference
cycle, where its release waits on the next explicit gc pass instead
of happening immediately (see :class:`~babase.GarbageCollectionSubsystem`).
"""
#: Holds at or beyond this many seconds log a warning at release.
LONG_HOLD_WARN_THRESHOLD = 10.0
def __init__(self) -> None:
# Note where we were created, for long-hold warnings. We pull
# simple strings out of the calling frame and immediately drop
# it; storing the frame itself would create exactly the sort of
# reference cycle that makes these holds go long.
frame = inspect.currentframe()
caller = frame.f_back if frame is not None else None
if caller is not None:
self._created_at = (
f'{caller.f_code.co_qualname}'
f' ({os.path.basename(caller.f_code.co_filename)}'
f':{caller.f_lineno})'
)
else:
self._created_at = '<unknown>'
del caller
del frame
self._created_time = time.monotonic()
_bauiv1.root_ui_pause_updates()
def __del__(self) -> None:
_bauiv1.root_ui_resume_updates()
held = time.monotonic() - self._created_time
if held >= self.LONG_HOLD_WARN_THRESHOLD:
babase.uilog.warning(
'RootUIUpdatePause created by %s was held %.1f seconds;'
' expected to be brief. A hold ending only when gc runs'
' implies the instance was stuck in a reference cycle.',
self._created_at,
held,
)
[docs]
class UIOpenState:
"""Keeps ui informed that something is open.
Generally instances of this are assigned as a class member of some
UI class, which will then keep the UI informed upon its death.
It is valid to have multiple states for one tag; the UI will keep a
tally.
"""
__slots__ = ['stateid', '_dormant']
def __init__(self, stateid: str) -> None:
self.stateid = stateid
self._dormant = False
_bauiv1.ui_open_state_change(self.stateid, 1)
@property
def dormant(self) -> bool:
"""Whether this state is currently not counting as open."""
return self._dormant
[docs]
def set_dormant(self, dormant: bool) -> None:
"""Stop (or resume) counting as open while staying alive.
For states held somewhere that isn't actually showing anything,
such as a saved UI state waiting to be restored after a game.
"""
if dormant == self._dormant:
return
self._dormant = dormant
_bauiv1.ui_open_state_change(self.stateid, -1 if dormant else 1)
def __del__(self) -> None:
if not self._dormant:
_bauiv1.ui_open_state_change(self.stateid, -1)
# Docs-generation hack; import some stuff that we likely only forward-declared
# in our actual source code so that docs tools can find it.
from typing import (Coroutine, Any, Literal, Callable,
Generator, Awaitable, Sequence, Self, TypeIs)
import asyncio
from concurrent.futures import Future
from pathlib import Path
from enum import Enum