# Released under the MIT License. See LICENSE for details.
#
"""UI for top level settings categories."""
from __future__ import annotations
from typing import TYPE_CHECKING, override
import logging
import bauiv1 as bui
if TYPE_CHECKING:
from typing import Callable
[docs]
class AllSettingsWindow(bui.MainWindow):
"""Window for selecting a settings category."""
def __init__(
self,
transition: str | None = 'in_right',
origin_widget: bui.Widget | None = None,
):
# pylint: disable=too-many-locals
# Preload some modules we use in a background thread so we won't
# have a visual hitch when the user taps them.
bui.app.threadpool.submit_no_wait(self._preload_modules)
bui.set_analytics_screen('Settings Window')
assert bui.app.classic is not None
uiscale = bui.app.ui_v1.uiscale
width = 1000 if uiscale is bui.UIScale.SMALL else 900
height = 800 if uiscale is bui.UIScale.SMALL else 450
self._r = 'settingsWindow'
uiscale = bui.app.ui_v1.uiscale
# Do some fancy math to fill all available screen area up to the
# size of our backing container. This lets us fit to the exact
# screen shape at small ui scale.
screensize = bui.get_virtual_screen_size()
safesize = bui.get_virtual_safe_area_size()
# We're a generally widescreen shaped window, so bump our
# overall scale up a bit when screen width is wider than safe
# bounds to take advantage of the extra space.
smallscale = min(2.0, 1.5 * screensize[0] / safesize[0])
scale = (
smallscale
if uiscale is bui.UIScale.SMALL
else 1.1 if uiscale is bui.UIScale.MEDIUM else 0.8
)
# Calc screen size in our local container space and clamp to a
# bit smaller than our container size.
target_height = min(height - 70, screensize[1] / scale)
# To get top/left coords, go to the center of our window and
# offset by half the width/height of our target area.
yoffs = 0.5 * height + 0.5 * target_height + 30.0
# scroll_width = target_width
# scroll_height = target_height - 25
# scroll_bottom = yoffs - 54 - scroll_height
super().__init__(
root_widget=bui.containerwidget(
size=(width, height),
toolbar_visibility=(
'menu_minimal'
if uiscale is bui.UIScale.SMALL
else 'menu_full'
),
scale=scale,
),
transition=transition,
origin_widget=origin_widget,
# We're affected by screen size only at small ui-scale.
refresh_on_screen_size_changes=uiscale is bui.UIScale.SMALL,
)
if uiscale is bui.UIScale.SMALL:
self._back_button = None
bui.containerwidget(
edit=self._root_widget, on_cancel_call=self.main_window_back
)
else:
self._back_button = btn = bui.buttonwidget(
parent=self._root_widget,
autoselect=True,
position=(50, yoffs - 80.0),
size=(70, 70),
scale=0.8,
text_scale=1.2,
label=bui.charstr(bui.SpecialChar.BACK),
button_type='backSmall',
on_activate_call=self.main_window_back,
)
bui.containerwidget(edit=self._root_widget, cancel_button=btn)
bui.textwidget(
parent=self._root_widget,
position=(0, yoffs - (70 if uiscale is bui.UIScale.SMALL else 60)),
size=(width, 25),
text=bui.Lstr(resource=f'{self._r}.titleText'),
color=bui.app.ui_v1.title_color,
h_align='center',
v_align='center',
scale=1.1,
maxwidth=130,
)
bwidth = 200
bheight = 230
margin = 1
all_buttons_width = 4.0 * bwidth + 3.0 * margin
x = width * 0.5 - all_buttons_width * 0.5
y = height * 0.5 - bheight * 0.5 - 20.0
def _button(
position: tuple[float, float],
label: bui.Lstr,
call: Callable[[], None],
texture: bui.Texture,
imgsize: float,
*,
color: tuple[float, float, float] = (1.0, 1.0, 1.0),
imgoffs: tuple[float, float] = (0.0, 0.0),
) -> bui.Widget:
x, y = position
btn = bui.buttonwidget(
parent=self._root_widget,
autoselect=True,
position=(x, y),
size=(bwidth, bheight),
button_type='square',
label='',
on_activate_call=call,
)
bui.textwidget(
parent=self._root_widget,
text=label,
position=(x + bwidth * 0.5, y + bheight * 0.25),
maxwidth=bwidth * 0.7,
size=(0, 0),
h_align='center',
v_align='center',
draw_controller=btn,
color=(0.7, 0.9, 0.7, 1.0),
)
bui.imagewidget(
parent=self._root_widget,
position=(
x + bwidth * 0.5 - imgsize * 0.5 + imgoffs[0],
y + bheight * 0.56 - imgsize * 0.5 + imgoffs[1],
),
size=(imgsize, imgsize),
texture=texture,
draw_controller=btn,
color=color,
)
return btn
self._controllers_button = _button(
position=(x, y),
label=bui.Lstr(resource=f'{self._r}.controllersText'),
call=self._do_controllers,
texture=bui.gettexture('controllerIcon'),
imgsize=150,
imgoffs=(-2.0, 2.0),
)
x += bwidth + margin
self._graphics_button = _button(
position=(x, y),
label=bui.Lstr(resource=f'{self._r}.graphicsText'),
call=self._do_graphics,
texture=bui.gettexture('graphicsIcon'),
imgsize=135,
imgoffs=(0, 4.0),
)
x += bwidth + margin
self._audio_button = _button(
position=(x, y),
label=bui.Lstr(resource=f'{self._r}.audioText'),
call=self._do_audio,
texture=bui.gettexture('audioIcon'),
imgsize=150,
color=(1, 1, 0),
)
x += bwidth + margin
self._advanced_button = _button(
position=(x, y),
label=bui.Lstr(resource=f'{self._r}.advancedText'),
call=self._do_advanced,
texture=bui.gettexture('advancedIcon'),
imgsize=150,
color=(0.8, 0.95, 1),
imgoffs=(0, 5.0),
)
# Hmm; we're now wide enough that being limited to pressing up
# might be ok.
if bool(False):
# Left from our leftmost button should go to back button.
if self._back_button is None:
bbtn = bui.get_special_widget('back_button')
bui.widget(edit=self._controllers_button, left_widget=bbtn)
# Right from our rightmost widget should go to squad button.
bui.widget(
edit=self._advanced_button,
right_widget=bui.get_special_widget('squad_button'),
)
self._restore_state()
[docs]
@override
def get_main_window_state(self) -> bui.MainWindowState:
# Support recreating our window for back/refresh purposes.
cls = type(self)
return bui.BasicMainWindowState(
create_call=lambda transition, origin_widget: cls(
transition=transition, origin_widget=origin_widget
)
)
[docs]
@override
def on_main_window_close(self) -> None:
self._save_state()
@staticmethod
def _preload_modules() -> None:
"""Preload modules we use; avoids hitches (called in bg thread)."""
import bauiv1lib.mainmenu as _unused1
import bauiv1lib.settings.controls as _unused2
import bauiv1lib.settings.graphics as _unused3
import bauiv1lib.settings.audio as _unused4
import bauiv1lib.settings.advanced as _unused5
def _do_controllers(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.settings.controls import ControlsSettingsWindow
# no-op if we're not in control.
if not self.main_window_has_control():
return
self.main_window_replace(
ControlsSettingsWindow(origin_widget=self._controllers_button)
)
def _do_graphics(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.settings.graphics import GraphicsSettingsWindow
# no-op if we're not in control.
if not self.main_window_has_control():
return
self.main_window_replace(
GraphicsSettingsWindow(origin_widget=self._graphics_button)
)
def _do_audio(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.settings.audio import AudioSettingsWindow
# no-op if we're not in control.
if not self.main_window_has_control():
return
self.main_window_replace(
AudioSettingsWindow(origin_widget=self._audio_button)
)
def _do_advanced(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.settings.advanced import AdvancedSettingsWindow
# no-op if we're not in control.
if not self.main_window_has_control():
return
self.main_window_replace(
AdvancedSettingsWindow(origin_widget=self._advanced_button)
)
def _save_state(self) -> None:
try:
sel = self._root_widget.get_selected_child()
if sel == self._controllers_button:
sel_name = 'Controllers'
elif sel == self._graphics_button:
sel_name = 'Graphics'
elif sel == self._audio_button:
sel_name = 'Audio'
elif sel == self._advanced_button:
sel_name = 'Advanced'
elif sel == self._back_button:
sel_name = 'Back'
else:
raise ValueError(f'unrecognized selection \'{sel}\'')
assert bui.app.classic is not None
bui.app.ui_v1.window_states[type(self)] = {'sel_name': sel_name}
except Exception:
logging.exception('Error saving state for %s.', self)
def _restore_state(self) -> None:
try:
assert bui.app.classic is not None
sel_name = bui.app.ui_v1.window_states.get(type(self), {}).get(
'sel_name'
)
sel: bui.Widget | None
if sel_name == 'Controllers':
sel = self._controllers_button
elif sel_name == 'Graphics':
sel = self._graphics_button
elif sel_name == 'Audio':
sel = self._audio_button
elif sel_name == 'Advanced':
sel = self._advanced_button
elif sel_name == 'Back':
sel = self._back_button
else:
sel = self._controllers_button
if sel is not None:
bui.containerwidget(edit=self._root_widget, selected_child=sel)
except Exception:
logging.exception('Error restoring state for %s.', self)
# 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)
import asyncio
from concurrent.futures import Future
from pathlib import Path
from enum import Enum