# Released under the MIT License. See LICENSE for details.
#
"""The top level settings window, as a doc-ui page.
A client-local doc-ui domain: one centered row of big buttons leading
to the controls, graphics, audio and advanced settings pages; opened
by the toolbar's settings button. (Replaces the legacy
``AllSettingsWindow``.)
"""
from __future__ import annotations # Docs-generation hack.
from enum import Enum
from dataclasses import dataclass
from typing import TYPE_CHECKING, Annotated, override, assert_never
from efro.dataclassio import ioprepped, IOAttrs
import bacommon.docui.v2 as dui2
from bacommon.docui.routes import (
DocUIRoute,
DocUILocalActionBase,
PressSound,
family_members,
)
import bauiv1 as bui
from bauiv1 import _classicassets
from bauiv1lib.docui import TypedDocUIController
if TYPE_CHECKING:
from typing import Literal
from bacommon.docui import DocUIResponse
from bacommon.langstr import LangStrSpec
from bacommon.assetspec import TextureSpec
from bauiv1lib.docui import DocUILocalAction
#: The buttons' unscaled size (the legacy window's), and their scale
#: and spacing: four just fill the wide layout's page width without
#: scrolling (the same total width as the play window's row).
_BW = 200.0
_BH = 230.0
_BUTTON_SCALE = 0.97
_BUTTON_SPACING = 20.0
#: How far below center the button row sits (space added above and
#: taken from below, so the page still fits without scrolling); reads
#: better under the title.
_ROW_DROP = 20.0
#: Labels sit a quarter of the way up the button and icons centered a
#: bit above middle, as in the legacy window. (Everything is centered
#: horizontally; the legacy window's small sideways icon nudges were
#: calibrated for its old, unevenly-fitting backing graphic.)
_LABEL_Y = _BH * 0.25 - _BH * 0.5
_ICON_Y = _BH * 0.56 - _BH * 0.5
_LABEL_COLOR = (0.7, 0.9, 0.7, 1.0)
[docs]
class Target(Enum):
"""Where the page's buttons lead."""
CONTROLLERS = 'controllers'
GRAPHICS = 'graphics'
AUDIO = 'audio'
ADVANCED = 'advanced'
[docs]
class SettingsRoute(DocUIRoute):
"""Family class for the settings routes."""
[docs]
@override
@classmethod
def get_route_types(cls) -> tuple[type[DocUIRoute], ...]:
return family_members(AnySettingsRoute)
[docs]
@override
@classmethod
def get_window_layout(cls) -> dui2.WindowLayout:
return dui2.WindowLayout.WIDE
[docs]
@ioprepped
@dataclass
class Root(SettingsRoute, path='/'):
"""The top level settings page."""
AnySettingsRoute = Root
[docs]
class SettingsLocalAction(DocUILocalActionBase):
"""Family class for the settings local-actions."""
[docs]
@override
@classmethod
def get_action_types(cls) -> tuple[type[DocUILocalActionBase], ...]:
return family_members(AnySettingsLocalAction)
[docs]
@ioprepped
@dataclass
class Open(SettingsLocalAction, name='open'):
"""Go to a settings category."""
[docs]
@override
@classmethod
def get_press_sound(cls) -> PressSound:
return PressSound.SWISH
target: Target
AnySettingsLocalAction = Open
[docs]
class AllSettingsController(
TypedDocUIController[AnySettingsRoute, AnySettingsLocalAction]
):
"""Doc-ui controller for the top level settings page."""
[docs]
@override
@classmethod
def get_route_type(cls) -> type[SettingsRoute]:
return SettingsRoute
[docs]
@override
@classmethod
def get_local_action_type(cls) -> type[SettingsLocalAction]:
return SettingsLocalAction
[docs]
@override
def fulfill_route(self, route: AnySettingsRoute) -> DocUIResponse:
match route:
case Root():
_preload_modules()
return _page()
case _:
assert_never(route)
[docs]
@override
def run_local_action(
self, action: AnySettingsLocalAction, context: DocUILocalAction
) -> None:
match action:
case Open():
_open(action.target, context)
case _:
assert_never(action)
def _preload_modules() -> None:
"""Preload modules our buttons lead to (avoids hitches)."""
# pylint: disable=cyclic-import
import bauiv1lib.settings.controls as _unused1
import bauiv1lib.settings.graphics as _unused2
import bauiv1lib.settings.audio as _unused3
import bauiv1lib.settings.advanced as _unused4
def _page() -> dui2.Response:
"""Build the page (called in a background thread)."""
strs = _classicassets.strings.settings
tex = _classicassets.textures
return dui2.Response(
page=dui2.Page(
title=strs.title.spec,
center_vertically=True,
rows=[
dui2.ButtonRow(
content_align=dui2.HAlign.CENTER,
button_spacing=_BUTTON_SPACING,
# Every bit of width goes to the buttons.
padding_left=0.0,
padding_right=0.0,
spacing_top=_ROW_DROP,
spacing_bottom=-_ROW_DROP,
buttons=[
_button(
Target.CONTROLLERS,
strs.controllers.title.spec,
tex.controller_icon,
icon_size=150.0,
icon_raise=2.0,
selected=True,
),
_button(
Target.GRAPHICS,
strs.graphics.title.spec,
tex.graphics_icon,
icon_size=135.0,
icon_raise=4.0,
),
_button(
Target.AUDIO,
strs.audio.title.spec,
tex.audio_icon,
icon_size=150.0,
icon_color=(1.0, 1.0, 0.0),
),
_button(
Target.ADVANCED,
strs.advanced.title.spec,
tex.advanced_icon,
icon_size=150.0,
icon_raise=5.0,
icon_color=(0.8, 0.95, 1.0),
),
],
)
],
)
)
def _button(
target: Target,
label: LangStrSpec,
icon: TextureSpec,
*,
icon_size: float,
icon_raise: float = 0.0,
icon_color: tuple[float, float, float] = (1.0, 1.0, 1.0),
selected: bool = False,
) -> dui2.Button:
return dui2.Button(
size=(_BW, _BH),
scale=_BUTTON_SCALE,
action=Open(target=target).local(),
widget_id=target.value,
selected=selected,
decorations=[
dui2.Image(
texture=icon,
position=(0.0, _ICON_Y + icon_raise),
size=(icon_size, icon_size),
color=(*icon_color, 1.0),
),
dui2.Text(
text=label,
position=(0.0, _LABEL_Y),
size=(_BW * 0.7, 40.0),
color=_LABEL_COLOR,
),
],
)
def _open(target: Target, context: DocUILocalAction) -> None:
# pylint: disable=cyclic-import
window = context.window
# No-op if we're not in control.
if not window.main_window_has_control():
return
match target:
case Target.CONTROLLERS:
from bauiv1lib.settings.controls import (
ControlsSettingsController,
Root as ControlsRoot,
)
window.main_window_replace(
lambda: ControlsSettingsController().create_window(
ControlsRoot(),
origin_widget=context.widget,
auxiliary_style=False,
),
extra_type_id=(
ControlsSettingsController.get_window_extra_type_id()
),
)
case Target.GRAPHICS:
from bauiv1lib.settings.graphics import (
GraphicsSettingsController,
Root as GraphicsRoot,
)
window.main_window_replace(
lambda: GraphicsSettingsController().create_window(
GraphicsRoot(),
origin_widget=context.widget,
auxiliary_style=False,
),
extra_type_id=(
GraphicsSettingsController.get_window_extra_type_id()
),
)
case Target.AUDIO:
from bauiv1lib.settings.audio import (
AudioSettingsController,
Root as AudioRoot,
)
window.main_window_replace(
lambda: AudioSettingsController().create_window(
AudioRoot(),
origin_widget=context.widget,
auxiliary_style=False,
),
extra_type_id=(
AudioSettingsController.get_window_extra_type_id()
),
)
case Target.ADVANCED:
from bauiv1lib.settings.advanced import (
AdvancedSettingsController,
Root as AdvancedRoot,
)
window.main_window_replace(
lambda: AdvancedSettingsController().create_window(
AdvancedRoot(),
origin_widget=context.widget,
auxiliary_style=False,
),
extra_type_id=(
AdvancedSettingsController.get_window_extra_type_id()
),
)
case _:
assert_never(target)
# 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