syncing with 1.7.53

This commit is contained in:
Ayush Saini 2025-10-21 19:08:49 +05:30
parent 4e5156073d
commit 06138dbc15
132 changed files with 4946 additions and 2561 deletions

View file

@ -142,6 +142,7 @@ from babase._apputils import (
)
from babase._cloud import CloudSubscription
from babase._devconsole import (
DevConsoleButtonDef,
DevConsoleTab,
DevConsoleTabEntry,
DevConsoleSubsystem,
@ -179,7 +180,14 @@ from babase._general import (
)
from babase._language import Lstr, LanguageSubsystem
from babase._locale import LocaleSubsystem
from babase._logging import balog, accountlog, applog, lifecyclelog, netlog
from babase._logging import (
balog,
accountlog,
applog,
lifecyclelog,
netlog,
uilog,
)
from babase._login import LoginAdapter, LoginInfo
from babase._mgen.enums import (
@ -250,6 +258,7 @@ __all__ = [
'ContextError',
'ContextRef',
'DelegateNotFoundError',
'DevConsoleButtonDef',
'DevConsoleTab',
'DevConsoleTabEntry',
'DevConsoleSubsystem',
@ -371,6 +380,7 @@ __all__ = [
'supports_unicode_display',
'TeamNotFoundError',
'timestring',
'uilog',
'UIScale',
'unlock_all_input',
'update_internal_logger_levels',

View file

@ -202,10 +202,16 @@ class App:
return _babase.app_is_active()
@property
def mode(self) -> AppMode | None:
"""The app's current mode."""
def mode(self) -> AppMode:
"""The app's current mode.
Raises :class:`ValueError` if no mode is set.
"""
assert _babase.in_logic_thread()
return self._mode
mode = self._mode
if mode is None:
raise ValueError('No app-mode set.')
return mode
@property
def asyncio_loop(self) -> asyncio.AbstractEventLoop:

View file

@ -6,7 +6,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from babase import AppIntent
from babase import AppIntent, DevConsoleButtonDef
class AppMode:
@ -125,3 +125,11 @@ class AppMode:
)
if babase.asset_loads_allowed():
babase.getsimplesound('cashRegister').play()
def get_dev_console_ui_tab_buttons(self) -> list[DevConsoleButtonDef]:
"""Define buttons to show up in the UI dev console.
This can be useful for exposing UI code examples or debugging
functionality.
"""
return []

View file

@ -14,6 +14,19 @@ if TYPE_CHECKING:
from typing import Callable, Any, Literal
class DevConsoleButtonDef:
"""A barebones way to define a custom button for the dev console.
Note that a :class:`DevConsoleTab` should use its
:meth:`DevConsoleTab.button()` method to create buttons; this is
instead for allowing basic customization.
"""
def __init__(self, name: str, call: Callable[[], Any]) -> None:
self.name = name
self.call = call
class DevConsoleTab:
"""Base class for a :class:`~babase.DevConsoleSubsystem` tab."""

View file

@ -119,14 +119,30 @@ class DevConsoleTabUI(DevConsoleTab):
def refresh(self) -> None:
from babase._mgen.enums import UIScale
xoffs = -305
yoffs = 10
xoffs = -305.0
yoffs = 10.0
custom_buttons = _babase.app.mode.get_dev_console_ui_tab_buttons()
cboffs = 50
cbwidth = 180
cbspacing = 10
if custom_buttons:
cbtotalwidth = (
len(custom_buttons) * cbwidth
+ max(0, len(custom_buttons) - 1) * cbspacing
+ cboffs
)
else:
cbtotalwidth = 0
xoffs -= cbtotalwidth * 0.5
self.text(
'A UI should either fit in the virtual safe area'
' or dynamically respond to screen size changes.',
scale=0.6,
pos=(xoffs + 15, yoffs + 65),
pos=(xoffs + 8, yoffs + 65),
h_align='left',
v_align='center',
)
@ -165,6 +181,19 @@ class DevConsoleTabUI(DevConsoleTab):
)
x += bwidth + 2
if custom_buttons:
x += cboffs
for custom_button in custom_buttons:
self.button(
custom_button.name,
pos=(xoffs + x, yoffs + 15),
size=(cbwidth, 40),
label_scale=0.6,
call=custom_button.call,
corner_radius=10.0,
)
x += cbwidth + cbspacing
def toggle_ui_overlay(self) -> None:
"""Toggle UI overlay drawing."""
_babase.set_draw_virtual_safe_area_bounds(
@ -544,12 +573,15 @@ class DevConsoleTabLogging(DevConsoleTab):
index = 0
effectivelevel = logger.getEffectiveLevel()
notsetname = 'Not Set'
bradius = 5.0
bspacing = 2.0
tab.button(
notsetname,
pos=(x + width - bwidth * 6.5 + xoffs + 1.0, y + 5.0),
size=(bwidth * 1.0 - 2.0, height - 10),
size=(bwidth * 1.0 - bspacing, height - 10),
label_scale=btextscale,
style='white_bright' if level == logging.NOTSET else 'black',
corner_radius=bradius,
call=partial(
self._set_entry_val, entry_index, entry, logging.NOTSET
),
@ -558,13 +590,14 @@ class DevConsoleTabLogging(DevConsoleTab):
tab.button(
'Debug',
pos=(x + width - bwidth * 5 + xoffs + 1.0, y + 5.0),
size=(bwidth - 2.0, height - 10),
size=(bwidth - bspacing, height - 10),
label_scale=btextscale,
style=(
'white_bright'
if level == logging.DEBUG
else 'blue' if effectivelevel <= logging.DEBUG else 'black'
),
corner_radius=bradius,
call=partial(
self._set_entry_val, entry_index, entry, logging.DEBUG
),
@ -573,26 +606,28 @@ class DevConsoleTabLogging(DevConsoleTab):
tab.button(
'Info',
pos=(x + width - bwidth * 4 + xoffs + 1.0, y + 5.0),
size=(bwidth - 2.0, height - 10),
size=(bwidth - bspacing, height - 10),
label_scale=btextscale,
style=(
'white_bright'
if level == logging.INFO
else 'white' if effectivelevel <= logging.INFO else 'black'
),
corner_radius=bradius,
call=partial(self._set_entry_val, entry_index, entry, logging.INFO),
)
index += 1
tab.button(
'Warning',
pos=(x + width - bwidth * 3 + xoffs + 1.0, y + 5.0),
size=(bwidth - 2.0, height - 10),
size=(bwidth - bspacing, height - 10),
label_scale=btextscale,
style=(
'white_bright'
if level == logging.WARNING
else 'yellow' if effectivelevel <= logging.WARNING else 'black'
),
corner_radius=bradius,
call=partial(
self._set_entry_val, entry_index, entry, logging.WARNING
),
@ -601,13 +636,14 @@ class DevConsoleTabLogging(DevConsoleTab):
tab.button(
'Error',
pos=(x + width - bwidth * 2 + xoffs + 1.0, y + 5.0),
size=(bwidth - 2.0, height - 10),
size=(bwidth - bspacing, height - 10),
label_scale=btextscale,
style=(
'white_bright'
if level == logging.ERROR
else 'red' if effectivelevel <= logging.ERROR else 'black'
),
corner_radius=bradius,
call=partial(
self._set_entry_val, entry_index, entry, logging.ERROR
),
@ -616,7 +652,7 @@ class DevConsoleTabLogging(DevConsoleTab):
tab.button(
'Critical',
pos=(x + width - bwidth * 1 + xoffs + 1.0, y + 5.0),
size=(bwidth - 2.0, height - 10),
size=(bwidth - bspacing, height - 10),
label_scale=btextscale,
style=(
'white_bright'
@ -625,6 +661,7 @@ class DevConsoleTabLogging(DevConsoleTab):
'purple' if effectivelevel <= logging.CRITICAL else 'black'
)
),
corner_radius=bradius,
call=partial(
self._set_entry_val, entry_index, entry, logging.CRITICAL
),

View file

@ -31,6 +31,7 @@ cloudsublog = logging.getLogger(ClientLoggerName.CLOUD_SUBSCRIPTION.value)
accountlog = logging.getLogger(ClientLoggerName.ACCOUNT.value)
accountclientv2log = logging.getLogger(ClientLoggerName.ACCOUNT_CLIENT_V2.value)
loginadapterlog = logging.getLogger(ClientLoggerName.LOGIN_ADAPTER.value)
uilog = logging.getLogger(ClientLoggerName.UI.value)
def description_for_logger(logger: str) -> str | None:

View file

@ -1,7 +1,6 @@
# Released under the MIT License. See LICENSE for details.
#
"""Contains ClassicAppMode."""
# pylint: disable=too-many-lines
from __future__ import annotations
@ -13,8 +12,8 @@ from typing import TYPE_CHECKING, override
from efro.error import CommunicationError
import bacommon.bs
import babase
import bauiv1
from babase import AppMode
import bauiv1 as bui
from bauiv1lib.connectivity import wait_for_connectivity
from bauiv1lib.account.signin import show_sign_in_prompt
@ -29,7 +28,7 @@ if TYPE_CHECKING:
# ba_meta export babase.AppMode
class ClassicAppMode(babase.AppMode):
class ClassicAppMode(AppMode):
"""AppMode for the classic BombSquad experience."""
_ACCOUNT_STATE_CONFIG_KEY = 'ClassicAccountState'
@ -42,17 +41,17 @@ class ClassicAppMode(babase.AppMode):
self._on_connectivity_changed_callback: CallbackRegistration | None = (
None
)
self._test_sub: babase.CloudSubscription | None = None
self._account_data_sub: babase.CloudSubscription | None = None
self._test_sub: bui.CloudSubscription | None = None
self._account_data_sub: bui.CloudSubscription | None = None
self._have_account_values = False
self._have_connectivity = False
self._current_account_id: str | None = None
self._purchase_ui_pause: bauiv1.RootUIUpdatePause | None = None
self._purchase_ui_pause: bui.RootUIUpdatePause | None = None
self._last_tokens_value = 0
self._purchases_update_timer: babase.AppTimer | None = None
self._purchases_update_timer: bui.AppTimer | None = None
self._purchase_request_in_flight = False
self._target_purchases_state: str | None = None
@ -62,18 +61,16 @@ class ClassicAppMode(babase.AppMode):
@override
@classmethod
def can_handle_intent(cls, intent: babase.AppIntent) -> bool:
def can_handle_intent(cls, intent: bui.AppIntent) -> bool:
# We support default and exec intents currently.
return isinstance(
intent, babase.AppIntentExec | babase.AppIntentDefault
)
return isinstance(intent, bui.AppIntentExec | bui.AppIntentDefault)
@override
def handle_intent(self, intent: babase.AppIntent) -> None:
if isinstance(intent, babase.AppIntentExec):
def handle_intent(self, intent: bui.AppIntent) -> None:
if isinstance(intent, bui.AppIntentExec):
_baclassic.classic_app_mode_handle_app_intent_exec(intent.code)
return
assert isinstance(intent, babase.AppIntentDefault)
assert isinstance(intent, bui.AppIntentDefault)
_baclassic.classic_app_mode_handle_app_intent_default()
@override
@ -82,7 +79,7 @@ class ClassicAppMode(babase.AppMode):
# Let the native layer do its thing.
_baclassic.classic_app_mode_activate()
app = babase.app
app = bui.app
plus = app.plus
assert plus is not None
@ -160,7 +157,7 @@ class ClassicAppMode(babase.AppMode):
@override
def on_deactivate(self) -> None:
classic = babase.app.classic
classic = bui.app.classic
# Store latest league vis vals for any active account.
self._save_account_state()
@ -183,11 +180,11 @@ class ClassicAppMode(babase.AppMode):
@override
def on_app_active_changed(self) -> None:
if not babase.app.active:
if not bui.app.active:
# If we're going inactive, ask for the main ui, which should
# have the side effect of pausing the action if we're in a
# game.
babase.request_main_ui()
bui.request_main_ui()
# Also store any league vis state for the active account.
# this may be our last chance to do this on mobile.
@ -208,13 +205,13 @@ class ClassicAppMode(babase.AppMode):
# need to explicitly kill this pause if we are deactivated since
# we wouldn't get the on_purchase_process_end() call; the next
# app-mode would.
self._purchase_ui_pause = bauiv1.RootUIUpdatePause()
self._purchase_ui_pause = bui.RootUIUpdatePause()
# Also grab our last known token count here to plug into animations.
# We need to do this here before the purchase gets submitted so that
# we know we're seeing the old value.
assert babase.app.classic is not None
self._last_tokens_value = babase.app.classic.tokens
assert bui.app.classic is not None
self._last_tokens_value = bui.app.classic.tokens
@override
def on_purchase_process_end(
@ -259,7 +256,7 @@ class ClassicAppMode(babase.AppMode):
'Unhandled item_id in on_purchase_process_end: %s', item_id
)
assert babase.app.classic is not None
assert bui.app.classic is not None
effects: list[bacommon.bs.ClientEffect] = [
bacommon.bs.ClientEffectTokensAnimation(
duration=anim_time,
@ -276,23 +273,23 @@ class ClassicAppMode(babase.AppMode):
sound=bacommon.bs.ClientEffectSound.Sound.CASH_REGISTER
),
]
babase.app.classic.run_bs_client_effects(effects)
bui.app.classic.run_bs_client_effects(effects)
elif item_id.startswith('gold_pass'):
babase.screenmessage(
babase.Lstr(
bui.screenmessage(
bui.Lstr(
translate=('serverResponses', 'You got a ${ITEM}!'),
subs=[
(
'${ITEM}',
babase.Lstr(resource='goldPass.goldPassText'),
bui.Lstr(resource='goldPass.goldPassText'),
)
],
),
color=(0, 1, 0),
)
if babase.asset_loads_allowed():
babase.getsimplesound('cashRegister').play()
if bui.asset_loads_allowed():
bui.getsound('cashRegister').play()
else:
@ -300,15 +297,15 @@ class ClassicAppMode(babase.AppMode):
logging.warning(
'on_purchase_process_end got unexpected item_id: %s.', item_id
)
babase.screenmessage(
babase.Lstr(
bui.screenmessage(
bui.Lstr(
translate=('serverResponses', 'You got a ${ITEM}!'),
subs=[('${ITEM}', item_id)],
),
color=(0, 1, 0),
)
if babase.asset_loads_allowed():
babase.getsimplesound('cashRegister').play()
if bui.asset_loads_allowed():
bui.getsound('cashRegister').play()
def on_engine_will_reset(self) -> None:
"""Called just before classic resets the engine.
@ -337,9 +334,9 @@ class ClassicAppMode(babase.AppMode):
return
self._purchase_request_in_flight = True
babase.accountlog.debug('Requesting latest purchases state...')
bui.accountlog.debug('Requesting latest purchases state...')
plus = babase.app.plus
plus = bui.app.plus
assert plus is not None
if plus.accounts.primary is None:
raise RuntimeError(
@ -349,7 +346,7 @@ class ClassicAppMode(babase.AppMode):
with plus.accounts.primary:
plus.cloud.send_message_cb(
bacommon.bs.GetClassicPurchasesMessage(),
on_response=babase.WeakCall(
on_response=bui.WeakCall(
self._on_get_classic_purchases_response
),
)
@ -365,12 +362,12 @@ class ClassicAppMode(babase.AppMode):
# No biggie; we expect these when offline/etc.
pass
else:
babase.netlog.exception('Error requesting classic purchases.')
bui.netlog.exception('Error requesting classic purchases.')
return
# If we're no longer looking for a state, we can abort early.
if self._target_purchases_state is None:
babase.accountlog.debug(
bui.accountlog.debug(
'No longer looking for new purchases state; aborting fetch.'
)
self._purchases_update_timer = None
@ -387,10 +384,10 @@ class ClassicAppMode(babase.AppMode):
self._current_purchases = frozenset(response.purchases)
self._current_purchases_state = state
assert babase.app.classic is not None
babase.app.classic.purchases = self._current_purchases
assert bui.app.classic is not None
bui.app.classic.purchases = self._current_purchases
babase.accountlog.debug(
bui.accountlog.debug(
'Updated purchases state to %s: (%s items)',
state,
len(self._current_purchases),
@ -402,15 +399,15 @@ class ClassicAppMode(babase.AppMode):
return hashlib.md5(','.join(sorted(purchases)).encode()).hexdigest()
def _update_for_primary_account(
self, account: babase.AccountV2Handle | None
self, account: bui.AccountV2Handle | None
) -> None:
"""Update subscriptions/etc. for a new primary account state."""
assert babase.in_logic_thread()
plus = babase.app.plus
assert bui.in_logic_thread()
plus = bui.app.plus
assert plus is not None
classic = babase.app.classic
classic = bui.app.classic
assert classic is not None
if account is not None:
@ -512,7 +509,7 @@ class ClassicAppMode(babase.AppMode):
) -> None:
achp = round(val.achievements / max(val.achievements_total, 1) * 100.0)
babase.accountlog.debug('Got new classic account data.')
bui.accountlog.debug('Got new classic account data.')
chest0 = val.chests.get('0')
chest1 = val.chests.get('1')
@ -521,7 +518,7 @@ class ClassicAppMode(babase.AppMode):
# Keep a few handy values on classic updated with the latest
# data.
classic = babase.app.classic
classic = bui.app.classic
assert classic is not None
classic.remove_ads = val.remove_ads
classic.gold_pass = val.gold_pass
@ -533,14 +530,14 @@ class ClassicAppMode(babase.AppMode):
# If they want us to ask for a review (and we haven't yet), do
# so.
if val.Flag.ASK_FOR_REVIEW in val.flags:
cfg = babase.app.config
cfg = bui.app.config
if (
not cfg.get(self._ASKED_FOR_REVIEW_CONFIG_KEY, False)
and babase.native_review_request_supported()
and bui.native_review_request_supported()
):
cfg[self._ASKED_FOR_REVIEW_CONFIG_KEY] = True
cfg.commit()
babase.native_review_request()
bui.native_review_request()
# If someone replaced our purchases in the classic subsystem,
# fix it.
@ -556,7 +553,7 @@ class ClassicAppMode(babase.AppMode):
self._target_purchases_state is not None
and self._current_purchases_state != self._target_purchases_state
):
babase.accountlog.debug(
bui.accountlog.debug(
'Account purchases state is %s; we have %s. Will fetch new.',
self._target_purchases_state,
self._current_purchases_state,
@ -566,7 +563,7 @@ class ClassicAppMode(babase.AppMode):
# doing its thing.
pass
else:
self._purchases_update_timer = babase.AppTimer(
self._purchases_update_timer = bui.AppTimer(
3.456, self._update_purchases, repeat=True
)
self._possibly_request_purchases()
@ -596,7 +593,7 @@ class ClassicAppMode(babase.AppMode):
inbox_count=val.inbox_count,
inbox_count_is_max=val.inbox_count_is_max,
inbox_announce_text=(
babase.Lstr(resource='unclaimedPrizesText').evaluate()
bui.Lstr(resource='unclaimedPrizesText').evaluate()
if val.inbox_contains_prize
else ''
),
@ -678,15 +675,15 @@ class ClassicAppMode(babase.AppMode):
def _root_ui_menu_press(self) -> None:
from babase import menu_press
ui = babase.app.ui_v1
ui = bui.app.ui_v1
# If *any* main-window is up, kill it and resume play.
old_window = ui.get_main_window()
if old_window is not None:
bauiv1.getsound('swish').play()
bui.getsound('swish').play()
classic = babase.app.classic
classic = bui.app.classic
assert classic is not None
classic.resume()
@ -698,126 +695,31 @@ class ClassicAppMode(babase.AppMode):
def _root_ui_account_press(self) -> None:
from bauiv1lib.account.settings import AccountSettingsWindow
self._auxiliary_window_nav(
bui.app.ui_v1.auxiliary_window_activate(
win_type=AccountSettingsWindow,
win_create_call=lambda: AccountSettingsWindow(
origin_widget=bauiv1.get_special_widget('account_button')
origin_widget=bui.get_special_widget('account_button')
),
)
def _root_ui_squad_press(self) -> None:
btn = bauiv1.get_special_widget('squad_button')
btn = bui.get_special_widget('squad_button')
center = btn.get_screen_space_center()
if bauiv1.app.classic is not None:
bauiv1.app.classic.party_icon_activate(center)
if bui.app.classic is not None:
bui.app.classic.party_icon_activate(center)
else:
logging.warning('party_icon_activate: no classic.')
def _root_ui_settings_press(self) -> None:
from bauiv1lib.settings.allsettings import AllSettingsWindow
self._auxiliary_window_nav(
bui.app.ui_v1.auxiliary_window_activate(
win_type=AllSettingsWindow,
win_create_call=lambda: AllSettingsWindow(
origin_widget=bauiv1.get_special_widget('settings_button')
origin_widget=bui.get_special_widget('settings_button')
),
)
def _auxiliary_window_nav(
self,
win_type: type[bauiv1.MainWindow],
win_create_call: Callable[[], bauiv1.MainWindow],
) -> None:
"""Navigate to or away from an Auxiliary window.
Auxiliary windows can be thought of as 'side quests' in the
window hierarchy; places such as settings windows or league
ranking windows that the user might want to visit without losing
their place in the regular hierarchy.
"""
# pylint: disable=unidiomatic-typecheck
ui = babase.app.ui_v1
current_main_window = ui.get_main_window()
# Scan our ancestors for auxiliary states matching our type as
# well as auxiliary states in general.
aux_matching_state: bauiv1.MainWindowState | None = None
aux_state: bauiv1.MainWindowState | None = None
if current_main_window is None:
raise RuntimeError(
'Not currently handling no-top-level-window case.'
)
state = current_main_window.main_window_back_state
while state is not None:
assert state.window_type is not None
if state.is_auxiliary:
if state.window_type is win_type:
aux_matching_state = state
else:
aux_state = state
state = state.parent
# If there's an ancestor auxiliary window-state matching our
# type, back out past it (example: poking settings, navigating
# down a level or two, and then poking settings again should
# back out of settings).
if aux_matching_state is not None:
current_main_window.main_window_back_state = (
aux_matching_state.parent
)
current_main_window.main_window_back()
return
# If there's an ancestory auxiliary state *not* matching our
# type, crop the state and swap in our new auxiliary UI
# (example: poking settings, then poking account, then poking
# back should end up where things were before the settings
# poke).
if aux_state is not None:
# Blow away the window stack and build a fresh one.
ui.clear_main_window()
ui.set_main_window(
win_create_call(),
from_window=False, # Disable from-check.
back_state=aux_state.parent,
suppress_warning=True,
is_auxiliary=True,
)
return
# Ok, no auxiliary states found. Now if current window is
# auxiliary and the type matches, simply do a back.
if (
current_main_window.main_window_is_auxiliary
and type(current_main_window) is win_type
):
current_main_window.main_window_back()
return
# If current window is auxiliary but type doesn't match,
# swap it out for our new auxiliary UI.
if current_main_window.main_window_is_auxiliary:
ui.clear_main_window()
ui.set_main_window(
win_create_call(),
from_window=False, # Disable from-check.
back_state=current_main_window.main_window_back_state,
suppress_warning=True,
is_auxiliary=True,
)
return
# Ok, no existing auxiliary stuff was found period. Just
# navigate forward to this UI.
current_main_window.main_window_replace(
win_create_call(), is_auxiliary=True
)
def _root_ui_achievements_press(self) -> None:
from bauiv1lib.achievements import AchievementsWindow
@ -825,12 +727,10 @@ class ClassicAppMode(babase.AppMode):
return
wait_for_connectivity(
on_connected=lambda: self._auxiliary_window_nav(
on_connected=lambda: bui.app.ui_v1.auxiliary_window_activate(
win_type=AchievementsWindow,
win_create_call=lambda: AchievementsWindow(
origin_widget=bauiv1.get_special_widget(
'achievements_button'
)
origin_widget=bui.get_special_widget('achievements_button')
),
)
)
@ -842,10 +742,10 @@ class ClassicAppMode(babase.AppMode):
return
wait_for_connectivity(
on_connected=lambda: self._auxiliary_window_nav(
on_connected=lambda: bui.app.ui_v1.auxiliary_window_activate(
win_type=InboxWindow,
win_create_call=lambda: InboxWindow(
origin_widget=bauiv1.get_special_widget('inbox_button')
origin_widget=bui.get_special_widget('inbox_button')
),
)
)
@ -857,10 +757,10 @@ class ClassicAppMode(babase.AppMode):
return
wait_for_connectivity(
on_connected=lambda: self._auxiliary_window_nav(
on_connected=lambda: bui.app.ui_v1.auxiliary_window_activate(
win_type=StoreBrowserWindow,
win_create_call=lambda: StoreBrowserWindow(
origin_widget=bauiv1.get_special_widget('store_button')
origin_widget=bui.get_special_widget('store_button')
),
)
)
@ -869,14 +769,14 @@ class ClassicAppMode(babase.AppMode):
from bauiv1lib.resourcetypeinfo import ResourceTypeInfoWindow
ResourceTypeInfoWindow(
'tickets', origin_widget=bauiv1.get_special_widget('tickets_meter')
'tickets', origin_widget=bui.get_special_widget('tickets_meter')
)
def _root_ui_tokens_meter_press(self) -> None:
from bauiv1lib.resourcetypeinfo import ResourceTypeInfoWindow
ResourceTypeInfoWindow(
'tokens', origin_widget=bauiv1.get_special_widget('tokens_meter')
'tokens', origin_widget=bui.get_special_widget('tokens_meter')
)
def _root_ui_trophy_meter_press(self) -> None:
@ -885,10 +785,10 @@ class ClassicAppMode(babase.AppMode):
if not self._ensure_signed_in_v1():
return
self._auxiliary_window_nav(
bui.app.ui_v1.auxiliary_window_activate(
win_type=LeagueRankWindow,
win_create_call=lambda: LeagueRankWindow(
origin_widget=bauiv1.get_special_widget('trophy_meter')
origin_widget=bui.get_special_widget('trophy_meter')
),
)
@ -896,7 +796,7 @@ class ClassicAppMode(babase.AppMode):
from bauiv1lib.resourcetypeinfo import ResourceTypeInfoWindow
ResourceTypeInfoWindow(
'xp', origin_widget=bauiv1.get_special_widget('level_meter')
'xp', origin_widget=bui.get_special_widget('level_meter')
)
def _root_ui_inventory_press(self) -> None:
@ -905,19 +805,19 @@ class ClassicAppMode(babase.AppMode):
if not self._ensure_signed_in_v1():
return
self._auxiliary_window_nav(
bui.app.ui_v1.auxiliary_window_activate(
win_type=InventoryWindow,
win_create_call=lambda: InventoryWindow(
origin_widget=bauiv1.get_special_widget('inventory_button')
origin_widget=bui.get_special_widget('inventory_button')
),
)
def _ensure_signed_in(self) -> bool:
"""Make sure we're signed in (requiring modern v2 accounts)."""
plus = bauiv1.app.plus
plus = bui.app.plus
if plus is None:
bauiv1.screenmessage('This requires plus.', color=(1, 0, 0))
bauiv1.getsound('error').play()
bui.screenmessage('This requires plus.', color=(1, 0, 0))
bui.getsound('error').play()
return False
if plus.accounts.primary is None:
show_sign_in_prompt()
@ -926,10 +826,10 @@ class ClassicAppMode(babase.AppMode):
def _ensure_signed_in_v1(self) -> bool:
"""Make sure we're signed in (allowing legacy v1-only accounts)."""
plus = bauiv1.app.plus
plus = bui.app.plus
if plus is None:
bauiv1.screenmessage('This requires plus.', color=(1, 0, 0))
bauiv1.getsound('error').play()
bui.screenmessage('This requires plus.', color=(1, 0, 0))
bui.getsound('error').play()
return False
if plus.get_v1_account_state() != 'signed_in':
show_sign_in_prompt()
@ -942,10 +842,10 @@ class ClassicAppMode(babase.AppMode):
if not self._ensure_signed_in():
return
self._auxiliary_window_nav(
bui.app.ui_v1.auxiliary_window_activate(
win_type=GetTokensWindow,
win_create_call=lambda: GetTokensWindow(
origin_widget=bauiv1.get_special_widget('get_tokens_button')
origin_widget=bui.get_special_widget('get_tokens_button')
),
)
@ -980,11 +880,11 @@ class ClassicAppMode(babase.AppMode):
raise RuntimeError(f'Invalid index {index}')
wait_for_connectivity(
on_connected=lambda: self._auxiliary_window_nav(
on_connected=lambda: bui.app.ui_v1.auxiliary_window_activate(
win_type=winclass,
win_create_call=lambda: winclass(
index=index,
origin_widget=bauiv1.get_special_widget(widgetid),
origin_widget=bui.get_special_widget(widgetid),
),
)
)
@ -1001,24 +901,24 @@ class ClassicAppMode(babase.AppMode):
assert 'a' not in vals
vals['a'] = self._current_account_id
assert babase.app.classic is not None
assert bui.app.classic is not None
assert 'p' not in vals
vals['p'] = list(babase.app.classic.purchases)
vals['p'] = list(bui.app.classic.purchases)
cfg = babase.app.config
cfg = bui.app.config
cfg[self._ACCOUNT_STATE_CONFIG_KEY] = vals
cfg.commit()
def _restore_account_state(self) -> None:
# If we've got a stored state for the current account, restore
# it.
assert babase.app.classic is not None
assert bui.app.classic is not None
if self._current_account_id is None:
return
cfg = babase.app.config
cfg = bui.app.config
vals = cfg.get(self._ACCOUNT_STATE_CONFIG_KEY)
if not isinstance(vals, dict):
@ -1036,12 +936,46 @@ class ClassicAppMode(babase.AppMode):
if isinstance(purchases, list):
if not all(isinstance(p, str) for p in purchases):
babase.balog.exception('Invalid purchases state on restore.')
bui.balog.exception('Invalid purchases state on restore.')
else:
self._current_purchases = frozenset(purchases)
self._current_purchases_state = self._state_from_purchases(
purchases
)
babase.app.classic.purchases = self._current_purchases
bui.app.classic.purchases = self._current_purchases
_baclassic.set_account_state(vals)
@override
def get_dev_console_ui_tab_buttons(
self,
) -> list[bui.DevConsoleButtonDef]:
return [
bui.DevConsoleButtonDef(
'MainWindow Template',
bui.WeakCall(self._main_win_template_press),
),
bui.DevConsoleButtonDef(
'CloudUI Test', bui.WeakCall(self._cloud_ui_test_press)
),
]
def _main_win_template_press(self) -> None:
from bauiv1lib.template import show_template_main_window
# Unintuitively, swish sounds come from buttons, not windows.
# And dev-console buttons don't make sounds. So we need to
# explicitly do so here.
bui.getsound('swish').play()
show_template_main_window()
def _cloud_ui_test_press(self) -> None:
from bauiv1 import show_cloud_ui_window
# Unintuitively, swish sounds come from buttons, not windows.
# And dev-console buttons don't make sounds. So we need to
# explicitly do so here.
bui.getsound('swish').play()
show_cloud_ui_window()

View file

@ -753,7 +753,7 @@ class ClassicAppSubsystem(babase.AppSubsystem):
origin_widget: bauiv1.Widget | None = None,
selected_profile: str | None = None,
) -> None:
"""(internal)"""
"""Pop up a browser window from within a game."""
from bauiv1lib.profile.browser import ProfileBrowserWindow
main_window = babase.app.ui_v1.get_main_window()
@ -772,6 +772,7 @@ class ClassicAppSubsystem(babase.AppSubsystem):
minimal_toolbar=True,
),
is_top_level=True,
back_state=None,
suppress_warning=True,
)
@ -830,7 +831,10 @@ class ClassicAppSubsystem(babase.AppSubsystem):
transition='scale_in', origin_widget=menu_button
),
is_top_level=True,
back_state=None,
suppress_warning=True,
# Reset selections to default for consistency.
restore_shared_state=False,
)
def save_ui_state(self) -> None:
@ -868,7 +872,10 @@ class ClassicAppSubsystem(babase.AppSubsystem):
from bauiv1lib.kiosk import KioskWindow
app.ui_v1.set_main_window(
KioskWindow(), is_top_level=True, suppress_warning=True
KioskWindow(),
is_top_level=True,
back_state=None,
suppress_warning=True,
)
else:
# If there's a saved ui state, restore that.
@ -881,6 +888,7 @@ class ClassicAppSubsystem(babase.AppSubsystem):
app.ui_v1.set_main_window(
MainMenuWindow(transition=None),
is_top_level=True,
back_state=None,
suppress_warning=True,
)
@ -895,17 +903,17 @@ class ClassicAppSubsystem(babase.AppSubsystem):
@staticmethod
def basic_client_ui_button_label_str(
label: bacommon.bs.BasicClientUI.ButtonLabel,
label: bacommon.bs.BasicCloudDialog.ButtonLabel,
) -> babase.Lstr:
"""Given a client-ui label, return an Lstr."""
import bacommon.bs
cls = bacommon.bs.BasicClientUI.ButtonLabel
cls = bacommon.bs.BasicCloudDialog.ButtonLabel
if label is cls.UNKNOWN:
# Server should not be sending us unknown stuff; make noise
# if they do.
logging.error(
'Got BasicClientUI.ButtonLabel.UNKNOWN; should not happen.'
'Got BasicCloudDialog.ButtonLabel.UNKNOWN; should not happen.'
)
return babase.Lstr(value='<error>')

View file

@ -0,0 +1,139 @@
# Released under the MIT License. See LICENSE for details.
#
"""Functionality related to bombsquad classic."""
from bacommon.bs._account import (
ClassicAccountLiveData,
)
from bacommon.bs._bs import (
TOKENS1_COUNT,
TOKENS2_COUNT,
TOKENS3_COUNT,
TOKENS4_COUNT,
)
from bacommon.bs._chest import (
ClassicChestAppearance,
)
from bacommon.bs._clienteffect import (
ClientEffect,
ClientEffectChestWaitTimeAnimation,
ClientEffectDelay,
ClientEffectScreenMessage,
ClientEffectSound,
ClientEffectTicketsAnimation,
ClientEffectTokensAnimation,
ClientEffectTypeID,
ClientEffectUnknown,
)
from bacommon.bs._clouddialog import (
BasicCloudDialog,
BasicCloudDialogComponent,
BasicCloudDialogBsClassicTourneyResult,
BasicCloudDialogComponentLink,
BasicCloudDialogComponentText,
BasicCloudDialogComponentTypeID,
BasicCloudDialogComponentUnknown,
BasicCloudDialogDisplayItems,
BasicCloudDialogExpireTime,
CloudDialog,
CloudDialogAction,
CloudDialogTypeID,
CloudDialogWrapper,
UnknownCloudDialog,
)
from bacommon.bs._cloudui import CloudUITypeID, CloudUI
from bacommon.bs._displayitem import (
ChestDisplayItem,
DisplayItem,
DisplayItemTypeID,
DisplayItemWrapper,
TestDisplayItem,
TicketsDisplayItem,
TokensDisplayItem,
UnknownDisplayItem,
)
from bacommon.bs._msg import (
ChestActionMessage,
ChestActionResponse,
ChestInfoMessage,
ChestInfoResponse,
CloudDialogActionMessage,
CloudDialogActionResponse,
GetClassicPurchasesMessage,
GetClassicPurchasesResponse,
GlobalProfileCheckMessage,
GlobalProfileCheckResponse,
InboxRequestMessage,
InboxRequestResponse,
LegacyRequest,
LegacyResponse,
PrivatePartyMessage,
PrivatePartyResponse,
ScoreSubmitMessage,
ScoreSubmitResponse,
SendInfoMessage,
SendInfoResponse,
)
__all__ = [
'BasicCloudDialog',
'BasicCloudDialogComponent',
'BasicCloudDialogBsClassicTourneyResult',
'BasicCloudDialogComponentLink',
'BasicCloudDialogComponentText',
'BasicCloudDialogComponentTypeID',
'BasicCloudDialogComponentUnknown',
'BasicCloudDialogDisplayItems',
'BasicCloudDialogExpireTime',
'ChestActionMessage',
'ChestActionResponse',
'ChestDisplayItem',
'ChestInfoMessage',
'ChestInfoResponse',
'ClassicAccountLiveData',
'ClassicChestAppearance',
'ClientEffect',
'ClientEffectChestWaitTimeAnimation',
'ClientEffectDelay',
'ClientEffectScreenMessage',
'ClientEffectSound',
'ClientEffectTicketsAnimation',
'ClientEffectTokensAnimation',
'ClientEffectTypeID',
'ClientEffectUnknown',
'CloudDialog',
'CloudDialogAction',
'CloudDialogActionMessage',
'CloudDialogActionResponse',
'CloudDialogTypeID',
'CloudDialogWrapper',
'CloudUI',
'CloudUITypeID',
'DisplayItem',
'DisplayItemTypeID',
'DisplayItemWrapper',
'GetClassicPurchasesMessage',
'GetClassicPurchasesResponse',
'GlobalProfileCheckMessage',
'GlobalProfileCheckResponse',
'InboxRequestMessage',
'InboxRequestResponse',
'LegacyRequest',
'LegacyResponse',
'PrivatePartyMessage',
'PrivatePartyResponse',
'ScoreSubmitMessage',
'ScoreSubmitResponse',
'SendInfoMessage',
'SendInfoResponse',
'TestDisplayItem',
'TicketsDisplayItem',
'TOKENS1_COUNT',
'TOKENS2_COUNT',
'TOKENS3_COUNT',
'TOKENS4_COUNT',
'TokensDisplayItem',
'UnknownCloudDialog',
'UnknownDisplayItem',
]

View file

@ -0,0 +1,73 @@
# Released under the MIT License. See LICENSE for details.
#
"""BombSquad specific bits."""
from __future__ import annotations
import datetime
from enum import Enum
from dataclasses import dataclass
from typing import Annotated
from efro.dataclassio import ioprepped, IOAttrs
from bacommon.bs._chest import ClassicChestAppearance
@ioprepped
@dataclass
class ClassicAccountLiveData:
"""Live account data fed to the client in the bs classic app mode."""
@dataclass
class Chest:
"""A lovely chest."""
appearance: Annotated[
ClassicChestAppearance,
IOAttrs('a', enum_fallback=ClassicChestAppearance.UNKNOWN),
]
create_time: Annotated[datetime.datetime, IOAttrs('c')]
unlock_time: Annotated[datetime.datetime, IOAttrs('t')]
unlock_tokens: Annotated[int, IOAttrs('k')]
ad_allow_time: Annotated[datetime.datetime | None, IOAttrs('at')]
class LeagueType(Enum):
"""Type of league we are in."""
BRONZE = 'b'
SILVER = 's'
GOLD = 'g'
DIAMOND = 'd'
class Flag(Enum):
"""Flags set for our account."""
ASK_FOR_REVIEW = 'r'
tickets: Annotated[int, IOAttrs('ti')]
tokens: Annotated[int, IOAttrs('to')]
gold_pass: Annotated[bool, IOAttrs('g')]
remove_ads: Annotated[bool, IOAttrs('r')]
achievements: Annotated[int, IOAttrs('a')]
achievements_total: Annotated[int, IOAttrs('at')]
league_type: Annotated[LeagueType | None, IOAttrs('lt')]
league_num: Annotated[int | None, IOAttrs('ln')]
league_rank: Annotated[int | None, IOAttrs('lr')]
level: Annotated[int, IOAttrs('lv')]
xp: Annotated[int, IOAttrs('xp')]
xpmax: Annotated[int, IOAttrs('xpm')]
inbox_count: Annotated[int, IOAttrs('ibc')]
inbox_count_is_max: Annotated[bool, IOAttrs('ibcm')]
inbox_contains_prize: Annotated[bool, IOAttrs('icp')]
chests: Annotated[dict[str, Chest], IOAttrs('c')]
# State id of our purchases for builds 22459+.
purchases_state: Annotated[str | None, IOAttrs('p')]
flags: Annotated[set[Flag], IOAttrs('f', soft_default_factory=set)]

View file

@ -0,0 +1,9 @@
# Released under the MIT License. See LICENSE for details.
#
"""BombSquad specific bits."""
# Token counts for our various packs.
TOKENS1_COUNT = 50
TOKENS2_COUNT = 500
TOKENS3_COUNT = 1200
TOKENS4_COUNT = 2600

View file

@ -0,0 +1,46 @@
# Released under the MIT License. See LICENSE for details.
#
"""BombSquad specific bits."""
from __future__ import annotations
from enum import Enum
from typing import assert_never
class ClassicChestAppearance(Enum):
"""Appearances bombsquad classic chests can have."""
UNKNOWN = 'u'
DEFAULT = 'd'
L1 = 'l1'
L2 = 'l2'
L3 = 'l3'
L4 = 'l4'
L5 = 'l5'
L6 = 'l6'
@property
def pretty_name(self) -> str:
"""Pretty name for the chest in English."""
# pylint: disable=too-many-return-statements
cls = type(self)
if self is cls.UNKNOWN:
return 'Unknown Chest'
if self is cls.DEFAULT:
return 'Chest'
if self is cls.L1:
return 'L1 Chest'
if self is cls.L2:
return 'L2 Chest'
if self is cls.L3:
return 'L3 Chest'
if self is cls.L4:
return 'L4 Chest'
if self is cls.L5:
return 'L5 Chest'
if self is cls.L6:
return 'L6 Chest'
assert_never(self)

View file

@ -0,0 +1,180 @@
# Released under the MIT License. See LICENSE for details.
#
"""ClientEffect related functionality."""
from __future__ import annotations
import datetime
from enum import Enum
from dataclasses import dataclass, field
from typing import Annotated, override, assert_never
from efro.dataclassio import ioprepped, IOAttrs, IOMultiType
class ClientEffectTypeID(Enum):
"""Type ID for each of our subclasses."""
UNKNOWN = 'u'
SCREEN_MESSAGE = 'm'
SOUND = 's'
DELAY = 'd'
CHEST_WAIT_TIME_ANIMATION = 't'
TICKETS_ANIMATION = 'ta'
TOKENS_ANIMATION = 'toa'
class ClientEffect(IOMultiType[ClientEffectTypeID]):
"""Something that can happen on the client.
This can include screen messages, sounds, visual effects, etc.
"""
@override
@classmethod
def get_type_id(cls) -> ClientEffectTypeID:
# Require child classes to supply this themselves. If we did a
# full type registry/lookup here it would require us to import
# everything and would prevent lazy loading.
raise NotImplementedError()
@override
@classmethod
def get_type(cls, type_id: ClientEffectTypeID) -> type[ClientEffect]:
"""Return the subclass for each of our type-ids."""
# pylint: disable=cyclic-import
# pylint: disable=too-many-return-statements
t = ClientEffectTypeID
if type_id is t.UNKNOWN:
return ClientEffectUnknown
if type_id is t.SCREEN_MESSAGE:
return ClientEffectScreenMessage
if type_id is t.SOUND:
return ClientEffectSound
if type_id is t.DELAY:
return ClientEffectDelay
if type_id is t.CHEST_WAIT_TIME_ANIMATION:
return ClientEffectChestWaitTimeAnimation
if type_id is t.TICKETS_ANIMATION:
return ClientEffectTicketsAnimation
if type_id is t.TOKENS_ANIMATION:
return ClientEffectTokensAnimation
# Important to make sure we provide all types.
assert_never(type_id)
@override
@classmethod
def get_unknown_type_fallback(cls) -> ClientEffect:
# If we encounter some future message type we don't know
# anything about, drop in a placeholder.
return ClientEffectUnknown()
@ioprepped
@dataclass
class ClientEffectUnknown(ClientEffect):
"""Fallback substitute for types we don't recognize."""
@override
@classmethod
def get_type_id(cls) -> ClientEffectTypeID:
return ClientEffectTypeID.UNKNOWN
@ioprepped
@dataclass
class ClientEffectScreenMessage(ClientEffect):
"""Display a screen-message."""
message: Annotated[str, IOAttrs('m')]
subs: Annotated[list[str], IOAttrs('s')] = field(default_factory=list)
color: Annotated[tuple[float, float, float], IOAttrs('c')] = (1.0, 1.0, 1.0)
@override
@classmethod
def get_type_id(cls) -> ClientEffectTypeID:
return ClientEffectTypeID.SCREEN_MESSAGE
@ioprepped
@dataclass
class ClientEffectSound(ClientEffect):
"""Play a sound."""
class Sound(Enum):
"""Sounds that can be made alongside the message."""
UNKNOWN = 'u'
CASH_REGISTER = 'c'
ERROR = 'e'
POWER_DOWN = 'p'
GUN_COCKING = 'g'
sound: Annotated[Sound, IOAttrs('s', enum_fallback=Sound.UNKNOWN)]
volume: Annotated[float, IOAttrs('v')] = 1.0
@override
@classmethod
def get_type_id(cls) -> ClientEffectTypeID:
return ClientEffectTypeID.SOUND
@ioprepped
@dataclass
class ClientEffectChestWaitTimeAnimation(ClientEffect):
"""Animate chest wait time changing."""
chestid: Annotated[str, IOAttrs('c')]
duration: Annotated[float, IOAttrs('u')]
startvalue: Annotated[datetime.datetime, IOAttrs('o')]
endvalue: Annotated[datetime.datetime, IOAttrs('n')]
@override
@classmethod
def get_type_id(cls) -> ClientEffectTypeID:
return ClientEffectTypeID.CHEST_WAIT_TIME_ANIMATION
@ioprepped
@dataclass
class ClientEffectTicketsAnimation(ClientEffect):
"""Animate tickets count."""
duration: Annotated[float, IOAttrs('u')]
startvalue: Annotated[int, IOAttrs('s')]
endvalue: Annotated[int, IOAttrs('e')]
@override
@classmethod
def get_type_id(cls) -> ClientEffectTypeID:
return ClientEffectTypeID.TICKETS_ANIMATION
@ioprepped
@dataclass
class ClientEffectTokensAnimation(ClientEffect):
"""Animate tokens count."""
duration: Annotated[float, IOAttrs('u')]
startvalue: Annotated[int, IOAttrs('s')]
endvalue: Annotated[int, IOAttrs('e')]
@override
@classmethod
def get_type_id(cls) -> ClientEffectTypeID:
return ClientEffectTypeID.TOKENS_ANIMATION
@ioprepped
@dataclass
class ClientEffectDelay(ClientEffect):
"""Delay effect processing."""
seconds: Annotated[float, IOAttrs('s')]
@override
@classmethod
def get_type_id(cls) -> ClientEffectTypeID:
return ClientEffectTypeID.DELAY

View file

@ -0,0 +1,308 @@
# Released under the MIT License. See LICENSE for details.
#
"""Simple cloud-defined UIs for things like notifications."""
from __future__ import annotations
import datetime
from enum import Enum
from dataclasses import dataclass, field
from typing import Annotated, override, assert_never
from efro.dataclassio import ioprepped, IOAttrs, IOMultiType
from bacommon.bs._displayitem import DisplayItemWrapper
class CloudDialogTypeID(Enum):
"""Type ID for each of our subclasses."""
UNKNOWN = 'u'
BASIC = 'b'
class CloudDialog(IOMultiType[CloudDialogTypeID]):
"""Small self-contained ui bit provided by the cloud.
These take care of updating and/or dismissing themselves based on
user input. Useful for things such as inbox messages. For more
complex UI construction, look at :class:`CloudUI`.
"""
@override
@classmethod
def get_type_id(cls) -> CloudDialogTypeID:
# Require child classes to supply this themselves. If we did a
# full type registry/lookup here it would require us to import
# everything and would prevent lazy loading.
raise NotImplementedError()
@override
@classmethod
def get_type(cls, type_id: CloudDialogTypeID) -> type[CloudDialog]:
"""Return the subclass for each of our type-ids."""
# pylint: disable=cyclic-import
out: type[CloudDialog]
t = CloudDialogTypeID
if type_id is t.UNKNOWN:
out = UnknownCloudDialog
elif type_id is t.BASIC:
out = BasicCloudDialog
else:
# Important to make sure we provide all types.
assert_never(type_id)
return out
@override
@classmethod
def get_unknown_type_fallback(cls) -> CloudDialog:
# If we encounter some future message type we don't know
# anything about, drop in a placeholder.
return UnknownCloudDialog()
@ioprepped
@dataclass
class UnknownCloudDialog(CloudDialog):
"""Fallback type for unrecognized entries."""
@override
@classmethod
def get_type_id(cls) -> CloudDialogTypeID:
return CloudDialogTypeID.UNKNOWN
class BasicCloudDialogComponentTypeID(Enum):
"""Type ID for each of our subclasses."""
UNKNOWN = 'u'
TEXT = 't'
LINK = 'l'
BS_CLASSIC_TOURNEY_RESULT = 'ct'
DISPLAY_ITEMS = 'di'
EXPIRE_TIME = 'd'
class BasicCloudDialogComponent(IOMultiType[BasicCloudDialogComponentTypeID]):
"""Top level class for our multitype."""
@override
@classmethod
def get_type_id(cls) -> BasicCloudDialogComponentTypeID:
# Require child classes to supply this themselves. If we did a
# full type registry/lookup here it would require us to import
# everything and would prevent lazy loading.
raise NotImplementedError()
@override
@classmethod
def get_type(
cls, type_id: BasicCloudDialogComponentTypeID
) -> type[BasicCloudDialogComponent]:
"""Return the subclass for each of our type-ids."""
# pylint: disable=cyclic-import
t = BasicCloudDialogComponentTypeID
if type_id is t.UNKNOWN:
return BasicCloudDialogComponentUnknown
if type_id is t.TEXT:
return BasicCloudDialogComponentText
if type_id is t.LINK:
return BasicCloudDialogComponentLink
if type_id is t.BS_CLASSIC_TOURNEY_RESULT:
return BasicCloudDialogBsClassicTourneyResult
if type_id is t.DISPLAY_ITEMS:
return BasicCloudDialogDisplayItems
if type_id is t.EXPIRE_TIME:
return BasicCloudDialogExpireTime
# Important to make sure we provide all types.
assert_never(type_id)
@override
@classmethod
def get_unknown_type_fallback(cls) -> BasicCloudDialogComponent:
# If we encounter some future message type we don't know
# anything about, drop in a placeholder.
return BasicCloudDialogComponentUnknown()
@ioprepped
@dataclass
class BasicCloudDialogComponentUnknown(BasicCloudDialogComponent):
"""An unknown basic client component type.
In practice these should never show up since the master-server
generates these on the fly for the client and so should not send
clients one they can't digest.
"""
@override
@classmethod
def get_type_id(cls) -> BasicCloudDialogComponentTypeID:
return BasicCloudDialogComponentTypeID.UNKNOWN
@ioprepped
@dataclass
class BasicCloudDialogComponentText(BasicCloudDialogComponent):
"""Show some text in the inbox message."""
text: Annotated[str, IOAttrs('t')]
subs: Annotated[list[str], IOAttrs('s', store_default=False)] = field(
default_factory=list
)
scale: Annotated[float, IOAttrs('sc', store_default=False)] = 1.0
color: Annotated[
tuple[float, float, float, float], IOAttrs('c', store_default=False)
] = (1.0, 1.0, 1.0, 1.0)
spacing_top: Annotated[float, IOAttrs('st', store_default=False)] = 0.0
spacing_bottom: Annotated[float, IOAttrs('sb', store_default=False)] = 0.0
@override
@classmethod
def get_type_id(cls) -> BasicCloudDialogComponentTypeID:
return BasicCloudDialogComponentTypeID.TEXT
@ioprepped
@dataclass
class BasicCloudDialogComponentLink(BasicCloudDialogComponent):
"""Show a link in the inbox message."""
url: Annotated[str, IOAttrs('u')]
label: Annotated[str, IOAttrs('l')]
subs: Annotated[list[str], IOAttrs('s', store_default=False)] = field(
default_factory=list
)
spacing_top: Annotated[float, IOAttrs('st', store_default=False)] = 0.0
spacing_bottom: Annotated[float, IOAttrs('sb', store_default=False)] = 0.0
@override
@classmethod
def get_type_id(cls) -> BasicCloudDialogComponentTypeID:
return BasicCloudDialogComponentTypeID.LINK
@ioprepped
@dataclass
class BasicCloudDialogBsClassicTourneyResult(BasicCloudDialogComponent):
"""Show info about a classic tourney."""
tournament_id: Annotated[str, IOAttrs('t')]
game: Annotated[str, IOAttrs('g')]
players: Annotated[int, IOAttrs('p')]
rank: Annotated[int, IOAttrs('r')]
trophy: Annotated[str | None, IOAttrs('tr')]
prizes: Annotated[list[DisplayItemWrapper], IOAttrs('pr')]
@override
@classmethod
def get_type_id(cls) -> BasicCloudDialogComponentTypeID:
return BasicCloudDialogComponentTypeID.BS_CLASSIC_TOURNEY_RESULT
@ioprepped
@dataclass
class BasicCloudDialogDisplayItems(BasicCloudDialogComponent):
"""Show some display-items."""
items: Annotated[list[DisplayItemWrapper], IOAttrs('d')]
width: Annotated[float, IOAttrs('w')] = 100.0
spacing_top: Annotated[float, IOAttrs('st', store_default=False)] = 0.0
spacing_bottom: Annotated[float, IOAttrs('sb', store_default=False)] = 0.0
@override
@classmethod
def get_type_id(cls) -> BasicCloudDialogComponentTypeID:
return BasicCloudDialogComponentTypeID.DISPLAY_ITEMS
@ioprepped
@dataclass
class BasicCloudDialogExpireTime(BasicCloudDialogComponent):
"""Show expire-time."""
time: Annotated[datetime.datetime, IOAttrs('d')]
spacing_top: Annotated[float, IOAttrs('st', store_default=False)] = 0.0
spacing_bottom: Annotated[float, IOAttrs('sb', store_default=False)] = 0.0
@override
@classmethod
def get_type_id(cls) -> BasicCloudDialogComponentTypeID:
return BasicCloudDialogComponentTypeID.EXPIRE_TIME
@ioprepped
@dataclass
class BasicCloudDialog(CloudDialog):
"""A basic UI for the client."""
class ButtonLabel(Enum):
"""Distinct button labels we support."""
UNKNOWN = 'u'
OK = 'o'
APPLY = 'a'
CANCEL = 'c'
ACCEPT = 'ac'
DECLINE = 'dn'
IGNORE = 'ig'
CLAIM = 'cl'
DISCARD = 'd'
class InteractionStyle(Enum):
"""Overall interaction styles we support."""
UNKNOWN = 'u'
BUTTON_POSITIVE = 'p'
BUTTON_POSITIVE_NEGATIVE = 'pn'
components: Annotated[list[BasicCloudDialogComponent], IOAttrs('s')]
interaction_style: Annotated[
InteractionStyle, IOAttrs('i', enum_fallback=InteractionStyle.UNKNOWN)
] = InteractionStyle.BUTTON_POSITIVE
button_label_positive: Annotated[
ButtonLabel, IOAttrs('p', enum_fallback=ButtonLabel.UNKNOWN)
] = ButtonLabel.OK
button_label_negative: Annotated[
ButtonLabel, IOAttrs('n', enum_fallback=ButtonLabel.UNKNOWN)
] = ButtonLabel.CANCEL
@override
@classmethod
def get_type_id(cls) -> CloudDialogTypeID:
return CloudDialogTypeID.BASIC
def contains_unknown_elements(self) -> bool:
"""Whether something within us is an unknown type or enum."""
return (
self.interaction_style is self.InteractionStyle.UNKNOWN
or self.button_label_positive is self.ButtonLabel.UNKNOWN
or self.button_label_negative is self.ButtonLabel.UNKNOWN
or any(
c.get_type_id() is BasicCloudDialogComponentTypeID.UNKNOWN
for c in self.components
)
)
@ioprepped
@dataclass
class CloudDialogWrapper:
"""Wrapper for a CloudDialog and its common data."""
id: Annotated[str, IOAttrs('i')]
createtime: Annotated[datetime.datetime, IOAttrs('c')]
ui: Annotated[CloudDialog, IOAttrs('e')]
class CloudDialogAction(Enum):
"""Types of actions we can run."""
BUTTON_PRESS_POSITIVE = 'p'
BUTTON_PRESS_NEGATIVE = 'n'

View file

@ -0,0 +1,180 @@
# Released under the MIT License. See LICENSE for details.
#
"""Full UIs defined in the cloud - similar to a basic form of html"""
from __future__ import annotations
from enum import Enum
from dataclasses import dataclass
from typing import Annotated, override, assert_never
from efro.dataclassio import ioprepped, IOAttrs, IOMultiType
class CloudUITypeID(Enum):
"""Type ID for each of our subclasses."""
UNKNOWN = 'u'
V1 = 'v1'
class CloudUI(IOMultiType[CloudUITypeID]):
"""UI defined by the cloud.
Conceptually similar to a basic html page, except using app UI.
"""
@override
@classmethod
def get_type_id(cls) -> CloudUITypeID:
# Require child classes to supply this themselves. If we did a
# full type registry/lookup here it would require us to import
# everything and would prevent lazy loading.
raise NotImplementedError()
@override
@classmethod
def get_type(cls, type_id: CloudUITypeID) -> type[CloudUI]:
"""Return the subclass for each of our type-ids."""
# pylint: disable=cyclic-import
out: type[CloudUI]
t = CloudUITypeID
if type_id is t.UNKNOWN:
out = UnknownCloudUI
elif type_id is t.V1:
out = V1CloudUI
else:
# Important to make sure we provide all types.
assert_never(type_id)
return out
@override
@classmethod
def get_unknown_type_fallback(cls) -> CloudUI:
# If we encounter some future message type we don't know
# anything about, drop in a placeholder.
return UnknownCloudUI()
@ioprepped
@dataclass
class UnknownCloudUI(CloudUI):
"""Fallback type for unrecognized UI types.
Will show the client a 'cannot display this UI' placeholder page.
"""
@override
@classmethod
def get_type_id(cls) -> CloudUITypeID:
return CloudUITypeID.UNKNOWN
class V1CloudUIComponentTypeID(Enum):
"""Type ID for each of our subclasses."""
UNKNOWN = 'u'
TEXT = 't'
class V1CloudUIComponent(IOMultiType[V1CloudUIComponentTypeID]):
"""Top level class for our multitype."""
@override
@classmethod
def get_type_id(cls) -> V1CloudUIComponentTypeID:
# Require child classes to supply this themselves. If we did a
# full type registry/lookup here it would require us to import
# everything and would prevent lazy loading.
raise NotImplementedError()
@override
@classmethod
def get_type(
cls, type_id: V1CloudUIComponentTypeID
) -> type[V1CloudUIComponent]:
"""Return the subclass for each of our type-ids."""
# pylint: disable=cyclic-import
t = V1CloudUIComponentTypeID
if type_id is t.UNKNOWN:
return V1CloudUIComponentUnknown
if type_id is t.TEXT:
return V1CloudUIComponentText
# Important to make sure we provide all types.
assert_never(type_id)
@override
@classmethod
def get_unknown_type_fallback(cls) -> V1CloudUIComponent:
# If we encounter some future message type we don't know
# anything about, drop in a placeholder.
return V1CloudUIComponentUnknown()
@ioprepped
@dataclass
class V1CloudUIComponentUnknown(V1CloudUIComponent):
"""An unknown basic client component type.
In practice these should never show up since the master-server
generates these on the fly for the client and so should not send
clients one they can't digest.
"""
@override
@classmethod
def get_type_id(cls) -> V1CloudUIComponentTypeID:
return V1CloudUIComponentTypeID.UNKNOWN
@ioprepped
@dataclass
class V1CloudUIComponentText(V1CloudUIComponent):
"""Show some text over a button."""
text: Annotated[str, IOAttrs('t')]
# position: Annotated[float, IOAttrs('p', store_default=False)] = 1.0
# scale: Annotated[float, IOAttrs('s', store_default=False)] = 1.0
# color: Annotated[
# tuple[float, float, float, float], IOAttrs('c', store_default=False)
# ] = (1.0, 1.0, 1.0, 1.0)
@override
@classmethod
def get_type_id(cls) -> V1CloudUIComponentTypeID:
return V1CloudUIComponentTypeID.TEXT
@ioprepped
@dataclass
class V1CloudUIButton:
"""A button in our cloud ui."""
color: Annotated[tuple[float, float, float], IOAttrs('cl')]
size: Annotated[tuple[float, float], IOAttrs('sz')]
components: Annotated[list[V1CloudUIComponent], IOAttrs('c')]
scale: Annotated[float, IOAttrs('sc', store_default=False)] = 1.0
@ioprepped
@dataclass
class V1CloudUIRow:
"""A row in our cloud ui."""
buttons: Annotated[list[V1CloudUIButton], IOAttrs('b')]
@ioprepped
@dataclass
class V1CloudUI(CloudUI):
"""Version 1 of our cloud-defined UI type."""
rows: Annotated[list[V1CloudUIRow], IOAttrs('r')]
@override
@classmethod
def get_type_id(cls) -> CloudUITypeID:
return CloudUITypeID.V1

View file

@ -0,0 +1,182 @@
# Released under the MIT License. See LICENSE for details.
#
"""DisplayItem related functionality."""
from __future__ import annotations
from enum import Enum
from dataclasses import dataclass
from typing import Annotated, override, assert_never
from efro.util import pairs_to_flat
from efro.dataclassio import ioprepped, IOAttrs, IOMultiType
from bacommon.bs._chest import ClassicChestAppearance
class DisplayItemTypeID(Enum):
"""Type ID for each of our subclasses."""
UNKNOWN = 'u'
TICKETS = 't'
TOKENS = 'k'
TEST = 's'
CHEST = 'c'
class DisplayItem(IOMultiType[DisplayItemTypeID]):
"""Some amount of something that can be shown or described.
Used to depict chest contents, inventory, rewards, etc.
"""
@override
@classmethod
def get_type_id(cls) -> DisplayItemTypeID:
# Require child classes to supply this themselves. If we did a
# full type registry/lookup here it would require us to import
# everything and would prevent lazy loading.
raise NotImplementedError()
@override
@classmethod
def get_type(cls, type_id: DisplayItemTypeID) -> type[DisplayItem]:
"""Return the subclass for each of our type-ids."""
# pylint: disable=cyclic-import
t = DisplayItemTypeID
if type_id is t.UNKNOWN:
return UnknownDisplayItem
if type_id is t.TICKETS:
return TicketsDisplayItem
if type_id is t.TOKENS:
return TokensDisplayItem
if type_id is t.TEST:
return TestDisplayItem
if type_id is t.CHEST:
return ChestDisplayItem
# Important to make sure we provide all types.
assert_never(type_id)
def get_description(self) -> tuple[str, list[tuple[str, str]]]:
"""Return a string description and subs for the item.
These decriptions are baked into the DisplayItemWrapper and
should be accessed from there when available. This allows
clients to give descriptions even for newer display items they
don't recognize.
"""
raise NotImplementedError()
# Implement fallbacks so client can digest item lists even if they
# contain unrecognized stuff. DisplayItemWrapper contains basic
# baked down info that they can still use in such cases.
@override
@classmethod
def get_unknown_type_fallback(cls) -> DisplayItem:
return UnknownDisplayItem()
@ioprepped
@dataclass
class UnknownDisplayItem(DisplayItem):
"""Something we don't know how to display."""
@override
@classmethod
def get_type_id(cls) -> DisplayItemTypeID:
return DisplayItemTypeID.UNKNOWN
@override
def get_description(self) -> tuple[str, list[tuple[str, str]]]:
import logging
# Make noise but don't break.
logging.exception(
'UnknownDisplayItem.get_description() should never be called.'
' Always access descriptions on the DisplayItemWrapper.'
)
return 'Unknown', []
@ioprepped
@dataclass
class TicketsDisplayItem(DisplayItem):
"""Some amount of tickets."""
count: Annotated[int, IOAttrs('c')]
@override
@classmethod
def get_type_id(cls) -> DisplayItemTypeID:
return DisplayItemTypeID.TICKETS
@override
def get_description(self) -> tuple[str, list[tuple[str, str]]]:
return '${C} Tickets', [('${C}', str(self.count))]
@ioprepped
@dataclass
class TokensDisplayItem(DisplayItem):
"""Some amount of tokens."""
count: Annotated[int, IOAttrs('c')]
@override
@classmethod
def get_type_id(cls) -> DisplayItemTypeID:
return DisplayItemTypeID.TOKENS
@override
def get_description(self) -> tuple[str, list[tuple[str, str]]]:
return '${C} Tokens', [('${C}', str(self.count))]
@ioprepped
@dataclass
class TestDisplayItem(DisplayItem):
"""Fills usable space for a display-item - good for calibration."""
@override
@classmethod
def get_type_id(cls) -> DisplayItemTypeID:
return DisplayItemTypeID.TEST
@override
def get_description(self) -> tuple[str, list[tuple[str, str]]]:
return 'Test Display Item Here', []
@ioprepped
@dataclass
class ChestDisplayItem(DisplayItem):
"""Display a chest."""
appearance: Annotated[ClassicChestAppearance, IOAttrs('a')]
@override
@classmethod
def get_type_id(cls) -> DisplayItemTypeID:
return DisplayItemTypeID.CHEST
@override
def get_description(self) -> tuple[str, list[tuple[str, str]]]:
return self.appearance.pretty_name, []
@ioprepped
@dataclass
class DisplayItemWrapper:
"""Wraps a DisplayItem and common info."""
item: Annotated[DisplayItem, IOAttrs('i')]
description: Annotated[str, IOAttrs('d')]
description_subs: Annotated[list[str] | None, IOAttrs('s')]
@classmethod
def for_display_item(cls, item: DisplayItem) -> DisplayItemWrapper:
"""Convenience method to wrap a DisplayItem."""
desc, subs = item.get_description()
return DisplayItemWrapper(item, desc, pairs_to_flat(subs))

336
dist/ba_data/python/bacommon/bs/_msg.py vendored Normal file
View file

@ -0,0 +1,336 @@
# Released under the MIT License. See LICENSE for details.
#
"""BombSquad specific bits."""
from __future__ import annotations
import datetime
from enum import Enum
from dataclasses import dataclass, field
from typing import Annotated, override
from efro.dataclassio import ioprepped, IOAttrs
from efro.message import Message, Response
from bacommon.bs._displayitem import DisplayItemWrapper
from bacommon.bs._clienteffect import ClientEffect
from bacommon.bs._clouddialog import CloudDialogAction, CloudDialogWrapper
from bacommon.bs._chest import ClassicChestAppearance
@ioprepped
@dataclass
class ChestActionMessage(Message):
"""Request action about a chest."""
class Action(Enum):
"""Types of actions we can request."""
# Unlocking (for free or with tokens).
UNLOCK = 'u'
# Watched an ad to reduce wait.
AD = 'ad'
action: Annotated[Action, IOAttrs('a')]
# Tokens we are paying (only applies to unlock).
token_payment: Annotated[int, IOAttrs('t')]
chest_id: Annotated[str, IOAttrs('i')]
@override
@classmethod
def get_response_types(cls) -> list[type[Response] | None]:
return [ChestActionResponse]
@ioprepped
@dataclass
class ChestActionResponse(Response):
"""Here's the results of that action you asked for, boss."""
# Tokens that were actually charged.
tokens_charged: Annotated[int, IOAttrs('t')] = 0
# If present, signifies the chest has been opened and we should show
# the user this stuff that was in it.
contents: Annotated[list[DisplayItemWrapper] | None, IOAttrs('c')] = None
# If contents are present, which of the chest's prize-sets they
# represent.
prizeindex: Annotated[int, IOAttrs('i')] = 0
# Printable error if something goes wrong.
error: Annotated[str | None, IOAttrs('e')] = None
# Printable warning. Shown in orange with an error sound. Does not
# mean the action failed; only that there's something to tell the
# users such as 'It looks like you are faking ad views; stop it or
# you won't have ad options anymore.'
warning: Annotated[str | None, IOAttrs('w', store_default=False)] = None
# Printable success message. Shown in green with a cash-register
# sound. Can be used for things like successful wait reductions via
# ad views. Used in builds earlier than 22311; can remove once
# 22311+ is ubiquitous.
success_msg: Annotated[str | None, IOAttrs('s', store_default=False)] = None
# Effects to show on the client. Replaces warning and success_msg in
# build 22311 or newer.
effects: Annotated[
list[ClientEffect], IOAttrs('fx', store_default=False)
] = field(default_factory=list)
@ioprepped
@dataclass
class CloudDialogActionMessage(Message):
"""Do something to a client ui."""
id: Annotated[str, IOAttrs('i')]
action: Annotated[CloudDialogAction, IOAttrs('a')]
@override
@classmethod
def get_response_types(cls) -> list[type[Response] | None]:
return [CloudDialogActionResponse]
@ioprepped
@dataclass
class CloudDialogActionResponse(Response):
"""Did something to that inbox entry, boss."""
class ErrorType(Enum):
"""Types of errors that may have occurred."""
# Probably a future error type we don't recognize.
UNKNOWN = 'u'
# Something went wrong on the server, but specifics are not
# relevant.
INTERNAL = 'i'
# The entry expired on the server. In various cases such as 'ok'
# buttons this can generally be ignored.
EXPIRED = 'e'
error_type: Annotated[
ErrorType | None, IOAttrs('et', enum_fallback=ErrorType.UNKNOWN)
]
# User facing error message in the case of errors.
error_message: Annotated[str | None, IOAttrs('em')]
effects: Annotated[list[ClientEffect], IOAttrs('fx')]
@ioprepped
@dataclass
class GetClassicPurchasesMessage(Message):
"""Asking for current account's classic purchases."""
@override
@classmethod
def get_response_types(cls) -> list[type[Response] | None]:
return [GetClassicPurchasesResponse]
@ioprepped
@dataclass
class GetClassicPurchasesResponse(Response):
"""Here's those classic purchases ya asked for boss."""
purchases: Annotated[set[str], IOAttrs('p')]
@ioprepped
@dataclass
class GlobalProfileCheckMessage(Message):
"""Is this global profile name available?"""
name: Annotated[str, IOAttrs('n')]
@override
@classmethod
def get_response_types(cls) -> list[type[Response] | None]:
return [GlobalProfileCheckResponse]
@ioprepped
@dataclass
class GlobalProfileCheckResponse(Response):
"""Here's that profile check ya asked for boss."""
available: Annotated[bool, IOAttrs('a')]
ticket_cost: Annotated[int, IOAttrs('tc')]
@ioprepped
@dataclass
class InboxRequestMessage(Message):
"""Message requesting our inbox."""
@override
@classmethod
def get_response_types(cls) -> list[type[Response] | None]:
return [InboxRequestResponse]
@ioprepped
@dataclass
class InboxRequestResponse(Response):
"""Here's that inbox contents you asked for, boss."""
wrappers: Annotated[list[CloudDialogWrapper], IOAttrs('w')]
# Printable error if something goes wrong.
error: Annotated[str | None, IOAttrs('e')] = None
@ioprepped
@dataclass
class LegacyRequest(Message):
"""A generic request for the legacy master server."""
request: Annotated[str, IOAttrs('r')]
request_type: Annotated[str, IOAttrs('t')]
user_agent_string: Annotated[str, IOAttrs('u')]
data: Annotated[str, IOAttrs('d')]
@override
@classmethod
def get_response_types(cls) -> list[type[Response] | None]:
return [LegacyResponse]
@ioprepped
@dataclass
class LegacyResponse(Response):
"""Response for generic legacy request."""
data: Annotated[str | None, IOAttrs('d')]
zipped: Annotated[bool, IOAttrs('z')]
@ioprepped
@dataclass
class ChestInfoMessage(Message):
"""Request info about a chest."""
chest_id: Annotated[str, IOAttrs('i')]
@override
@classmethod
def get_response_types(cls) -> list[type[Response] | None]:
return [ChestInfoResponse]
@ioprepped
@dataclass
class ChestInfoResponse(Response):
"""Here's that chest info you asked for, boss."""
@dataclass
class Chest:
"""A lovely chest."""
@dataclass
class PrizeSet:
"""A possible set of prizes for this chest."""
weight: Annotated[float, IOAttrs('w')]
contents: Annotated[list[DisplayItemWrapper], IOAttrs('c')]
appearance: Annotated[
ClassicChestAppearance,
IOAttrs('a', enum_fallback=ClassicChestAppearance.UNKNOWN),
]
# How much it costs to unlock *now*.
unlock_tokens: Annotated[int, IOAttrs('tk')]
# When it unlocks on its own.
unlock_time: Annotated[datetime.datetime, IOAttrs('t')]
# Possible prizes we contain.
prizesets: Annotated[list[PrizeSet], IOAttrs('p')]
# Are ads allowed now?
ad_allow: Annotated[bool, IOAttrs('aa')]
chest: Annotated[Chest | None, IOAttrs('c')]
user_tokens: Annotated[int | None, IOAttrs('t')]
@ioprepped
@dataclass
class PrivatePartyMessage(Message):
"""Message asking about info we need for private-party UI."""
need_datacode: Annotated[bool, IOAttrs('d')]
@override
@classmethod
def get_response_types(cls) -> list[type[Response] | None]:
return [PrivatePartyResponse]
@ioprepped
@dataclass
class PrivatePartyResponse(Response):
"""Here's that private party UI info you asked for, boss."""
success: Annotated[bool, IOAttrs('s')]
tokens: Annotated[int, IOAttrs('t')]
gold_pass: Annotated[bool, IOAttrs('g')]
datacode: Annotated[str | None, IOAttrs('d')]
@ioprepped
@dataclass
class ScoreSubmitMessage(Message):
"""Let the server know we got some score in something."""
score_token: Annotated[str, IOAttrs('t')]
@override
@classmethod
def get_response_types(cls) -> list[type[Response] | None]:
return [ScoreSubmitResponse]
@ioprepped
@dataclass
class ScoreSubmitResponse(Response):
"""Did something to that inbox entry, boss."""
# Things we should show on our end.
effects: Annotated[list[ClientEffect], IOAttrs('fx')]
@ioprepped
@dataclass
class SendInfoMessage(Message):
"""User is using the send-info function."""
description: Annotated[str, IOAttrs('c')]
@override
@classmethod
def get_response_types(cls) -> list[type[Response] | None]:
return [SendInfoResponse]
@ioprepped
@dataclass
class SendInfoResponse(Response):
"""Response to sending info to the server."""
handled: Annotated[bool, IOAttrs('v')]
message: Annotated[str | None, IOAttrs('m', store_default=False)] = None
effects: Annotated[
list[ClientEffect], IOAttrs('e', store_default=False)
] = field(default_factory=list)
legacy_code: Annotated[str | None, IOAttrs('l', store_default=False)] = None

View file

@ -41,6 +41,7 @@ class ClientLoggerName(Enum):
ACCOUNT_CLIENT_V2 = 'ba.accountclientv2'
ACCOUNT = 'ba.account'
LOGIN_ADAPTER = 'ba.loginadapter'
UI = 'ba.ui'
@property
def description(self) -> str:
@ -86,6 +87,8 @@ class ClientLoggerName(Enum):
return 'account functionality'
if self is cls.LOGIN_ADAPTER:
return 'support for particular login types'
if self is cls.UI:
return 'anything user-interface related'
assert_never(self)

View file

@ -56,8 +56,8 @@ logger = logging.getLogger('ba.env')
# Build number and version of the ballistica binary we expect to be
# using.
TARGET_BALLISTICA_BUILD = 22535
TARGET_BALLISTICA_VERSION = '1.7.51'
TARGET_BALLISTICA_BUILD = 22584
TARGET_BALLISTICA_VERSION = '1.7.53'
@dataclass

View file

@ -250,9 +250,9 @@ class CloudSubsystem(babase.AppSubsystem):
@overload
def send_message_cb(
self,
msg: bacommon.bs.ClientUIActionMessage,
msg: bacommon.bs.CloudDialogActionMessage,
on_response: Callable[
[bacommon.bs.ClientUIActionResponse | Exception], None
[bacommon.bs.CloudDialogActionResponse | Exception], None
],
) -> None: ...

View file

@ -260,7 +260,6 @@ class CoopSession(Session):
with activity.context:
activity.end(results={'outcome': 'restart'}, force=True)
# noinspection PyUnresolvedReferences
@override
def on_activity_end(
self, activity: bascenev1.Activity, results: Any

View file

@ -258,7 +258,6 @@ class GameActivity[PlayerT: bascenev1.Player, TeamT: bascenev1.Team](
"""Return a name for this particular game instance."""
return self.get_display_string(self.settings_raw)
# noinspection PyUnresolvedReferences
def get_instance_scoreboard_display_string(self) -> babase.Lstr:
"""Return a name for this particular game instance.

View file

@ -83,7 +83,6 @@ def animate(
# FIXME: Even if we are looping we should have a way to die once we
# get disconnected.
if not loop:
# noinspection PyUnresolvedReferences
_bascenev1.timer(
(int(mult * items[-1][0]) + 1000) / 1000.0, curve.delete
)
@ -146,8 +145,6 @@ def animate_array(
# If we're not looping, set a timer to kill this
# curve after its done its job.
if not loop:
# (PyCharm seems to think item is a float, not a tuple)
# noinspection PyUnresolvedReferences
_bascenev1.timer(
(int(mult * items[-1][0]) + 1000) / 1000.0,
curve.delete,
@ -159,8 +156,6 @@ def animate_array(
# FIXME: Even if we are looping we should have a way to die
# once we get disconnected.
if not loop:
# (PyCharm seems to think item is a float, not a tuple)
# noinspection PyUnresolvedReferences
_bascenev1.timer(
(int(mult * items[-1][0]) + 1000) / 1000.0, combine.delete
)

View file

@ -502,7 +502,6 @@ class Chooser:
self._profileindex = self._profilenames.index(self._profilename)
else:
self._profileindex = 0
# noinspection PyUnresolvedReferences
self._profilename = self._profilenames[self._profileindex]
def update_position(self) -> None:

View file

@ -101,7 +101,6 @@ class MultiTeamScoreScreenActivity(bs.ScoreScreenActivity):
player_records = []
valid_players = list(self.stats.get_records().items())
# noinspection PyUnresolvedReferences
def _get_player_score_set_entry(
player: bs.SessionPlayer,
) -> bs.PlayerRecord | None:

View file

@ -700,7 +700,6 @@ class RaceGame(bs.TeamGameActivity[Player, Team]):
# FIXME: This is not type-safe!
# This call is expected to always return an Actor!
# Perhaps we need something like can_spawn_player()...
# noinspection PyTypeChecker
return None # type: ignore
pos = self._regions[player.last_region].pos

View file

@ -72,7 +72,6 @@ class Point(Enum):
class Spawn:
"""Defines a bot spawn event."""
# noinspection PyUnresolvedReferences
type: type[SpazBot]
path: int = 0
point: Point | None = None

View file

@ -4,7 +4,6 @@
# This file was automatically generated from "big_g.ma"
# pylint: disable=all
points = {}
# noinspection PyDictCreation
boxes = {}
boxes['area_of_interest_bounds'] = (
(-0.4011866709, 2.331310176, -0.5426286416)

View file

@ -4,7 +4,6 @@
# This file was automatically generated from "bridgit.ma"
# pylint: disable=all
points = {}
# noinspection PyDictCreation
boxes = {}
boxes['area_of_interest_bounds'] = (
(-0.2457963347, 3.828181068, -1.528362695)

View file

@ -4,7 +4,6 @@
# This file was automatically generated from "courtyard.ma"
# pylint: disable=all
points = {}
# noinspection PyDictCreation
boxes = {}
boxes['area_of_interest_bounds'] = (
(0.3544110667, 3.958431362, -2.175025358)

View file

@ -4,7 +4,6 @@
# This file was automatically generated from "crag_castle.ma"
# pylint: disable=all
points = {}
# noinspection PyDictCreation
boxes = {}
boxes['area_of_interest_bounds'] = (
(0.7033834902, 6.55869393, -3.153439808)

View file

@ -4,7 +4,6 @@
# This file was automatically generated from "doom_shroom.ma"
# pylint: disable=all
points = {}
# noinspection PyDictCreation
boxes = {}
boxes['area_of_interest_bounds'] = (
(0.4687647786, 2.320345088, -3.219423694)

View file

@ -4,7 +4,6 @@
# This file was automatically generated from "football_stadium.ma"
# pylint: disable=all
points = {}
# noinspection PyDictCreation
boxes = {}
boxes['area_of_interest_bounds'] = (
(0.0, 1.185751251, 0.4326226188)

View file

@ -4,7 +4,6 @@
# This file was automatically generated from "happy_thoughts.ma"
# pylint: disable=all
points = {}
# noinspection PyDictCreation
boxes = {}
boxes['area_of_interest_bounds'] = (
(-1.045859963, 12.67722855, -5.401537075)

View file

@ -4,7 +4,6 @@
# This file was automatically generated from "hockey_stadium.ma"
# pylint: disable=all
points = {}
# noinspection PyDictCreation
boxes = {}
boxes['area_of_interest_bounds'] = (
(0.0, 0.7956858119, 0.0)

View file

@ -4,7 +4,6 @@
# This file was automatically generated from "lake_frigid.ma"
# pylint: disable=all
points = {}
# noinspection PyDictCreation
boxes = {}
boxes['area_of_interest_bounds'] = (
(0.622753268, 3.958431362, -2.48708008)

View file

@ -4,7 +4,6 @@
# This file was automatically generated from "monkey_face.ma"
# pylint: disable=all
points = {}
# noinspection PyDictCreation
boxes = {}
boxes['area_of_interest_bounds'] = (
(-1.657177611, 4.132574186, -1.580485661)

View file

@ -4,7 +4,6 @@
# This file was automatically generated from "rampage.ma"
# pylint: disable=all
points = {}
# noinspection PyDictCreation
boxes = {}
boxes['area_of_interest_bounds'] = (
(0.3544110667, 5.616383286, -4.066055072)

View file

@ -4,7 +4,6 @@
# This file was automatically generated from "roundabout.ma"
# pylint: disable=all
points = {}
# noinspection PyDictCreation
boxes = {}
boxes['area_of_interest_bounds'] = (
(-1.552280404, 3.189001207, -2.40908495)

View file

@ -4,7 +4,6 @@
# This file was automatically generated from "step_right_up.ma"
# pylint: disable=all
points = {}
# noinspection PyDictCreation
boxes = {}
boxes['area_of_interest_bounds'] = (
(0.3544110667, 6.07676405, -2.271833016)

View file

@ -4,7 +4,6 @@
# This file was automatically generated from "the_pad.ma"
# pylint: disable=all
points = {}
# noinspection PyDictCreation
boxes = {}
boxes['area_of_interest_bounds'] = (
(0.3544110667, 4.493562578, -2.518391331)

View file

@ -4,7 +4,6 @@
# This file was automatically generated from "tip_top.ma"
# pylint: disable=all
points = {}
# noinspection PyDictCreation
boxes = {}
boxes['area_of_interest_bounds'] = (
(0.004375512593, 7.141135803, -0.01745294675)

View file

@ -4,7 +4,6 @@
# This file was automatically generated from "tower_d.ma"
# pylint: disable=all
points = {}
# noinspection PyDictCreation
boxes = {}
boxes['area_of_interest_bounds'] = (
(-0.4714933293, 2.887077774, -1.505479919)

View file

@ -4,7 +4,6 @@
# This file was automatically generated from "zig_zag.ma"
# pylint: disable=all
points = {}
# noinspection PyDictCreation
boxes = {}
boxes['area_of_interest_bounds'] = (
(-1.807378035, 3.943412768, -1.61304303)

View file

@ -42,7 +42,6 @@ def register_all_maps() -> None:
class HockeyStadium(bs.Map):
"""Stadium map used for ice hockey games."""
# noinspection PyUnresolvedReferences
from bascenev1lib.mapdata import hockey_stadium as defs
name = 'Hockey Stadium'
@ -207,7 +206,6 @@ class FootballStadium(bs.Map):
class Bridgit(bs.Map):
"""Map with a narrow bridge in the middle."""
# noinspection PyUnresolvedReferences
from bascenev1lib.mapdata import bridgit as defs
name = 'Bridgit'
@ -316,7 +314,6 @@ class Bridgit(bs.Map):
class BigG(bs.Map):
"""Large G shaped map for racing"""
# noinspection PyUnresolvedReferences
from bascenev1lib.mapdata import big_g as defs
name = 'Big G'
@ -430,7 +427,6 @@ class BigG(bs.Map):
class Roundabout(bs.Map):
"""CTF map featuring two platforms and a long way around between them"""
# noinspection PyUnresolvedReferences
from bascenev1lib.mapdata import roundabout as defs
name = 'Roundabout'
@ -538,7 +534,6 @@ class Roundabout(bs.Map):
class MonkeyFace(bs.Map):
"""Map sorta shaped like a monkey face; teehee!"""
# noinspection PyUnresolvedReferences
from bascenev1lib.mapdata import monkey_face as defs
name = 'Monkey Face'
@ -646,7 +641,6 @@ class MonkeyFace(bs.Map):
class ZigZag(bs.Map):
"""A very long zig-zaggy map"""
# noinspection PyUnresolvedReferences
from bascenev1lib.mapdata import zig_zag as defs
name = 'Zigzag'
@ -757,7 +751,6 @@ class ZigZag(bs.Map):
class ThePad(bs.Map):
"""A simple square shaped map with a raised edge."""
# noinspection PyUnresolvedReferences
from bascenev1lib.mapdata import the_pad as defs
name = 'The Pad'
@ -849,7 +842,6 @@ class ThePad(bs.Map):
class DoomShroom(bs.Map):
"""A giant mushroom. Of doom!"""
# noinspection PyUnresolvedReferences
from bascenev1lib.mapdata import doom_shroom as defs
name = 'Doom Shroom'
@ -949,7 +941,6 @@ class DoomShroom(bs.Map):
class LakeFrigid(bs.Map):
"""An icy lake fit for racing."""
# noinspection PyUnresolvedReferences
from bascenev1lib.mapdata import lake_frigid as defs
name = 'Lake Frigid'
@ -1039,7 +1030,6 @@ class LakeFrigid(bs.Map):
class TipTop(bs.Map):
"""A pointy map good for king-of-the-hill-ish games."""
# noinspection PyUnresolvedReferences
from bascenev1lib.mapdata import tip_top as defs
name = 'Tip Top'
@ -1120,7 +1110,6 @@ class TipTop(bs.Map):
class CragCastle(bs.Map):
"""A lovely castle map."""
# noinspection PyUnresolvedReferences
from bascenev1lib.mapdata import crag_castle as defs
name = 'Crag Castle'
@ -1343,7 +1332,6 @@ class TowerD(bs.Map):
class HappyThoughts(bs.Map):
"""Flying map."""
# noinspection PyUnresolvedReferences
from bascenev1lib.mapdata import happy_thoughts as defs
name = 'Happy Thoughts'
@ -1463,7 +1451,6 @@ class HappyThoughts(bs.Map):
class StepRightUp(bs.Map):
"""Wide stepped map good for CTF or Assault."""
# noinspection PyUnresolvedReferences
from bascenev1lib.mapdata import step_right_up as defs
name = 'Step Right Up'

View file

@ -234,7 +234,6 @@ class RemoveGloves:
def run(self, a: TutorialActivity) -> None:
# pylint: disable=protected-access
assert a.current_spaz is not None
# noinspection PyProtectedMember
a.current_spaz._gloves_wear_off()

View file

@ -16,8 +16,9 @@ from __future__ import annotations
import logging
# from efro.util import set_canonical_module_names
from babase import (
accountlog,
AccountV2Handle,
add_clean_frame_callback,
allows_ticket_sales,
app,
@ -34,7 +35,13 @@ from babase import (
AppTime,
apptimer,
AppTimer,
asset_loads_allowed,
balog,
Call,
DevConsoleButtonDef,
DevConsoleTab,
DevConsoleTabEntry,
DevConsoleSubsystem,
fullscreen_control_available,
fullscreen_control_get,
fullscreen_control_key_shortcut,
@ -43,6 +50,7 @@ from babase import (
clipboard_is_supported,
clipboard_set_text,
commit_app_config,
CloudSubscription,
ContextRef,
displaytime,
DisplayTime,
@ -90,6 +98,7 @@ from babase import (
pushcall,
quit,
QuitType,
request_main_ui,
request_permission,
safecolor,
screenmessage,
@ -103,6 +112,7 @@ from babase import (
supports_vsync,
supports_unicode_display,
timestring,
uilog,
UIScale,
unlock_all_input,
utc_now_cloud,
@ -116,6 +126,7 @@ from _bauiv1 import (
columnwidget,
containerwidget,
get_qrcode_texture,
get_selected_widget,
get_special_widget,
getmesh,
getsound,
@ -136,20 +147,26 @@ from _bauiv1 import (
uibounds,
Widget,
widget,
widget_by_id,
)
from bauiv1._cloudui import show_cloud_ui_window
from bauiv1._keyboard import Keyboard
from bauiv1._uitypes import (
uicleanupcheck,
RootUIUpdatePause,
)
from bauiv1._appsubsystem import UIV1AppSubsystem
from bauiv1._window import (
Window,
MainWindowState,
BasicMainWindowState,
uicleanupcheck,
MainWindow,
RootUIUpdatePause,
MainWindowAutoRecreateSuppress,
)
from bauiv1._appsubsystem import UIV1AppSubsystem
__all__ = [
'accountlog',
'AccountV2Handle',
'add_clean_frame_callback',
'allows_ticket_sales',
'app',
@ -167,9 +184,15 @@ __all__ = [
'AppTime',
'apptimer',
'AppTimer',
'asset_loads_allowed',
'balog',
'BasicMainWindowState',
'buttonwidget',
'Call',
'DevConsoleButtonDef',
'DevConsoleTab',
'DevConsoleTabEntry',
'DevConsoleSubsystem',
'fullscreen_control_available',
'fullscreen_control_get',
'fullscreen_control_key_shortcut',
@ -181,6 +204,7 @@ __all__ = [
'columnwidget',
'commit_app_config',
'containerwidget',
'CloudSubscription',
'ContextRef',
'displaytime',
'DisplayTime',
@ -197,6 +221,7 @@ __all__ = [
'get_qrcode_texture',
'get_remote_app_name',
'get_replays_dir',
'get_selected_widget',
'get_special_widget',
'get_string_height',
'get_string_width',
@ -241,6 +266,7 @@ __all__ = [
'quit',
'QuitType',
'reload_hooks',
'request_main_ui',
'request_permission',
'root_ui_pause_updates',
'root_ui_resume_updates',
@ -253,6 +279,7 @@ __all__ = [
'set_low_level_config_value',
'set_party_window_open',
'set_main_ui_input_device',
'show_cloud_ui_window',
'shutdown_suppress_begin',
'shutdown_suppress_end',
'Sound',
@ -266,22 +293,19 @@ __all__ = [
'timestring',
'uibounds',
'uicleanupcheck',
'uilog',
'UIScale',
'UIV1AppSubsystem',
'unlock_all_input',
'utc_now_cloud',
'WeakCall',
'widget',
'widget_by_id',
'Widget',
'Window',
'workspaces_in_use',
]
# We want stuff to show up as bauiv1.Foo instead of bauiv1._sub.Foo.
# UPDATE: Trying without this for now. Seems like this might cause more
# harm than good. Can flip it back on if it is missed.
# set_canonical_module_names(globals())
# Sanity check: we want to keep ballistica's dependencies and
# bootstrapping order clearly defined; let's check a few particular
# modules to make sure they never directly or indirectly import us

View file

@ -4,12 +4,14 @@
from __future__ import annotations
import os
import time
import logging
import inspect
import weakref
import warnings
from enum import Enum
from dataclasses import dataclass
from typing import TYPE_CHECKING, override
from efro.util import empty_weakref
@ -20,14 +22,13 @@ import _bauiv1
if TYPE_CHECKING:
from typing import Any, Callable
from bauiv1._uitypes import (
UICleanupCheck,
Window,
MainWindow,
MainWindowState,
)
from bauiv1._window import Window, MainWindow, MainWindowState
import bauiv1
# Set environment variable BA_DEBUG_UI_CLEANUP_CHECKS to 1
# to print detailed info about what is getting cleaned up when.
DEBUG_UI_CLEANUP_CHECKS = os.environ.get('BA_DEBUG_UI_CLEANUP_CHECKS') == '1'
class UIV1AppSubsystem(babase.AppSubsystem):
"""Consolidated UI functionality for the app.
@ -57,7 +58,7 @@ class UIV1AppSubsystem(babase.AppSubsystem):
CHEST_SLOT_3 = 'chest_slot_3'
def __init__(self) -> None:
from bauiv1._uitypes import MainWindow
from bauiv1._window import MainWindow
super().__init__()
@ -73,12 +74,7 @@ class UIV1AppSubsystem(babase.AppSubsystem):
# For storing arbitrary class-level state data for Windows or
# other UI related classes.
self.window_states: dict[type, Any] = {}
self._uiscale: babase.UIScale
self._update_ui_scale()
self.cleanupchecks: list[UICleanupCheck] = []
self.upkeeptimer: babase.AppTimer | None = None
self.main_window_shared_states: dict = {}
self.title_color = (0.72, 0.7, 0.75)
self.heading_color = (0.72, 0.7, 0.75)
@ -86,10 +82,15 @@ class UIV1AppSubsystem(babase.AppSubsystem):
self.window_auto_recreate_suppress_count = 0
self._uiscale: babase.UIScale
self._update_ui_scale()
self._upkeeptimer: babase.AppTimer | None = None
self._cleanupchecks: list[_UICleanupCheck] = []
self._last_win_recreate_screen_size: tuple[float, float] | None = None
self._last_win_recreate_uiscale: bauiv1.UIScale | None = None
self._last_win_recreate_time: float | None = None
self._win_recreate_timer: babase.AppTimer | None = None
self._base_ids: dict[str, int] = {}
# Elements in our root UI will call anything here when
# activated.
@ -97,6 +98,15 @@ class UIV1AppSubsystem(babase.AppSubsystem):
UIV1AppSubsystem.RootUIElement, Callable[[], None]
] = {}
def new_id_prefix(self, name: str) -> str:
"""Generate a unique id given a base name.
Useful to ensure widgets have globally unique ids even if
a particular window type is instantiated multiple times.
"""
val = self._base_ids[name] = self._base_ids.get(name, 0) + 1
return f'{name}{val}'
def _update_ui_scale(self) -> None:
uiscalestr = babase.get_ui_scale()
if uiscalestr == 'large':
@ -121,7 +131,7 @@ class UIV1AppSubsystem(babase.AppSubsystem):
@override
def reset(self) -> None:
from bauiv1._uitypes import MainWindow
from bauiv1._window import MainWindow
self.root_ui_calls.clear()
self._main_window = empty_weakref(MainWindow)
@ -134,10 +144,9 @@ class UIV1AppSubsystem(babase.AppSubsystem):
@override
def on_app_loading(self) -> None:
from bauiv1._uitypes import ui_upkeep
# Kick off our periodic UI upkeep.
self.upkeeptimer = babase.AppTimer(2.6543, ui_upkeep, repeat=True)
self._upkeeptimer = babase.AppTimer(2.6543, self._upkeep, repeat=True)
def get_main_window(self) -> bauiv1.MainWindow | None:
"""Return main window, if any."""
@ -147,12 +156,13 @@ class UIV1AppSubsystem(babase.AppSubsystem):
self,
window: bauiv1.MainWindow,
*,
back_state: MainWindowState | None,
from_window: bauiv1.MainWindow | None | bool = True,
is_back: bool = False,
is_top_level: bool = False,
is_auxiliary: bool = False,
back_state: MainWindowState | None = None,
suppress_warning: bool = False,
restore_shared_state: bool = True,
) -> None:
"""Set the current 'main' window.
@ -163,10 +173,10 @@ class UIV1AppSubsystem(babase.AppSubsystem):
The caller is responsible for cleaning up any previous main
window.
"""
# pylint: disable=too-many-locals
# pylint: disable=too-many-branches
# pylint: disable=too-many-statements
from bauiv1._uitypes import MainWindow
# pylint: disable=too-many-locals
from bauiv1._window import MainWindow
# If we haven't grabbed initial uiscale or screen size for
# recreate comparision purposes, this is a good time to do so.
@ -191,6 +201,10 @@ class UIV1AppSubsystem(babase.AppSubsystem):
# We used to accept Widgets but now want MainWindows.
if not isinstance(window, MainWindow):
# if callable(window):
# window = window()
# else:
raise RuntimeError(
f'set_main_window() now takes a MainWindow as its "window" arg.'
f' You passed a {type(window)}.',
@ -293,38 +307,23 @@ class UIV1AppSubsystem(babase.AppSubsystem):
if is_top_level:
# Top level windows don't have or expect anywhere to go
# back to.
window.main_window_back_state = None
elif back_state is not None:
window.main_window_back_state = back_state
else:
oldwin = self._main_window()
if oldwin is None:
# We currenty only hold weak refs to windows so that
# they are free to die on their own, but we expect
# the main menu window to keep itself alive as long
# as its the main one. Holler if that seems to not
# be happening.
logging.warning(
'set_main_window: No old MainWindow found'
' and is_top_level is False;'
' this should not happen.'
)
window.main_window_back_state = None
else:
window.main_window_back_state = self.save_main_window_state(
oldwin
)
assert back_state is None
window.main_window_back_state = back_state
self._main_window = window_weakref
self._main_window_widget = window_widget
# Now that we're all set up, restore any state.
if restore_shared_state:
window.main_window_restore_shared_state()
def has_main_window(self) -> bool:
"""Return whether a main menu window is present."""
return bool(self._main_window_widget)
def clear_main_window(self, transition: str | None = None) -> None:
"""Clear any existing main window."""
from bauiv1._uitypes import MainWindow
from bauiv1._window import MainWindow
main_window = self._main_window()
if main_window:
@ -358,6 +357,23 @@ class UIV1AppSubsystem(babase.AppSubsystem):
return winstate
def save_current_main_window_state(self) -> MainWindowState | None:
"""Save state for the current window, if any."""
# Calc a back-state from the current window.
current_main_win = self._main_window()
if current_main_win is None:
# We currenty only hold weak refs to windows so that
# they are free to die on their own, but we expect
# the main menu window to keep itself alive as long
# as its the main one. Holler if that seems to not
# be happening.
babase.uilog.warning(
'save_current_main_window_state: No old MainWindow found;'
' this should not happen.'
)
return None
return self.save_main_window_state(current_main_win)
def restore_main_window_state(self, state: MainWindowState) -> None:
"""Restore UI to a saved state."""
existing = self.get_main_window()
@ -411,6 +427,142 @@ class UIV1AppSubsystem(babase.AppSubsystem):
self._schedule_main_win_recreate()
def add_ui_cleanup_check(self, obj: Any, widget: bauiv1.Widget) -> None:
"""Checks to ensure a widget-owning object gets cleaned up properly.
This adds a check which will print an error message if the provided
object still exists ~5 seconds after the provided bauiv1.Widget
dies.
This is a good sanity check for any sort of object that wraps or
controls a bauiv1.Widget. For instance, a 'Window' class instance
has no reason to still exist once its root container bauiv1.Widget
has fully transitioned out and been destroyed. Circular references
or careless strong referencing can lead to such objects never
getting destroyed, however, and this helps detect such cases to
avoid memory leaks.
"""
if DEBUG_UI_CLEANUP_CHECKS:
print(f'adding uicleanup to {obj}')
if not isinstance(widget, _bauiv1.Widget):
raise TypeError('widget arg is not a bauiv1.Widget')
if bool(False):
def foobar() -> None:
"""Just testing."""
if DEBUG_UI_CLEANUP_CHECKS:
print('uicleanupcheck widget dying...')
widget.add_delete_callback(foobar)
self._cleanupchecks.append(
_UICleanupCheck(
obj=weakref.ref(obj), widget=widget, widget_death_time=None
)
)
def auxiliary_window_activate(
self,
win_type: type[bauiv1.MainWindow],
win_create_call: Callable[[], bauiv1.MainWindow],
) -> None:
"""Navigate to or away from an Auxiliary window.
Auxiliary windows can be thought of as 'side quests' in the
window hierarchy; places such as settings windows or league
ranking windows that the user might want to visit without losing
their place in the regular hierarchy.
Calling this method with a MainWindow of the provided type
already in the stack will back out past it (effectively toggling
the 'side quest' back off).
Calling this method with a *different* auxiliary window in the
stack will back out past that and replace it with this
(effectively ending the old side-quest and starting a new one).
"""
# pylint: disable=unidiomatic-typecheck
current_main_window = self.get_main_window()
# Scan our ancestors for auxiliary states matching our type as
# well as auxiliary states in general.
aux_matching_state: bauiv1.MainWindowState | None = None
aux_state: bauiv1.MainWindowState | None = None
if current_main_window is None:
raise RuntimeError(
'Not currently handling no-top-level-window case.'
)
state = current_main_window.main_window_back_state
while state is not None:
assert state.window_type is not None
if state.is_auxiliary:
if state.window_type is win_type:
aux_matching_state = state
else:
aux_state = state
state = state.parent
# If there's an ancestor auxiliary window-state matching our
# type, back out past it (example: poking settings, navigating
# down a level or two, and then poking settings again should
# back out of settings).
if aux_matching_state is not None:
current_main_window.main_window_back_state = (
aux_matching_state.parent
)
current_main_window.main_window_back()
return
# If there's an ancestory auxiliary state *not* matching our
# type, crop the state and swap in our new auxiliary UI
# (example: poking settings, then poking account, then poking
# back should end up where things were before the settings
# poke).
if aux_state is not None:
# Blow away the window stack and build a fresh one.
self.clear_main_window()
self.set_main_window(
win_create_call(),
from_window=False, # Disable from-check.
back_state=aux_state.parent,
suppress_warning=True,
is_auxiliary=True,
)
return
# Ok, no auxiliary states found. Now if current window is
# auxiliary and the type matches, simply do a back.
if (
current_main_window.main_window_is_auxiliary
and type(current_main_window) is win_type
):
current_main_window.main_window_back()
return
# If current window is auxiliary but type doesn't match,
# swap it out for our new auxiliary UI.
if current_main_window.main_window_is_auxiliary:
self.clear_main_window()
self.set_main_window(
win_create_call(),
from_window=False, # Disable from-check.
back_state=current_main_window.main_window_back_state,
suppress_warning=True,
is_auxiliary=True,
)
return
# Ok, no existing auxiliary stuff was found period. Just
# navigate forward to this UI.
current_main_window.main_window_replace(
win_create_call, is_auxiliary=True
)
def _schedule_main_win_recreate(self) -> None:
# If there is a timer set already, do nothing.
@ -483,3 +635,48 @@ class UIV1AppSubsystem(babase.AppSubsystem):
# future recreates.
self._last_win_recreate_uiscale = uiscale
self._last_win_recreate_screen_size = virtual_screen_size
def _upkeep(self) -> None:
"""Run UI cleanup checks, etc. should be called periodically."""
assert babase.app.classic is not None
remainingchecks = []
now = babase.apptime()
for check in self._cleanupchecks:
obj = check.obj()
# If the object has died, ignore and don't re-add.
if obj is None:
if DEBUG_UI_CLEANUP_CHECKS:
print('uicleanupcheck object is dead; hooray!')
continue
# If the widget hadn't died yet, note if it has.
if check.widget_death_time is None:
remainingchecks.append(check)
if not check.widget:
check.widget_death_time = now
else:
# Widget was already dead; complain if its been too long.
if now - check.widget_death_time > 5.0:
print(
'WARNING:',
obj,
'is still alive 5 second after its Widget died;'
' you might have a memory leak. Look for circular'
' references or outside things referencing your Window'
' class instance. See efro.debug module'
' for tools that can help debug this sort of thing.',
)
else:
remainingchecks.append(check)
self._cleanupchecks = remainingchecks
@dataclass
class _UICleanupCheck:
"""Holds info about a uicleanupcheck target."""
obj: weakref.ref
widget: bauiv1.Widget
widget_death_time: float | None

304
dist/ba_data/python/bauiv1/_cloudui.py vendored Normal file
View file

@ -0,0 +1,304 @@
# Released under the MIT License. See LICENSE for details.
#
"""UIs provided by the cloud (similar-ish to html in concept)."""
from __future__ import annotations
import random
from dataclasses import dataclass
from typing import TYPE_CHECKING, override, Annotated
from efro.dataclassio import ioprepped, IOAttrs
import babase
from bauiv1._window import MainWindow, BasicMainWindowState
import _bauiv1
if TYPE_CHECKING:
from bauiv1._window import MainWindowState
def show_cloud_ui_window() -> None:
"""Bust out a cloud-ui window."""
# Pop up an auxiliary window wherever we are in the nav stack.
babase.app.ui_v1.auxiliary_window_activate(
win_type=CloudUIWindow,
win_create_call=lambda: CloudUIWindow(state=None),
)
@ioprepped
@dataclass
class CloudUIButton:
"""Represents a button in a cloud-ui."""
@ioprepped
@dataclass
class CloudUIRow:
"""Represents a row in a cloud-ui."""
buttons: Annotated[list[CloudUIButton], IOAttrs('b')]
@ioprepped
@dataclass
class CloudUIRoot:
"""Represents an entire cloud-ui."""
title: Annotated[str, IOAttrs('t')]
rows: Annotated[list[CloudUIRow], IOAttrs('r')]
class CloudUIWindow(MainWindow):
"""An example of a well-behaved main-window."""
@dataclass
class _State:
root: CloudUIRoot | None
def __init__(
self,
state: _State | None,
*,
transition: str | None = 'in_right',
origin_widget: _bauiv1.Widget | None = None,
auxiliary_style: bool = True,
):
ui = babase.app.ui_v1
self._state: CloudUIWindow._State | None = None
# We want to display differently whether we're an auxiliary
# window or not, but unfortunately that value is not yet
# available until we're added to the main-window-stack so it
# must be explicitly passed in.
self._auxiliary_style = auxiliary_style
# Calc scale and size for our backing window. For medium & large
# ui-scale we aim for a window small enough to always be fully
# visible on-screen and for small mode we aim for a window big
# enough that we never see the window edges; only the window
# texture covering the whole screen.
uiscale = ui.uiscale
self._width = 1400 if uiscale is babase.UIScale.SMALL else 750
self._height = 1200 if uiscale is babase.UIScale.SMALL else 500
scale = (
1.5
if uiscale is babase.UIScale.SMALL
else 1.2 if uiscale is babase.UIScale.MEDIUM else 1.0
)
# Do some fancy math to calculate our visible area; this will be
# limited by the screen size in small mode and our backing size
# otherwise.
screensize = babase.get_virtual_screen_size()
self._vis_width = min(self._width - 100, screensize[0] / scale)
self._vis_height = min(self._height - 100, screensize[1] / scale)
self._vis_top = 0.5 * self._height + 0.5 * self._vis_height
self._vis_left = 0.5 * self._width - 0.5 * self._vis_width
# Nudge our vis area up a bit when we can see the full backing
# (visual fudge factor).
if uiscale is not babase.UIScale.SMALL:
self._vis_top += 12.0
super().__init__(
root_widget=_bauiv1.containerwidget(
size=(self._width, self._height),
toolbar_visibility='menu_full',
toolbar_cancel_button_style=(
'close' if auxiliary_style else 'back'
),
scale=scale,
),
transition=transition,
origin_widget=origin_widget,
# We respond to screen size changes only at small ui-scale;
# in other cases we assume our window remains fully visible
# always (flip to windowed mode and resize the app window to
# confirm this).
refresh_on_screen_size_changes=uiscale is babase.UIScale.SMALL,
)
# Avoid complaints if nothing is selected under us.
_bauiv1.widget(edit=self._root_widget, allow_preserve_selection=False)
# Title.
self._title = _bauiv1.textwidget(
parent=self._root_widget,
position=(self._width * 0.5, self._vis_top - 20),
size=(0, 0),
text='',
color=ui.title_color,
scale=0.9 if uiscale is babase.UIScale.SMALL else 1.0,
# Make sure we avoid overlapping meters in small mode.
maxwidth=(130 if uiscale is babase.UIScale.SMALL else 200),
h_align='center',
v_align='center',
)
# For small UI-scale we use the system back/close button;
# otherwise we make our own.
if uiscale is babase.UIScale.SMALL:
_bauiv1.containerwidget(
edit=self._root_widget, on_cancel_call=self.main_window_back
)
else:
btn = _bauiv1.buttonwidget(
parent=self._root_widget,
id=f'{self.main_window_id_prefix}|close',
scale=0.8,
position=(self._vis_left - 15, self._vis_top - 30),
size=(50, 50) if auxiliary_style else (60, 55),
extra_touch_border_scale=2.0,
button_type=None if auxiliary_style else 'backSmall',
on_activate_call=self.main_window_back,
autoselect=True,
label=babase.charstr(
babase.SpecialChar.CLOSE
if auxiliary_style
else babase.SpecialChar.BACK
),
)
_bauiv1.containerwidget(edit=self._root_widget, cancel_button=btn)
# Show our vis-area bounds (for debugging).
if bool(True):
# Skip top-left since its always overlapping back/close
# buttons.
if bool(False):
_bauiv1.textwidget(
parent=self._root_widget,
position=(self._vis_left, self._vis_top),
size=(0, 0),
color=(1, 1, 1, 0.5),
scale=0.5,
text='TL',
h_align='left',
v_align='top',
)
_bauiv1.textwidget(
parent=self._root_widget,
position=(self._vis_left + self._vis_width, self._vis_top),
size=(0, 0),
color=(1, 1, 1, 0.5),
scale=0.5,
text='TR',
h_align='right',
v_align='top',
)
_bauiv1.textwidget(
parent=self._root_widget,
position=(self._vis_left, self._vis_top - self._vis_height),
size=(0, 0),
color=(1, 1, 1, 0.5),
scale=0.5,
text='BL',
h_align='left',
v_align='bottom',
)
_bauiv1.textwidget(
parent=self._root_widget,
position=(
self._vis_left + self._vis_width,
self._vis_top - self._vis_height,
),
size=(0, 0),
scale=0.5,
color=(1, 1, 1, 0.5),
text='BR',
h_align='right',
v_align='bottom',
)
self._spinner: _bauiv1.Widget | None = _bauiv1.spinnerwidget(
parent=self._root_widget,
position=(
self._vis_left + self._vis_width * 0.5,
self._vis_top - self._vis_height * 0.5,
),
size=48,
style='bomb',
)
if state is not None:
self._set_state(state)
else:
if random.random() < 0.3:
babase.apptimer(1.0, babase.WeakCall(self._on_error_response))
else:
babase.apptimer(1.0, babase.WeakCall(self._on_response))
def _on_error_response(self) -> None:
self._set_state(self._State(None))
def _on_response(self) -> None:
self._set_state(self._State(CloudUIRoot(title='Testing', rows=[])))
def _set_state(self, state: _State) -> None:
"""Set a final state (error or page contents).
This state may be instantly restored if the window is recreated
(depending on cache lifespan/etc.)
"""
assert self._state is None
self._state = state
if self._spinner:
self._spinner.delete()
self._spinner = None
if self._state.root is None:
_bauiv1.textwidget(
edit=self._title,
literal=False, # Allow Lstr.
text=babase.Lstr(resource='errorText'),
)
_bauiv1.textwidget(
parent=self._root_widget,
position=(
self._vis_left + 0.5 * self._vis_width,
self._vis_top - 0.5 * self._vis_height,
),
size=(0, 0),
scale=0.6,
text=babase.Lstr(resource='store.loadErrorText'),
h_align='center',
v_align='center',
)
else:
_bauiv1.textwidget(
edit=self._title,
literal=True, # Never interpret as Lstr.
text=self._state.root.title,
)
@override
def get_main_window_state(self) -> MainWindowState:
# Support recreating our window for back/refresh purposes.
cls = type(self)
# IMPORTANT - Pull values from self HERE; if we do it in the
# lambda below it'll keep self alive which will lead to
# 'ui-not-getting-cleaned-up' warnings and memory leaks.
auxiliary_style = self._auxiliary_style
state = self._state
return BasicMainWindowState(
create_call=lambda transition, origin_widget: cls(
state=state,
transition=transition,
origin_widget=origin_widget,
auxiliary_style=auxiliary_style,
),
)
@override
def main_window_should_preserve_selection(self) -> bool:
return True
@override
def get_main_window_shared_state_id(self) -> str | None:
return 'cloudui'

View file

@ -1,13 +1,10 @@
# Released under the MIT License. See LICENSE for details.
#
"""Provide top level UI related functionality."""
"""Misc UI related types."""
from __future__ import annotations
import os
import weakref
import logging
from dataclasses import dataclass
import warnings
from typing import TYPE_CHECKING, override
import babase
@ -19,405 +16,21 @@ if TYPE_CHECKING:
import bauiv1
# Set environment variable BA_DEBUG_UI_CLEANUP_CHECKS to 1
# to print detailed info about what is getting cleaned up when.
DEBUG_UI_CLEANUP_CHECKS = os.environ.get('BA_DEBUG_UI_CLEANUP_CHECKS') == '1'
class Window:
"""A basic window.
Essentially wraps a ContainerWidget with some higher level
functionality.
"""
def __init__(
self,
root_widget: bauiv1.Widget,
cleanupcheck: bool = True,
prevent_main_window_auto_recreate: bool = True,
):
self._root_widget = root_widget
# By default, the presence of any generic windows prevents the
# app from running its fancy main-window-auto-recreate mechanism
# on screen-resizes and whatnot. This avoids things like
# temporary popup windows getting stuck under auto-re-created
# main-windows.
self._window_main_window_auto_recreate_suppress = (
MainWindowAutoRecreateSuppress()
if prevent_main_window_auto_recreate
else None
)
# Generally we complain if we outlive our root widget.
if cleanupcheck:
uicleanupcheck(self, root_widget)
def get_root_widget(self) -> bauiv1.Widget:
"""Return the root widget."""
return self._root_widget
class MainWindow(Window):
"""A special type of window that can be set as 'main'.
The UI system has at most one main window at any given time.
MainWindows support high level functionality such as saving and
restoring states, allowing them to be automatically recreated when
navigating back from other locations or when something like ui-scale
changes.
"""
def __init__(
self,
root_widget: bauiv1.Widget,
*,
transition: str | None,
origin_widget: bauiv1.Widget | None,
cleanupcheck: bool = True,
refresh_on_screen_size_changes: bool = False,
):
"""Create a MainWindow given a root widget and transition info.
Automatically handles in and out transitions on the provided
widget, so there is no need to set transitions when creating it.
"""
# A back-state supplied by the ui system.
self.main_window_back_state: MainWindowState | None = None
self.main_window_is_top_level: bool = False
# Windows that size tailor themselves to exact screen dimensions
# can pass True for this. Generally this only applies to small
# ui scale and at larger scales windows simply fit in the
# virtual safe area.
self.refreshes_on_screen_size_changes = refresh_on_screen_size_changes
# Windows can be flagged as auxiliary when not related to the
# main UI task at hand. UI code may choose to handle auxiliary
# windows in special ways, such as by implicitly replacing
# existing auxiliary windows with new ones instead of keeping
# old ones as back targets.
self.main_window_is_auxiliary: bool = False
self._main_window_transition = transition
self._main_window_origin_widget = origin_widget
super().__init__(
root_widget,
cleanupcheck=cleanupcheck,
prevent_main_window_auto_recreate=False,
)
scale_origin: tuple[float, float] | None
if origin_widget is not None:
self._main_window_transition_out = 'out_scale'
scale_origin = origin_widget.get_screen_space_center()
transition = 'in_scale'
else:
self._main_window_transition_out = 'out_right'
scale_origin = None
_bauiv1.containerwidget(
edit=root_widget,
transition=transition,
scale_origin_stack_offset=scale_origin,
)
def main_window_close(self, transition: str | None = None) -> None:
"""Get window transitioning out if still alive."""
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
# Transition ourself out.
try:
self.on_main_window_close()
except Exception:
logging.exception('Error in on_main_window_close() for %s.', self)
# Note: normally transition of None means instant, but we use
# that to mean 'do the default' so we support a special
# 'instant' string.
if transition == 'instant':
self._root_widget.delete()
else:
_bauiv1.containerwidget(
edit=self._root_widget,
transition=(
self._main_window_transition_out
if transition is None
else transition
),
)
def main_window_has_control(self) -> bool:
"""Is this MainWindow allowed to change the global main window?
It is a good idea to make sure this is True before calling
main_window_replace(). This prevents fluke UI breakage such as
multiple simultaneous events causing a MainWindow to spawn
multiple replacements for itself.
"""
# We are allowed to change main windows if we are the current one
# AND our underlying widget is still alive and not transitioning out.
return (
babase.app.ui_v1.get_main_window() is self
and bool(self._root_widget)
and not self._root_widget.transitioning_out
)
def main_window_back(self) -> None:
"""Move back in the main window stack.
Is a no-op if the main window does not have control;
no need to check main_window_has_control() first.
"""
# Users should always check main_window_has_control() before
# calling us. Error if it seems they did not.
if not self.main_window_has_control():
return
uiv1 = babase.app.ui_v1
# Get the 'back' window coming in.
if not self.main_window_is_top_level:
back_state = self.main_window_back_state
if back_state is None:
raise RuntimeError(
f'Main window {self} provides no back-state.'
)
# Valid states should have values here.
assert back_state.is_top_level is not None
assert back_state.is_auxiliary is not None
assert back_state.window_type is not None
# When leaving an auxiliary window, scale the destination
# window in instead of sliding to convey that its more of a
# 'swapping out' than a 'back' action.
backwin = back_state.create_window(
transition=(
'in_scale' if self.main_window_is_auxiliary else 'in_left'
)
)
uiv1.set_main_window(
backwin,
from_window=self,
is_back=True,
back_state=back_state,
suppress_warning=True,
)
# Transition ourself out.
self.main_window_close()
def main_window_replace(
self,
new_window: MainWindow,
back_state: MainWindowState | None = None,
is_auxiliary: bool = False,
) -> None:
"""Replace ourself with a new MainWindow."""
# Users should always check main_window_has_control() *before*
# creating new MainWindows and passing them in here. Kill the
# passed window and Error if it seems they did not.
if not self.main_window_has_control():
new_window.get_root_widget().delete()
raise RuntimeError(
f'main_window_replace() called on a not-in-control window'
f' ({self}); always check main_window_has_control() before'
f' calling main_window_replace().'
)
# For auxiliary windows, use scale to give a feel that we're
# switching over to a totally separate 'side quest' ui. For
# regular back/forward relationships, shove the old out the left
# to give the feel that we're adding to a nav stack.
if is_auxiliary:
transition = 'out_scale'
else:
transition = 'out_left'
# Transition ourself out.
try:
self.on_main_window_close()
except Exception:
logging.exception('Error in on_main_window_close() for %s.', self)
_bauiv1.containerwidget(edit=self._root_widget, transition=transition)
babase.app.ui_v1.set_main_window(
new_window,
from_window=self,
back_state=back_state,
is_auxiliary=is_auxiliary,
suppress_warning=True,
)
def on_main_window_close(self) -> None:
"""Called before transitioning out a main window.
A good opportunity to save window state/etc.
"""
def get_main_window_state(self) -> MainWindowState:
"""Return a WindowState to recreate this window, if supported."""
raise NotImplementedError()
class MainWindowState:
"""Persistent state for a specific MainWindow.
This allows MainWindows to be automatically recreated for back-button
purposes, when switching app-modes, etc.
"""
def __init__(self) -> None:
# The window that back/cancel navigation should take us to.
self.parent: MainWindowState | None = None
self.is_top_level: bool | None = None
self.is_auxiliary: bool | None = None
self.window_type: type[MainWindow] | None = None
self.selection: str | None = None
def create_window(
self,
transition: Literal['in_right', 'in_left', 'in_scale'] | None = None,
origin_widget: bauiv1.Widget | None = None,
) -> MainWindow:
"""Create a window based on this state.
WindowState child classes should override this to recreate their
particular type of window.
"""
raise NotImplementedError()
class BasicMainWindowState(MainWindowState):
"""A basic MainWindowState holding a lambda to recreate a MainWindow."""
def __init__(
self,
create_call: Callable[
[
Literal['in_right', 'in_left', 'in_scale'] | None,
bauiv1.Widget | None,
],
bauiv1.MainWindow,
],
) -> None:
super().__init__()
self.create_call = create_call
@override
def create_window(
self,
transition: Literal['in_right', 'in_left', 'in_scale'] | None = None,
origin_widget: bauiv1.Widget | None = None,
) -> bauiv1.MainWindow:
return self.create_call(transition, origin_widget)
class MainWindowAutoRecreateSuppress:
"""Suppresses main-window auto-recreate while in existence.
Can be instantiated and held by windows or processes within windows
for the purpose of preventing the main-window auto-recreate
mechanism from firing. This mechanism normally fires when the screen
is resized or the ui-scale is changed, allowing windows to be
recreated to adapt to the new configuration.
"""
def __init__(self) -> None:
babase.app.ui_v1.window_auto_recreate_suppress_count += 1
def __del__(self) -> None:
babase.app.ui_v1.window_auto_recreate_suppress_count -= 1
@dataclass
class UICleanupCheck:
"""Holds info about a uicleanupcheck target."""
obj: weakref.ref
widget: bauiv1.Widget
widget_death_time: float | None
# REMOVE WHEN API 9 SUPPORT ENDS
def uicleanupcheck(obj: Any, widget: bauiv1.Widget) -> None:
"""Checks to ensure a widget-owning object gets cleaned up properly.
This adds a check which will print an error message if the provided
object still exists ~5 seconds after the provided bauiv1.Widget dies.
This is a good sanity check for any sort of object that wraps or
controls a bauiv1.Widget. For instance, a 'Window' class instance has
no reason to still exist once its root container bauiv1.Widget has fully
transitioned out and been destroyed. Circular references or careless
strong referencing can lead to such objects never getting destroyed,
however, and this helps detect such cases to avoid memory leaks.
"""
if DEBUG_UI_CLEANUP_CHECKS:
print(f'adding uicleanup to {obj}')
if not isinstance(widget, _bauiv1.Widget):
raise TypeError('widget arg is not a bauiv1.Widget')
if bool(False):
def foobar() -> None:
"""Just testing."""
if DEBUG_UI_CLEANUP_CHECKS:
print('uicleanupcheck widget dying...')
widget.add_delete_callback(foobar)
assert babase.app.classic is not None
babase.app.ui_v1.cleanupchecks.append(
UICleanupCheck(
obj=weakref.ref(obj), widget=widget, widget_death_time=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,
)
def ui_upkeep() -> None:
"""Run UI cleanup checks, etc. should be called periodically."""
assert babase.app.classic is not None
ui = babase.app.ui_v1
remainingchecks = []
now = babase.apptime()
for check in ui.cleanupchecks:
obj = check.obj()
# If the object has died, ignore and don't re-add.
if obj is None:
if DEBUG_UI_CLEANUP_CHECKS:
print('uicleanupcheck object is dead; hooray!')
continue
# If the widget hadn't died yet, note if it has.
if check.widget_death_time is None:
remainingchecks.append(check)
if not check.widget:
check.widget_death_time = now
else:
# Widget was already dead; complain if its been too long.
if now - check.widget_death_time > 5.0:
print(
'WARNING:',
obj,
'is still alive 5 second after its Widget died;'
' you might have a memory leak. Look for circular'
' references or outside things referencing your Window'
' class instance. See efro.debug module'
' for tools that can help debug this sort of thing.',
)
else:
remainingchecks.append(check)
ui.cleanupchecks = remainingchecks
babase.app.ui_v1.add_ui_cleanup_check(obj, widget)
class TextWidgetStringEditAdapter(babase.StringEditAdapter):

604
dist/ba_data/python/bauiv1/_window.py vendored Normal file
View file

@ -0,0 +1,604 @@
# Released under the MIT License. See LICENSE for details.
#
"""Window related UI bits."""
from __future__ import annotations
import logging
import warnings
from typing import TYPE_CHECKING, override
import babase
import _bauiv1
if TYPE_CHECKING:
from typing import Any, Type, Literal, Callable
import bauiv1
class Window:
"""A basic window.
Essentially wraps a ContainerWidget with some higher level
functionality.
"""
def __init__(
self,
root_widget: bauiv1.Widget,
cleanupcheck: bool = True,
prevent_main_window_auto_recreate: bool = True,
):
self._root_widget = root_widget
# By default, the presence of any generic windows prevents the
# app from running its fancy main-window-auto-recreate mechanism
# on screen-resizes and whatnot. This avoids things like
# temporary popup windows getting stuck under auto-re-created
# main-windows.
self._window_main_window_auto_recreate_suppress = (
MainWindowAutoRecreateSuppress()
if prevent_main_window_auto_recreate
else None
)
# Generally we complain if we outlive our root widget.
if cleanupcheck:
babase.app.ui_v1.add_ui_cleanup_check(self, root_widget)
def get_root_widget(self) -> bauiv1.Widget:
"""Return the root widget."""
return self._root_widget
class MainWindow(Window):
"""A special type of window that can be set as 'main'.
The UI system has at most one main window at any given time.
MainWindows support high level functionality such as saving and
restoring states, allowing them to be automatically recreated when
navigating back from other locations or when something like ui-scale
changes.
"""
def __init__(
self,
root_widget: bauiv1.Widget,
*,
transition: str | None,
origin_widget: bauiv1.Widget | None,
cleanupcheck: bool = True,
refresh_on_screen_size_changes: bool = False,
):
"""Create a MainWindow given a root widget and transition info.
Automatically handles in and out transitions on the provided
widget, so there is no need to set transitions when creating it.
"""
self.main_window_id_prefix = babase.app.ui_v1.new_id_prefix(
type(self).__name__.lower()
)
# A back-state supplied by the ui system.
self.main_window_back_state: MainWindowState | None = None
self.main_window_is_top_level: bool = False
# Windows that size tailor themselves to exact screen dimensions
# can pass True for this. Generally this only applies to small
# ui scale and at larger scales windows simply fit in the
# virtual safe area.
self.refreshes_on_screen_size_changes = refresh_on_screen_size_changes
# Windows can be flagged as auxiliary when not related to the
# main UI task at hand. UI code may choose to handle auxiliary
# windows in special ways, such as by implicitly replacing
# existing auxiliary windows with new ones instead of keeping
# old ones as back targets.
self.main_window_is_auxiliary: bool = False
self._main_window_transition = transition
self._main_window_origin_widget = origin_widget
super().__init__(
root_widget,
cleanupcheck=cleanupcheck,
prevent_main_window_auto_recreate=False,
)
scale_origin: tuple[float, float] | None
if origin_widget is not None:
self._main_window_transition_out = 'out_scale'
scale_origin = origin_widget.get_screen_space_center()
transition = 'in_scale'
else:
self._main_window_transition_out = 'out_right'
scale_origin = None
_bauiv1.containerwidget(
edit=root_widget,
transition=transition,
scale_origin_stack_offset=scale_origin,
)
def main_window_save_shared_state(self) -> None:
"""Save shared state (such as widget selection).
This is automatically called just before main-windows are
destroyed, but the user may opt to call it at other times such
as before refreshing a UI (so that selection can be restored
after the refresh, etc.)
State contained here is intended to operate on
already-constructed UI; state that influences which UI is
contructed should go through other mechanisms.
"""
# pylint: disable=assignment-from-none
key = self.get_main_window_shared_state_id()
assert isinstance(key, str | None)
keyfin = type(self) if key is None else key
shared_state: dict = {}
# Save selection if desired.
if self._get_main_window_should_preserve_selection():
sel = _bauiv1.get_selected_widget()
if sel is None:
selfin = None
else:
if sel.allow_preserve_selection:
selfin = sel.id
if selfin is not None:
pre = f'{self.main_window_id_prefix}|'
if selfin.startswith(pre):
selfin = f'$(WIN)|{selfin.removeprefix(pre)}'
babase.uilog.debug(
"Saving ui selection from '%s': '%s'.",
self.main_window_id_prefix,
selfin,
)
else:
# if not sel.allow_preserve_selection:
babase.uilog.warning(
'main_window_should_preserve_selection()'
' returned True for %s but no id was assigned'
' to the currently selected widget %s. All'
' selectable widgets must be assigned unique'
' ids for selection-preserving to work'
' properly.',
self,
sel,
)
else:
selfin = None
babase.uilog.debug(
"Not saving ui selection from '%s';"
' selected widget disallows it (%s).',
self.main_window_id_prefix,
sel,
)
shared_state['selection'] = selfin
# Allow win to save any custom state. (Do this after selection
# save so user can manipulate save output if they want).
try:
self.main_window_do_save_shared_state(shared_state)
except Exception:
logging.exception(
'Error in main_window_do_save_shared_state() for %s.', self
)
assert isinstance(shared_state, dict)
babase.uilog.debug(
"Saving shared state from '%s' using key %r.",
self.main_window_id_prefix,
keyfin,
)
babase.app.ui_v1.main_window_shared_states[keyfin] = shared_state
def main_window_restore_shared_state(self) -> None:
"""Restore shared state (such as widget selection), if any.
This is automatically called just after main-windows are
created, but the user may opt to call it at other times such as
after explicitly refreshing some UI.
State contained here is intended to operate on
already-constructed UI; state that influences which UI is
contructed should go through other mechanisms.
"""
# pylint: disable=assignment-from-none
key = self.get_main_window_shared_state_id()
assert isinstance(key, str | None)
keyfin = type(self) if key is None else key
babase.uilog.debug(
"Restoring shared state to '%s' using key %r.",
self.main_window_id_prefix,
keyfin,
)
shared_state = babase.app.ui_v1.main_window_shared_states.get(keyfin)
if shared_state is None:
shared_state = {}
assert isinstance(shared_state, dict)
# Allow win to restore any custom state. (Do this before
# selection restore so user can manipulate input if they want).
try:
self.main_window_do_restore_shared_state(shared_state)
except Exception:
logging.exception(
'Error in main_window_do_restore_shared_state() for %s.', self
)
# Restore selection if desired.
if self._get_main_window_should_preserve_selection():
sel = shared_state.get('selection')
if isinstance(sel, str):
babase.uilog.debug(
"Restoring ui selection to '%s': '%s'.",
self.main_window_id_prefix,
sel,
)
pre = '$(WIN)|'
if sel.startswith(pre):
sel = (
f'{self.main_window_id_prefix}|{sel.removeprefix(pre)}'
)
widget = _bauiv1.widget_by_id(sel)
if widget is not None:
if widget.selectable:
widget.global_select()
widget.scroll_into_view()
else:
babase.uilog.debug(
"Unable to restore selection '%s';"
' widget is not selectable.',
sel,
)
else:
# We expect this to happen sometimes (windows may come
# up with different UIs visible/etc.). Let's note it but
# subtly.
babase.uilog.debug(
"Unable to restore selection '%s'; widget not found.",
sel,
)
def main_window_close(self, transition: str | None = None) -> None:
"""Get window transitioning out if still alive."""
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
# Save selection, etc.
self.main_window_save_shared_state()
# Give the user a chance to do whatever.
try:
self.on_main_window_close()
except Exception:
logging.exception('Error in on_main_window_close() for %s.', self)
# Transition ourself out.
# Note: normally transition of None means instant, but we use
# that to mean 'do the default' so we support a special
# 'instant' string.
if transition == 'instant':
self._root_widget.delete()
else:
_bauiv1.containerwidget(
edit=self._root_widget,
transition=(
self._main_window_transition_out
if transition is None
else transition
),
)
def main_window_has_control(self) -> bool:
"""Is this MainWindow allowed to change the global main window?
This is called internally by methods such as
:meth:`main_window_replace()` and :meth:`main_window_back()` so
generally you do not need to call it directly when using those.
However you may still opt to check this if doing other actions
besides main-window navigation (such as displaying pop-ups).
"""
# We are allowed to change main windows if we are the current one
# AND our underlying widget is still alive and not transitioning out.
return (
babase.app.ui_v1.get_main_window() is self
and bool(self._root_widget)
and not self._root_widget.transitioning_out
)
def main_window_back(self) -> None:
"""Move back in the main window stack.
Is a no-op if the main window does not have control;
no need to check main_window_has_control() first.
"""
# Users should always check main_window_has_control() before
# calling us. Error if it seems they did not.
if not self.main_window_has_control():
return
uiv1 = babase.app.ui_v1
# Get ourself transitioning out.
self.main_window_close()
# Get the 'back' window coming in.
if not self.main_window_is_top_level:
back_state = self.main_window_back_state
if back_state is None:
raise RuntimeError(
f'Main window {self} provides no back-state.'
)
# Valid states should have values here.
assert back_state.is_top_level is not None
assert back_state.is_auxiliary is not None
assert back_state.window_type is not None
# When leaving an auxiliary window, scale the destination
# window in instead of sliding to convey that its more of a
# 'swapping out' than a 'back' action.
backwin = back_state.create_window(
transition=(
'in_scale' if self.main_window_is_auxiliary else 'in_left'
)
)
uiv1.set_main_window(
backwin,
from_window=self,
is_back=True,
back_state=back_state,
suppress_warning=True,
)
def main_window_replace(
self,
new_window: MainWindow | Callable[[], MainWindow],
back_state: MainWindowState | None = None,
is_auxiliary: bool = False,
) -> MainWindow | None:
"""Replace ourself with a new MainWindow.
Returns the new MainWindow. Will no-op and return None if
we are not allowed to replace the MainWindow.
"""
ui = babase.app.ui_v1
# If they didn't provide an explicit back-state, calc one to
# recreate this window.
if back_state is None:
back_state = ui.save_current_main_window_state()
# Save selection, etc.
self.main_window_save_shared_state()
if not isinstance(new_window, MainWindow):
# If we're not in control, we're not allowed to change things.
if not self.main_window_has_control():
babase.uilog.debug(
'main_window_replace:'
' no-op due to main_window_has_control() returning False.',
stack_info=True,
)
return None
new_window = new_window()
else:
# We originally were passed MainWindows directly, but want
# to phase this out, as it prevents our automatic selection
# save/restore from working (we need to save the old
# selection *before* the replacement window is created since
# the creation itself will change the selection).
warnings.warn(
'Passing MainWindow objects to main_window_replace() is'
' deprecated and will be removed when api 9 support ends.'
' You should instead pass calls to generate MainWindow objects.'
' So `main_window_replace(MyWin(some_arg))` would become'
' `main_win_replace(lambda: MyWin(some_arg))`.',
DeprecationWarning,
stacklevel=2,
)
# In this old path, users should always check
# main_window_has_control() *before* creating new
# MainWindows and passing them in here. Kill the passed
# window and Error if it seems they did not.
if not self.main_window_has_control():
new_window.get_root_widget().delete()
raise RuntimeError(
f'main_window_replace() called on a not-in-control window'
f' ({self}); always check main_window_has_control() before'
f' calling main_window_replace().'
)
# Give user a chance to do whatever.
try:
self.on_main_window_close()
except Exception:
logging.exception('Error in on_main_window_close() for %s.', self)
# For auxiliary windows, use scale to give a feel that we're
# switching over to a totally separate 'side quest' ui. For
# regular back/forward relationships, shove the old out the left
# to give the feel that we're adding to a nav stack.
if is_auxiliary:
transition = 'out_scale'
else:
transition = 'out_left'
# Transition ourself out.
_bauiv1.containerwidget(edit=self._root_widget, transition=transition)
babase.app.ui_v1.set_main_window(
new_window,
from_window=self,
back_state=back_state,
is_auxiliary=is_auxiliary,
suppress_warning=True,
)
return new_window
def on_main_window_close(self) -> None:
"""Called before transitioning out a main window.
A good opportunity to save window state/etc.
"""
def get_main_window_state(self) -> MainWindowState:
"""Return a WindowState to recreate this specific window.
Used to gracefully return to a window from another window or ui
system.
"""
raise NotImplementedError()
def main_window_should_preserve_selection(self) -> bool | None:
"""Whether this window should auto-save/restore selection.
If enabled, selection will be stored in the window's shared
state. See :meth:`~bauiv1.MainWindow.get_main_window_shared_state_id()`
for more info about main-window shared-state.
The default value of None results in a warning to explicitly
override this (as the implicit default will change from False to
True after api 9 support ends).
"""
return None
def get_main_window_shared_state_id(self) -> str | None:
"""Provide a custom id for window shared state.
Unlike :class:`~bauiv1.MainWindowState`, which is used to save
and restore a single main-window instance, shared-state is
intended to hold values that can apply to multiple instances of
a window.
By default, shared state uses the window class as an index (so
is shared by all windows of the same class), but this method can
be overridden to provide more distinct states. For example, a
store-page main-window class might want to keep distinct states
for different sub-pages it can display instead of having a
single state for the whole class.
Note that shared state only persists for the current run of the
app.
"""
return None
def main_window_do_save_shared_state(self, state: dict) -> None:
"""Save state into the provided shared state dict.
Can be overridden by subclasses to save custom data.
"""
def main_window_do_restore_shared_state(self, state: dict) -> None:
"""Restore state from the provided shared state dict.
Can be overridden by subclasses to restore custom data.
"""
def _get_main_window_should_preserve_selection(self) -> bool:
# pylint: disable=assignment-from-none
val = self.main_window_should_preserve_selection()
if val is None:
warnings.warn(
f'{type(self)} should override'
f' main_window_should_preserve_selection()'
' to return True or False.'
f' The current default is False (for backward compatibility)'
f' but it will change to True when api 9 support ends.',
FutureWarning,
stacklevel=2,
)
val = False
return val
class MainWindowState:
"""Persistent state for a specific MainWindow.
This allows MainWindows to be automatically recreated for back-button
purposes, when switching app-modes, etc.
"""
def __init__(self) -> None:
# The window that back/cancel navigation should take us to.
self.parent: MainWindowState | None = None
self.is_top_level: bool | None = None
self.is_auxiliary: bool | None = None
self.window_type: type[MainWindow] | None = None
def create_window(
self,
transition: Literal['in_right', 'in_left', 'in_scale'] | None = None,
origin_widget: bauiv1.Widget | None = None,
) -> MainWindow:
"""Create a window based on this state.
WindowState child classes should override this to recreate their
particular type of window.
"""
raise NotImplementedError()
class BasicMainWindowState(MainWindowState):
"""A basic MainWindowState.
Holds some call to recreate a window and optionally a selection to
restore.
"""
def __init__(
self,
create_call: Callable[
[
Literal['in_right', 'in_left', 'in_scale'] | None,
bauiv1.Widget | None,
],
bauiv1.MainWindow,
],
) -> None:
super().__init__()
self.create_call = create_call
@override
def create_window(
self,
transition: Literal['in_right', 'in_left', 'in_scale'] | None = None,
origin_widget: bauiv1.Widget | None = None,
) -> bauiv1.MainWindow:
win = self.create_call(transition, origin_widget)
return win
class MainWindowAutoRecreateSuppress:
"""Suppresses main-window auto-recreate while in existence.
Can be instantiated and held by windows or processes within windows
for the purpose of preventing the main-window auto-recreate
mechanism from firing. This mechanism normally fires when the screen
is resized or the ui-scale is changed, allowing main-windows to be
recreated to adapt to the new configuration.
"""
def __init__(self) -> None:
babase.app.ui_v1.window_auto_recreate_suppress_count += 1
def __del__(self) -> None:
babase.app.ui_v1.window_auto_recreate_suppress_count -= 1

View file

@ -13,7 +13,7 @@ import babase
import _bauiv1
from bauiv1._keyboard import Keyboard
from bauiv1._uitypes import Window
from bauiv1._window import Window
if TYPE_CHECKING:
from babase import StringEditAdapter

View file

@ -13,6 +13,7 @@ from bacommon.login import LoginType
import bacommon.cloud
import bauiv1 as bui
from bauiv1lib.utils import scroll_fade_bottom, scroll_fade_top
from bauiv1lib.connectivity import wait_for_connectivity
@ -90,6 +91,11 @@ class AccountSettingsWindow(bui.MainWindow):
self._scroll_height = target_height - 33
scroll_bottom = yoffs - 61 - self._scroll_height
# Go with full-screen scrollable area in small ui.
if uiscale is bui.UIScale.SMALL:
self._scroll_height += 35
scroll_bottom -= 3
self._sign_in_button = None
self._sign_in_text = None
@ -115,7 +121,7 @@ class AccountSettingsWindow(bui.MainWindow):
super().__init__(
root_widget=bui.containerwidget(
size=(self._width, self._height),
toolbar_visibility=('menu_full'),
toolbar_visibility='menu_full',
toolbar_cancel_button_style=(
'close' if auxiliary_style else 'back'
),
@ -134,6 +140,7 @@ class AccountSettingsWindow(bui.MainWindow):
else:
self._back_button = btn = bui.buttonwidget(
parent=self._root_widget,
id=f'{self.main_window_id_prefix}|back',
position=(51, yoffs - 52.0),
size=(60, 56),
scale=0.8,
@ -149,6 +156,37 @@ class AccountSettingsWindow(bui.MainWindow):
)
bui.containerwidget(edit=self._root_widget, cancel_button=btn)
self._scrollwidget = bui.scrollwidget(
parent=self._root_widget,
highlight=False,
size=(self._scroll_width, self._scroll_height),
position=(
self._width * 0.5 - self._scroll_width * 0.5,
scroll_bottom,
),
claims_left_right=True,
selection_loops_to_parent=True,
border_opacity=0.4,
)
# With full-screen scrolling, fade content as it approaches
# toolbars.
if uiscale is bui.UIScale.SMALL:
scroll_fade_top(
self._root_widget,
self._width * 0.5 - self._scroll_width * 0.5,
scroll_bottom,
self._scroll_width,
self._scroll_height,
)
scroll_fade_bottom(
self._root_widget,
self._width * 0.5 - self._scroll_width * 0.5,
scroll_bottom,
self._scroll_width,
self._scroll_height,
)
titleyoffs = -45.0 if uiscale is bui.UIScale.SMALL else -28.0
titlescale = 0.7 if uiscale is bui.UIScale.SMALL else 1.0
bui.textwidget(
@ -166,21 +204,8 @@ class AccountSettingsWindow(bui.MainWindow):
v_align='center',
)
self._scrollwidget = bui.scrollwidget(
parent=self._root_widget,
highlight=False,
size=(self._scroll_width, self._scroll_height),
position=(
self._width * 0.5 - self._scroll_width * 0.5,
scroll_bottom,
),
claims_left_right=True,
selection_loops_to_parent=True,
border_opacity=0.4,
)
self._subcontainer: bui.Widget | None = None
self._refresh()
self._restore_state()
@override
def get_main_window_state(self) -> bui.MainWindowState:
@ -193,8 +218,8 @@ class AccountSettingsWindow(bui.MainWindow):
)
@override
def on_main_window_close(self) -> None:
self._save_state()
def main_window_should_preserve_selection(self) -> bool:
return True
def _update(self) -> None:
plus = bui.app.plus
@ -399,6 +424,12 @@ class AccountSettingsWindow(bui.MainWindow):
if self._subcontainer is not None:
self._subcontainer.delete()
self._sub_height = 90.0
# For fullscreen scrollable, account for toolbar.
uiscale = bui.app.ui_v1.uiscale
if uiscale is bui.UIScale.SMALL:
self._sub_height += 35
if show_signed_in_as:
self._sub_height += signed_in_as_space
self._sub_height += via_space * len(via_lines)
@ -449,6 +480,10 @@ class AccountSettingsWindow(bui.MainWindow):
first_selectable = None
v = self._sub_height - 10.0
# For fullscreen scrollable, account for toolbar.
if uiscale is bui.UIScale.SMALL:
v -= 35
assert bui.app.classic is not None
self._account_name_text: bui.Widget | None
if show_signed_in_as:
@ -580,8 +615,9 @@ class AccountSettingsWindow(bui.MainWindow):
if show_google_play_sign_in_button:
button_width = 350
v -= sign_in_button_space
self._sign_in_google_play_button = btn = bui.buttonwidget(
btn = bui.buttonwidget(
parent=self._subcontainer,
id=f'{self.main_window_id_prefix}|signingoogleplay',
position=((self._sub_width - button_width) * 0.5, v - 20),
autoselect=True,
size=(button_width, 60),
@ -620,8 +656,9 @@ class AccountSettingsWindow(bui.MainWindow):
if show_game_center_sign_in_button:
button_width = 350
v -= sign_in_button_space
self._sign_in_google_play_button = btn = bui.buttonwidget(
btn = bui.buttonwidget(
parent=self._subcontainer,
id=f'{self.main_window_id_prefix}|signingamecenter',
position=((self._sub_width - button_width) * 0.5, v - 20),
autoselect=True,
size=(button_width, 60),
@ -662,6 +699,7 @@ class AccountSettingsWindow(bui.MainWindow):
v -= sign_in_button_space
self._sign_in_v2_proxy_button = btn = bui.buttonwidget(
parent=self._subcontainer,
id=f'{self.main_window_id_prefix}|signinv2',
position=((self._sub_width - button_width) * 0.5, v - 20),
autoselect=True,
size=(button_width, 60),
@ -729,6 +767,7 @@ class AccountSettingsWindow(bui.MainWindow):
v -= sign_in_button_space + deprecated_space
self._sign_in_device_button = btn = bui.buttonwidget(
parent=self._subcontainer,
id=f'{self.main_window_id_prefix}|signindevice',
position=((self._sub_width - button_width) * 0.5, v - 20),
autoselect=True,
size=(button_width, 60),
@ -816,6 +855,7 @@ class AccountSettingsWindow(bui.MainWindow):
v -= manage_account_button_space
self._manage_button = btn = bui.buttonwidget(
parent=self._subcontainer,
id=f'{self.main_window_id_prefix}|manage',
position=((self._sub_width - button_width) * 0.5, v),
autoselect=True,
size=(button_width, 60),
@ -837,6 +877,7 @@ class AccountSettingsWindow(bui.MainWindow):
v -= create_account_button_space
self._create_button = btn = bui.buttonwidget(
parent=self._subcontainer,
id=f'{self.main_window_id_prefix}|create',
position=((self._sub_width - button_width) * 0.5, v - 30),
autoselect=True,
size=(button_width, 60),
@ -870,6 +911,7 @@ class AccountSettingsWindow(bui.MainWindow):
)
self._game_center_button = btn = bui.buttonwidget(
parent=self._subcontainer,
id=f'{self.main_window_id_prefix}|gamecenter',
position=((self._sub_width - button_width) * 0.5, v),
color=(0.55, 0.5, 0.6),
textcolor=(0.75, 0.7, 0.8),
@ -914,6 +956,7 @@ class AccountSettingsWindow(bui.MainWindow):
v -= leaderboards_button_space
self._leaderboards_button = btn = bui.buttonwidget(
parent=self._subcontainer,
id=f'{self.main_window_id_prefix}|leaderboards',
position=((self._sub_width - button_width) * 0.5, v),
color=(0.55, 0.5, 0.6),
textcolor=(0.75, 0.7, 0.8),
@ -977,6 +1020,7 @@ class AccountSettingsWindow(bui.MainWindow):
v -= sign_out_button_space
self._sign_out_button = btn = bui.buttonwidget(
parent=self._subcontainer,
id=f'{self.main_window_id_prefix}|signout',
position=((self._sub_width - button_width) * 0.5, v),
size=(button_width, 60),
label=bui.Lstr(resource=f'{self._r}.signOutText'),
@ -996,6 +1040,7 @@ class AccountSettingsWindow(bui.MainWindow):
v -= cancel_sign_in_button_space
self._cancel_sign_in_button = btn = bui.buttonwidget(
parent=self._subcontainer,
id=f'{self.main_window_id_prefix}|cancelsignin',
position=((self._sub_width - button_width) * 0.5, v),
size=(button_width, 60),
label=bui.Lstr(resource='cancelText'),
@ -1015,6 +1060,7 @@ class AccountSettingsWindow(bui.MainWindow):
v -= delete_account_button_space
self._delete_account_button = btn = bui.buttonwidget(
parent=self._subcontainer,
id=f'{self.main_window_id_prefix}|deleteaccount',
position=((self._sub_width - button_width) * 0.5, v),
size=(button_width, 60),
label=bui.Lstr(resource=f'{self._r}.deleteAccountText'),
@ -1355,34 +1401,6 @@ class AccountSettingsWindow(bui.MainWindow):
assert self._sign_in_v2_proxy_button is not None
V2ProxySignInWindow(origin_widget=self._sign_in_v2_proxy_button)
def _save_state(self) -> None:
try:
sel = self._root_widget.get_selected_child()
if sel == self._back_button:
sel_name = 'Back'
elif sel == self._scrollwidget:
sel_name = 'Scroll'
else:
raise ValueError('unrecognized selection')
assert bui.app.classic is not None
bui.app.ui_v1.window_states[type(self)] = 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))
if sel_name == 'Back':
sel = self._back_button
elif sel_name == 'Scroll':
sel = self._scrollwidget
else:
sel = self._back_button
bui.containerwidget(edit=self._root_widget, selected_child=sel)
except Exception:
logging.exception('Error restoring state for %s.', self)
def show_what_is_legacy_unlinking_page() -> None:
"""Show the webpage describing legacy unlinking."""

View file

@ -35,13 +35,16 @@ def _show_account_settings() -> None:
if isinstance(prev_main_window, AccountSettingsWindow):
return
ui = bui.app.ui_v1
# Set our new main window.
bui.app.ui_v1.set_main_window(
ui.set_main_window(
AccountSettingsWindow(
close_once_signed_in=True,
origin_widget=bui.get_special_widget('account_button'),
),
from_window=False,
back_state=ui.save_current_main_window_state(),
from_window=False, # Don't check where we're coming from.
is_auxiliary=True,
suppress_warning=True,
)

View file

@ -23,6 +23,7 @@ class V2ProxySignInWindow(bui.Window):
self._proxyid: str | None = None
self._proxykey: str | None = None
self._overlay_web_browser_open = False
self._idprefix = bui.app.ui_v1.new_id_prefix('resourcetypeinfo')
assert bui.app.classic is not None
uiscale = bui.app.ui_v1.uiscale
@ -256,6 +257,7 @@ class V2ProxySignInWindow(bui.Window):
if bui.is_browser_likely_available():
bui.buttonwidget(
parent=self._root_widget,
id=f'{self._idprefix}|address',
position=(
(self._width * 0.5 - button_width * 0.5),
self._height - 185,

View file

@ -6,6 +6,7 @@ from __future__ import annotations
from typing import override
from bauiv1lib.utils import scroll_fade_bottom, scroll_fade_top
import bauiv1 as bui
@ -58,10 +59,15 @@ class AchievementsWindow(bui.MainWindow):
scroll_height = target_height - 25
scroll_bottom = yoffs - 54 - scroll_height
# Go with full-screen scrollable area in small ui.
if uiscale is bui.UIScale.SMALL:
scroll_height += 30
scroll_bottom -= 3
super().__init__(
root_widget=bui.containerwidget(
size=(self._width, self._height),
toolbar_visibility=('menu_full'),
toolbar_visibility='menu_full',
toolbar_cancel_button_style=(
'close' if auxiliary_style else 'back'
),
@ -81,6 +87,7 @@ class AchievementsWindow(bui.MainWindow):
else:
self._back_button = bui.buttonwidget(
parent=self._root_widget,
id=f'{self.main_window_id_prefix}|back',
autoselect=True,
position=(50, yoffs - 48),
size=(60, 60),
@ -100,6 +107,43 @@ class AchievementsWindow(bui.MainWindow):
achievements = bui.app.classic.ach.achievements
num_complete = len([a for a in achievements if a.complete])
self._scrollwidget = bui.scrollwidget(
parent=self._root_widget,
id=f'{self.main_window_id_prefix}|scroll',
size=(scroll_width, scroll_height),
position=(self._width * 0.5 - scroll_width * 0.5, scroll_bottom),
capture_arrows=True,
simple_culling_v=10,
border_opacity=0.4,
)
bui.containerwidget(
edit=self._root_widget, selected_child=self._scrollwidget
)
bui.widget(edit=self._scrollwidget, autoselect=True)
if uiscale is bui.UIScale.SMALL:
bui.widget(
edit=self._scrollwidget,
left_widget=bui.get_special_widget('back_button'),
)
# With full-screen scrolling, fade content as it approaches
# toolbars.
if uiscale is bui.UIScale.SMALL:
scroll_fade_top(
self._root_widget,
self._width * 0.5 - scroll_width * 0.5,
scroll_bottom,
scroll_width,
scroll_height,
)
scroll_fade_bottom(
self._root_widget,
self._width * 0.5 - scroll_width * 0.5,
scroll_bottom,
scroll_width,
scroll_height,
)
# In small UI mode when the screen is narrow enough we need to
# go with a smaller title to avoid it overlapping with toolbar
# bits.
@ -144,59 +188,6 @@ class AchievementsWindow(bui.MainWindow):
color=bui.app.ui_v1.title_color,
)
self._scrollwidget = bui.scrollwidget(
parent=self._root_widget,
size=(scroll_width, scroll_height),
position=(self._width * 0.5 - scroll_width * 0.5, scroll_bottom),
capture_arrows=True,
simple_culling_v=10,
border_opacity=0.4,
)
bui.widget(edit=self._scrollwidget, autoselect=True)
if uiscale is bui.UIScale.SMALL:
bui.widget(
edit=self._scrollwidget,
left_widget=bui.get_special_widget('back_button'),
)
# Add some blotches so our contents fades out as it approaches
# the bottom toolbar.
if uiscale is bui.UIScale.SMALL:
blotchwidth = 500.0
blotchheight = 200.0
bimg = bui.imagewidget(
parent=self._root_widget,
texture=bui.gettexture('uiAtlas'),
mesh_transparent=bui.getmesh('windowBGBlotch'),
position=(
self._width * 0.5
- scroll_width * 0.5
+ 60.0
- blotchwidth * 0.5,
scroll_bottom - blotchheight * 0.5,
),
size=(blotchwidth, blotchheight),
color=(0.4, 0.37, 0.49),
# color=(1, 0, 0),
)
bui.widget(edit=bimg, depth_range=(0.9, 1.0))
bimg = bui.imagewidget(
parent=self._root_widget,
texture=bui.gettexture('uiAtlas'),
mesh_transparent=bui.getmesh('windowBGBlotch'),
position=(
self._width * 0.5
+ scroll_width * 0.5
- 60.0
- blotchwidth * 0.5,
scroll_bottom - blotchheight * 0.5,
),
size=(blotchwidth, blotchheight),
color=(0.4, 0.37, 0.49),
# color=(1, 0, 0),
)
bui.widget(edit=bimg, depth_range=(0.9, 1.0))
bui.containerwidget(
edit=self._root_widget, cancel_button=self._back_button
)
@ -205,21 +196,32 @@ class AchievementsWindow(bui.MainWindow):
sub_width = scroll_width - 25
sub_height = 85 + len(achievements) * incr
# For fullscreen scrollable, account for toolbar.
if uiscale is bui.UIScale.SMALL:
sub_height += 30
eq_rsrc = 'coopSelectWindow.powerRankingPointsEqualsText'
pts_rsrc = 'coopSelectWindow.powerRankingPointsText'
self._subcontainer = bui.containerwidget(
parent=self._scrollwidget,
id=f'{self.main_window_id_prefix}|subcontainer',
size=(sub_width, sub_height),
background=False,
)
basey = sub_height
# For fullscreen scrollable, account for toolbar.
if uiscale is bui.UIScale.SMALL:
basey -= 30
total_pts = 0
for i, ach in enumerate(achievements):
complete = ach.complete
bui.textwidget(
parent=self._subcontainer,
position=(sub_width * 0.08 - 5, sub_height - 20 - incr * i),
position=(sub_width * 0.08 - 5, basey - 20 - incr * i),
maxwidth=20,
scale=0.5,
color=(0.6, 0.6, 0.7) if complete else (0.6, 0.6, 0.7, 0.2),
@ -234,9 +236,9 @@ class AchievementsWindow(bui.MainWindow):
bui.imagewidget(
parent=self._subcontainer,
position=(
(sub_width * 0.10 + 1, sub_height - 20 - incr * i - 9)
(sub_width * 0.10 + 1, basey - 20 - incr * i - 9)
if complete
else (sub_width * 0.10 - 4, sub_height - 20 - incr * i - 14)
else (sub_width * 0.10 - 4, basey - 20 - incr * i - 14)
),
size=(18, 18) if complete else (27, 27),
opacity=1.0 if complete else 0.3,
@ -248,7 +250,7 @@ class AchievementsWindow(bui.MainWindow):
parent=self._subcontainer,
position=(
sub_width * 0.10 - 4,
sub_height - 25 - incr * i - 9,
basey - 25 - incr * i - 9,
),
size=(28, 28),
color=(2, 1.4, 0),
@ -256,7 +258,7 @@ class AchievementsWindow(bui.MainWindow):
)
bui.textwidget(
parent=self._subcontainer,
position=(sub_width * 0.19, sub_height - 19 - incr * i + 3),
position=(sub_width * 0.19, basey - 19 - incr * i + 3),
maxwidth=sub_width * 0.62,
scale=0.6,
flatness=1.0,
@ -270,7 +272,7 @@ class AchievementsWindow(bui.MainWindow):
bui.textwidget(
parent=self._subcontainer,
position=(sub_width * 0.19, sub_height - 19 - incr * i - 10),
position=(sub_width * 0.19, basey - 19 - incr * i - 10),
maxwidth=sub_width * 0.62,
scale=0.4,
flatness=1.0,
@ -295,7 +297,7 @@ class AchievementsWindow(bui.MainWindow):
opacity=0.0 if complete else 1.0,
position=(
sub_width * 0.92 - 40.0 - chestsize * 0.5,
sub_height - 20 - incr * i - chestsize * 0.5,
basey - 20 - incr * i - chestsize * 0.5,
),
size=(chestsize, chestsize),
color=chestdisplayinfo.color,
@ -308,7 +310,7 @@ class AchievementsWindow(bui.MainWindow):
pts = ach.power_ranking_value
bui.textwidget(
parent=self._subcontainer,
position=(sub_width * 0.92, sub_height - 20 - incr * i),
position=(sub_width * 0.92, basey - 20 - incr * i),
maxwidth=sub_width * 0.15,
color=(0.7, 0.8, 1.0) if complete else (0.9, 0.9, 1.0, 0.3),
flatness=1.0,
@ -328,7 +330,7 @@ class AchievementsWindow(bui.MainWindow):
parent=self._subcontainer,
position=(
sub_width * 1.0,
sub_height - 20 - incr * len(achievements),
basey - 20 - incr * len(achievements),
),
maxwidth=sub_width * 0.5,
scale=0.7,
@ -362,3 +364,7 @@ class AchievementsWindow(bui.MainWindow):
transition=transition, origin_widget=origin_widget
)
)
@override
def main_window_should_preserve_selection(self) -> bool:
return True

View file

@ -33,6 +33,7 @@ class ChestWindow(bui.MainWindow):
index: int,
transition: str | None = 'in_right',
origin_widget: bui.Widget | None = None,
auxiliary_style: bool = True,
):
# pylint: disable=too-many-statements
self._index = index
@ -101,6 +102,9 @@ class ChestWindow(bui.MainWindow):
root_widget=bui.containerwidget(
size=(self._width, self._height),
toolbar_visibility='menu_full',
toolbar_cancel_button_style=(
'close' if auxiliary_style else 'back'
),
scale=scale,
),
transition=transition,
@ -136,10 +140,14 @@ class ChestWindow(bui.MainWindow):
btn = bui.buttonwidget(
parent=self._root_widget,
position=(50, self._yoffs - 44),
size=(60, 55),
size=(60, 60),
scale=0.8,
label=bui.charstr(bui.SpecialChar.BACK),
button_type='backSmall',
label=bui.charstr(
bui.SpecialChar.CLOSE
if auxiliary_style
else bui.SpecialChar.BACK
),
button_type=None if auxiliary_style else 'backSmall',
extra_touch_border_scale=2.0,
autoselect=True,
on_activate_call=self.main_window_back,
@ -210,6 +218,14 @@ class ChestWindow(bui.MainWindow):
)
)
@override
def main_window_should_preserve_selection(self) -> bool:
# This doesn't really benefit us since we do lots of widget
# creates/destroys throughout our lifetime and also we're an
# auxliary window so should never need to restore toolbar
# selections.
return False
def _update_time_display(self, unlock_time: datetime.datetime) -> None:
# Once our target text widget disappears, kill our timer.
if not self._time_string_text:
@ -961,6 +977,7 @@ class ChestWindow(bui.MainWindow):
self, response: bacommon.bs.ChestActionResponse
) -> float:
# pylint: disable=too-many-locals
# pylint: disable=too-many-statements
from baclassic import show_display_item
@ -1101,9 +1118,10 @@ class ChestWindow(bui.MainWindow):
# through highlighting our options and stop on the winner when
# the chest opens. To do this, we start at the end at the prize
# and work backwards setting timers.
ease_out = False # Experimenting...
if self._prizesets:
toffs2 = toffsopen - 0.01
amt = 0.02
amt = 0.25 if ease_out else 0.02
i = self._prizeindex
while toffs2 > 0.0:
bui.apptimer(
@ -1111,7 +1129,10 @@ class ChestWindow(bui.MainWindow):
bui.WeakCall(self._highlight_odds_row, i),
)
toffs2 -= amt
amt *= 1.05 * random.uniform(0.9, 1.1)
if ease_out:
amt = max(0.032, amt * 0.75 * random.uniform(0.9, 1.1))
else:
amt *= 1.05 * random.uniform(0.9, 1.1)
i = (i - 1) % len(self._prizesets)
# Let the caller know how long we'll take in case they want to

View file

@ -336,7 +336,6 @@ class ColorPickerExact(PopupWindow):
# Store the current text for our next comparison.
self._hex_prev_text = hextext
# noinspection PyUnresolvedReferences
def _update_for_color(self) -> None:
if not self.root_widget:
return

View file

@ -34,6 +34,7 @@ class ConfigCheckBox:
maxwidth: float | None = None,
autoselect: bool = True,
value_change_call: Callable[[Any], Any] | None = None,
check_box_id: str | None = None,
):
if displayname is None:
displayname = configkey
@ -41,6 +42,7 @@ class ConfigCheckBox:
self._configkey = configkey
self.widget = bui.checkboxwidget(
parent=parent,
id=check_box_id,
autoselect=autoselect,
position=position,
size=size,
@ -51,8 +53,8 @@ class ConfigCheckBox:
scale=scale,
maxwidth=maxwidth,
)
# complain if we outlive our checkbox
bui.uicleanupcheck(self, self.widget)
# Complain if we outlive our checkbox.
bui.app.ui_v1.add_ui_cleanup_check(self, self.widget)
def _value_changed(self, val: bool) -> None:
cfg = bui.app.config
@ -98,7 +100,9 @@ class ConfigNumberEdit:
as_percent: bool = False,
fallback_value: float = 0.0,
f: int = 1,
idprefix: str | None = None,
):
# pylint: disable=too-many-locals
if displayname is None:
displayname = configkey
@ -143,6 +147,7 @@ class ConfigNumberEdit:
)
self.minusbutton = bui.buttonwidget(
parent=parent,
id=None if idprefix is None else f'{idprefix}|minus',
position=(position[0] + 230 + xoffset, position[1]),
size=(28, 28),
label='-',
@ -153,6 +158,7 @@ class ConfigNumberEdit:
)
self.plusbutton = bui.buttonwidget(
parent=parent,
id=None if idprefix is None else f'{idprefix}|plus',
position=(position[0] + 280 + xoffset, position[1]),
size=(28, 28),
label='+',
@ -162,7 +168,7 @@ class ConfigNumberEdit:
enable_sound=changesound,
)
# Complain if we outlive our widgets.
bui.uicleanupcheck(self, self.nametext)
bui.app.ui_v1.add_ui_cleanup_check(self, self.nametext)
self._update_display()
def _up(self) -> None:

View file

@ -5,7 +5,6 @@
from __future__ import annotations
from typing import TYPE_CHECKING
import logging
import bauiv1 as bui
@ -30,8 +29,15 @@ class ConfirmWindow:
ok_text: str | bui.Lstr | None = None,
cancel_text: str | bui.Lstr | None = None,
origin_widget: bui.Widget | None = None,
permanent_ok_fade: bool = False,
):
# pylint: disable=too-many-locals
ui = bui.app.ui_v1
# Make sure our widgets have globally unique ids.
self._id_prefix = ui.new_id_prefix('confirm')
if text is None:
text = bui.Lstr(resource='areYouSureText')
if ok_text is None:
@ -42,6 +48,8 @@ class ConfirmWindow:
width = max(width, 360)
self._action = action
self._permanent_ok_fade = permanent_ok_fade
# If they provided an origin-widget, scale up from that.
self._transition_out: str | None
scale_origin: tuple[float, float] | None
@ -87,6 +95,7 @@ class ConfirmWindow:
if cancel_button:
cbtn = btn = bui.buttonwidget(
parent=self.root_widget,
id=f'{self._id_prefix}|cancel',
autoselect=True,
position=(20, 20),
size=(150, 50),
@ -103,6 +112,7 @@ class ConfirmWindow:
cbtn = None
btn = bui.buttonwidget(
parent=self.root_widget,
id=f'{self._id_prefix}|ok',
autoselect=True,
position=(ok_button_h, 20),
size=(150, 50),
@ -140,6 +150,7 @@ class ConfirmWindow:
return
bui.containerwidget(
edit=self.root_widget,
darken_behind_is_permanent=self._permanent_ok_fade,
transition=(
'out_left'
if self._transition_out is None
@ -159,10 +170,9 @@ class QuitWindow:
swish: bool = False,
origin_widget: bui.Widget | None = None,
):
classic = bui.app.classic
assert classic is not None
ui = bui.app.ui_v1
app = bui.app
platform = bui.app.env.platform
self._quit_type = quit_type
# If there's already one of us up somewhere, kill it.
@ -172,18 +182,13 @@ class QuitWindow:
if swish:
bui.getsound('swish').play()
if app.classic is None:
if bui.do_once():
logging.warning(
'QuitWindow needs to be updated to work without classic.'
)
quit_resource = 'exitGameText'
else:
quit_resource = (
'quitGameText'
if app.classic.platform == 'mac'
else 'exitGameText'
)
# Generally Macs say Quit and other stuff says Exit
quit_resource = (
'quitGameText'
if platform is type(platform).MACOS
else 'exitGameText'
)
self._root_widget = ui.quit_window = ConfirmWindow(
bui.Lstr(
resource=quit_resource,
@ -195,4 +200,10 @@ class QuitWindow:
else bui.quit(confirm=False)
),
origin_widget=origin_widget,
# In situations where the quit action will *actually* kill
# the process, tell the confirm to not fade back in when the
# confirm button is pressed. It just looks a bit visually
# odd if the confirm fades back in just before the app fades
# out to quit.
permanent_ok_fade=not bui.app.env.supports_soft_quit,
).root_widget

View file

@ -49,6 +49,7 @@ class WaitForConnectivityWindow(bui.Window):
self._on_cancel = on_cancel
self._width = 650
self._height = 300
self._idprefix = bui.app.ui_v1.new_id_prefix('connectivity')
super().__init__(
root_widget=bui.containerwidget(
size=(self._width, self._height),
@ -90,6 +91,7 @@ class WaitForConnectivityWindow(bui.Window):
self._info_text_str = ''
cancel_button = bui.buttonwidget(
parent=self._root_widget,
id=f'{self._idprefix}|cancel',
autoselect=True,
position=(50, 30),
size=(150, 50),

View file

@ -11,6 +11,7 @@ from typing import TYPE_CHECKING, override
import bauiv1 as bui
from bauiv1lib.utils import scroll_fade_top, scroll_fade_bottom
if TYPE_CHECKING:
from typing import Any
@ -131,8 +132,14 @@ class CoopBrowserWindow(bui.MainWindow):
yoffs = 0.5 * self._height + 0.5 * target_height + 30.0
self._scroll_width = target_width
self._scroll_height = target_height - 40
self._scroll_bottom = yoffs - 70 - self._scroll_height
self._scroll_height = target_height - (
-5 if uiscale is bui.UIScale.SMALL else 40
)
self._scroll_bottom = (
yoffs
- (27 if uiscale is bui.UIScale.SMALL else 70)
- self._scroll_height
)
super().__init__(
root_widget=bui.containerwidget(
@ -154,6 +161,7 @@ class CoopBrowserWindow(bui.MainWindow):
else:
self._back_button = bui.buttonwidget(
parent=self._root_widget,
id=f'{self.main_window_id_prefix}|back',
position=(75, yoffs - 48.0),
size=(60, 50),
scale=1.2,
@ -184,7 +192,50 @@ class CoopBrowserWindow(bui.MainWindow):
# Don't want initial construction affecting our last-selected.
self._do_selection_callbacks = False
bui.textwidget(
self._selected_row = cfg.get('Selected Coop Row', None)
self._subcontainerwidth = 800.0
self._subcontainerheight = 1400.0
# Allow empty space at top when our toolbar overlaps scroll area.
if uiscale is bui.UIScale.SMALL:
self._subcontainerheight += 40
self._scrollwidget = bui.scrollwidget(
parent=self._root_widget,
highlight=False,
size=(self._scroll_width, self._scroll_height),
position=(
self._width * 0.5 - self._scroll_width * 0.5,
self._scroll_bottom,
),
simple_culling_v=10.0,
claims_left_right=True,
selection_loops_to_parent=True,
border_opacity=0.4,
)
# Splotches at the top to fade scrollable content as it hits
# toolbars.
if uiscale is bui.UIScale.SMALL and bool(True):
scroll_fade_top(
self._root_widget,
self._width * 0.5 - self._scroll_width * 0.5,
self._scroll_bottom,
self._scroll_width,
self._scroll_height,
)
scroll_fade_bottom(
self._root_widget,
self._width * 0.5 - self._scroll_width * 0.5,
self._scroll_bottom,
self._scroll_width,
self._scroll_height,
)
# Title.
ttxt = bui.textwidget(
parent=self._root_widget,
position=(
self._width * 0.5,
@ -201,61 +252,7 @@ class CoopBrowserWindow(bui.MainWindow):
maxwidth=tmaxw,
v_align='center',
)
self._selected_row = cfg.get('Selected Coop Row', None)
self._subcontainerwidth = 800.0
self._subcontainerheight = 1400.0
self._scrollwidget = bui.scrollwidget(
parent=self._root_widget,
highlight=False,
size=(self._scroll_width, self._scroll_height),
position=(
self._width * 0.5 - self._scroll_width * 0.5,
self._scroll_bottom,
),
simple_culling_v=10.0,
claims_left_right=True,
selection_loops_to_parent=True,
border_opacity=0.4,
)
if uiscale is bui.UIScale.SMALL:
blotchwidth = 500.0
blotchheight = 200.0
bimg = bui.imagewidget(
parent=self._root_widget,
texture=bui.gettexture('uiAtlas'),
mesh_transparent=bui.getmesh('windowBGBlotch'),
position=(
self._width * 0.5
- self._scroll_width * 0.5
+ 60.0
- blotchwidth * 0.5,
self._scroll_bottom - blotchheight * 0.5,
),
size=(blotchwidth, blotchheight),
color=(0.4, 0.37, 0.49),
# color=(1, 0, 0),
)
bui.widget(edit=bimg, depth_range=(0.9, 1.0))
bimg = bui.imagewidget(
parent=self._root_widget,
texture=bui.gettexture('uiAtlas'),
mesh_transparent=bui.getmesh('windowBGBlotch'),
position=(
self._width * 0.5
+ self._scroll_width * 0.5
- 60.0
- blotchwidth * 0.5,
self._scroll_bottom - blotchheight * 0.5,
),
size=(blotchwidth, blotchheight),
color=(0.4, 0.37, 0.49),
# color=(1, 0, 0),
)
bui.widget(edit=bimg, depth_range=(0.9, 1.0))
bui.widget(edit=ttxt, depth_range=(0.9, 1.0))
self._subcontainer: bui.Widget | None = None
@ -266,7 +263,6 @@ class CoopBrowserWindow(bui.MainWindow):
self._fg_state = app.fg_state
self._refresh()
self._restore_state()
# Even though we might display cached tournament data immediately, we
# don't consider it valid until we've pinged.
@ -307,6 +303,10 @@ class CoopBrowserWindow(bui.MainWindow):
)
)
@override
def main_window_should_preserve_selection(self) -> bool:
return True
@override
def on_main_window_close(self) -> None:
self._save_state()
@ -532,6 +532,7 @@ class CoopBrowserWindow(bui.MainWindow):
un_sel_textcolor = (0.6, 0.6, 0.6)
self._easy_button = bui.buttonwidget(
parent=parent_widget,
id=f'{self.main_window_id_prefix}|easy',
position=(h + 30, v2 + 105),
size=(120, 70),
label=bui.Lstr(resource='difficultyEasyText'),
@ -562,6 +563,7 @@ class CoopBrowserWindow(bui.MainWindow):
self._hard_button = bui.buttonwidget(
parent=parent_widget,
id=f'{self.main_window_id_prefix}|hard',
position=(h + 30, v2 + 32),
size=(120, 70),
label=bui.Lstr(resource='difficultyHardText'),
@ -716,6 +718,11 @@ class CoopBrowserWindow(bui.MainWindow):
v = self._subcontainerheight - 90
# Move down past toolbar when it overlaps us.
uiscale = bui.app.ui_v1.uiscale
if uiscale is bui.UIScale.SMALL:
v -= 40.0
self._campaign_percent_text = bui.textwidget(
parent=w_parent,
position=(h_base + 27, v + 30),
@ -727,7 +734,10 @@ class CoopBrowserWindow(bui.MainWindow):
scale=1.1,
)
row_v_show_buffer = 80
# Need a bit more show-buffer on top to account for our
# non-selectable titles above our selectable button rows.
row_v_show_buffer_top = 120
row_v_show_buffer_bottom = 70
v -= 198
h_scroll = bui.hscrollwidget(
@ -743,8 +753,8 @@ class CoopBrowserWindow(bui.MainWindow):
self._campaign_h_scroll = h_scroll
bui.widget(
edit=h_scroll,
show_buffer_top=row_v_show_buffer,
show_buffer_bottom=row_v_show_buffer,
show_buffer_top=row_v_show_buffer_top,
show_buffer_bottom=row_v_show_buffer_bottom,
autoselect=True,
)
if self._selected_row == 'campaign':
@ -778,6 +788,7 @@ class CoopBrowserWindow(bui.MainWindow):
)
self._tournament_info_button = bui.buttonwidget(
parent=w_parent,
id=f'{self.main_window_id_prefix}|tourneyinfo',
label='?',
size=(20, 20),
text_scale=0.6,
@ -837,8 +848,8 @@ class CoopBrowserWindow(bui.MainWindow):
)
bui.widget(
edit=h_scroll,
show_buffer_top=row_v_show_buffer,
show_buffer_bottom=row_v_show_buffer,
show_buffer_top=row_v_show_buffer_top,
show_buffer_bottom=row_v_show_buffer_bottom,
autoselect=True,
)
if self._selected_row == 'tournament' + str(i + 1):
@ -920,8 +931,8 @@ class CoopBrowserWindow(bui.MainWindow):
)
bui.widget(
edit=h_scroll,
show_buffer_top=row_v_show_buffer,
show_buffer_bottom=1.5 * row_v_show_buffer,
show_buffer_top=row_v_show_buffer_top,
show_buffer_bottom=1.5 * row_v_show_buffer_bottom,
autoselect=True,
)
if self._selected_row == 'custom':
@ -1168,18 +1179,6 @@ class CoopBrowserWindow(bui.MainWindow):
)
return
# assert required_purchases
# if plus.get_v1_account_state() != 'signed_in':
# show_sign_in_prompt()
# else:
# # Hmm; just show the first requirement. They can come
# # back to see more after they purchase the first.
# PurchaseWindow(
# items=[required_purchases[0]],
# origin_widget=tournament_button.button,
# )
# return
if tournament_button.time_remaining <= 0:
bui.screenmessage(
bui.Lstr(resource='tournamentEndedText'), color=(1, 0, 0)
@ -1197,40 +1196,11 @@ class CoopBrowserWindow(bui.MainWindow):
def _save_state(self) -> None:
cfg = bui.app.config
try:
sel = self._root_widget.get_selected_child()
if sel == self._back_button:
sel_name = 'Back'
elif sel == self._scrollwidget:
sel_name = 'Scroll'
else:
raise ValueError('unrecognized selection')
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)
cfg['Selected Coop Row'] = self._selected_row
cfg['Selected Coop Custom Level'] = self._selected_custom_level
cfg['Selected Coop Campaign Level'] = self._selected_campaign_level
cfg.commit()
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'
)
if sel_name == 'Back':
sel = self._back_button
elif sel_name == 'Scroll':
sel = self._scrollwidget
else:
sel = self._scrollwidget
bui.containerwidget(edit=self._root_widget, selected_child=sel)
except Exception:
logging.exception('Error restoring state for %s.', self)
def sel_change(self, row: str, game: str) -> None:
"""(internal)"""
if self._do_selection_callbacks:

View file

@ -79,6 +79,8 @@ class GameButton:
show_buffer_top=50,
show_buffer_left=400,
show_buffer_right=200,
# We handle reselection manually for these so no ids.
allow_preserve_selection=False,
)
if select:
bui.containerwidget(

View file

@ -60,6 +60,8 @@ class TournamentButton:
show_buffer_top=50,
show_buffer_left=400,
show_buffer_right=200,
# We handle reselection manually for these so no ids.
allow_preserve_selection=False,
)
if select:
bui.containerwidget(
@ -331,6 +333,11 @@ class TournamentButton:
color=value_color,
flatness=1.0,
)
# We handle reselection manually for these so no ids.
bui.widget(
edit=self.current_leader_name_text, allow_preserve_selection=False
)
self.current_leader_score_text = bui.textwidget(
parent=parent,
draw_controller=btn,
@ -357,6 +364,8 @@ class TournamentButton:
text_scale=0.6,
on_activate_call=bui.WeakCall(self._show_scores),
)
# We handle reselection manually for these so no ids.
bui.widget(edit=self.more_scores_button, allow_preserve_selection=False)
bui.widget(
edit=self.current_leader_name_text,
down_widget=self.more_scores_button,
@ -446,6 +455,10 @@ class TournamentButton:
def _update_lock_state(self) -> None:
# no-op if our widget is dead.
if not self.button:
return
if self.game is None:
return

View file

@ -94,8 +94,9 @@ class CreditsWindow(bui.MainWindow):
else:
btn = bui.buttonwidget(
parent=self._root_widget,
id=f'{self.main_window_id_prefix}|back',
position=(40, yoffs - 46),
size=(60, 48),
size=(60, 55),
scale=0.8,
label=bui.charstr(bui.SpecialChar.BACK),
button_type='backSmall',
@ -113,15 +114,16 @@ class CreditsWindow(bui.MainWindow):
center_small_content_horizontally=True,
)
bui.widget(
edit=scroll,
right_widget=bui.get_special_widget('squad_button'),
)
if uiscale is bui.UIScale.SMALL:
bui.widget(
edit=scroll,
left_widget=bui.get_special_widget('back_button'),
)
bui.widget(
edit=scroll,
right_widget=bui.get_special_widget('squad_button'),
)
bui.containerwidget(edit=self._root_widget, selected_child=scroll)
def _format_names(names2: Sequence[str], inset: float) -> str:
sval = ''
@ -340,6 +342,7 @@ class CreditsWindow(bui.MainWindow):
container = self._subcontainer = bui.containerwidget(
parent=scroll,
id=f'{self.main_window_id_prefix}|sub',
size=(self._sub_width, self._sub_height),
background=False,
claims_left_right=False,
@ -396,3 +399,7 @@ class CreditsWindow(bui.MainWindow):
transition=transition, origin_widget=origin_widget
)
)
@override
def main_window_should_preserve_selection(self) -> bool:
return True

View file

@ -164,6 +164,11 @@ class FileSelectorWindow(bui.MainWindow):
)
)
@override
def main_window_should_preserve_selection(self) -> bool:
# TODO - wire this up.
return False
def _on_up_press(self) -> None:
self._on_entry_activated('..')

View file

@ -2,390 +2,7 @@
#
"""Provides UI for inviting/joining friends."""
from __future__ import annotations
import weakref
import logging
from enum import Enum
from typing import override, TYPE_CHECKING
from bauiv1lib.gather._gather import GatherTab, GatherWindow
from bauiv1lib.tabs import TabRow
import bauiv1 as bui
if TYPE_CHECKING:
from bauiv1lib.play import PlaylistSelectContext
class GatherTab:
"""Defines a tab for use in the gather UI."""
def __init__(self, window: GatherWindow) -> None:
self._window = weakref.ref(window)
@property
def window(self) -> GatherWindow:
"""The GatherWindow that this tab belongs to."""
window = self._window()
if window is None:
raise bui.NotFoundError("GatherTab's window no longer exists.")
return window
def on_activate(
self,
parent_widget: bui.Widget,
tab_button: bui.Widget,
region_width: float,
region_height: float,
region_left: float,
region_bottom: float,
) -> bui.Widget:
"""Called when the tab becomes the active one.
The tab should create and return a container widget covering the
specified region.
"""
# pylint: disable=too-many-positional-arguments
raise RuntimeError('Should not get here.')
def on_deactivate(self) -> None:
"""Called when the tab will no longer be the active one."""
def save_state(self) -> None:
"""Called when the parent window is saving state."""
def restore_state(self) -> None:
"""Called when the parent window is restoring state."""
class GatherWindow(bui.MainWindow):
"""Window for joining/inviting friends."""
class TabID(Enum):
"""Our available tab types."""
ABOUT = 'about'
INTERNET = 'internet'
PRIVATE = 'private'
NEARBY = 'nearby'
MANUAL = 'manual'
def __init__(
self,
transition: str | None = 'in_right',
origin_widget: bui.Widget | None = None,
):
# pylint: disable=too-many-locals
# pylint: disable=cyclic-import
from bauiv1lib.gather.abouttab import AboutGatherTab
from bauiv1lib.gather.manualtab import ManualGatherTab
from bauiv1lib.gather.privatetab import PrivateGatherTab
from bauiv1lib.gather.publictab import PublicGatherTab
from bauiv1lib.gather.nearbytab import NearbyGatherTab
plus = bui.app.plus
assert plus is not None
bui.set_analytics_screen('Gather Window')
uiscale = bui.app.ui_v1.uiscale
self._width = (
1640
if uiscale is bui.UIScale.SMALL
else 1100 if uiscale is bui.UIScale.MEDIUM else 1200
)
self._height = (
1000
if uiscale is bui.UIScale.SMALL
else 730 if uiscale is bui.UIScale.MEDIUM else 900
)
self._current_tab: GatherWindow.TabID | None = None
self._r = 'gatherWindow'
# 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()
scale = (
1.4
if uiscale is bui.UIScale.SMALL
else 0.88 if uiscale is bui.UIScale.MEDIUM else 0.66
)
# Calc screen size in our local container space and clamp to a
# bit smaller than our container size.
target_width = min(self._width - 130, screensize[0] / scale)
target_height = min(self._height - 130, 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 * self._height + 0.5 * target_height + 30.0
self._scroll_width = target_width
self._scroll_height = target_height - 65
self._scroll_bottom = yoffs - 93 - self._scroll_height
self._scroll_left = (self._width - self._scroll_width) * 0.5
super().__init__(
root_widget=bui.containerwidget(
size=(self._width, self._height),
toolbar_visibility=(
'menu_tokens'
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:
bui.containerwidget(
edit=self._root_widget, on_cancel_call=self.main_window_back
)
self._back_button = None
else:
self._back_button = btn = bui.buttonwidget(
parent=self._root_widget,
position=(70, yoffs - 43),
size=(60, 60),
scale=1.1,
autoselect=True,
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=(
(
self._width * 0.5
+ (
(self._scroll_width * -0.5 + 170.0 - 70.0)
if uiscale is bui.UIScale.SMALL
else 0.0
)
),
yoffs - (64 if uiscale is bui.UIScale.SMALL else 4),
),
size=(0, 0),
color=bui.app.ui_v1.title_color,
scale=1.3 if uiscale is bui.UIScale.SMALL else 1.0,
h_align='left' if uiscale is bui.UIScale.SMALL else 'center',
v_align='center',
text=(bui.Lstr(resource=f'{self._r}.titleText')),
maxwidth=135 if uiscale is bui.UIScale.SMALL else 320,
)
# Build up the set of tabs we want.
tabdefs: list[tuple[GatherWindow.TabID, bui.Lstr]] = [
(self.TabID.ABOUT, bui.Lstr(resource=f'{self._r}.aboutText'))
]
if plus.get_v1_account_misc_read_val('enablePublicParties', True):
tabdefs.append(
(
self.TabID.INTERNET,
bui.Lstr(resource=f'{self._r}.publicText'),
)
)
tabdefs.append(
(self.TabID.PRIVATE, bui.Lstr(resource=f'{self._r}.privateText'))
)
tabdefs.append(
(self.TabID.NEARBY, bui.Lstr(resource=f'{self._r}.nearbyText'))
)
tabdefs.append(
(self.TabID.MANUAL, bui.Lstr(resource=f'{self._r}.manualText'))
)
tab_inset = 250.0 if uiscale is bui.UIScale.SMALL else 100.0
self._tab_row = TabRow(
self._root_widget,
tabdefs,
size=(self._scroll_width - 2.0 * tab_inset, 50),
pos=(
self._scroll_left + tab_inset,
self._scroll_bottom + self._scroll_height - 4.0,
),
on_select_call=bui.WeakCall(self._set_tab),
)
# Now instantiate handlers for these tabs.
tabtypes: dict[GatherWindow.TabID, type[GatherTab]] = {
self.TabID.ABOUT: AboutGatherTab,
self.TabID.MANUAL: ManualGatherTab,
self.TabID.PRIVATE: PrivateGatherTab,
self.TabID.INTERNET: PublicGatherTab,
self.TabID.NEARBY: NearbyGatherTab,
}
self._tabs: dict[GatherWindow.TabID, GatherTab] = {}
for tab_id in self._tab_row.tabs:
tabtype = tabtypes.get(tab_id)
if tabtype is not None:
self._tabs[tab_id] = tabtype(self)
# Eww; tokens meter may or may not be here; should be smarter
# about this.
bui.widget(
edit=self._tab_row.tabs[tabdefs[-1][0]].button,
right_widget=bui.get_special_widget('tokens_meter'),
)
if uiscale is bui.UIScale.SMALL:
bui.widget(
edit=self._tab_row.tabs[tabdefs[0][0]].button,
left_widget=bui.get_special_widget('back_button'),
up_widget=bui.get_special_widget('back_button'),
)
# Not actually using a scroll widget anymore; just an image.
bui.imagewidget(
parent=self._root_widget,
size=(self._scroll_width, self._scroll_height),
position=(
self._width * 0.5 - self._scroll_width * 0.5,
self._scroll_bottom,
),
texture=bui.gettexture('scrollWidget'),
mesh_transparent=bui.getmesh('softEdgeOutside'),
opacity=0.4,
)
self._tab_container: bui.Widget | None = None
self._restore_state()
@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
)
)
@override
def on_main_window_close(self) -> None:
self._save_state()
def playlist_select(
self,
origin_widget: bui.Widget,
context: PlaylistSelectContext,
) -> None:
"""Called by the private-hosting tab to select a playlist."""
from bauiv1lib.play import PlayWindow
# Avoid redundant window spawns.
if not self.main_window_has_control():
return
playwindow = PlayWindow(
origin_widget=origin_widget, playlist_select_context=context
)
self.main_window_replace(playwindow)
# Grab the newly-set main-window's back-state; that will lead us
# back here once we're done going down our main-window
# rabbit-hole for playlist selection.
context.back_state = playwindow.main_window_back_state
def _set_tab(self, tab_id: TabID) -> None:
if self._current_tab is tab_id:
return
prev_tab_id = self._current_tab
self._current_tab = tab_id
# We wanna preserve our current tab between runs.
cfg = bui.app.config
cfg['Gather Tab'] = tab_id.value
cfg.commit()
# Update tab colors based on which is selected.
self._tab_row.update_appearance(tab_id)
if prev_tab_id is not None:
prev_tab = self._tabs.get(prev_tab_id)
if prev_tab is not None:
prev_tab.on_deactivate()
# Clear up prev container if it hasn't been done.
if self._tab_container:
self._tab_container.delete()
tab = self._tabs.get(tab_id)
if tab is not None:
self._tab_container = tab.on_activate(
self._root_widget,
self._tab_row.tabs[tab_id].button,
self._scroll_width,
self._scroll_height,
self._scroll_left,
self._scroll_bottom,
)
return
def _save_state(self) -> None:
try:
for tab in self._tabs.values():
tab.save_state()
sel = self._root_widget.get_selected_child()
selected_tab_ids = [
tab_id
for tab_id, tab in self._tab_row.tabs.items()
if sel == tab.button
]
if sel == self._back_button:
sel_name = 'Back'
elif selected_tab_ids:
assert len(selected_tab_ids) == 1
sel_name = f'Tab:{selected_tab_ids[0].value}'
elif sel == self._tab_container:
sel_name = 'TabContainer'
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:
for tab in self._tabs.values():
tab.restore_state()
sel: bui.Widget | None
assert bui.app.classic is not None
winstate = bui.app.ui_v1.window_states.get(type(self), {})
sel_name = winstate.get('sel_name', None)
assert isinstance(sel_name, (str, type(None)))
current_tab = self.TabID.ABOUT
gather_tab_val = bui.app.config.get('Gather Tab')
try:
stored_tab = self.TabID(gather_tab_val)
if stored_tab in self._tab_row.tabs:
current_tab = stored_tab
except ValueError:
pass
self._set_tab(current_tab)
if sel_name == 'Back':
sel = self._back_button
elif sel_name == 'TabContainer':
sel = self._tab_container
elif isinstance(sel_name, str) and sel_name.startswith('Tab:'):
try:
sel_tab_id = self.TabID(sel_name.split(':')[-1])
except ValueError:
sel_tab_id = self.TabID.ABOUT
sel = self._tab_row.tabs[sel_tab_id].button
else:
sel = self._tab_row.tabs[current_tab].button
bui.containerwidget(edit=self._root_widget, selected_child=sel)
except Exception:
logging.exception('Error restoring state for %s.', self)
__all__ = ['GatherTab', 'GatherWindow']

View file

@ -0,0 +1,361 @@
# Released under the MIT License. See LICENSE for details.
#
"""Provides UI for inviting/joining friends."""
from __future__ import annotations
import weakref
import logging
from enum import Enum
from typing import override, TYPE_CHECKING
from bauiv1lib.tabs import TabRow
import bauiv1 as bui
if TYPE_CHECKING:
from bauiv1lib.play import PlaylistSelectContext
class GatherTab:
"""Defines a tab for use in the gather UI."""
def __init__(self, window: GatherWindow) -> None:
self._window = weakref.ref(window)
@property
def window(self) -> GatherWindow:
"""The GatherWindow that this tab belongs to."""
window = self._window()
if window is None:
raise bui.NotFoundError("GatherTab's window no longer exists.")
return window
def on_activate(
self,
parent_widget: bui.Widget,
tab_button: bui.Widget,
region_width: float,
region_height: float,
region_left: float,
region_bottom: float,
) -> bui.Widget:
"""Called when the tab becomes the active one.
The tab should create and return a container widget covering the
specified region.
"""
# pylint: disable=too-many-positional-arguments
raise RuntimeError('Should not get here.')
def on_deactivate(self) -> None:
"""Called when the tab will no longer be the active one."""
def save_state(self) -> None:
"""Called when the parent window is saving state."""
def restore_state(self) -> None:
"""Called when the parent window is restoring state."""
class GatherWindow(bui.MainWindow):
"""Window for joining/inviting friends."""
class TabID(Enum):
"""Our available tab types."""
ABOUT = 'about'
INTERNET = 'internet'
PRIVATE = 'private'
NEARBY = 'nearby'
MANUAL = 'manual'
def __init__(
self,
transition: str | None = 'in_right',
origin_widget: bui.Widget | None = None,
):
# pylint: disable=too-many-locals
# pylint: disable=cyclic-import
from bauiv1lib.gather.abouttab import AboutGatherTab
from bauiv1lib.gather.manualtab import ManualGatherTab
from bauiv1lib.gather.privatetab import PrivateGatherTab
from bauiv1lib.gather.publictab import PublicGatherTab
from bauiv1lib.gather.nearbytab import NearbyGatherTab
plus = bui.app.plus
assert plus is not None
bui.set_analytics_screen('Gather Window')
uiscale = bui.app.ui_v1.uiscale
self._width = (
1640
if uiscale is bui.UIScale.SMALL
else 1100 if uiscale is bui.UIScale.MEDIUM else 1200
)
self._height = (
1000
if uiscale is bui.UIScale.SMALL
else 730 if uiscale is bui.UIScale.MEDIUM else 900
)
self._current_tab: GatherWindow.TabID | None = None
self._r = 'gatherWindow'
# 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()
scale = (
1.4
if uiscale is bui.UIScale.SMALL
else 0.88 if uiscale is bui.UIScale.MEDIUM else 0.66
)
# Calc screen size in our local container space and clamp to a
# bit smaller than our container size.
target_width = min(self._width - 130, screensize[0] / scale)
target_height = min(self._height - 130, 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 * self._height + 0.5 * target_height + 30.0
self._scroll_width = target_width
self._scroll_height = target_height - 65
self._scroll_bottom = yoffs - 93 - self._scroll_height
self._scroll_left = (self._width - self._scroll_width) * 0.5
super().__init__(
root_widget=bui.containerwidget(
size=(self._width, self._height),
toolbar_visibility=(
'menu_tokens'
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:
bui.containerwidget(
edit=self._root_widget, on_cancel_call=self.main_window_back
)
self._back_button = None
else:
self._back_button = btn = bui.buttonwidget(
parent=self._root_widget,
id=f'{self.main_window_id_prefix}|back',
position=(70, yoffs - 43),
size=(60, 60),
scale=1.1,
autoselect=True,
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=(
(
self._width * 0.5
+ (
(self._scroll_width * -0.5 + 170.0 - 70.0)
if uiscale is bui.UIScale.SMALL
else 0.0
)
),
yoffs - (64 if uiscale is bui.UIScale.SMALL else 4),
),
size=(0, 0),
color=bui.app.ui_v1.title_color,
scale=1.3 if uiscale is bui.UIScale.SMALL else 1.0,
h_align='left' if uiscale is bui.UIScale.SMALL else 'center',
v_align='center',
text=(bui.Lstr(resource=f'{self._r}.titleText')),
maxwidth=135 if uiscale is bui.UIScale.SMALL else 320,
)
# Build up the set of tabs we want.
tabdefs: list[tuple[GatherWindow.TabID, bui.Lstr]] = [
(self.TabID.ABOUT, bui.Lstr(resource=f'{self._r}.aboutText'))
]
if plus.get_v1_account_misc_read_val('enablePublicParties', True):
tabdefs.append(
(
self.TabID.INTERNET,
bui.Lstr(resource=f'{self._r}.publicText'),
)
)
tabdefs.append(
(self.TabID.PRIVATE, bui.Lstr(resource=f'{self._r}.privateText'))
)
tabdefs.append(
(self.TabID.NEARBY, bui.Lstr(resource=f'{self._r}.nearbyText'))
)
tabdefs.append(
(self.TabID.MANUAL, bui.Lstr(resource=f'{self._r}.manualText'))
)
tab_inset = 250.0 if uiscale is bui.UIScale.SMALL else 100.0
self._tab_row = TabRow(
self._root_widget,
tabdefs,
idprefix=self.main_window_id_prefix,
size=(self._scroll_width - 2.0 * tab_inset, 50),
pos=(
self._scroll_left + tab_inset,
self._scroll_bottom + self._scroll_height - 4.0,
),
on_select_call=bui.WeakCall(self._set_tab),
)
# Now instantiate handlers for these tabs.
tabtypes: dict[GatherWindow.TabID, type[GatherTab]] = {
self.TabID.ABOUT: AboutGatherTab,
self.TabID.MANUAL: ManualGatherTab,
self.TabID.PRIVATE: PrivateGatherTab,
self.TabID.INTERNET: PublicGatherTab,
self.TabID.NEARBY: NearbyGatherTab,
}
self._tabs: dict[GatherWindow.TabID, GatherTab] = {}
for tab_id in self._tab_row.tabs:
tabtype = tabtypes.get(tab_id)
if tabtype is not None:
self._tabs[tab_id] = tabtype(self)
# Eww; tokens meter may or may not be here; should be smarter
# about this.
bui.widget(
edit=self._tab_row.tabs[tabdefs[-1][0]].button,
right_widget=bui.get_special_widget('tokens_meter'),
)
if uiscale is bui.UIScale.SMALL:
bui.widget(
edit=self._tab_row.tabs[tabdefs[0][0]].button,
left_widget=bui.get_special_widget('back_button'),
up_widget=bui.get_special_widget('back_button'),
)
# Not actually using a scroll widget anymore; just an image.
bui.imagewidget(
parent=self._root_widget,
size=(self._scroll_width, self._scroll_height),
position=(
self._width * 0.5 - self._scroll_width * 0.5,
self._scroll_bottom,
),
texture=bui.gettexture('scrollWidget'),
mesh_transparent=bui.getmesh('softEdgeOutside'),
opacity=0.4,
)
self._tab_container: bui.Widget | None = None
self._restore_state()
@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
)
)
@override
def main_window_should_preserve_selection(self) -> bool:
return True
@override
def on_main_window_close(self) -> None:
self._save_state()
def playlist_select(
self,
origin_widget: bui.Widget,
context: PlaylistSelectContext,
) -> None:
"""Called by the private-hosting tab to select a playlist."""
from bauiv1lib.play import PlayWindow
# Avoid redundant window spawns.
if not self.main_window_has_control():
return
new_window = self.main_window_replace(
lambda: PlayWindow(
origin_widget=origin_widget, playlist_select_context=context
)
)
assert new_window is not None
# Grab the newly-set main-window's back-state; that will lead us
# back here once we're done going down our main-window
# rabbit-hole for playlist selection.
context.back_state = new_window.main_window_back_state
def _set_tab(self, tab_id: TabID) -> None:
if self._current_tab is tab_id:
return
prev_tab_id = self._current_tab
self._current_tab = tab_id
# We wanna preserve our current tab between runs.
cfg = bui.app.config
cfg['Gather Tab'] = tab_id.value
cfg.commit()
# Update tab colors based on which is selected.
self._tab_row.update_appearance(tab_id)
if prev_tab_id is not None:
prev_tab = self._tabs.get(prev_tab_id)
if prev_tab is not None:
prev_tab.on_deactivate()
# Clear up prev container if it hasn't been done.
if self._tab_container:
self._tab_container.delete()
tab = self._tabs.get(tab_id)
if tab is not None:
self._tab_container = tab.on_activate(
self._root_widget,
self._tab_row.tabs[tab_id].button,
self._scroll_width,
self._scroll_height,
self._scroll_left,
self._scroll_bottom,
)
return
def _save_state(self) -> None:
try:
for tab in self._tabs.values():
tab.save_state()
except Exception:
logging.exception('Error saving state for %s.', self)
def _restore_state(self) -> None:
try:
for tab in self._tabs.values():
tab.restore_state()
current_tab = self.TabID.ABOUT
gather_tab_val = bui.app.config.get('Gather Tab')
try:
stored_tab = self.TabID(gather_tab_val)
if stored_tab in self._tab_row.tabs:
current_tab = stored_tab
except ValueError:
pass
self._set_tab(current_tab)
except Exception:
logging.exception('Error restoring state for %s.', self)

View file

@ -27,11 +27,14 @@ class AboutGatherTab(GatherTab):
region_bottom: float,
) -> bui.Widget:
# pylint: disable=too-many-locals
# pylint: disable=too-many-statements
# pylint: disable=too-many-positional-arguments
plus = bui.app.plus
assert plus is not None
idprefix = f'{self.window.main_window_id_prefix}|about'
try_tickets = plus.get_v1_account_misc_read_val(
'friendTryTickets', None
)
@ -147,6 +150,7 @@ class AboutGatherTab(GatherTab):
)
invite_button = bui.buttonwidget(
parent=container,
id=f'{idprefix}|invitefriend',
position=(region_width * 0.59, y - 25),
size=(230, 50),
color=(0.54, 0.42, 0.56),
@ -179,6 +183,7 @@ class AboutGatherTab(GatherTab):
)
discord_button = bui.buttonwidget(
parent=container,
id=f'{idprefix}|discordjoin',
position=(region_width * 0.59, y - 25),
size=(230, 50),
color=(0.54, 0.42, 0.56),

View file

@ -81,6 +81,7 @@ class ManualGatherTab(GatherTab):
def __init__(self, window: GatherWindow) -> None:
super().__init__(window)
self._idprefix = f'{window.main_window_id_prefix}|manual'
self._check_button: bui.Widget | None = None
self._doing_access_check: bool | None = None
self._access_check_count: int | None = None
@ -135,6 +136,7 @@ class ManualGatherTab(GatherTab):
v = c_height - 30
self._join_by_address_text = bui.textwidget(
parent=self._container,
id=f'{self._idprefix}|joinbyaddress',
position=(c_width * 0.5 - 245, v - 13),
color=(0.6, 1.0, 0.6),
scale=1.3,
@ -156,6 +158,7 @@ class ManualGatherTab(GatherTab):
)
self._favorites_text = bui.textwidget(
parent=self._container,
id=f'{self._idprefix}|favorites',
position=(c_width * 0.5 + 45, v - 13),
color=(0.6, 1.0, 0.6),
scale=1.3,
@ -271,6 +274,7 @@ class ManualGatherTab(GatherTab):
)
txt = bui.textwidget(
parent=self._container,
id=f'{self._idprefix}|manualaddress',
editable=True,
description=bui.Lstr(resource='gatherWindow.' 'manualAddressText'),
position=(c_width * 0.5 - 240 - 50, v - 30),
@ -298,6 +302,7 @@ class ManualGatherTab(GatherTab):
)
txt2 = bui.textwidget(
parent=self._container,
id=f'{self._idprefix}|manualport',
editable=True,
description=bui.Lstr(resource='gatherWindow.' 'portText'),
text=str(last_port),
@ -313,6 +318,7 @@ class ManualGatherTab(GatherTab):
btn = bui.buttonwidget(
parent=self._container,
id=f'{self._idprefix}|manualconnect',
size=(300, 70),
label=bui.Lstr(resource='gatherWindow.' 'manualConnectText'),
position=(c_width * 0.5 - 300, v),
@ -321,6 +327,7 @@ class ManualGatherTab(GatherTab):
)
savebutton = bui.buttonwidget(
parent=self._container,
id=f'{self._idprefix}|savefavorite',
size=(300, 70),
label=bui.Lstr(resource='gatherWindow.favoritesSaveText'),
position=(c_width * 0.5 - 240 + 490 - 200, v),
@ -336,6 +343,7 @@ class ManualGatherTab(GatherTab):
self._check_button = bui.textwidget(
parent=self._container,
size=(250, 60),
id=f'{self._idprefix}|showmyaddress',
text=bui.Lstr(resource='gatherWindow.showMyAddressText'),
v_align='center',
h_align='center',
@ -408,6 +416,7 @@ class ManualGatherTab(GatherTab):
self._favorites_connect_button = btn1 = bui.buttonwidget(
parent=self._container,
id=f'{self._idprefix}|favoritesconnect',
size=(b_width, b_height),
position=(140 if uiscale is bui.UIScale.SMALL else 40, btnv),
button_type='square',
@ -426,6 +435,7 @@ class ManualGatherTab(GatherTab):
btnv -= b_height + b_space_extra
bui.buttonwidget(
parent=self._container,
id=f'{self._idprefix}|favoritesedit',
size=(b_width, b_height),
position=(140 if uiscale is bui.UIScale.SMALL else 40, btnv),
button_type='square',
@ -439,6 +449,7 @@ class ManualGatherTab(GatherTab):
btnv -= b_height + b_space_extra
bui.buttonwidget(
parent=self._container,
id=f'{self._idprefix}|favoritesdelete',
size=(b_width, b_height),
position=(140 if uiscale is bui.UIScale.SMALL else 40, btnv),
button_type='square',
@ -462,6 +473,7 @@ class ManualGatherTab(GatherTab):
)
self._columnwidget = bui.columnwidget(
parent=scrlw,
id=f'{self._idprefix}|favoritescolumn',
left_border=10,
border=2,
margin=0,
@ -517,6 +529,7 @@ class ManualGatherTab(GatherTab):
assert bui.app.classic is not None
uiscale = bui.app.ui_v1.uiscale
self._favorite_edit_window = cnt = bui.containerwidget(
parent=bui.get_special_widget('overlay_stack'),
scale=(
1.8
if uiscale is bui.UIScale.SMALL
@ -524,6 +537,7 @@ class ManualGatherTab(GatherTab):
),
size=(c_width, c_height),
transition='in_scale',
darken_behind=True,
)
bui.textwidget(
@ -693,8 +707,8 @@ class ManualGatherTab(GatherTab):
],
),
self._delete_saved_party,
450,
150,
width=450,
height=150,
)
def _delete_saved_party(self) -> None:
@ -735,6 +749,7 @@ class ManualGatherTab(GatherTab):
for i, server in enumerate(servers):
txt = bui.textwidget(
parent=self._columnwidget,
id=f'{self._idprefix}|favorite{i}',
size=(self._favorites_scroll_width / t_scale, 30),
selectable=True,
color=(1.0, 1, 0.4),

View file

@ -23,16 +23,23 @@ class NetScanner:
def __init__(
self,
*,
tab: GatherTab,
scrollwidget: bui.Widget,
tab_button: bui.Widget,
width: float,
idprefix: str,
):
self._idprefix = idprefix
self._tab = weakref.ref(tab)
self._scrollwidget = scrollwidget
self._tab_button = tab_button
self._columnwidget = bui.columnwidget(
parent=self._scrollwidget, border=2, margin=0, left_border=10
parent=self._scrollwidget,
id=f'{self._idprefix}|col',
border=2,
margin=0,
left_border=10,
)
bui.widget(edit=self._columnwidget, up_widget=tab_button)
self._width = width
@ -92,6 +99,11 @@ class NetScanner:
corner_scale=t_scale,
maxwidth=(self._width / t_scale) * 0.93,
)
# We don't give these ids since they pop in and out and it
# doesn't make sense to save/restore selections for them.
# But we need to suppress the warning from that.
bui.widget(edit=txt3, allow_preserve_selection=False)
if host == last_selected_host:
bui.containerwidget(
edit=self._columnwidget,
@ -107,6 +119,7 @@ class NearbyGatherTab(GatherTab):
def __init__(self, window: GatherWindow) -> None:
super().__init__(window)
self._idprefix = f'{window.main_window_id_prefix}|nearby'
self._net_scanner: NetScanner | None = None
self._container: bui.Widget | None = None
@ -158,7 +171,11 @@ class NearbyGatherTab(GatherTab):
)
self._net_scanner = NetScanner(
self, scrollw, tab_button, width=sub_scroll_width
idprefix=self._idprefix,
tab=self,
scrollwidget=scrollw,
tab_button=tab_button,
width=sub_scroll_width,
)
bui.widget(edit=scrollw, autoselect=True, up_widget=tab_button)

View file

@ -58,6 +58,7 @@ class PrivateGatherTab(GatherTab):
def __init__(self, window: GatherWindow) -> None:
super().__init__(window)
self._idprefix = f'{window.main_window_id_prefix}|private'
self._container: bui.Widget | None = None
self._state: State = State()
self._last_datacode_refresh_time: float | None = None
@ -115,6 +116,7 @@ class PrivateGatherTab(GatherTab):
v = self._c_height - 30.0
self._join_sub_tab_text = bui.textwidget(
parent=self._container,
id=f'{self._idprefix}|jointab',
position=(self._c_width * 0.5 - 245, v - 13),
color=(0.6, 1.0, 0.6),
scale=1.3,
@ -134,6 +136,7 @@ class PrivateGatherTab(GatherTab):
)
self._host_sub_tab_text = bui.textwidget(
parent=self._container,
id=f'{self._idprefix}|hosttab',
position=(self._c_width * 0.5 + 45, v - 13),
color=(0.6, 1.0, 0.6),
scale=1.3,
@ -526,6 +529,7 @@ class PrivateGatherTab(GatherTab):
self._join_party_code_text = bui.textwidget(
parent=self._container,
id=f'{self._idprefix}|joinpartycode',
position=(self._c_width * 0.5 - 150, self._c_height - 250),
flatness=1.0,
scale=1.5,
@ -540,6 +544,7 @@ class PrivateGatherTab(GatherTab):
)
btn = bui.buttonwidget(
parent=self._container,
id=f'{self._idprefix}|connect',
size=(300, 70),
label=bui.Lstr(resource='gatherWindow.manualConnectText'),
position=(self._c_width * 0.5 - 150, self._c_height - 350),
@ -667,6 +672,7 @@ class PrivateGatherTab(GatherTab):
)
self._host_playlist_button = bui.buttonwidget(
parent=self._container,
id=f'{self._idprefix}|host',
size=(400, 70),
color=(0.6, 0.5, 0.6),
textcolor=(0.8, 0.75, 0.8),
@ -726,6 +732,7 @@ class PrivateGatherTab(GatherTab):
cbtnoffs = 10
self._host_copy_button = bui.buttonwidget(
parent=self._container,
id=f'{self._idprefix}|hostcopy',
size=(140, 40),
color=(0.6, 0.5, 0.6),
textcolor=(0.8, 0.75, 0.8),
@ -738,6 +745,7 @@ class PrivateGatherTab(GatherTab):
cbtnoffs = -70
self._host_connect_button = bui.buttonwidget(
parent=self._container,
id=f'{self._idprefix}|hostconnect',
size=(140, 40),
color=(0.6, 0.5, 0.6),
textcolor=(0.8, 0.75, 0.8),
@ -898,6 +906,7 @@ class PrivateGatherTab(GatherTab):
waiting = self._waiting_for_start_stop_response
self._host_start_stop_button = bui.buttonwidget(
parent=self._container,
id=f'{self._idprefix}|hoststartstop',
size=(400, 80),
color=(
(0.6, 0.6, 0.6)

View file

@ -130,6 +130,11 @@ class UIRow:
h_align='left',
v_align='center',
)
# These are popping in and out too chaotically to try and do
# auto-select-save/restore, so we don't supply ids for them. We
# need to suppress the warning that comes with that though.
bui.widget(edit=self._name_widget, allow_preserve_selection=False)
bui.widget(
edit=self._name_widget,
left_widget=join_text,
@ -164,6 +169,12 @@ class UIRow:
position=(sub_scroll_width - 270.0, 1 + vpos),
scale=0.9,
)
# These are popping in and out too chaotically to try and do
# auto-select-save/restore, so we don't supply ids for them.
# We need to suppress the warning that comes with that
# though.
bui.widget(edit=self._stats_button, allow_preserve_selection=False)
if existing_selection == Selection(
party.get_key(), SelectionComponent.STATS_BUTTON
):
@ -356,6 +367,7 @@ class PublicGatherTab(GatherTab):
def __init__(self, window: GatherWindow) -> None:
super().__init__(window)
self._idprefix = f'{window.main_window_id_prefix}|public'
self._container: bui.Widget | None = None
self._join_text: bui.Widget | None = None
self._host_text: bui.Widget | None = None
@ -428,6 +440,7 @@ class PublicGatherTab(GatherTab):
v = c_height - 30
self._join_text = bui.textwidget(
parent=self._container,
id=f'{self._idprefix}|jointab',
position=(c_width * 0.5 - 245, v - 13),
color=(0.6, 1.0, 0.6),
scale=1.3,
@ -451,6 +464,7 @@ class PublicGatherTab(GatherTab):
)
self._host_text = bui.textwidget(
parent=self._container,
id=f'{self._idprefix}|hosttab',
position=(c_width * 0.5 + 45, v - 13),
color=(0.6, 1.0, 0.6),
scale=1.3,
@ -593,6 +607,7 @@ class PublicGatherTab(GatherTab):
filter_txt = bui.Lstr(resource='filterText')
self._filter_text = bui.textwidget(
parent=self._container,
id=f'{self._idprefix}|filter',
text=self._filter_value,
size=(350, 45),
position=(c_width * 0.5 - 150, v - 10),
@ -670,6 +685,7 @@ class PublicGatherTab(GatherTab):
)
self._join_list_column = bui.containerwidget(
parent=scrollw,
id=f'{self._idprefix}|joinlistcolumn',
background=False,
size=(400, 400),
claims_left_right=True,
@ -755,6 +771,7 @@ class PublicGatherTab(GatherTab):
)
self._host_name_text = bui.textwidget(
parent=self._container,
id=f'{self._idprefix}|hostingname',
editable=True,
size=(535, 40),
position=(230 + xoffs, v - 30),
@ -795,6 +812,7 @@ class PublicGatherTab(GatherTab):
)
btn1 = self._host_max_party_size_minus_button = bui.buttonwidget(
parent=self._container,
id=f'{self._idprefix}|maxsizeminus',
size=(40, 40),
on_activate_call=bui.WeakCall(
self._on_max_public_party_size_minus_press
@ -805,6 +823,7 @@ class PublicGatherTab(GatherTab):
)
btn2 = self._host_max_party_size_plus_button = bui.buttonwidget(
parent=self._container,
id=f'{self._idprefix}|maxsizeplus',
size=(40, 40),
on_activate_call=bui.WeakCall(
self._on_max_public_party_size_plus_press
@ -827,6 +846,7 @@ class PublicGatherTab(GatherTab):
)
self._host_toggle_button = bui.buttonwidget(
parent=self._container,
id=f'{self._idprefix}|hosttoggle',
label=label,
size=(400, 80),
on_activate_call=(

View file

@ -20,7 +20,7 @@ class GetBSRemoteWindow(PopupWindow):
scale = (
2.3
if uiscale is bui.UIScale.SMALL
else 1.65 if uiscale is bui.UIScale.MEDIUM else 1.23
else 1.7 if uiscale is bui.UIScale.MEDIUM else 1.4
)
self._transitioning_out = False
self._width = 570
@ -54,7 +54,7 @@ class GetBSRemoteWindow(PopupWindow):
size=(0, 0),
h_align='center',
v_align='center',
maxwidth=self._width * 0.9,
maxwidth=self._width * 0.8,
position=(self._width * 0.5, 60),
text=bui.Lstr(
resource='remoteAppInfoShortText',

View file

@ -357,6 +357,8 @@ class GetTokensWindow(bui.MainWindow):
# We're affected by screen size only at small ui-scale.
refresh_on_screen_size_changes=uiscale is bui.UIScale.SMALL,
)
# Am seeing preserve-selection try to restore this sometimes.
bui.widget(edit=self._root_widget, allow_preserve_selection=False)
if uiscale is bui.UIScale.SMALL:
bui.containerwidget(
@ -366,6 +368,7 @@ class GetTokensWindow(bui.MainWindow):
else:
self._back_button = bui.buttonwidget(
parent=self._root_widget,
id=f'{self.main_window_id_prefix}|back',
position=(60, self._yoffs - 90),
size=((60, 60)),
scale=1.0,
@ -444,6 +447,10 @@ class GetTokensWindow(bui.MainWindow):
)
)
@override
def main_window_should_preserve_selection(self) -> bool:
return True
def _update(self) -> None:
# No-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
@ -596,6 +603,7 @@ class GetTokensWindow(bui.MainWindow):
)
tinfobtn = bui.buttonwidget(
parent=self._root_widget,
id=f'{self.main_window_id_prefix}|learnmore',
autoselect=True,
label=bui.Lstr(resource='learnMoreText'),
text_scale=0.7,
@ -634,6 +642,7 @@ class GetTokensWindow(bui.MainWindow):
btn = bui.buttonwidget(
autoselect=True,
label='',
id=f'{self.main_window_id_prefix}|button{i}',
color=buttondef.color,
transition_delay=tdelay,
up_widget=tinfobtn,
@ -741,6 +750,14 @@ class GetTokensWindow(bui.MainWindow):
text=bui.Lstr(resource='removeInGameAdsTokenPurchaseText'),
)
# Most of our UI won't exist until this point so we need to
# explicitly restore state for selection restore to work.
#
# Note to self: perhaps we should *not* do this if significant
# time has passed since the window was made or if input commands
# have happened.
self.main_window_restore_shared_state()
def _purchase_press(self, itemid: str) -> None:
plus = bui.app.plus
@ -802,8 +819,14 @@ def show_get_tokens_window(origin_widget: bui.Widget | None = None) -> None:
# NOTE TO USERS: The code below is not the proper way to do things;
# whenever possible one should use a MainWindow's
# main_window_replace() or main_window_back() methods. We just need
# to do things a bit more manually in this particular case.
# main_window_replace() or main_window_back() methods or
# bauiv1.auxiliary_window_activate(). We just need to do things a
# bit more manually in this particular case.
# Basically we want to pop up our auxiliary window but we don't want
# to replace any existing auxiliary windows; we want our close
# button to go back to whatever was there already, no matter whether
# it was an auxiliary window or not.
prev_main_window = bui.app.ui_v1.get_main_window()
@ -811,10 +834,12 @@ def show_get_tokens_window(origin_widget: bui.Widget | None = None) -> None:
if isinstance(prev_main_window, GetTokensWindow):
return
ui = bui.app.ui_v1
# Set our new main window.
bui.app.ui_v1.set_main_window(
ui.set_main_window(
GetTokensWindow(origin_widget=origin_widget),
from_window=False,
from_window=False, # Don't check where we're coming from.
back_state=ui.save_current_main_window_state(),
is_auxiliary=True,
suppress_warning=True,
)

View file

@ -91,6 +91,7 @@ class HelpWindow(bui.MainWindow):
else:
btn = bui.buttonwidget(
parent=self._root_widget,
id=f'{self.main_window_id_prefix}|back',
position=(50, yoffs - 45),
size=(60, 55),
scale=0.8,
@ -117,7 +118,6 @@ class HelpWindow(bui.MainWindow):
edit=self._scrollwidget,
left_widget=bui.get_special_widget('back_button'),
)
bui.widget(
edit=self._scrollwidget,
right_widget=bui.get_special_widget('squad_button'),
@ -143,6 +143,7 @@ class HelpWindow(bui.MainWindow):
self._subcontainer = bui.containerwidget(
parent=self._scrollwidget,
id=f'{self.main_window_id_prefix}|sub',
size=(self._sub_width, self._sub_height),
background=False,
claims_left_right=False,
@ -174,7 +175,8 @@ class HelpWindow(bui.MainWindow):
)
spacing = 1.0
h = self._sub_width * 0.5
baseh = self._sub_width * 0.5
h = baseh + 30
v = self._sub_height - 55
if uiscale is bui.UIScale.SMALL:
v -= inline_title_height
@ -217,6 +219,7 @@ class HelpWindow(bui.MainWindow):
position=(hval2 - 0.5 * icon_size, v - 0.45 * icon_size),
texture=logo_tex,
)
h = baseh
app = bui.app
assert app.classic is not None
@ -284,6 +287,8 @@ class HelpWindow(bui.MainWindow):
flatness=1.0,
)
h = baseh + 20
v -= spacing * 40.0
txt_scale = 0.74
txt = bui.Lstr(resource=f'{self._r}.friendsText').evaluate()
@ -373,6 +378,8 @@ class HelpWindow(bui.MainWindow):
v -= spacing * 150.0
h = baseh + 30
txt = bui.Lstr(resource=f'{self._r}.controlsText').evaluate()
txt_scale = 1.4
txt_maxwidth = 480
@ -405,6 +412,8 @@ class HelpWindow(bui.MainWindow):
v -= spacing * 45.0
h = baseh
txt_scale = 0.7
txt = bui.Lstr(
resource=f'{self._r}.controlsSubtitleText',
@ -557,6 +566,8 @@ class HelpWindow(bui.MainWindow):
v -= spacing * 280.0
h = baseh + 30
txt = bui.Lstr(resource=f'{self._r}.powerupsText').evaluate()
txt_scale = 1.4
txt_maxwidth = 480
@ -585,6 +596,8 @@ class HelpWindow(bui.MainWindow):
texture=logo_tex,
)
h = baseh + 20
v -= spacing * 50.0
txt_scale = getres(f'{self._r}.powerupsSubtitleTextScale')
txt = bui.Lstr(resource=f'{self._r}.powerupsSubtitleText').evaluate()
@ -601,6 +614,8 @@ class HelpWindow(bui.MainWindow):
flatness=1.0,
)
h = baseh + 20
v -= spacing * 1.0
mm1 = -270
@ -670,7 +685,7 @@ class HelpWindow(bui.MainWindow):
position=(h + mm3, v),
size=(0, 0),
scale=txt_scale,
maxwidth=300,
maxwidth=290,
flatness=1.0,
text=txtl,
h_align='left',
@ -691,3 +706,7 @@ class HelpWindow(bui.MainWindow):
transition=transition, origin_widget=origin_widget
)
)
@override
def main_window_should_preserve_selection(self) -> bool:
return True

View file

@ -13,6 +13,7 @@ from typing import override, assert_never, TYPE_CHECKING
from efro.util import strict_partial, pairs_from_flat
from efro.error import CommunicationError
import bacommon.bs
from bauiv1lib.utils import scroll_fade_bottom, scroll_fade_top
import bauiv1 as bui
if TYPE_CHECKING:
@ -29,7 +30,7 @@ class _Section:
"""Return rows of selectable controls."""
return []
def emit(self, subcontainer: bui.Widget, y: float) -> None:
def emit(self, subcontainer: bui.Widget, y: float, idprefix: str) -> None:
"""Emit the section."""
@ -75,7 +76,7 @@ class _TextSection(_Section):
return self.full_height
@override
def emit(self, subcontainer: bui.Widget, y: float) -> None:
def emit(self, subcontainer: bui.Widget, y: float, idprefix: str) -> None:
bui.textwidget(
parent=subcontainer,
position=(
@ -132,9 +133,10 @@ class _ButtonSection(_Section):
section_strong.call(section_strong)
@override
def emit(self, subcontainer: bui.Widget, y: float) -> None:
def emit(self, subcontainer: bui.Widget, y: float, idprefix: str) -> None:
self.button = bui.buttonwidget(
parent=subcontainer,
id=f'{idprefix}|button',
position=(
self.sub_width * 0.5 - self.button_width * 0.5,
y - self.spacing_top - self.button_height,
@ -184,7 +186,7 @@ class _DisplayItemsSection(_Section):
return self.full_height
@override
def emit(self, subcontainer: bui.Widget, y: float) -> None:
def emit(self, subcontainer: bui.Widget, y: float, idprefix: str) -> None:
# pylint: disable=cyclic-import
from baclassic import show_display_item
@ -267,7 +269,7 @@ class _ExpireTimeSection(_Section):
bui.textwidget(edit=self._widget, text=val, color=color)
@override
def emit(self, subcontainer: bui.Widget, y: float) -> None:
def emit(self, subcontainer: bui.Widget, y: float, idprefix: str) -> None:
self._widget = bui.textwidget(
parent=subcontainer,
position=(
@ -290,9 +292,9 @@ class _ExpireTimeSection(_Section):
@dataclass
class _EntryDisplay:
interaction_style: bacommon.bs.BasicClientUI.InteractionStyle
button_label_positive: bacommon.bs.BasicClientUI.ButtonLabel
button_label_negative: bacommon.bs.BasicClientUI.ButtonLabel
interaction_style: bacommon.bs.BasicCloudDialog.InteractionStyle
button_label_positive: bacommon.bs.BasicCloudDialog.ButtonLabel
button_label_negative: bacommon.bs.BasicCloudDialog.ButtonLabel
sections: list[_Section]
id: str
total_height: float
@ -351,10 +353,15 @@ class InboxWindow(bui.MainWindow):
scroll_height = target_height - 31
scroll_bottom = yoffs - 59 - scroll_height
# Go with full screen area scrollable on small ui.
if uiscale is bui.UIScale.SMALL:
scroll_height += 36
scroll_bottom -= 4
super().__init__(
root_widget=bui.containerwidget(
size=(self._width, self._height),
toolbar_visibility=('menu_full'),
toolbar_visibility='menu_full',
toolbar_cancel_button_style=(
'close' if auxiliary_style else 'back'
),
@ -374,6 +381,7 @@ class InboxWindow(bui.MainWindow):
else:
self._back_button = bui.buttonwidget(
parent=self._root_widget,
id=f'{self.main_window_id_prefix}|back',
autoselect=True,
position=(50, yoffs - 48),
size=(60, 60),
@ -390,21 +398,6 @@ class InboxWindow(bui.MainWindow):
edit=self._root_widget, cancel_button=self._back_button
)
self._title_text = bui.textwidget(
parent=self._root_widget,
position=(
self._width * 0.5,
yoffs - (45 if uiscale is bui.UIScale.SMALL else 30),
),
size=(0, 0),
h_align='center',
v_align='center',
scale=0.6 if uiscale is bui.UIScale.SMALL else 0.8,
text=bui.Lstr(resource='inboxText'),
maxwidth=200,
color=bui.app.ui_v1.title_color,
)
# Shows 'loading', 'no messages', etc.
self._infotext = bui.textwidget(
parent=self._root_widget,
@ -427,6 +420,7 @@ class InboxWindow(bui.MainWindow):
)
self._scrollwidget = bui.scrollwidget(
parent=self._root_widget,
id=f'{self.main_window_id_prefix}|scroll',
size=(scroll_width, scroll_height),
position=(self._width * 0.5 - scroll_width * 0.5, scroll_bottom),
capture_arrows=True,
@ -446,12 +440,45 @@ class InboxWindow(bui.MainWindow):
left_widget=bui.get_special_widget('back_button'),
)
# When we're doing fullscreen scrolling, fade content around
# toolbars.
if uiscale is bui.UIScale.SMALL:
scroll_fade_top(
self._root_widget,
self._width * 0.5 - scroll_width * 0.5,
scroll_bottom,
scroll_width,
scroll_height,
)
scroll_fade_bottom(
self._root_widget,
self._width * 0.5 - scroll_width * 0.5,
scroll_bottom,
scroll_width,
scroll_height,
)
bui.containerwidget(
edit=self._root_widget,
cancel_button=self._back_button,
single_depth=True,
)
self._title_text = bui.textwidget(
parent=self._root_widget,
position=(
self._width * 0.5,
yoffs - (45 if uiscale is bui.UIScale.SMALL else 30),
),
size=(0, 0),
h_align='center',
v_align='center',
scale=0.6 if uiscale is bui.UIScale.SMALL else 0.8,
text=bui.Lstr(resource='inboxText'),
maxwidth=200,
color=bui.app.ui_v1.title_color,
)
# Kick off request.
plus = bui.app.plus
if plus is None or plus.accounts.primary is None:
@ -474,6 +501,10 @@ class InboxWindow(bui.MainWindow):
)
)
@override
def main_window_should_preserve_selection(self) -> bool:
return True
def _error(self, errmsg: bui.Lstr | str) -> None:
"""Put ourself in a permanent error state."""
bui.spinnerwidget(edit=self._loading_spinner, visible=False)
@ -486,7 +517,7 @@ class InboxWindow(bui.MainWindow):
def _on_entry_display_press(
self,
display_weak: weakref.ReferenceType[_EntryDisplay],
action: bacommon.bs.ClientUIAction,
action: bacommon.bs.CloudDialogAction,
) -> None:
display = display_weak()
if display is None:
@ -500,7 +531,7 @@ class InboxWindow(bui.MainWindow):
# interaction types.
if (
display.interaction_style
is bacommon.bs.BasicClientUI.InteractionStyle.UNKNOWN
is bacommon.bs.BasicCloudDialog.InteractionStyle.UNKNOWN
):
display.processing_complete = True
self._close_soon_if_all_processed()
@ -523,7 +554,7 @@ class InboxWindow(bui.MainWindow):
# Ask the master-server to run our action.
with plus.accounts.primary:
plus.cloud.send_message_cb(
bacommon.bs.ClientUIActionMessage(display.id, action),
bacommon.bs.CloudDialogActionMessage(display.id, action),
on_response=bui.WeakCall(
self._on_client_ui_action_response,
display_weak,
@ -534,12 +565,12 @@ class InboxWindow(bui.MainWindow):
# Tweak the UI to show that things are in motion.
button = (
display.button_positive
if action is bacommon.bs.ClientUIAction.BUTTON_PRESS_POSITIVE
if action is bacommon.bs.CloudDialogAction.BUTTON_PRESS_POSITIVE
else display.button_negative
)
button_spinner = (
display.button_spinner_positive
if action is bacommon.bs.ClientUIAction.BUTTON_PRESS_POSITIVE
if action is bacommon.bs.CloudDialogAction.BUTTON_PRESS_POSITIVE
else display.button_spinner_negative
)
if button is not None:
@ -578,8 +609,8 @@ class InboxWindow(bui.MainWindow):
def _on_client_ui_action_response(
self,
display_weak: weakref.ReferenceType[_EntryDisplay],
action: bacommon.bs.ClientUIAction,
response: bacommon.bs.ClientUIActionResponse | Exception,
action: bacommon.bs.CloudDialogAction,
response: bacommon.bs.CloudDialogActionResponse | Exception,
) -> None:
# pylint: disable=too-many-branches
@ -602,12 +633,12 @@ class InboxWindow(bui.MainWindow):
# Tweak the button to show results.
button = (
display.button_positive
if action is bacommon.bs.ClientUIAction.BUTTON_PRESS_POSITIVE
if action is bacommon.bs.CloudDialogAction.BUTTON_PRESS_POSITIVE
else display.button_negative
)
button_spinner = (
display.button_spinner_positive
if action is bacommon.bs.ClientUIAction.BUTTON_PRESS_POSITIVE
if action is bacommon.bs.CloudDialogAction.BUTTON_PRESS_POSITIVE
else display.button_spinner_negative
)
# Always hide spinner at this point.
@ -717,6 +748,10 @@ class InboxWindow(bui.MainWindow):
sub_width = 400.0
sub_height = margin_top
# For fullscreen scrollable, account for toolbar.
if uiscale is bui.UIScale.SMALL:
sub_height += 36
# Construct entries for everything we'll display.
for i, wrapper in enumerate(response.wrappers):
@ -724,9 +759,9 @@ class InboxWindow(bui.MainWindow):
# textfin: str
color: tuple[float, float, float]
interaction_style: bacommon.bs.BasicClientUI.InteractionStyle
button_label_positive: bacommon.bs.BasicClientUI.ButtonLabel
button_label_negative: bacommon.bs.BasicClientUI.ButtonLabel
interaction_style: bacommon.bs.BasicCloudDialog.InteractionStyle
button_label_positive: bacommon.bs.BasicCloudDialog.ButtonLabel
button_label_negative: bacommon.bs.BasicCloudDialog.ButtonLabel
sections: list[_Section] = []
total_height = 80.0
@ -734,7 +769,7 @@ class InboxWindow(bui.MainWindow):
# Display only entries where we recognize all style/label
# values and ui component types.
if (
isinstance(wrapper.ui, bacommon.bs.BasicClientUI)
isinstance(wrapper.ui, bacommon.bs.BasicCloudDialog)
and not wrapper.ui.contains_unknown_elements()
):
color = (0.55, 0.5, 0.7)
@ -742,14 +777,14 @@ class InboxWindow(bui.MainWindow):
button_label_positive = wrapper.ui.button_label_positive
button_label_negative = wrapper.ui.button_label_negative
idcls = bacommon.bs.BasicClientUIComponentTypeID
idcls = bacommon.bs.BasicCloudDialogComponentTypeID
for component in wrapper.ui.components:
ctypeid = component.get_type_id()
section: _Section
if ctypeid is idcls.TEXT:
assert isinstance(
component, bacommon.bs.BasicClientUIComponentText
component, bacommon.bs.BasicCloudDialogComponentText
)
section = _TextSection(
sub_width=sub_width,
@ -767,7 +802,7 @@ class InboxWindow(bui.MainWindow):
elif ctypeid is idcls.LINK:
assert isinstance(
component, bacommon.bs.BasicClientUIComponentLink
component, bacommon.bs.BasicCloudDialogComponentLink
)
def _do_open_url(url: str, sec: _ButtonSection) -> None:
@ -792,7 +827,7 @@ class InboxWindow(bui.MainWindow):
elif ctypeid is idcls.DISPLAY_ITEMS:
assert isinstance(
component,
bacommon.bs.BasicClientUIDisplayItems,
bacommon.bs.BasicCloudDialogDisplayItems,
)
section = _DisplayItemsSection(
sub_width=sub_width,
@ -809,7 +844,7 @@ class InboxWindow(bui.MainWindow):
assert isinstance(
component,
bacommon.bs.BasicClientUIBsClassicTourneyResult,
bacommon.bs.BasicCloudDialogBsClassicTourneyResult,
)
campaignname, levelname = component.game.split(':')
assert bui.app.classic is not None
@ -934,7 +969,7 @@ class InboxWindow(bui.MainWindow):
elif ctypeid is idcls.EXPIRE_TIME:
assert isinstance(
component, bacommon.bs.BasicClientUIExpireTime
component, bacommon.bs.BasicCloudDialogExpireTime
)
section = _ExpireTimeSection(
sub_width=sub_width,
@ -957,11 +992,13 @@ class InboxWindow(bui.MainWindow):
# 'upgrade your app to see this' message.
color = (0.6, 0.6, 0.6)
interaction_style = (
bacommon.bs.BasicClientUI.InteractionStyle.UNKNOWN
bacommon.bs.BasicCloudDialog.InteractionStyle.UNKNOWN
)
button_label_positive = (
bacommon.bs.BasicCloudDialog.ButtonLabel.OK
)
button_label_positive = bacommon.bs.BasicClientUI.ButtonLabel.OK
button_label_negative = (
bacommon.bs.BasicClientUI.ButtonLabel.CANCEL
bacommon.bs.BasicCloudDialog.ButtonLabel.CANCEL
)
section = _TextSection(
@ -989,7 +1026,7 @@ class InboxWindow(bui.MainWindow):
sub_height += margin_bottom
subcontainer = bui.containerwidget(
id='inboxsub',
id=f'{self.main_window_id_prefix}|subc',
parent=self._scrollwidget,
size=(sub_width, sub_height),
background=False,
@ -1004,8 +1041,15 @@ class InboxWindow(bui.MainWindow):
buttonrows: list[list[bui.Widget]] = []
y = sub_height - margin_top
for i, _wrapper in enumerate(response.wrappers):
entry_display = self._entry_displays[i]
# For fullscreen scrollable, account for toolbar.
uiscale = bui.app.ui_v1.uiscale
if uiscale is bui.UIScale.SMALL:
y -= 36
# for i, _wrapper in enumerate(response.wrappers):
for entry_display in self._entry_displays:
# entry_display = self._entry_displays[i]
entry_display_weak = weakref.ref(entry_display)
bwidth = 140
bheight = 40
@ -1027,8 +1071,15 @@ class InboxWindow(bui.MainWindow):
bui.widget(edit=img, depth_range=(0, 0.1))
# Section contents.
for sec in entry_display.sections:
sec.emit(subcontainer, ysection)
for s, sec in enumerate(entry_display.sections):
sec.emit(
subcontainer,
ysection,
(
f'{self.main_window_id_prefix}|entry_{entry_display.id}'
f'|section{s}'
),
)
# Wire up any widgets created by this section.
sec_button_row = sec.get_button_row()
if sec_button_row:
@ -1039,7 +1090,7 @@ class InboxWindow(bui.MainWindow):
have_negative_button = (
entry_display.interaction_style
is (
bacommon.bs.BasicClientUI
bacommon.bs.BasicCloudDialog
).InteractionStyle.BUTTON_POSITIVE_NEGATIVE
)
@ -1053,6 +1104,10 @@ class InboxWindow(bui.MainWindow):
)
entry_display.button_positive = btn = bui.buttonwidget(
parent=subcontainer,
id=(
f'{self.main_window_id_prefix}|entry_{entry_display.id}'
f'|buttonpositive'
),
position=bpos,
autoselect=True,
size=(bwidth, bheight),
@ -1064,7 +1119,7 @@ class InboxWindow(bui.MainWindow):
on_activate_call=bui.WeakCall(
self._on_entry_display_press,
entry_display_weak,
bacommon.bs.ClientUIAction.BUTTON_PRESS_POSITIVE,
bacommon.bs.CloudDialogAction.BUTTON_PRESS_POSITIVE,
),
enable_sound=False,
)
@ -1084,6 +1139,10 @@ class InboxWindow(bui.MainWindow):
bpos = (25, y - entry_display.total_height + 15.0)
entry_display.button_negative = btn2 = bui.buttonwidget(
parent=subcontainer,
id=(
f'{self.main_window_id_prefix}'
f'|entry_{entry_display.id}|buttonnegative'
),
position=bpos,
autoselect=True,
size=(bwidth, bheight),
@ -1095,7 +1154,7 @@ class InboxWindow(bui.MainWindow):
on_activate_call=bui.WeakCall(
self._on_entry_display_press,
entry_display_weak,
(bacommon.bs.ClientUIAction).BUTTON_PRESS_NEGATIVE,
(bacommon.bs.CloudDialogAction).BUTTON_PRESS_NEGATIVE,
),
enable_sound=False,
)
@ -1142,6 +1201,14 @@ class InboxWindow(bui.MainWindow):
above_widget = buttons[0]
# Most of our UI won't exist until this point so we need to
# explicitly restore state for selection restore to work.
#
# Note to self: perhaps we should *not* do this if significant
# time has passed since the window was made or if input commands
# have happened.
self.main_window_restore_shared_state()
def _get_bs_classic_tourney_results_sections() -> list[_Section]:
return []

View file

@ -58,6 +58,10 @@ class InGameMenuWindow(bui.MainWindow):
)
)
@override
def main_window_should_preserve_selection(self) -> bool:
return True
def _refresh(self) -> None:
# Clear everything that was there.
@ -91,6 +95,7 @@ class InGameMenuWindow(bui.MainWindow):
if bs.is_in_replay():
self._end_button = bui.buttonwidget(
parent=self._root_widget,
id=f'{self.main_window_id_prefix}|end',
position=(h - self._button_width * 0.5 * scale, v),
scale=scale,
size=(self._button_width, self._button_height),
@ -101,6 +106,7 @@ class InGameMenuWindow(bui.MainWindow):
elif bs.get_foreground_host_session() is not None:
self._end_button = bui.buttonwidget(
parent=self._root_widget,
id=f'{self.main_window_id_prefix}|end',
position=(h - self._button_width * 0.5 * scale, v),
scale=scale,
size=(self._button_width, self._button_height),
@ -124,6 +130,7 @@ class InGameMenuWindow(bui.MainWindow):
# button.
self._end_button = bui.buttonwidget(
parent=self._root_widget,
id=f'{self.main_window_id_prefix}|end',
position=(h - self._button_width * 0.5 * scale, v),
scale=scale,
size=(self._button_width, self._button_height),
@ -174,6 +181,7 @@ class InGameMenuWindow(bui.MainWindow):
)
btn = bui.buttonwidget(
parent=self._root_widget,
id=f'{self.main_window_id_prefix}|replayspeedminus',
position=(
h - b_size - b_buffer_1,
v - b_size - b_buffer_2 + v_offs,
@ -199,6 +207,7 @@ class InGameMenuWindow(bui.MainWindow):
)
btn = bui.buttonwidget(
parent=self._root_widget,
id=f'{self.main_window_id_prefix}|replayspeedplus',
position=(h + b_buffer_1, v - b_size - b_buffer_2 + v_offs),
button_type='square',
size=(b_size, b_size),
@ -221,6 +230,7 @@ class InGameMenuWindow(bui.MainWindow):
)
self._pause_resume_button = btn = bui.buttonwidget(
parent=self._root_widget,
id=f'{self.main_window_id_prefix}|pauseresume',
position=(h - b_size * 0.5, v - b_size - b_buffer_2 + v_offs),
button_type='square',
size=(b_size, b_size),
@ -234,6 +244,7 @@ class InGameMenuWindow(bui.MainWindow):
)
btn = bui.buttonwidget(
parent=self._root_widget,
id=f'{self.main_window_id_prefix}|rewind',
position=(
h - b_size * 1.5 - b_buffer_1 * 2,
v - b_size - b_buffer_2 + v_offs,
@ -260,6 +271,7 @@ class InGameMenuWindow(bui.MainWindow):
)
btn = bui.buttonwidget(
parent=self._root_widget,
id=f'{self.main_window_id_prefix}|forward',
position=(
h + b_size * 0.5 + b_buffer_1 * 2,
v - b_size - b_buffer_2 + v_offs,
@ -376,6 +388,7 @@ class InGameMenuWindow(bui.MainWindow):
self._p_index += 1
btn = bui.buttonwidget(
parent=self._root_widget,
id=f'{self.main_window_id_prefix}|resume',
position=(h - self._button_width / 2, v),
size=(self._button_width, self._button_height),
scale=scale,
@ -386,7 +399,7 @@ class InGameMenuWindow(bui.MainWindow):
bui.containerwidget(edit=self._root_widget, cancel_button=btn)
# Add any custom options defined by the current game.
for entry in custom_menu_entries:
for i, entry in enumerate(custom_menu_entries):
h, v, scale = positions[self._p_index]
self._p_index += 1
@ -401,6 +414,7 @@ class InGameMenuWindow(bui.MainWindow):
bui.buttonwidget(
parent=self._root_widget,
id=f'{self.main_window_id_prefix}|custom{i}',
position=(h - self._button_width / 2, v),
size=(self._button_width, self._button_height),
scale=scale,
@ -417,6 +431,7 @@ class InGameMenuWindow(bui.MainWindow):
self._p_index += 1
btn = bui.buttonwidget(
parent=self._root_widget,
id=f'{self.main_window_id_prefix}|leave',
position=(h - self._button_width / 2, v),
size=(self._button_width, self._button_height),
scale=scale,

View file

@ -19,9 +19,6 @@ class InventoryWindow(bui.MainWindow):
auxiliary_style: bool = True,
):
bui.set_analytics_screen('Help Window')
assert bui.app.classic is not None
uiscale = bui.app.ui_v1.uiscale
self._width = 1400 if uiscale is bui.UIScale.SMALL else 750
self._height = (
@ -87,6 +84,7 @@ class InventoryWindow(bui.MainWindow):
else:
btn = bui.buttonwidget(
parent=self._root_widget,
id=f'{self.main_window_id_prefix}|back',
scale=0.8,
position=(50, yoffs - 50),
size=(50, 50) if auxiliary_style else (60, 55),
@ -108,6 +106,7 @@ class InventoryWindow(bui.MainWindow):
button_width = 300
self._player_profiles_button = btn = bui.buttonwidget(
parent=self._root_widget,
id=f'{self.main_window_id_prefix}|playerprofiles',
position=(self._width * 0.5 - button_width * 0.5, yoffs - 200),
autoselect=True,
size=(button_width, 60),
@ -117,6 +116,8 @@ class InventoryWindow(bui.MainWindow):
textcolor=(0.75, 0.7, 0.8),
on_activate_call=self._player_profiles_press,
)
# Select this by default.
bui.containerwidget(edit=self._root_widget, selected_child=btn)
bui.textwidget(
parent=self._root_widget,
position=(self._width * 0.5, yoffs - 250),
@ -132,12 +133,10 @@ class InventoryWindow(bui.MainWindow):
# pylint: disable=cyclic-import
from bauiv1lib.profile.browser import ProfileBrowserWindow
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
self.main_window_replace(
ProfileBrowserWindow(origin_widget=self._player_profiles_button)
lambda: ProfileBrowserWindow(
origin_widget=self._player_profiles_button
)
)
@override
@ -149,3 +148,7 @@ class InventoryWindow(bui.MainWindow):
transition=transition, origin_widget=origin_widget
)
)
@override
def main_window_should_preserve_selection(self) -> bool:
return True

View file

@ -374,6 +374,11 @@ class KioskWindow(bui.MainWindow):
)
)
@override
def main_window_should_preserve_selection(self) -> bool:
# TODO: Wire this up.
return False
@override
def on_main_window_close(self) -> None:
self._save_state()
@ -529,13 +534,15 @@ class KioskWindow(bui.MainWindow):
# pylint: disable=cyclic-import
from bauiv1lib.mainmenu import MainMenuWindow
# no-op if we're not in control.
# No-op if we're not in control.
if not self.main_window_has_control():
return
assert bui.app.classic is not None
self._save_state()
bui.app.classic.did_menu_intro = True # prevent delayed transition-in
self.main_window_replace(MainMenuWindow())
# Prevent delayed transition-in.
bui.app.classic.did_menu_intro = True
self.main_window_replace(MainMenuWindow)

View file

@ -9,6 +9,7 @@ import copy
import logging
from typing import TYPE_CHECKING, override
from bauiv1lib.utils import scroll_fade_bottom, scroll_fade_top
from bauiv1lib.popup import PopupMenu
import bauiv1 as bui
@ -57,9 +58,6 @@ class LeagueRankWindow(bui.MainWindow):
)
self._r = 'coopSelectWindow'
self._rdict = bui.app.lang.get_resource(self._r)
# top_extra = 20 if uiscale is bui.UIScale.SMALL else 0
# self._xoffs = 80.0 if uiscale is bui.UIScale.SMALL else 0
self._xoffs = 40
self._league_url_arg = ''
@ -86,19 +84,19 @@ class LeagueRankWindow(bui.MainWindow):
yoffs = 0.5 * self._height + 0.5 * target_height + 30.0
self._scroll_width = target_width
self._scroll_height = target_height - 35
self._scroll_height = target_height - 50
scroll_bottom = yoffs - 80 - self._scroll_height
# Go with full-screen scrollable area in small ui.
if uiscale is bui.UIScale.SMALL:
self._scroll_height += 53
scroll_bottom -= 1
super().__init__(
root_widget=bui.containerwidget(
size=(self._width, self._height),
stack_offset=(
(0, 0)
if uiscale is bui.UIScale.SMALL
else (0, 10) if uiscale is bui.UIScale.MEDIUM else (0, 0)
),
scale=scale,
toolbar_visibility=('menu_full'),
toolbar_visibility='menu_full',
toolbar_cancel_button_style=(
'close' if auxiliary_style else 'back'
),
@ -117,6 +115,7 @@ class LeagueRankWindow(bui.MainWindow):
else:
self._back_button = bui.buttonwidget(
parent=self._root_widget,
id=f'{self.main_window_id_prefix}|back',
position=(75 + x_inset, yoffs - 60),
size=(60, 55),
scale=1.2,
@ -135,6 +134,40 @@ class LeagueRankWindow(bui.MainWindow):
selected_child=self._back_button,
)
self._scrollwidget = bui.scrollwidget(
parent=self._root_widget,
highlight=False,
size=(self._scroll_width, self._scroll_height),
position=(
self._width * 0.5 - self._scroll_width * 0.5,
scroll_bottom,
),
center_small_content=True,
center_small_content_horizontally=True,
border_opacity=0.4,
)
bui.widget(edit=self._scrollwidget, autoselect=True)
bui.containerwidget(edit=self._scrollwidget, claims_left_right=True)
# With full-screen scrolling, fade content as it approaches
# toolbars.
if uiscale is bui.UIScale.SMALL:
scroll_fade_top(
self._root_widget,
self._width * 0.5 - self._scroll_width * 0.5,
scroll_bottom,
self._scroll_width,
self._scroll_height,
yscale=0.5,
)
scroll_fade_bottom(
self._root_widget,
self._width * 0.5 - self._scroll_width * 0.5,
scroll_bottom,
self._scroll_width,
self._scroll_height,
)
self._title_text = bui.textwidget(
parent=self._root_widget,
position=(
@ -153,27 +186,17 @@ class LeagueRankWindow(bui.MainWindow):
v_align='center',
)
self._scrollwidget = bui.scrollwidget(
parent=self._root_widget,
highlight=False,
size=(self._scroll_width, self._scroll_height),
position=(
self._width * 0.5 - self._scroll_width * 0.5,
scroll_bottom,
),
center_small_content=True,
center_small_content_horizontally=True,
border_opacity=0.4,
)
bui.widget(edit=self._scrollwidget, autoselect=True)
bui.containerwidget(edit=self._scrollwidget, claims_left_right=True)
self._last_power_ranking_query_time: float | None = None
self._doing_power_ranking_query = False
self._subcontainer: bui.Widget | None = None
self._subcontainerwidth = 1024
self._subcontainerheight = 573
# For fullscreen scrollable, account for toolbar.
if uiscale is bui.UIScale.SMALL:
self._subcontainerheight += 53
self._power_ranking_score_widgets: list[bui.Widget] = []
self._season_popup_menu: PopupMenu | None = None
@ -185,7 +208,6 @@ class LeagueRankWindow(bui.MainWindow):
self._account_state = plus.get_v1_account_state()
self._refresh()
self._restore_state()
# If we've got cached power-ranking data already, display it.
assert bui.app.classic is not None
@ -209,8 +231,8 @@ class LeagueRankWindow(bui.MainWindow):
)
@override
def on_main_window_close(self) -> None:
self._save_state()
def main_window_should_preserve_selection(self) -> bool:
return True
def _on_achievements_press(self) -> None:
from bauiv1lib.achievements import AchievementsWindow
@ -221,7 +243,9 @@ class LeagueRankWindow(bui.MainWindow):
if self._season == 'a' or self._is_current_season:
prab = self._power_ranking_achievements_button
assert prab is not None
self.main_window_replace(AchievementsWindow(origin_widget=prab))
self.main_window_replace(
lambda: AchievementsWindow(origin_widget=prab)
)
else:
bui.screenmessage(
bui.Lstr(
@ -317,9 +341,6 @@ class LeagueRankWindow(bui.MainWindow):
self._league_rank_data = copy.deepcopy(data)
self._update_for_league_rank_data(data)
def _restore_state(self) -> None:
pass
def _update(self, show: bool = False) -> None:
plus = bui.app.plus
assert plus is not None
@ -330,7 +351,6 @@ class LeagueRankWindow(bui.MainWindow):
account_state = plus.get_v1_account_state()
if account_state != self._account_state:
self._account_state = account_state
self._save_state()
self._refresh()
# And power ranking too.
@ -384,6 +404,11 @@ class LeagueRankWindow(bui.MainWindow):
v -= 0
# For fullscreen scrollable, account for toolbar.
uiscale = bui.app.ui_v1.uiscale
if uiscale is bui.UIScale.SMALL:
v -= 52
h2 = 80
v2 = v - 60
worth_color = (0.6, 0.6, 0.65)
@ -410,6 +435,7 @@ class LeagueRankWindow(bui.MainWindow):
self._power_ranking_achievements_button = bui.buttonwidget(
parent=w_parent,
id=f'{self.main_window_id_prefix}|ach',
position=(self._xoffs + h2 - 60, v2 + 10),
size=(200, 80),
icon=bui.gettexture('achievementsIcon'),
@ -440,6 +466,7 @@ class LeagueRankWindow(bui.MainWindow):
self._power_ranking_trophies_button = bui.buttonwidget(
parent=w_parent,
id=f'{self.main_window_id_prefix}|trophies',
position=(self._xoffs + h2 - 60, v2 + 10),
size=(200, 80),
icon=bui.gettexture('medalSilver'),
@ -484,6 +511,7 @@ class LeagueRankWindow(bui.MainWindow):
if plus.get_v1_account_misc_read_val('act', False):
self._activity_mult_button = bui.buttonwidget(
parent=w_parent,
id=f'{self.main_window_id_prefix}|amult',
position=(self._xoffs + h2 - 60, v2 + 10),
size=(200, 60),
icon=bui.gettexture('heart'),
@ -515,6 +543,7 @@ class LeagueRankWindow(bui.MainWindow):
self._up_to_date_bonus_button = bui.buttonwidget(
parent=w_parent,
id=f'{self.main_window_id_prefix}|uptodatebonus',
position=(self._xoffs + h2 - 60, v2 + 10),
size=(200, 60),
icon=bui.gettexture('logo'),
@ -712,6 +741,7 @@ class LeagueRankWindow(bui.MainWindow):
self._see_more_button = bui.buttonwidget(
parent=w_parent,
id=f'{self.main_window_id_prefix}|seemore',
label=self._rdict.seeMoreText,
position=(self._xoffs + h, v),
color=(0.5, 0.5, 0.6),
@ -811,6 +841,11 @@ class LeagueRankWindow(bui.MainWindow):
self._season = data['s'] if data is not None else None
v = self._subcontainerheight - 20
# For fullscreen scrollable, account for toolbar.
uiscale = bui.app.ui_v1.uiscale
if uiscale is bui.UIScale.SMALL:
v -= 52
popup_was_selected = False
if self._season_popup_menu is not None:
btn = self._season_popup_menu.get_button()
@ -853,6 +888,7 @@ class LeagueRankWindow(bui.MainWindow):
assert self._subcontainer
self._season_popup_menu = PopupMenu(
parent=self._subcontainer,
button_id=f'{self.main_window_id_prefix}|season',
position=(self._xoffs + 390, v - 45),
width=150,
button_size=(200, 50),
@ -1160,6 +1196,9 @@ class LeagueRankWindow(bui.MainWindow):
v_align='center',
scale=0.9,
)
# Giving these ids doesn't work cleanly with sel
# save/restore due to refreshing, so let's just not for now.
bui.widget(edit=txt, allow_preserve_selection=False)
self._power_ranking_score_widgets.append(txt)
bui.textwidget(
edit=txt,
@ -1187,6 +1226,3 @@ class LeagueRankWindow(bui.MainWindow):
self._requested_season = value
self._last_power_ranking_query_time = None # Update asap.
self._update(show=True)
def _save_state(self) -> None:
pass

View file

@ -22,6 +22,7 @@ class MainMenuWindow(bui.MainWindow):
transition: str | None = 'in_right',
origin_widget: bui.Widget | None = None,
):
ui = bui.app.ui_v1
# Preload some modules we use in a background thread so we won't
# have a visual hitch when the user taps them.
@ -30,13 +31,13 @@ class MainMenuWindow(bui.MainWindow):
bui.set_analytics_screen('Main Menu')
self._show_remote_app_info_on_first_launch()
uiscale = bui.app.ui_v1.uiscale
uiscale = ui.uiscale
# Make a vanilla container; we'll modify it to our needs in
# refresh.
super().__init__(
root_widget=bui.containerwidget(
toolbar_visibility=('menu_full_no_back')
toolbar_visibility=('menu_full_no_back'),
),
transition=transition,
origin_widget=origin_widget,
@ -44,10 +45,6 @@ class MainMenuWindow(bui.MainWindow):
refresh_on_screen_size_changes=uiscale is bui.UIScale.SMALL,
)
# Grab this stuff in case it changes.
# self._is_demo = bui.app.env.demo
# self._is_arcade = bui.app.env.arcade
self._tdelay = 0.0
self._t_delay_inc = 0.02
self._t_delay_play = 1.7
@ -65,22 +62,28 @@ class MainMenuWindow(bui.MainWindow):
self._refresh()
self._restore_state()
@override
def on_main_window_close(self) -> None:
self._save_state()
@override
def get_main_window_state(self) -> bui.MainWindowState:
# Support recreating our window for back/refresh purposes.
cls = type(self)
# Pull values from self here; if we do it in the lambda we'll
# keep self alive which we don't want.
# id_prefix = self._id_prefix
return bui.BasicMainWindowState(
create_call=lambda transition, origin_widget: cls(
transition=transition, origin_widget=origin_widget
)
transition=transition,
origin_widget=origin_widget,
# id_prefix=id_prefix,
),
# restore_selection=True,
)
@override
def main_window_should_preserve_selection(self) -> bool:
return True
@staticmethod
def _preload_modules() -> None:
"""Preload modules we use; avoids hitches (called in bg thread)."""
@ -257,7 +260,6 @@ class MainMenuWindow(bui.MainWindow):
),
h_align='center',
v_align='center',
# transition_delay=self._t_delay_play,
transition_delay=thistdelay,
)
@ -267,20 +269,14 @@ class MainMenuWindow(bui.MainWindow):
# In kiosk mode, provide a button to get back to the kiosk menu.
if arcade_or_demo:
# h, v, scale = positions[self._p_index]
h = self._width * 0.5
v = button_y_offs
scale = 1.0
this_b_width = self._button_width * 0.4 * scale
# demo_menu_delay = (
# 0.0
# if self._t_delay_play == 0.0
# else max(0, self._t_delay_play + 0.1)
# )
demo_menu_delay = 0.0
self._demo_menu_button = bui.buttonwidget(
parent=self._root_widget,
id='demo',
id=f'{self.main_window_id_prefix}|demo',
position=(self._width * 0.5 - this_b_width * 0.5, v + 90),
size=(this_b_width, 45),
autoselect=True,
@ -312,6 +308,7 @@ class MainMenuWindow(bui.MainWindow):
thistdelay = self._tdelay + td2 * self._t_delay_inc
self._gather_button = bui.buttonwidget(
parent=self._root_widget,
id=f'{self.main_window_id_prefix}|gather',
position=(h - side_button_width * side_button_scale * 0.5, v),
size=(side_button_width, side_button_height),
scale=side_button_scale,
@ -359,7 +356,7 @@ class MainMenuWindow(bui.MainWindow):
self._how_to_play_button = bui.buttonwidget(
parent=self._root_widget,
id='howtoplay',
id=f'{self.main_window_id_prefix}|howtoplay',
position=(h, v),
autoselect=self._use_autoselect,
size=(side_button_2_width, side_button_2_height * 2.0),
@ -380,8 +377,9 @@ class MainMenuWindow(bui.MainWindow):
assert play_button_width is not None
assert play_button_height is not None
thistdelay = self._tdelay + td3 * self._t_delay_inc
self._play_button = start_button = bui.buttonwidget(
self._play_button = play_button = bui.buttonwidget(
parent=self._root_widget,
id=f'{self.main_window_id_prefix}|play',
position=(h - play_button_width * 0.5 * play_button_scale, v),
size=(play_button_width, play_button_height),
autoselect=self._use_autoselect,
@ -393,12 +391,10 @@ class MainMenuWindow(bui.MainWindow):
)
bui.containerwidget(
edit=self._root_widget,
start_button=start_button,
selected_child=start_button,
start_button=play_button,
selected_child=play_button,
)
# self._tdelay += self._t_delay_inc
h = (
self._width * 0.5
+ play_button_width * play_button_scale * 0.5
@ -409,6 +405,7 @@ class MainMenuWindow(bui.MainWindow):
thistdelay = self._tdelay + td4 * self._t_delay_inc
self._watch_button = bui.buttonwidget(
parent=self._root_widget,
id=f'{self.main_window_id_prefix}|watch',
position=(h - side_button_width * side_button_scale * 0.5, v),
size=(side_button_width, side_button_height),
scale=side_button_scale,
@ -457,6 +454,7 @@ class MainMenuWindow(bui.MainWindow):
self._credits_button = bui.buttonwidget(
parent=self._root_widget,
id=f'{self.main_window_id_prefix}|credits',
position=(h, v),
button_type=None if self._have_quit_button else 'square',
size=(
@ -477,6 +475,7 @@ class MainMenuWindow(bui.MainWindow):
# credits button to get to it.
self._quit_button = quit_button = bui.buttonwidget(
parent=self._root_widget,
id=f'{self.main_window_id_prefix}|quit',
autoselect=self._use_autoselect,
position=(h + 4.0, v),
size=(side_button_2_width, side_button_2_height),
@ -534,111 +533,38 @@ class MainMenuWindow(bui.MainWindow):
# pylint: disable=cyclic-import
from bauiv1lib.credits import CreditsWindow
# no-op if we're not currently in control.
if not self.main_window_has_control():
return
self.main_window_replace(
CreditsWindow(origin_widget=self._credits_button),
lambda: CreditsWindow(origin_widget=self._credits_button),
)
def _howtoplay(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.help import HelpWindow
# no-op if we're not currently in control.
if not self.main_window_has_control():
return
self.main_window_replace(
HelpWindow(origin_widget=self._how_to_play_button),
lambda: HelpWindow(origin_widget=self._how_to_play_button),
)
def _save_state(self) -> None:
try:
sel = self._root_widget.get_selected_child()
if sel == self._play_button:
sel_name = 'Start'
elif sel == self._gather_button:
sel_name = 'Gather'
elif sel == self._watch_button:
sel_name = 'Watch'
elif sel == self._how_to_play_button:
sel_name = 'HowToPlay'
elif sel == self._credits_button:
sel_name = 'Credits'
elif sel == self._quit_button:
sel_name = 'Quit'
elif sel == self._demo_menu_button:
sel_name = 'DemoMenu'
else:
print(f'Unknown widget in main menu selection: {sel}.')
sel_name = 'Start'
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:
sel: bui.Widget | None
sel_name = bui.app.ui_v1.window_states.get(type(self), {}).get(
'sel_name'
)
assert isinstance(sel_name, (str, type(None)))
if sel_name is None:
sel_name = 'Start'
if sel_name == 'HowToPlay':
sel = self._how_to_play_button
elif sel_name == 'Gather':
sel = self._gather_button
elif sel_name == 'Watch':
sel = self._watch_button
elif sel_name == 'Credits':
sel = self._credits_button
elif sel_name == 'Quit':
sel = self._quit_button
elif sel_name == 'DemoMenu':
sel = self._demo_menu_button
else:
sel = self._play_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)
def _gather_press(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.gather import GatherWindow
# no-op if we're not currently in control.
if not self.main_window_has_control():
return
self.main_window_replace(
GatherWindow(origin_widget=self._gather_button)
lambda: GatherWindow(origin_widget=self._gather_button)
)
def _watch_press(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.watch import WatchWindow
# no-op if we're not currently in control.
if not self.main_window_has_control():
return
self.main_window_replace(
WatchWindow(origin_widget=self._watch_button),
lambda: WatchWindow(origin_widget=self._watch_button),
)
def _play_press(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.play import PlayWindow
# no-op if we're not currently in control.
if not self.main_window_has_control():
return
self.main_window_replace(PlayWindow(origin_widget=self._play_button))
self.main_window_replace(
lambda: PlayWindow(origin_widget=self._play_button)
)

View file

@ -38,6 +38,7 @@ class PartyWindow(bui.Window):
if uiscale is bui.UIScale.SMALL
else 480 if uiscale is bui.UIScale.MEDIUM else 600
)
self._idprefix = bui.app.ui_v1.new_id_prefix('party')
self._display_old_msgs = True
super().__init__(
root_widget=bui.containerwidget(
@ -68,6 +69,7 @@ class PartyWindow(bui.Window):
self._cancel_button = bui.buttonwidget(
parent=self._root_widget,
id=f'{self._idprefix}|cancel',
scale=0.7,
position=(30, self._height - 47),
size=(50, 50),
@ -82,6 +84,7 @@ class PartyWindow(bui.Window):
self._menu_button = bui.buttonwidget(
parent=self._root_widget,
id=f'{self._idprefix}|menu',
scale=0.7,
position=(self._width - 60, self._height - 47),
size=(50, 50),
@ -138,13 +141,18 @@ class PartyWindow(bui.Window):
self._scroll_width = self._width - 50
self._scrollwidget = bui.scrollwidget(
parent=self._root_widget,
id=f'{self._idprefix}|scroll',
size=(self._scroll_width, self._height - 200),
position=(30, 80),
color=(0.4, 0.6, 0.3),
border_opacity=0.6,
)
self._columnwidget = bui.columnwidget(
parent=self._scrollwidget, border=2, left_border=-200, margin=0
parent=self._scrollwidget,
id=f'{self._idprefix}|column',
border=2,
left_border=-200,
margin=0,
)
bui.widget(edit=self._menu_button, down_widget=self._columnwidget)
@ -160,6 +168,7 @@ class PartyWindow(bui.Window):
self._text_field = txt = bui.textwidget(
parent=self._root_widget,
id=f'{self._idprefix}|messagetext',
editable=True,
size=(530, 40),
position=(44, 39),
@ -190,6 +199,7 @@ class PartyWindow(bui.Window):
btn = bui.buttonwidget(
parent=self._root_widget,
id=f'{self._idprefix}|send',
size=(50, 35),
label=bui.Lstr(resource=f'{self._r}.sendText'),
button_type='square',

View file

@ -4,7 +4,7 @@
from __future__ import annotations
import logging
# import logging
from typing import override, TYPE_CHECKING
import bascenev1 as bs
@ -34,6 +34,8 @@ class PlayWindow(bui.MainWindow):
import bacommon.cloud
ui = bui.app.ui_v1
# TEMP TESTING
if bool(False):
print('HELLO FROM TEST')
@ -59,7 +61,7 @@ class PlayWindow(bui.MainWindow):
self._playlist_select_context = playlist_select_context
uiscale = bui.app.ui_v1.uiscale
uiscale = ui.uiscale
width = 1300 if uiscale is bui.UIScale.SMALL else 1000
height = 1000 if uiscale is bui.UIScale.SMALL else 550
@ -127,6 +129,7 @@ class PlayWindow(bui.MainWindow):
else:
self._back_button = bui.buttonwidget(
parent=self._root_widget,
id=f'{self.main_window_id_prefix}|back',
position=(50, yoffs - 100),
size=(60, 60),
scale=1.1,
@ -158,7 +161,7 @@ class PlayWindow(bui.MainWindow):
scale=1.2 if uiscale is bui.UIScale.SMALL else 1.7,
res_scale=2.0,
maxwidth=250,
color=bui.app.ui_v1.heading_color,
color=ui.heading_color,
h_align='center',
v_align='center',
)
@ -203,6 +206,7 @@ class PlayWindow(bui.MainWindow):
if self._playlist_select_context is None:
self._coop_button = btn = bui.buttonwidget(
parent=self._root_widget,
id=f'{self.main_window_id_prefix}|coop',
position=(hoffs, v),
size=(
scl * button_width,
@ -303,6 +307,7 @@ class PlayWindow(bui.MainWindow):
self._teams_button = btn = bui.buttonwidget(
parent=self._root_widget,
id=f'{self.main_window_id_prefix}|teams',
position=(hoffs, v),
size=(
scl * button_width,
@ -424,6 +429,7 @@ class PlayWindow(bui.MainWindow):
hoffs += scl * button_width + button_spacing
self._free_for_all_button = btn = bui.buttonwidget(
parent=self._root_widget,
id=f'{self.main_window_id_prefix}|ffa',
position=(hoffs, v),
size=(scl * button_width, scl * button_height),
extra_touch_border_scale=0.1,
@ -555,8 +561,6 @@ class PlayWindow(bui.MainWindow):
),
)
self._restore_state()
@override
def get_main_window_state(self) -> bui.MainWindowState:
# Support recreating our window for back/refresh purposes.
@ -565,17 +569,18 @@ class PlayWindow(bui.MainWindow):
# Pull any values out of self here; if we do it in the lambda
# we'll keep our window alive inadvertantly.
playlist_select_context = self._playlist_select_context
return bui.BasicMainWindowState(
create_call=lambda transition, origin_widget: cls(
transition=transition,
origin_widget=origin_widget,
playlist_select_context=playlist_select_context,
)
),
)
@override
def on_main_window_close(self) -> None:
self._save_state()
def main_window_should_preserve_selection(self) -> bool:
return True
@staticmethod
def _preload_modules() -> None:
@ -602,19 +607,15 @@ class PlayWindow(bui.MainWindow):
return
self.main_window_replace(
CoopBrowserWindow(origin_widget=self._coop_button)
lambda: CoopBrowserWindow(origin_widget=self._coop_button)
)
def _team_tourney(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.playlist.browser import PlaylistBrowserWindow
# no-op if we're not currently in control.
if not self.main_window_has_control():
return
self.main_window_replace(
PlaylistBrowserWindow(
lambda: PlaylistBrowserWindow(
origin_widget=self._teams_button,
sessiontype=bs.DualTeamSession,
playlist_select_context=self._playlist_select_context,
@ -625,12 +626,8 @@ class PlayWindow(bui.MainWindow):
# pylint: disable=cyclic-import
from bauiv1lib.playlist.browser import PlaylistBrowserWindow
# no-op if we're not currently in control.
if not self.main_window_has_control():
return
self.main_window_replace(
PlaylistBrowserWindow(
lambda: PlaylistBrowserWindow(
origin_widget=self._free_for_all_button,
sessiontype=bs.FreeForAllSession,
playlist_select_context=self._playlist_select_context,
@ -755,43 +752,3 @@ class PlayWindow(bui.MainWindow):
color=eye_color,
mesh_transparent=self._eyes_mesh,
)
def _save_state(self) -> None:
try:
sel = self._root_widget.get_selected_child()
if sel == self._teams_button:
sel_name = 'Team Games'
elif self._coop_button is not None and sel == self._coop_button:
sel_name = 'Co-op Games'
elif sel == self._free_for_all_button:
sel_name = 'Free-for-All Games'
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
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))
if sel_name == 'Team Games':
sel = self._teams_button
elif sel_name == 'Co-op Games' and self._coop_button is not None:
sel = self._coop_button
elif sel_name == 'Free-for-All Games':
sel = self._free_for_all_button
elif sel_name == 'Back' and self._back_button is not None:
sel = self._back_button
else:
sel = (
self._coop_button
if self._coop_button is not None
else self._teams_button
)
bui.containerwidget(edit=self._root_widget, selected_child=sel)
except Exception:
logging.exception('Error restoring state for %s.', self)

View file

@ -202,6 +202,10 @@ class PlaylistAddGameWindow(bui.MainWindow):
)
)
@override
def main_window_should_preserve_selection(self) -> bool:
return False
def _on_game_types_loaded(
self, gametypes: list[type[bs.GameActivity]]
) -> None:
@ -288,7 +292,7 @@ class PlaylistAddGameWindow(bui.MainWindow):
return
self.main_window_replace(
StoreBrowserWindow(
lambda: StoreBrowserWindow(
show_tab=StoreBrowserWindow.TabID.MINIGAMES,
origin_widget=self._get_more_games_button,
minimal_toolbars=True,

View file

@ -10,6 +10,7 @@ import logging
from typing import override, TYPE_CHECKING
import bascenev1 as bs
from bauiv1lib.utils import scroll_fade_bottom, scroll_fade_top
import bauiv1 as bui
if TYPE_CHECKING:
@ -27,7 +28,7 @@ class PlaylistBrowserWindow(bui.MainWindow):
playlist_select_context: PlaylistSelectContext | None = None,
):
# pylint: disable=cyclic-import
# pylint: disable=too-many-locals
# pylint: disable=too-many-statements
from bauiv1lib.playlist import PlaylistTypeVars
# Store state for when we exit the next game.
@ -87,6 +88,11 @@ class PlaylistBrowserWindow(bui.MainWindow):
self._scroll_height = target_height - 31
scroll_bottom = yoffs - 60 - self._scroll_height
# Go with full-screen scrollable area in small ui.
if uiscale is bui.UIScale.SMALL:
self._scroll_height += 35
scroll_bottom -= 2
super().__init__(
root_widget=bui.containerwidget(
size=(self._width, self._height),
@ -112,6 +118,7 @@ class PlaylistBrowserWindow(bui.MainWindow):
else:
self._back_button = bui.buttonwidget(
parent=self._root_widget,
id=f'{self.main_window_id_prefix}|back',
position=(59, yoffs - 45),
size=(60, 54),
scale=1.0,
@ -124,21 +131,6 @@ class PlaylistBrowserWindow(bui.MainWindow):
edit=self._root_widget, cancel_button=self._back_button
)
self._title_text = bui.textwidget(
parent=self._root_widget,
position=(
self._width * 0.5,
yoffs - (45 if uiscale is bui.UIScale.SMALL else 20),
),
size=(0, 0),
text=self._pvars.window_title_name,
scale=(0.8 if uiscale is bui.UIScale.SMALL else 1.3),
res_scale=1.5,
color=bui.app.ui_v1.heading_color,
h_align='center',
v_align='center',
)
self._scrollwidget = bui.scrollwidget(
parent=self._root_widget,
highlight=False,
@ -155,43 +147,44 @@ class PlaylistBrowserWindow(bui.MainWindow):
self._config_name_full = self._pvars.config_name + ' Playlists'
self._last_config = None
# Add some blotches so our contents fades out as it approaches
# the bottom toolbar.
if uiscale is bui.UIScale.SMALL and playlist_select_context is None:
blotchwidth = 500.0
blotchheight = 200.0
bimg = bui.imagewidget(
parent=self._root_widget,
texture=bui.gettexture('uiAtlas'),
mesh_transparent=bui.getmesh('windowBGBlotch'),
position=(
self._width * 0.5
- self._scroll_width * 0.5
+ 60.0
- blotchwidth * 0.5,
scroll_bottom - blotchheight * 0.5,
),
size=(blotchwidth, blotchheight),
color=(0.4, 0.37, 0.49),
# color=(1, 0, 0),
# With full-screen scrolling, fade content as it approaches
# toolbars.
if uiscale is bui.UIScale.SMALL and bool(True):
scroll_fade_top(
self._root_widget,
self._width * 0.5 - self._scroll_width * 0.5,
scroll_bottom,
self._scroll_width,
self._scroll_height,
)
bui.widget(edit=bimg, depth_range=(0.9, 1.0))
bimg = bui.imagewidget(
parent=self._root_widget,
texture=bui.gettexture('uiAtlas'),
mesh_transparent=bui.getmesh('windowBGBlotch'),
position=(
self._width * 0.5
+ self._scroll_width * 0.5
- 60.0
- blotchwidth * 0.5,
scroll_bottom - blotchheight * 0.5,
),
size=(blotchwidth, blotchheight),
color=(0.4, 0.37, 0.49),
# color=(1, 0, 0),
)
bui.widget(edit=bimg, depth_range=(0.9, 1.0))
if playlist_select_context is None:
scroll_fade_bottom(
self._root_widget,
self._width * 0.5 - self._scroll_width * 0.5,
scroll_bottom,
self._scroll_width,
self._scroll_height,
)
self._title_text = bui.textwidget(
parent=self._root_widget,
position=(
self._width * 0.5,
yoffs - (45 if uiscale is bui.UIScale.SMALL else 20),
),
size=(0, 0),
text=self._pvars.window_title_name,
scale=(0.8 if uiscale is bui.UIScale.SMALL else 1.3),
res_scale=1.5,
color=bui.app.ui_v1.heading_color,
h_align='center',
v_align='center',
)
# By default, start with the scroll-widget selected.
bui.containerwidget(
edit=self._root_widget, selected_child=self._scrollwidget
)
# Update now and once per second (this should do our initial
# refresh).
@ -223,8 +216,8 @@ class PlaylistBrowserWindow(bui.MainWindow):
)
@override
def on_main_window_close(self) -> None:
self._save_state()
def main_window_should_preserve_selection(self) -> bool:
return True
def _ensure_standard_playlists_exist(self) -> None:
plus = bui.app.plus
@ -396,7 +389,6 @@ class PlaylistBrowserWindow(bui.MainWindow):
if not self._root_widget:
return
if self._subcontainer is not None:
self._save_state()
self._subcontainer.delete()
# Make sure config exists.
@ -438,6 +430,12 @@ class PlaylistBrowserWindow(bui.MainWindow):
+ 90
+ extra_bottom_buffer
)
# For fullscreen scrollable, account for toolbar.
uiscale = bui.app.ui_v1.uiscale
if uiscale is bui.UIScale.SMALL:
self._sub_height += 35
assert self._sub_width is not None
assert self._sub_height is not None
self._subcontainer = bui.containerwidget(
@ -459,11 +457,16 @@ class PlaylistBrowserWindow(bui.MainWindow):
40 if uiscale is bui.UIScale.SMALL and screensize[0] < 1400 else 0
)
# For fullscreen scrollable, account for toolbar.
yoffs = 0
if uiscale is bui.UIScale.SMALL:
yoffs -= 35
assert bui.app.classic is not None
bui.textwidget(
parent=self._subcontainer,
text=bui.Lstr(resource='playlistsText'),
position=(40 + xoffs, self._sub_height - 26),
position=(40 + xoffs, self._sub_height + yoffs - 26),
size=(0, 0),
scale=1.0,
maxwidth=400,
@ -494,6 +497,7 @@ class PlaylistBrowserWindow(bui.MainWindow):
+ 8
+ h_offs,
self._sub_height
+ yoffs
- 47
- (y + 1) * (button_height + 2 * button_buffer_v),
)
@ -505,6 +509,11 @@ class PlaylistBrowserWindow(bui.MainWindow):
label='',
position=pos,
)
# We handle reselecting these manually below so don't
# provide them proper ids for auto-reselection. Let's
# suppress the warnings that usually happen in that
# case.
bui.widget(edit=btn, allow_preserve_selection=False)
if x == 0 and uiscale is bui.UIScale.SMALL:
bui.widget(
@ -527,7 +536,7 @@ class PlaylistBrowserWindow(bui.MainWindow):
# Top row biases things up more to show header above it.
if y == 0:
bui.widget(
edit=btn, show_buffer_top=60, show_buffer_bottom=5
edit=btn, show_buffer_top=80, show_buffer_bottom=5
)
else:
bui.widget(
@ -710,6 +719,7 @@ class PlaylistBrowserWindow(bui.MainWindow):
break
self._customize_button = btn = bui.buttonwidget(
parent=self._subcontainer,
id=f'{self.main_window_id_prefix}|customize',
size=(100, 30),
position=(34 + h_offs_bottom, 50 + extra_bottom_buffer),
text_scale=0.6,
@ -720,7 +730,6 @@ class PlaylistBrowserWindow(bui.MainWindow):
autoselect=True,
)
bui.widget(edit=btn, show_buffer_top=22, show_buffer_bottom=60)
self._restore_state()
def on_play_options_window_run_game(self) -> None:
"""(internal)"""
@ -775,7 +784,6 @@ class PlaylistBrowserWindow(bui.MainWindow):
if not exists:
return
self._save_state()
PlayOptionsWindow(
sessiontype=self._sessiontype,
scale_origin=button.get_screen_space_center(),
@ -790,20 +798,14 @@ class PlaylistBrowserWindow(bui.MainWindow):
PlaylistCustomizeBrowserWindow,
)
# no-op if we're not in control.
if not self.main_window_has_control():
return
self.main_window_replace(
PlaylistCustomizeBrowserWindow(
lambda: PlaylistCustomizeBrowserWindow(
origin_widget=self._customize_button,
sessiontype=self._sessiontype,
)
)
def _on_back_press(self) -> None:
# pylint: disable=cyclic-import
# from bauiv1lib.play import PlayWindow
# no-op if we're not in control.
if not self.main_window_has_control():
@ -822,43 +824,3 @@ class PlaylistBrowserWindow(bui.MainWindow):
cfg.commit()
self.main_window_back()
def _save_state(self) -> None:
try:
sel = self._root_widget.get_selected_child()
if sel == self._back_button:
sel_name = 'Back'
elif sel == self._scrollwidget:
assert self._subcontainer is not None
subsel = self._subcontainer.get_selected_child()
if subsel == self._customize_button:
sel_name = 'Customize'
else:
sel_name = 'Scroll'
else:
raise RuntimeError('Unrecognized selected widget.')
assert bui.app.classic is not None
bui.app.ui_v1.window_states[type(self)] = 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))
if sel_name == 'Back':
sel = self._back_button
elif sel_name == 'Scroll':
sel = self._scrollwidget
elif sel_name == 'Customize':
sel = self._scrollwidget
bui.containerwidget(
edit=self._subcontainer,
selected_child=self._customize_button,
visible_child=self._customize_button,
)
else:
sel = self._scrollwidget
bui.containerwidget(edit=self._root_widget, selected_child=sel)
except Exception:
logging.exception('Error restoring state for %s.', self)

View file

@ -103,6 +103,7 @@ class PlaylistCustomizeBrowserWindow(bui.MainWindow):
else:
self._back_button = bui.buttonwidget(
parent=self._root_widget,
id=f'{self.main_window_id_prefix}|back',
position=(43, yoffs - 87),
size=(60, 60),
scale=0.77,
@ -134,11 +135,12 @@ class PlaylistCustomizeBrowserWindow(bui.MainWindow):
ymargin = 0.05
def _make_button(
i: int, label: bui.Lstr, call: Callable[[], None]
i: int, button_id: str, label: bui.Lstr, call: Callable[[], None]
) -> bui.Widget:
v = self._scroll_bottom + self._button_height * i
return bui.buttonwidget(
parent=self._root_widget,
id=button_id,
position=(
h + xmargin * self._button_width,
v + ymargin * self._button_height,
@ -158,6 +160,7 @@ class PlaylistCustomizeBrowserWindow(bui.MainWindow):
new_button = _make_button(
5,
f'{self.main_window_id_prefix}|new',
bui.Lstr(
resource='newText', fallback_resource=f'{self._r}.newText'
),
@ -165,6 +168,7 @@ class PlaylistCustomizeBrowserWindow(bui.MainWindow):
)
self._edit_button = _make_button(
4,
f'{self.main_window_id_prefix}|edit',
bui.Lstr(
resource='editText',
fallback_resource=f'{self._r}.editText',
@ -174,6 +178,7 @@ class PlaylistCustomizeBrowserWindow(bui.MainWindow):
duplicate_button = _make_button(
3,
f'{self.main_window_id_prefix}|duplicate',
bui.Lstr(
resource='duplicateText',
fallback_resource=f'{self._r}.duplicateText',
@ -183,6 +188,7 @@ class PlaylistCustomizeBrowserWindow(bui.MainWindow):
delete_button = _make_button(
2,
f'{self.main_window_id_prefix}|delete',
bui.Lstr(
resource='deleteText', fallback_resource=f'{self._r}.deleteText'
),
@ -190,11 +196,17 @@ class PlaylistCustomizeBrowserWindow(bui.MainWindow):
)
self._import_button = _make_button(
1, bui.Lstr(resource='importText'), self._import_playlist
1,
f'{self.main_window_id_prefix}|import',
bui.Lstr(resource='importText'),
self._import_playlist,
)
share_button = _make_button(
0, bui.Lstr(resource='shareText'), self._share_playlist
0,
f'{self.main_window_id_prefix}|share',
bui.Lstr(resource='shareText'),
self._share_playlist,
)
scrollwidget = bui.scrollwidget(
@ -284,6 +296,10 @@ class PlaylistCustomizeBrowserWindow(bui.MainWindow):
)
)
@override
def main_window_should_preserve_selection(self) -> bool:
return True
@override
def on_main_window_close(self) -> None:
if self._selected_playlist_name is not None:
@ -353,7 +369,15 @@ class PlaylistCustomizeBrowserWindow(bui.MainWindow):
on_activate_call=bui.Call(self._edit_button.activate),
selectable=True,
)
bui.widget(edit=txtw, show_buffer_top=50, show_buffer_bottom=50)
# We don't give these widgets ids because we handle
# re-selecting them ourself, but we need to suppress the
# warning this usually causes.
bui.widget(
edit=txtw,
show_buffer_top=50,
show_buffer_bottom=50,
allow_preserve_selection=False,
)
# Hitting up from top widget should jump to 'back'.
if index == 0:
@ -593,11 +617,11 @@ class PlaylistCustomizeBrowserWindow(bui.MainWindow):
subs=[('${LIST}', self._selected_playlist_name)],
),
self._do_delete_playlist,
450,
150,
width=450,
height=150,
)
def _get_playlist_display_name(self, playlist: str) -> bui.Lstr:
def _get_playlist_display_name(self, playlist: str | bui.Lstr) -> bui.Lstr:
if playlist == '__default__':
return self._pvars.default_list_name
return (

View file

@ -289,6 +289,10 @@ class PlaylistEditWindow(bui.MainWindow):
)
)
@override
def main_window_should_preserve_selection(self) -> bool:
return False
def _set_ui_selection(self, selection: str) -> None:
self._editcontroller.set_edit_ui_selection(selection)

View file

@ -92,8 +92,10 @@ class PlaylistEditController:
# and that's all they can do.
self._edit_ui_selection = 'add_button'
editwindow = PlaylistEditWindow(editcontroller=self)
from_window.main_window_replace(editwindow)
editwindow = from_window.main_window_replace(
lambda: PlaylistEditWindow(editcontroller=self)
)
assert editwindow is not None
# Once we've set our start window, store the back state. We'll
# skip back to there once we're fully done.
@ -151,17 +153,17 @@ class PlaylistEditController:
"""(internal)"""
from bauiv1lib.playlist.addgame import PlaylistAddGameWindow
# assert bui.app.classic is not None
# No op if we're not in control.
if not from_window.main_window_has_control():
return
addwindow = PlaylistAddGameWindow(editcontroller=self)
from_window.main_window_replace(addwindow)
addwindow = from_window.main_window_replace(
lambda: PlaylistAddGameWindow(editcontroller=self)
)
assert addwindow is not None
# Once we're there, store the back state. We'll use that to jump
# back to our current location once the edit is done.
# back out to our current location once the edit is done.
assert self._pre_game_add_state is None
self._pre_game_add_state = addwindow.main_window_back_state
@ -197,16 +199,18 @@ class PlaylistEditController:
assert self._sessiontype is not None
# Jump into an edit window.
editwindow = PlaylistEditGameWindow(
gametype,
self._sessiontype,
copy.deepcopy(settings),
completion_call=self._edit_game_done,
editwindow = from_window.main_window_replace(
lambda: PlaylistEditGameWindow(
gametype,
self._sessiontype,
copy.deepcopy(settings),
completion_call=self._edit_game_done,
)
)
from_window.main_window_replace(editwindow)
assert editwindow is not None
# Once we're there, store the back state. We'll use that to jump
# back to our current location once the edit is done.
# back out to our current location once the edit is done.
assert self._pre_game_edit_state is None
self._pre_game_edit_state = editwindow.main_window_back_state

View file

@ -541,6 +541,10 @@ class PlaylistEditGameWindow(bui.MainWindow):
)
)
@override
def main_window_should_preserve_selection(self) -> bool:
return False
def _get_localized_setting_name(self, name: str) -> bui.Lstr:
return bui.Lstr(translate=('settingNames', name))
@ -552,14 +556,14 @@ class PlaylistEditGameWindow(bui.MainWindow):
if not self.main_window_has_control():
return
self._config = self._getconfig()
config = self._config = self._getconfig()
# Replace ourself with the map-select UI.
self.main_window_replace(
PlaylistMapSelectWindow(
lambda: PlaylistMapSelectWindow(
self._gametype,
self._sessiontype,
self._config,
config,
self._edit_info,
self._completion_call,
)

View file

@ -148,6 +148,10 @@ class PlaylistMapSelectWindow(bui.MainWindow):
)
)
@override
def main_window_should_preserve_selection(self) -> bool:
return False
def _refresh(self, select_get_more_maps_button: bool = False) -> None:
# pylint: disable=too-many-statements
# pylint: disable=too-many-branches
@ -301,7 +305,7 @@ class PlaylistMapSelectWindow(bui.MainWindow):
self._selected_get_more_maps = True
self.main_window_replace(
StoreBrowserWindow(
lambda: StoreBrowserWindow(
show_tab=StoreBrowserWindow.TabID.MAPS,
origin_widget=self._get_more_maps_button,
minimal_toolbars=True,

View file

@ -40,6 +40,8 @@ class PlayOptionsWindow(PopupWindow):
from bauiv1lib.playlist import PlaylistTypeVars
from bauiv1lib.config import ConfigNumberEdit
ui = bui.app.ui_v1
self._r = 'gameListWindow'
self._delegate = delegate
self._pvars = PlaylistTypeVars(sessiontype)
@ -145,7 +147,7 @@ class PlayOptionsWindow(PopupWindow):
if show_shuffle_check_box:
self._height += 40
uiscale = bui.app.ui_v1.uiscale
uiscale = ui.uiscale
scale = (
1.69
if uiscale is bui.UIScale.SMALL
@ -184,6 +186,7 @@ class PlayOptionsWindow(PopupWindow):
on_activate_call=self._on_cancel_press,
autoselect=True,
)
bui.widget(edit=self._cancel_button, allow_preserve_selection=False)
h_offs_img = self._width * 0.5 - c_width_total * 0.5
v_offs_img = self._height - 118 - scl * 125.0 + 50
@ -247,6 +250,8 @@ class PlayOptionsWindow(PopupWindow):
mesh_transparent=mesh_transparent if owned else None,
mask_texture=mask_tex if owned else None,
)
bui.widget(edit=btn, allow_preserve_selection=False)
if row == 0 and col == 0:
bui.widget(edit=self._cancel_button, down_widget=btn)
if row == rows - 1:
@ -316,6 +321,11 @@ class PlayOptionsWindow(PopupWindow):
textcolor=(0.8, 0.8, 0.8),
label=bui.Lstr(resource='teamNamesColorText'),
)
bui.widget(
edit=self._custom_colors_names_button,
allow_preserve_selection=False,
)
assert bui.app.classic is not None
if REQUIRE_PRO and not bui.app.classic.accounts.have_pro():
bui.imagewidget(
@ -414,6 +424,7 @@ class PlayOptionsWindow(PopupWindow):
)
),
)
bui.widget(edit=self._ok_button, allow_preserve_selection=False)
bui.widget(
edit=self._ok_button, up_widget=self._show_tutorial_check_box
@ -505,8 +516,6 @@ class PlayOptionsWindow(PopupWindow):
# Head back to the gather window in playlist-select mode or
# start the game in regular mode.
if self._playlist_select_context is not None:
# from bauiv1lib.gather import GatherWindow
if self._sessiontype is bs.FreeForAllSession:
typename = 'ffa'
elif self._sessiontype is bs.DualTeamSession:

View file

@ -102,8 +102,8 @@ class PopupWindow:
on_cancel_call=self.on_popup_cancel,
darken_behind=darken_behind,
)
# complain if we outlive our root widget
bui.uicleanupcheck(self, self.root_widget)
# Complain if we outlive our root widget.
bui.app.ui_v1.add_ui_cleanup_check(self, self.root_widget)
def on_popup_cancel(self) -> None:
"""Called when the popup is canceled.
@ -253,6 +253,8 @@ class PopupMenuWindow(PopupWindow):
selectable=(not inactive),
glow_type='uniform',
)
bui.widget(edit=wdg, allow_preserve_selection=False)
if choice == self._current_choice:
bui.containerwidget(
edit=self._columnwidget,
@ -312,6 +314,7 @@ class PopupMenu:
position: tuple[float, float],
choices: Sequence[str],
*,
button_id: str | None = None,
current_choice: str | None = None,
on_value_change_call: Callable[[str], Any] | None = None,
opening_call: Callable[[], Any] | None = None,
@ -359,6 +362,7 @@ class PopupMenu:
self._button = bui.buttonwidget(
parent=self._parent,
id=button_id,
position=(self._position[0], self._position[1]),
autoselect=autoselect,
size=self._button_size,
@ -375,7 +379,7 @@ class PopupMenu:
self._window_widget: bui.Widget | None = None
# Complain if we outlive our button.
bui.uicleanupcheck(self, self._button)
bui.app.ui_v1.add_ui_cleanup_check(self, self._button)
def _make_popup(self) -> None:
if not self._button:

View file

@ -4,23 +4,24 @@
from __future__ import annotations
import logging
from typing import TYPE_CHECKING, override
import bauiv1 as bui
import bascenev1 as bs
if TYPE_CHECKING:
from typing import Any
from typing import Any, ClassVar
class ProfileBrowserWindow(bui.MainWindow):
"""Window for browsing player profiles."""
# Keep track of this at the class level to share between instances.
selected_profile: ClassVar[str | None] = None
def __init__(
self,
transition: str | None = 'in_right',
# in_main_menu: bool = True,
selected_profile: str | None = None,
origin_widget: bui.Widget | None = None,
minimal_toolbar: bool = False,
@ -80,6 +81,7 @@ class ProfileBrowserWindow(bui.MainWindow):
else:
self._back_button = btn = bui.buttonwidget(
parent=self._root_widget,
id=f'{self.main_window_id_prefix}|back',
position=(40 + x_inset, self._height - 59),
size=(120, 60),
scale=0.8,
@ -122,6 +124,7 @@ class ProfileBrowserWindow(bui.MainWindow):
v -= 70.0 * scl
self._new_button = bui.buttonwidget(
parent=self._root_widget,
id=f'{self.main_window_id_prefix}|new',
position=(h, v),
size=(80, 66.0 * scl),
on_activate_call=self._new_profile,
@ -135,6 +138,7 @@ class ProfileBrowserWindow(bui.MainWindow):
v -= 70.0 * scl
self._edit_button = bui.buttonwidget(
parent=self._root_widget,
id=f'{self.main_window_id_prefix}|edit',
position=(h, v),
size=(80, 66.0 * scl),
on_activate_call=self._edit_profile,
@ -148,6 +152,7 @@ class ProfileBrowserWindow(bui.MainWindow):
v -= 70.0 * scl
self._delete_button = bui.buttonwidget(
parent=self._root_widget,
id=f'{self.main_window_id_prefix}|delete',
position=(h, v),
size=(80, 66.0 * scl),
on_activate_call=self._delete_profile,
@ -194,10 +199,10 @@ class ProfileBrowserWindow(bui.MainWindow):
)
v -= 255
self._profiles: dict[str, dict[str, Any]] | None = None
self._selected_profile = selected_profile
if selected_profile is not None:
type(self).selected_profile = selected_profile
self._profile_widgets: list[bui.Widget] = []
self._refresh()
self._restore_state()
@override
def get_main_window_state(self) -> bui.MainWindowState:
@ -215,8 +220,19 @@ class ProfileBrowserWindow(bui.MainWindow):
)
@override
def on_main_window_close(self) -> None:
self._save_state()
def main_window_should_preserve_selection(self) -> bool:
return True
# @override
# def main_window_do_save_shared_state(self, state: dict) -> None:
# state['selected_profile'] = self._selected_profile
# @override
# def main_window_do_restore_shared_state(self, state: dict) -> None:
# pval = state.get('selected_profile')
# if isinstance(pval, str | None):
# print('RESTORING', pval)
# self._selected_profile = pval
def _new_profile(self) -> None:
# pylint: disable=cyclic-import
@ -263,19 +279,21 @@ class ProfileBrowserWindow(bui.MainWindow):
bui.getsound('error').play()
return
self.main_window_replace(EditProfileWindow(existing_profile=None))
self.main_window_replace(
lambda: EditProfileWindow(existing_profile=None)
)
def _delete_profile(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib import confirm
if self._selected_profile is None:
if self.selected_profile is None:
bui.getsound('error').play()
bui.screenmessage(
bui.Lstr(resource='nothingIsSelectedErrorText'), color=(1, 0, 0)
)
return
if self._selected_profile == '__account__':
if self.selected_profile == '__account__':
bui.getsound('error').play()
bui.screenmessage(
bui.Lstr(resource=f'{self._r}.cantDeleteAccountProfileText'),
@ -285,18 +303,21 @@ class ProfileBrowserWindow(bui.MainWindow):
confirm.ConfirmWindow(
bui.Lstr(
resource=f'{self._r}.deleteConfirmText',
subs=[('${PROFILE}', self._selected_profile)],
subs=[('${PROFILE}', self.selected_profile)],
),
self._do_delete_profile,
350,
width=350,
)
def _do_delete_profile(self) -> None:
plus = bui.app.plus
assert plus is not None
# Go back to default selection.
type(self).selected_profile = None
plus.add_v1_account_transaction(
{'type': 'REMOVE_PLAYER_PROFILE', 'name': self._selected_profile}
{'type': 'REMOVE_PLAYER_PROFILE', 'name': self.selected_profile}
)
plus.run_v1_account_transactions()
bui.getsound('shieldDown').play()
@ -315,18 +336,20 @@ class ProfileBrowserWindow(bui.MainWindow):
if not self.main_window_has_control():
return
if self._selected_profile is None:
if self.selected_profile is None:
bui.getsound('error').play()
bui.screenmessage(
bui.Lstr(resource='nothingIsSelectedErrorText'), color=(1, 0, 0)
)
return
self.main_window_replace(EditProfileWindow(self._selected_profile))
self.main_window_replace(
lambda: EditProfileWindow(self.selected_profile)
)
def _select(self, name: str, index: int) -> None:
del index # Unused.
self._selected_profile = name
type(self).selected_profile = name
def _refresh(self) -> None:
# pylint: disable=too-many-locals
@ -340,7 +363,7 @@ class ProfileBrowserWindow(bui.MainWindow):
plus = bui.app.plus
assert plus is not None
old_selection = self._selected_profile
old_selection = self.selected_profile
# Delete old.
while self._profile_widgets:
@ -390,6 +413,7 @@ class ProfileBrowserWindow(bui.MainWindow):
assert isinstance(tval, str)
txtw = bui.textwidget(
parent=self._subcontainer,
id=f'{self.main_window_id_prefix}|profile{index}',
position=(5, y_val),
size=((self._width - 210) / scl, 28),
text=bui.Lstr(value=f' {tval}'),
@ -403,6 +427,9 @@ class ProfileBrowserWindow(bui.MainWindow):
on_activate_call=bui.Call(self._edit_button.activate),
selectable=True,
)
# We handle reselection of these manually; no need for ids.
bui.widget(edit=txtw, allow_preserve_selection=False)
character = bui.imagewidget(
parent=self._subcontainer,
position=(0, y_val),
@ -416,16 +443,16 @@ class ProfileBrowserWindow(bui.MainWindow):
)
if index == 0:
bui.widget(edit=txtw, up_widget=self._back_button)
if self._selected_profile is None:
self._selected_profile = p_name
if self.selected_profile is None:
type(self).selected_profile = p_name
bui.widget(edit=txtw, show_buffer_top=40, show_buffer_bottom=40)
self._profile_widgets.append(txtw)
self._profile_widgets.append(character)
# Select/show this one if it was previously selected
# Select/show this one if it was previously selected.
# (but defer till after this loop since our height is
# still changing).
if p_name == old_selection:
if p_name == old_selection or widget_to_select is None:
widget_to_select = txtw
index += 1
@ -447,46 +474,3 @@ class ProfileBrowserWindow(bui.MainWindow):
session = bs.get_foreground_host_session()
if session is not None:
session.handlemessage(PlayerProfilesChangedMessage())
def _save_state(self) -> None:
try:
sel = self._root_widget.get_selected_child()
if sel == self._new_button:
sel_name = 'New'
elif sel == self._edit_button:
sel_name = 'Edit'
elif sel == self._delete_button:
sel_name = 'Delete'
elif sel == self._scrollwidget:
sel_name = 'Scroll'
else:
sel_name = 'Back'
assert bui.app.classic is not None
bui.app.ui_v1.window_states[type(self)] = 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))
if sel_name == 'Scroll':
sel = self._scrollwidget
elif sel_name == 'New':
sel = self._new_button
elif sel_name == 'Delete':
sel = self._delete_button
elif sel_name == 'Edit':
sel = self._edit_button
elif sel_name == 'Back':
sel = self._back_button
else:
# By default we select our scroll widget if we have profiles;
# otherwise our new widget.
if not self._profile_widgets:
sel = self._new_button
else:
sel = self._scrollwidget
bui.containerwidget(edit=self._root_widget, selected_child=sel)
except Exception:
logging.exception('Error restoring state for %s.', self)

View file

@ -22,20 +22,13 @@ class EditProfileWindow(
def reload_window(self) -> None:
"""Transitions out and recreates ourself."""
# no-op if we're not in control.
if not self.main_window_has_control():
return
# Replace ourself with ourself, but keep the same back location.
assert self.main_window_back_state is not None
self.main_window_replace(
EditProfileWindow(self.getname()),
lambda: EditProfileWindow(self.getname()),
back_state=self.main_window_back_state,
)
# def __del__(self) -> None:
# print(f'~EditProfileWindow({id(self)})')
def __init__(
self,
existing_profile: str | None,
@ -535,6 +528,11 @@ class EditProfileWindow(
)
)
@override
def main_window_should_preserve_selection(self) -> bool:
# Not bothering with this for now.
return False
def assign_random_name(self) -> None:
"""Assigning a random name to the player."""
names = bs.get_random_names()
@ -652,11 +650,8 @@ class EditProfileWindow(
"""User wants to get more icons."""
from bauiv1lib.store.browser import StoreBrowserWindow
if not self.main_window_has_control():
return
self.main_window_replace(
StoreBrowserWindow(
lambda: StoreBrowserWindow(
minimal_toolbars=True,
show_tab=StoreBrowserWindow.TabID.ICONS,
)
@ -680,11 +675,8 @@ class EditProfileWindow(
def on_character_picker_get_more_press(self) -> None:
from bauiv1lib.store.browser import StoreBrowserWindow
if not self.main_window_has_control():
return
self.main_window_replace(
StoreBrowserWindow(
lambda: StoreBrowserWindow(
minimal_toolbars=True,
show_tab=StoreBrowserWindow.TabID.CHARACTERS,
)
@ -832,6 +824,9 @@ class EditProfileWindow(
def save(self, transition_out: bool = True) -> bool:
"""Save has been selected."""
# pylint: disable=cyclic-import
from bauiv1lib.profile.browser import ProfileBrowserWindow
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
@ -856,6 +851,9 @@ class EditProfileWindow(
bui.getsound('error').play()
return False
# Set the profile-browser to have this one selected by default.
ProfileBrowserWindow.selected_profile = new_name
if transition_out:
bui.getsound('gunCocking').play()

View file

@ -36,6 +36,8 @@ class PurchaseWindow(bui.Window):
)
if len(items) != 1:
raise ValueError('expected exactly 1 item')
self._idprefix = bui.app.ui_v1.new_id_prefix('purchase')
self._items = list(items)
self._width = 580
self._height = 520
@ -81,6 +83,7 @@ class PurchaseWindow(bui.Window):
instantiate_store_item_display(
items[0],
display,
idprefix=self._idprefix,
parent_widget=self._root_widget,
b_pos=(
self._width * 0.5

View file

@ -32,6 +32,7 @@ class ResourceTypeInfoWindow(PopupWindow):
self._width = 570
self._height = 400
self._get_tokens_button: bui.Widget | None = None
self._idprefix = bui.app.ui_v1.new_id_prefix('resourcetypeinfo')
bg_color = (0.5, 0.4, 0.6)
super().__init__(
size=(self._width, self._height),
@ -43,17 +44,15 @@ class ResourceTypeInfoWindow(PopupWindow):
)
self._cancel_button = bui.buttonwidget(
parent=self.root_widget,
id=f'{self._idprefix}|cancel',
position=(40, self._height - 40),
size=(50, 50),
scale=0.7,
# label='',
color=bg_color,
on_activate_call=self._on_cancel_press,
autoselect=True,
label=bui.charstr(bui.SpecialChar.CLOSE),
textcolor=(1, 1, 1),
# icon=bui.gettexture('crossOut'),
# iconscale=1.2,
)
yoffs = self._height - 145
@ -78,6 +77,7 @@ class ResourceTypeInfoWindow(PopupWindow):
if not bui.app.classic.gold_pass:
self._get_tokens_button = bui.buttonwidget(
parent=self.root_widget,
id=f'{self._idprefix}|gettokens',
position=(
self._width * 0.5 - bwidth * 0.5,
yoffs - 15.0 - bheight - max_rdesc_height,

View file

@ -72,6 +72,7 @@ class SendInfoWindow(bui.MainWindow):
if uiscale is not bui.UIScale.SMALL:
close_button = bui.buttonwidget(
parent=self._root_widget,
id=f'{self.main_window_id_prefix}|close',
position=(25, yoffs - 35),
size=(60, 60),
scale=0.7,
@ -114,6 +115,7 @@ class SendInfoWindow(bui.MainWindow):
self._text_field = bui.textwidget(
parent=self._root_widget,
id=f'{self.main_window_id_prefix}|desc',
position=(width * 0.5 + txoffs + 125, v),
size=(380, 46),
text='',
@ -134,6 +136,7 @@ class SendInfoWindow(bui.MainWindow):
b_width = 200
self._enter_button = btn2 = bui.buttonwidget(
parent=self._root_widget,
id=f'{self.main_window_id_prefix}|enter',
position=(width * 0.5 - b_width * 0.5, v),
size=(b_width, 60),
scale=1.0,
@ -170,12 +173,14 @@ class SendInfoWindow(bui.MainWindow):
)
)
@override
def main_window_should_preserve_selection(self) -> bool:
return True
def _activate_enter_button(self) -> None:
self._enter_button.activate()
def _do_enter(self) -> None:
# pylint: disable=cyclic-import
# from bauiv1lib.settings.advanced import AdvancedSettingsWindow
plus = bui.app.plus
assert plus is not None

View file

@ -1,6 +1,5 @@
# Released under the MIT License. See LICENSE for details.
#
# pylint: disable=too-many-lines
"""UI functionality for advanced settings."""
@ -12,6 +11,7 @@ from typing import TYPE_CHECKING, override
from bacommon.locale import LocaleResolved
from bauiv1lib.popup import PopupMenu
from bauiv1lib.utils import scroll_fade_bottom, scroll_fade_top
import bauiv1 as bui
if TYPE_CHECKING:
@ -73,6 +73,11 @@ class AdvancedSettingsWindow(bui.MainWindow):
self._scroll_height = target_height - 25
scroll_bottom = yoffs - 56 - self._scroll_height
# Go with full-screen scrollable area in small ui.
if uiscale is bui.UIScale.SMALL:
self._scroll_height += 27
scroll_bottom += 1
super().__init__(
root_widget=bui.containerwidget(
size=(self._width, self._height),
@ -100,6 +105,10 @@ class AdvancedSettingsWindow(bui.MainWindow):
self._sub_width = min(550, self._scroll_width * 0.95)
self._sub_height = 920.0
# For fullscreen scrollable, account for toolbar.
if uiscale is bui.UIScale.SMALL:
self._sub_height += 27
if self._show_always_use_internal_keyboard:
self._sub_height += 62
@ -132,6 +141,7 @@ class AdvancedSettingsWindow(bui.MainWindow):
else:
self._back_button = bui.buttonwidget(
parent=self._root_widget,
id=f'{self.main_window_id_prefix}|back',
position=(50, yoffs - 48),
size=(60, 60),
scale=0.8,
@ -144,6 +154,50 @@ class AdvancedSettingsWindow(bui.MainWindow):
edit=self._root_widget, cancel_button=self._back_button
)
self._scrollwidget = bui.scrollwidget(
parent=self._root_widget,
size=(self._scroll_width, self._scroll_height),
position=(
self._width * 0.5 - self._scroll_width * 0.5,
scroll_bottom,
),
simple_culling_v=20.0,
highlight=False,
center_small_content_horizontally=True,
selection_loops_to_parent=True,
border_opacity=0.4,
)
bui.containerwidget(
edit=self._root_widget, selected_child=self._scrollwidget
)
bui.widget(edit=self._scrollwidget, right_widget=self._scrollwidget)
self._subcontainer = bui.containerwidget(
parent=self._scrollwidget,
size=(self._sub_width, self._sub_height),
background=False,
selection_loops_to_parent=True,
)
# With full-screen scrolling, fade content as it approaches
# toolbars. (but only in the main menu where we're showing said
# toolbars).
if uiscale is bui.UIScale.SMALL and bui.in_main_menu():
scroll_fade_top(
self._root_widget,
self._width * 0.5 - self._scroll_width * 0.5,
scroll_bottom,
self._scroll_width,
self._scroll_height,
)
scroll_fade_bottom(
self._root_widget,
self._width * 0.5 - self._scroll_width * 0.5,
scroll_bottom,
self._scroll_width,
self._scroll_height,
)
self._title_text = bui.textwidget(
parent=self._root_widget,
position=(
@ -158,66 +212,6 @@ class AdvancedSettingsWindow(bui.MainWindow):
v_align='center',
)
self._scrollwidget = bui.scrollwidget(
parent=self._root_widget,
size=(self._scroll_width, self._scroll_height),
position=(
self._width * 0.5 - self._scroll_width * 0.5,
scroll_bottom,
),
simple_culling_v=20.0,
highlight=False,
center_small_content_horizontally=True,
selection_loops_to_parent=True,
border_opacity=0.4,
)
bui.widget(edit=self._scrollwidget, right_widget=self._scrollwidget)
self._subcontainer = bui.containerwidget(
parent=self._scrollwidget,
size=(self._sub_width, self._sub_height),
background=False,
selection_loops_to_parent=True,
)
# Add some blotches so our contents fades out as it approaches
# the bottom toolbar (but only in the main menu when there's
# something down there).
if uiscale is bui.UIScale.SMALL and bui.in_main_menu():
blotchwidth = 500.0
blotchheight = 200.0
bimg = bui.imagewidget(
parent=self._root_widget,
texture=bui.gettexture('uiAtlas'),
mesh_transparent=bui.getmesh('windowBGBlotch'),
position=(
self._width * 0.5
- self._scroll_width * 0.5
+ 60.0
- blotchwidth * 0.5,
scroll_bottom - blotchheight * 0.5,
),
size=(blotchwidth, blotchheight),
color=(0.4, 0.37, 0.49),
# color=(1, 0, 0),
)
bui.widget(edit=bimg, depth_range=(0.9, 1.0))
bimg = bui.imagewidget(
parent=self._root_widget,
texture=bui.gettexture('uiAtlas'),
mesh_transparent=bui.getmesh('windowBGBlotch'),
position=(
self._width * 0.5
+ self._scroll_width * 0.5
- 60.0
- blotchwidth * 0.5,
scroll_bottom - blotchheight * 0.5,
),
size=(blotchwidth, blotchheight),
color=(0.4, 0.37, 0.49),
# color=(1, 0, 0),
)
bui.widget(edit=bimg, depth_range=(0.9, 1.0))
self._rebuild()
# Rebuild periodically to pick up language changes/additions/etc.
@ -243,8 +237,8 @@ class AdvancedSettingsWindow(bui.MainWindow):
)
@override
def on_main_window_close(self) -> None:
self._save_state()
def main_window_should_preserve_selection(self) -> bool:
return True
@staticmethod
def _preload_modules() -> None:
@ -342,6 +336,11 @@ class AdvancedSettingsWindow(bui.MainWindow):
v = self._sub_height - 35
# For fullscreen scrollable, account for toolbar.
uiscale = bui.app.ui_v1.uiscale
if uiscale is bui.UIScale.SMALL:
v -= 27
v -= self._spacing * 1.2
# Update our existing back button and title.
@ -412,6 +411,7 @@ class AdvancedSettingsWindow(bui.MainWindow):
self._language_popup = PopupMenu(
parent=self._subcontainer,
button_id=f'{self.main_window_id_prefix}|language',
position=(210, v - 19),
width=250,
opening_call=bui.WeakCall(self._on_menu_open),
@ -440,6 +440,7 @@ class AdvancedSettingsWindow(bui.MainWindow):
),
current_choice=cur_lang,
)
if self._back_button is not None:
bui.widget(
edit=self._language_popup.get_button(),
@ -469,6 +470,7 @@ class AdvancedSettingsWindow(bui.MainWindow):
this_button_width = 410
self._translation_editor_button = bui.buttonwidget(
parent=self._subcontainer,
id=f'{self.main_window_id_prefix}|translationedit',
position=(self._sub_width / 2 - this_button_width / 2, v - 24),
size=(this_button_width, 60),
label=bui.Lstr(
@ -499,6 +501,7 @@ class AdvancedSettingsWindow(bui.MainWindow):
self._language_inform_checkbox = cbw = bui.checkboxwidget(
parent=self._subcontainer,
id=f'{self.main_window_id_prefix}|langinform',
position=(50, v - 50),
size=(self._sub_width - 100, 30),
autoselect=True,
@ -521,6 +524,7 @@ class AdvancedSettingsWindow(bui.MainWindow):
self._kick_idle_players_check_box = ConfigCheckBox(
parent=self._subcontainer,
check_box_id=f'{self.main_window_id_prefix}|kickidleplayers',
position=(50, v),
size=(self._sub_width - 100, 30),
configkey='Kick Idle Players',
@ -532,6 +536,7 @@ class AdvancedSettingsWindow(bui.MainWindow):
v -= 42
self._show_game_ping_check_box = ConfigCheckBox(
parent=self._subcontainer,
check_box_id=f'{self.main_window_id_prefix}|showping',
position=(50, v),
size=(self._sub_width - 100, 30),
configkey='Show Ping',
@ -543,6 +548,7 @@ class AdvancedSettingsWindow(bui.MainWindow):
v -= 42
self._show_demos_when_idle_check_box = ConfigCheckBox(
parent=self._subcontainer,
check_box_id=f'{self.main_window_id_prefix}|showdemoswhenidle',
position=(50, v),
size=(self._sub_width - 100, 30),
configkey='Show Demos When Idle',
@ -554,6 +560,9 @@ class AdvancedSettingsWindow(bui.MainWindow):
v -= 42
self._show_deprecated_login_types_check_box = ConfigCheckBox(
parent=self._subcontainer,
check_box_id=(
f'{self.main_window_id_prefix}|showdeprecatedlogintypes'
),
position=(50, v),
size=(self._sub_width - 100, 30),
configkey='Show Deprecated Login Types',
@ -567,6 +576,7 @@ class AdvancedSettingsWindow(bui.MainWindow):
v -= 42
self._disable_camera_shake_check_box = ConfigCheckBox(
parent=self._subcontainer,
check_box_id=f'{self.main_window_id_prefix}|disablecamerashake',
position=(50, v),
size=(self._sub_width - 100, 30),
configkey='Disable Camera Shake',
@ -580,6 +590,7 @@ class AdvancedSettingsWindow(bui.MainWindow):
v -= 42
self._disable_gyro_check_box = ConfigCheckBox(
parent=self._subcontainer,
check_box_id=f'{self.main_window_id_prefix}|disablegyro',
position=(50, v),
size=(self._sub_width - 100, 30),
configkey='Disable Camera Gyro',
@ -595,17 +606,16 @@ class AdvancedSettingsWindow(bui.MainWindow):
v -= 42
self._use_insecure_connections_check_box = ConfigCheckBox(
parent=self._subcontainer,
check_box_id=(
f'{self.main_window_id_prefix}|useinsecureconnections'
),
position=(50, v),
size=(self._sub_width - 100, 30),
configkey='Use Insecure Connections',
autoselect=True,
# displayname='USE INSECURE CONNECTIONS',
displayname=bui.Lstr(
resource=(f'{self._r}.insecureConnectionsText')
),
# displayname=bui.Lstr(
# resource=f'{self._r}.alwaysUseInternalKeyboardText'
# ),
scale=1.0,
maxwidth=430,
)
@ -613,10 +623,6 @@ class AdvancedSettingsWindow(bui.MainWindow):
parent=self._subcontainer,
position=(90, v - 20),
size=(0, 0),
# text=(
# 'not recommended, but may allow online play\n'
# 'from restricted countries or networks'
# ),
text=bui.Lstr(
resource=(f'{self._r}.insecureConnectionsDescriptionText')
),
@ -636,6 +642,9 @@ class AdvancedSettingsWindow(bui.MainWindow):
v -= 42
self._always_use_internal_keyboard_check_box = ConfigCheckBox(
parent=self._subcontainer,
check_box_id=(
f'{self.main_window_id_prefix}|alwaysuseinternalkb'
),
position=(50, v),
size=(self._sub_width - 100, 30),
configkey='Always Use Internal Keyboard',
@ -671,6 +680,7 @@ class AdvancedSettingsWindow(bui.MainWindow):
this_button_width = 410
self._modding_guide_button = bui.buttonwidget(
parent=self._subcontainer,
id=f'{self.main_window_id_prefix}|moddingguide',
position=(self._sub_width / 2 - this_button_width / 2, v - 10),
size=(this_button_width, 60),
autoselect=True,
@ -685,6 +695,7 @@ class AdvancedSettingsWindow(bui.MainWindow):
self._dev_tools_button = bui.buttonwidget(
parent=self._subcontainer,
id=f'{self.main_window_id_prefix}|devtools',
position=(self._sub_width / 2 - this_button_width / 2, v - 10),
size=(this_button_width, 60),
autoselect=True,
@ -723,6 +734,7 @@ class AdvancedSettingsWindow(bui.MainWindow):
self._show_user_mods_button = bui.buttonwidget(
parent=self._subcontainer,
id=f'{self.main_window_id_prefix}|showusermods',
position=(self._sub_width / 2 - this_button_width / 2, v - 10),
size=(this_button_width, 60),
autoselect=True,
@ -735,6 +747,7 @@ class AdvancedSettingsWindow(bui.MainWindow):
self._plugins_button = bui.buttonwidget(
parent=self._subcontainer,
id=f'{self.main_window_id_prefix}|plugins',
position=(self._sub_width / 2 - this_button_width / 2, v - 10),
size=(this_button_width, 60),
autoselect=True,
@ -750,6 +763,7 @@ class AdvancedSettingsWindow(bui.MainWindow):
v -= self._extra_button_spacing
self._vr_test_button = bui.buttonwidget(
parent=self._subcontainer,
id=f'{self.main_window_id_prefix}|vrtest',
position=(self._sub_width / 2 - this_button_width / 2, v - 14),
size=(this_button_width, 60),
autoselect=True,
@ -765,6 +779,7 @@ class AdvancedSettingsWindow(bui.MainWindow):
v -= self._extra_button_spacing
self._net_test_button = bui.buttonwidget(
parent=self._subcontainer,
id=f'{self.main_window_id_prefix}|nettest',
position=(self._sub_width / 2 - this_button_width / 2, v - 14),
size=(this_button_width, 60),
autoselect=True,
@ -778,6 +793,7 @@ class AdvancedSettingsWindow(bui.MainWindow):
v -= 70
self._benchmarks_button = bui.buttonwidget(
parent=self._subcontainer,
id=f'{self.main_window_id_prefix}|benchmarks',
position=(self._sub_width / 2 - this_button_width / 2, v - 14),
size=(this_button_width, 60),
autoselect=True,
@ -789,6 +805,7 @@ class AdvancedSettingsWindow(bui.MainWindow):
v -= 100
self._send_info_button = bui.buttonwidget(
parent=self._subcontainer,
id=f'{self.main_window_id_prefix}|sendinfo',
position=(self._sub_width / 2 - this_button_width / 2, v - 14),
size=(this_button_width, 60),
autoselect=True,
@ -798,7 +815,7 @@ class AdvancedSettingsWindow(bui.MainWindow):
)
for child in self._subcontainer.get_children():
bui.widget(edit=child, show_buffer_bottom=60, show_buffer_top=20)
bui.widget(edit=child, show_buffer_bottom=80, show_buffer_top=80)
pbtn = bui.get_special_widget('squad_button')
bui.widget(edit=self._scrollwidget, right_widget=pbtn)
@ -808,8 +825,6 @@ class AdvancedSettingsWindow(bui.MainWindow):
left_widget=bui.get_special_widget('back_button'),
)
self._restore_state()
def _show_restart_needed(self, value: Any) -> None:
del value # Unused.
bui.screenmessage(
@ -827,20 +842,14 @@ class AdvancedSettingsWindow(bui.MainWindow):
def _on_vr_test_press(self) -> None:
from bauiv1lib.settings.vrtesting import VRTestingWindow
# no-op if we're not in control.
if not self.main_window_has_control():
return
self.main_window_replace(VRTestingWindow(transition='in_right'))
self.main_window_replace(lambda: VRTestingWindow(transition='in_right'))
def _on_net_test_press(self) -> None:
from bauiv1lib.settings.nettesting import NetTestingWindow
# no-op if we're not in control.
if not self.main_window_has_control():
return
self.main_window_replace(NetTestingWindow(transition='in_right'))
self.main_window_replace(
lambda: NetTestingWindow(transition='in_right')
)
def _on_friend_promo_code_press(self) -> None:
from bauiv1lib import appinvite
@ -857,192 +866,32 @@ class AdvancedSettingsWindow(bui.MainWindow):
def _on_plugins_button_press(self) -> None:
from bauiv1lib.settings.plugins import PluginWindow
# no-op if we're not in control.
if not self.main_window_has_control():
return
self.main_window_replace(
PluginWindow(origin_widget=self._plugins_button)
lambda: PluginWindow(origin_widget=self._plugins_button)
)
def _on_dev_tools_button_press(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.settings.devtools import DevToolsWindow
# no-op if we're not in control.
if not self.main_window_has_control():
return
self.main_window_replace(
DevToolsWindow(origin_widget=self._dev_tools_button)
lambda: DevToolsWindow(origin_widget=self._dev_tools_button)
)
def _on_send_info_press(self) -> None:
from bauiv1lib.sendinfo import SendInfoWindow
# no-op if we're not in control.
if not self.main_window_has_control():
return
self.main_window_replace(
SendInfoWindow(origin_widget=self._send_info_button)
lambda: SendInfoWindow(origin_widget=self._send_info_button)
)
def _on_benchmark_press(self) -> None:
from bauiv1lib.settings.benchmarks import BenchmarksAndStressTestsWindow
# no-op if we're not in control.
if not self.main_window_has_control():
return
self.main_window_replace(
BenchmarksAndStressTestsWindow(transition='in_right')
lambda: BenchmarksAndStressTestsWindow(transition='in_right')
)
def _save_state(self) -> None:
# pylint: disable=too-many-branches
# pylint: disable=too-many-statements
try:
sel = self._root_widget.get_selected_child()
if sel == self._scrollwidget:
sel = self._subcontainer.get_selected_child()
if sel == self._vr_test_button:
sel_name = 'VRTest'
elif sel == self._net_test_button:
sel_name = 'NetTest'
elif sel == self._send_info_button:
sel_name = 'SendInfo'
elif sel == self._benchmarks_button:
sel_name = 'Benchmarks'
elif sel == self._kick_idle_players_check_box.widget:
sel_name = 'KickIdlePlayers'
elif sel == self._show_demos_when_idle_check_box.widget:
sel_name = 'ShowDemosWhenIdle'
elif sel == self._show_deprecated_login_types_check_box.widget:
sel_name = 'ShowDeprecatedLoginTypes'
elif sel == self._show_game_ping_check_box.widget:
sel_name = 'ShowPing'
elif sel == self._disable_camera_shake_check_box.widget:
sel_name = 'DisableCameraShake'
elif (
self._always_use_internal_keyboard_check_box is not None
and sel
== self._always_use_internal_keyboard_check_box.widget
):
sel_name = 'AlwaysUseInternalKeyboard'
elif (
self._use_insecure_connections_check_box is not None
and sel == self._use_insecure_connections_check_box.widget
):
sel_name = 'UseInsecureConnections'
elif (
self._disable_gyro_check_box is not None
and sel == self._disable_gyro_check_box.widget
):
sel_name = 'DisableGyro'
elif (
self._language_popup is not None
and sel == self._language_popup.get_button()
):
sel_name = 'Languages'
elif sel == self._translation_editor_button:
sel_name = 'TranslationEditor'
elif sel == self._show_user_mods_button:
sel_name = 'ShowUserMods'
elif sel == self._plugins_button:
sel_name = 'Plugins'
elif sel == self._dev_tools_button:
sel_name = 'DevTools'
elif sel == self._modding_guide_button:
sel_name = 'ModdingGuide'
elif sel == self._language_inform_checkbox:
sel_name = 'LangInform'
else:
raise ValueError(f'unrecognized selection \'{sel}\'')
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:
# pylint: disable=too-many-branches
# pylint: disable=too-many-statements
try:
assert bui.app.classic is not None
sel_name = bui.app.ui_v1.window_states.get(type(self), {}).get(
'sel_name'
)
if sel_name == 'Back':
sel = self._back_button
else:
bui.containerwidget(
edit=self._root_widget, selected_child=self._scrollwidget
)
if sel_name == 'VRTest':
sel = self._vr_test_button
elif sel_name == 'NetTest':
sel = self._net_test_button
elif sel_name == 'SendInfo':
sel = self._send_info_button
elif sel_name == 'Benchmarks':
sel = self._benchmarks_button
elif sel_name == 'KickIdlePlayers':
sel = self._kick_idle_players_check_box.widget
elif sel_name == 'ShowDemosWhenIdle':
sel = self._show_demos_when_idle_check_box.widget
elif sel_name == 'ShowDeprecatedLoginTypes':
sel = self._show_deprecated_login_types_check_box.widget
elif sel_name == 'ShowPing':
sel = self._show_game_ping_check_box.widget
elif sel_name == 'DisableCameraShake':
sel = self._disable_camera_shake_check_box.widget
elif (
sel_name == 'AlwaysUseInternalKeyboard'
and self._always_use_internal_keyboard_check_box is not None
):
sel = self._always_use_internal_keyboard_check_box.widget
elif (
sel_name == 'UseInsecureConnections'
and self._use_insecure_connections_check_box is not None
):
sel = self._use_insecure_connections_check_box.widget
elif (
sel_name == 'DisableGyro'
and self._disable_gyro_check_box is not None
):
sel = self._disable_gyro_check_box.widget
elif (
sel_name == 'Languages' and self._language_popup is not None
):
sel = self._language_popup.get_button()
elif sel_name == 'TranslationEditor':
sel = self._translation_editor_button
elif sel_name == 'ShowUserMods':
sel = self._show_user_mods_button
elif sel_name == 'Plugins':
sel = self._plugins_button
elif sel_name == 'DevTools':
sel = self._dev_tools_button
elif sel_name == 'ModdingGuide':
sel = self._modding_guide_button
elif sel_name == 'LangInform':
sel = self._language_inform_checkbox
else:
sel = None
if sel is not None:
bui.containerwidget(
edit=self._subcontainer,
selected_child=sel,
visible_child=sel,
)
except Exception:
logging.exception('Error restoring state for %s.', self)
def _on_menu_open(self) -> None:
self._menu_open = True
@ -1062,9 +911,8 @@ class AdvancedSettingsWindow(bui.MainWindow):
cfg.apply_and_commit()
self._save_state()
self.main_window_save_shared_state()
# bui.app.lang.setlanguage(None if choice == 'Auto' else choice)
bui.apptimer(0.1, bui.WeakCall(self._rebuild))
def _completed_langs_cb(self, results: dict[str, Any] | None) -> None:

View file

@ -5,7 +5,6 @@
from __future__ import annotations
from typing import TYPE_CHECKING, override
import logging
import bauiv1 as bui
@ -61,10 +60,6 @@ class AllSettingsWindow(bui.MainWindow):
# 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),
@ -90,6 +85,7 @@ class AllSettingsWindow(bui.MainWindow):
else:
self._back_button = btn = bui.buttonwidget(
parent=self._root_widget,
id=f'{self.main_window_id_prefix}|back',
autoselect=True,
position=(50, yoffs - 80.0),
size=(70, 70),
@ -134,18 +130,20 @@ class AllSettingsWindow(bui.MainWindow):
y = height * 0.5 - bheight * 0.5 + ynudge
def _button(
*,
widgetid: str,
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,
id=widgetid,
autoselect=True,
position=(x, y),
size=(bwidth, bheight),
@ -178,6 +176,7 @@ class AllSettingsWindow(bui.MainWindow):
return btn
self._controllers_button = _button(
widgetid=f'{self.main_window_id_prefix}|controllers',
position=(x, y),
label=bui.Lstr(resource=f'{self._r}.controllersText'),
call=self._do_controllers,
@ -188,6 +187,7 @@ class AllSettingsWindow(bui.MainWindow):
x += bwidth + margin
self._graphics_button = _button(
widgetid=f'{self.main_window_id_prefix}|graphics',
position=(x, y),
label=bui.Lstr(resource=f'{self._r}.graphicsText'),
call=self._do_graphics,
@ -198,6 +198,7 @@ class AllSettingsWindow(bui.MainWindow):
x += bwidth + margin
self._audio_button = _button(
widgetid=f'{self.main_window_id_prefix}|audio',
position=(x, y),
label=bui.Lstr(resource=f'{self._r}.audioText'),
call=self._do_audio,
@ -208,6 +209,7 @@ class AllSettingsWindow(bui.MainWindow):
x += bwidth + margin
self._advanced_button = _button(
widgetid=f'{self.main_window_id_prefix}|advanced',
position=(x, y),
label=bui.Lstr(resource=f'{self._r}.advancedText'),
call=self._do_advanced,
@ -217,6 +219,11 @@ class AllSettingsWindow(bui.MainWindow):
imgoffs=(0, 5.0),
)
# Select controllers by default.
bui.containerwidget(
edit=self._root_widget, selected_child=self._controllers_button
)
# Hmm; we're now wide enough that being limited to pressing up
# might be ok.
if bool(True):
@ -231,8 +238,6 @@ class AllSettingsWindow(bui.MainWindow):
right_widget=bui.get_special_widget('squad_button'),
)
self._restore_state()
@override
def get_main_window_state(self) -> bui.MainWindowState:
# Support recreating our window for back/refresh purposes.
@ -244,8 +249,8 @@ class AllSettingsWindow(bui.MainWindow):
)
@override
def on_main_window_close(self) -> None:
self._save_state()
def main_window_should_preserve_selection(self) -> bool:
return True
@staticmethod
def _preload_modules() -> None:
@ -260,90 +265,32 @@ class AllSettingsWindow(bui.MainWindow):
# 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)
lambda: 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)
lambda: 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)
lambda: 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)
lambda: 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)

Some files were not shown because too many files have changed in this diff Show more