Initial commit

This commit is contained in:
vortex 2024-02-26 00:17:10 +05:30
parent bc49523c99
commit 44d606cce7
1929 changed files with 612166 additions and 0 deletions

View file

@ -0,0 +1 @@
# Released under the MIT License. See LICENSE for details.

View file

@ -0,0 +1,865 @@
# Released under the MIT License. See LICENSE for details.
#
"""UI functionality for advanced settings."""
from __future__ import annotations
from typing import TYPE_CHECKING
import ba
import ba.internal
from bastd.ui import popup as popup_ui
if TYPE_CHECKING:
from typing import Any
class AdvancedSettingsWindow(ba.Window):
"""Window for editing advanced game settings."""
def __init__(
self,
transition: str = 'in_right',
origin_widget: ba.Widget | None = None,
):
# pylint: disable=too-many-statements
from ba.internal import master_server_get
import threading
# Preload some modules we use in a background thread so we won't
# have a visual hitch when the user taps them.
threading.Thread(target=self._preload_modules).start()
app = ba.app
# If they provided an origin-widget, scale up from that.
scale_origin: tuple[float, float] | None
if origin_widget is not None:
self._transition_out = 'out_scale'
scale_origin = origin_widget.get_screen_space_center()
transition = 'in_scale'
else:
self._transition_out = 'out_right'
scale_origin = None
uiscale = ba.app.ui.uiscale
self._width = 870.0 if uiscale is ba.UIScale.SMALL else 670.0
x_inset = 100 if uiscale is ba.UIScale.SMALL else 0
self._height = (
390.0
if uiscale is ba.UIScale.SMALL
else 450.0
if uiscale is ba.UIScale.MEDIUM
else 520.0
)
self._spacing = 32
self._menu_open = False
top_extra = 10 if uiscale is ba.UIScale.SMALL else 0
super().__init__(
root_widget=ba.containerwidget(
size=(self._width, self._height + top_extra),
transition=transition,
toolbar_visibility='menu_minimal',
scale_origin_stack_offset=scale_origin,
scale=(
2.06
if uiscale is ba.UIScale.SMALL
else 1.4
if uiscale is ba.UIScale.MEDIUM
else 1.0
),
stack_offset=(0, -25)
if uiscale is ba.UIScale.SMALL
else (0, 0),
)
)
self._prev_lang = ''
self._prev_lang_list: list[str] = []
self._complete_langs_list: list | None = None
self._complete_langs_error = False
self._language_popup: popup_ui.PopupMenu | None = None
# In vr-mode, the internal keyboard is currently the *only* option,
# so no need to show this.
self._show_always_use_internal_keyboard = (
not app.vr_mode and not app.iircade_mode
)
self._scroll_width = self._width - (100 + 2 * x_inset)
self._scroll_height = self._height - 115.0
self._sub_width = self._scroll_width * 0.95
self._sub_height = 724.0
if self._show_always_use_internal_keyboard:
self._sub_height += 62
self._show_disable_gyro = app.platform in {'ios', 'android'}
if self._show_disable_gyro:
self._sub_height += 42
self._do_vr_test_button = app.vr_mode
self._do_net_test_button = True
self._extra_button_spacing = self._spacing * 2.5
if self._do_vr_test_button:
self._sub_height += self._extra_button_spacing
if self._do_net_test_button:
self._sub_height += self._extra_button_spacing
self._sub_height += self._spacing * 2.0 # plugins
self._r = 'settingsWindowAdvanced'
if app.ui.use_toolbars and uiscale is ba.UIScale.SMALL:
ba.containerwidget(
edit=self._root_widget, on_cancel_call=self._do_back
)
self._back_button = None
else:
self._back_button = ba.buttonwidget(
parent=self._root_widget,
position=(53 + x_inset, self._height - 60),
size=(140, 60),
scale=0.8,
autoselect=True,
label=ba.Lstr(resource='backText'),
button_type='back',
on_activate_call=self._do_back,
)
ba.containerwidget(
edit=self._root_widget, cancel_button=self._back_button
)
self._title_text = ba.textwidget(
parent=self._root_widget,
position=(0, self._height - 52),
size=(self._width, 25),
text=ba.Lstr(resource=f'{self._r}.titleText'),
color=app.ui.title_color,
h_align='center',
v_align='top',
)
if self._back_button is not None:
ba.buttonwidget(
edit=self._back_button,
button_type='backSmall',
size=(60, 60),
label=ba.charstr(ba.SpecialChar.BACK),
)
self._scrollwidget = ba.scrollwidget(
parent=self._root_widget,
position=(50 + x_inset, 50),
simple_culling_v=20.0,
highlight=False,
size=(self._scroll_width, self._scroll_height),
selection_loops_to_parent=True,
)
ba.widget(edit=self._scrollwidget, right_widget=self._scrollwidget)
self._subcontainer = ba.containerwidget(
parent=self._scrollwidget,
size=(self._sub_width, self._sub_height),
background=False,
selection_loops_to_parent=True,
)
self._rebuild()
# Rebuild periodically to pick up language changes/additions/etc.
self._rebuild_timer = ba.Timer(
1.0,
ba.WeakCall(self._rebuild),
repeat=True,
timetype=ba.TimeType.REAL,
)
# Fetch the list of completed languages.
master_server_get(
'bsLangGetCompleted',
{'b': app.build_number},
callback=ba.WeakCall(self._completed_langs_cb),
)
# noinspection PyUnresolvedReferences
@staticmethod
def _preload_modules() -> None:
"""Preload modules we use (called in bg thread)."""
from bastd.ui import config as _unused1
from ba import modutils as _unused2
from bastd.ui.settings import vrtesting as _unused3
from bastd.ui.settings import nettesting as _unused4
from bastd.ui import appinvite as _unused5
from bastd.ui import account as _unused6
from bastd.ui import promocode as _unused7
from bastd.ui import debug as _unused8
from bastd.ui.settings import plugins as _unused9
def _update_lang_status(self) -> None:
if self._complete_langs_list is not None:
up_to_date = ba.app.lang.language in self._complete_langs_list
ba.textwidget(
edit=self._lang_status_text,
text=''
if ba.app.lang.language == 'Test'
else ba.Lstr(
resource=f'{self._r}.translationNoUpdateNeededText'
)
if up_to_date
else ba.Lstr(resource=f'{self._r}.translationUpdateNeededText'),
color=(0.2, 1.0, 0.2, 0.8)
if up_to_date
else (1.0, 0.2, 0.2, 0.8),
)
else:
ba.textwidget(
edit=self._lang_status_text,
text=ba.Lstr(resource=f'{self._r}.translationFetchErrorText')
if self._complete_langs_error
else ba.Lstr(
resource=f'{self._r}.translationFetchingStatusText'
),
color=(1.0, 0.5, 0.2)
if self._complete_langs_error
else (0.7, 0.7, 0.7),
)
def _rebuild(self) -> None:
# pylint: disable=too-many-statements
# pylint: disable=too-many-branches
# pylint: disable=too-many-locals
from bastd.ui.config import ConfigCheckBox
from ba.modutils import show_user_scripts
available_languages = ba.app.lang.available_languages
# Don't rebuild if the menu is open or if our language and
# language-list hasn't changed.
# NOTE - although we now support widgets updating their own
# translations, we still change the label formatting on the language
# menu based on the language so still need this. ...however we could
# make this more limited to it only rebuilds that one menu instead
# of everything.
if self._menu_open or (
self._prev_lang == ba.app.config.get('Lang', None)
and self._prev_lang_list == available_languages
):
return
self._prev_lang = ba.app.config.get('Lang', None)
self._prev_lang_list = available_languages
# Clear out our sub-container.
children = self._subcontainer.get_children()
for child in children:
child.delete()
v = self._sub_height - 35
v -= self._spacing * 1.2
# Update our existing back button and title.
if self._back_button is not None:
ba.buttonwidget(
edit=self._back_button, label=ba.Lstr(resource='backText')
)
ba.buttonwidget(
edit=self._back_button, label=ba.charstr(ba.SpecialChar.BACK)
)
ba.textwidget(
edit=self._title_text, text=ba.Lstr(resource=f'{self._r}.titleText')
)
this_button_width = 410
self._promo_code_button = ba.buttonwidget(
parent=self._subcontainer,
position=(self._sub_width / 2 - this_button_width / 2, v - 14),
size=(this_button_width, 60),
autoselect=True,
label=ba.Lstr(resource=f'{self._r}.enterPromoCodeText'),
text_scale=1.0,
on_activate_call=self._on_promo_code_press,
)
if self._back_button is not None:
ba.widget(
edit=self._promo_code_button,
up_widget=self._back_button,
left_widget=self._back_button,
)
v -= self._extra_button_spacing * 0.8
ba.textwidget(
parent=self._subcontainer,
position=(200, v + 10),
size=(0, 0),
text=ba.Lstr(resource=f'{self._r}.languageText'),
maxwidth=150,
scale=0.95,
color=ba.app.ui.title_color,
h_align='right',
v_align='center',
)
languages = ba.app.lang.available_languages
cur_lang = ba.app.config.get('Lang', None)
if cur_lang is None:
cur_lang = 'Auto'
# We have a special dict of language names in that language
# so we don't have to go digging through each full language.
try:
import json
with open('ba_data/data/langdata.json', encoding='utf-8') as infile:
lang_names_translated = json.loads(infile.read())[
'lang_names_translated'
]
except Exception:
ba.print_exception('Error reading lang data.')
lang_names_translated = {}
langs_translated = {}
for lang in languages:
langs_translated[lang] = lang_names_translated.get(lang, lang)
langs_full = {}
for lang in languages:
lang_translated = ba.Lstr(translate=('languages', lang)).evaluate()
if langs_translated[lang] == lang_translated:
langs_full[lang] = lang_translated
else:
langs_full[lang] = (
langs_translated[lang] + ' (' + lang_translated + ')'
)
self._language_popup = popup_ui.PopupMenu(
parent=self._subcontainer,
position=(210, v - 19),
width=150,
opening_call=ba.WeakCall(self._on_menu_open),
closing_call=ba.WeakCall(self._on_menu_close),
autoselect=False,
on_value_change_call=ba.WeakCall(self._on_menu_choice),
choices=['Auto'] + languages,
button_size=(250, 60),
choices_display=(
[
ba.Lstr(
value=(
ba.Lstr(resource='autoText').evaluate()
+ ' ('
+ ba.Lstr(
translate=(
'languages',
ba.app.lang.default_language,
)
).evaluate()
+ ')'
)
)
]
+ [ba.Lstr(value=langs_full[l]) for l in languages]
),
current_choice=cur_lang,
)
v -= self._spacing * 1.8
ba.textwidget(
parent=self._subcontainer,
position=(self._sub_width * 0.5, v + 10),
size=(0, 0),
text=ba.Lstr(
resource=f'{self._r}.helpTranslateText',
subs=[('${APP_NAME}', ba.Lstr(resource='titleText'))],
),
maxwidth=self._sub_width * 0.9,
max_height=55,
flatness=1.0,
scale=0.65,
color=(0.4, 0.9, 0.4, 0.8),
h_align='center',
v_align='center',
)
v -= self._spacing * 1.9
this_button_width = 410
self._translation_editor_button = ba.buttonwidget(
parent=self._subcontainer,
position=(self._sub_width / 2 - this_button_width / 2, v - 24),
size=(this_button_width, 60),
label=ba.Lstr(
resource=f'{self._r}.translationEditorButtonText',
subs=[('${APP_NAME}', ba.Lstr(resource='titleText'))],
),
autoselect=True,
on_activate_call=ba.Call(
ba.open_url, 'https://legacy.ballistica.net/translate'
),
)
self._lang_status_text = ba.textwidget(
parent=self._subcontainer,
position=(self._sub_width * 0.5, v - 40),
size=(0, 0),
text='',
flatness=1.0,
scale=0.63,
h_align='center',
v_align='center',
maxwidth=400.0,
)
self._update_lang_status()
v -= 40
lang_inform = ba.internal.get_v1_account_misc_val('langInform', False)
self._language_inform_checkbox = cbw = ba.checkboxwidget(
parent=self._subcontainer,
position=(50, v - 50),
size=(self._sub_width - 100, 30),
autoselect=True,
maxwidth=430,
textcolor=(0.8, 0.8, 0.8),
value=lang_inform,
text=ba.Lstr(resource=f'{self._r}.translationInformMe'),
on_value_change_call=ba.WeakCall(self._on_lang_inform_value_change),
)
ba.widget(
edit=self._translation_editor_button,
down_widget=cbw,
up_widget=self._language_popup.get_button(),
)
v -= self._spacing * 3.0
self._kick_idle_players_check_box = ConfigCheckBox(
parent=self._subcontainer,
position=(50, v),
size=(self._sub_width - 100, 30),
configkey='Kick Idle Players',
displayname=ba.Lstr(resource=f'{self._r}.kickIdlePlayersText'),
scale=1.0,
maxwidth=430,
)
v -= 42
self._show_game_ping_check_box = ConfigCheckBox(
parent=self._subcontainer,
position=(50, v),
size=(self._sub_width - 100, 30),
configkey='Show Ping',
displayname=ba.Lstr(resource=f'{self._r}.showInGamePingText'),
scale=1.0,
maxwidth=430,
)
v -= 42
self._disable_camera_shake_check_box = ConfigCheckBox(
parent=self._subcontainer,
position=(50, v),
size=(self._sub_width - 100, 30),
configkey='Disable Camera Shake',
displayname=ba.Lstr(resource=f'{self._r}.disableCameraShakeText'),
scale=1.0,
maxwidth=430,
)
self._disable_gyro_check_box: ConfigCheckBox | None = None
if self._show_disable_gyro:
v -= 42
self._disable_gyro_check_box = ConfigCheckBox(
parent=self._subcontainer,
position=(50, v),
size=(self._sub_width - 100, 30),
configkey='Disable Camera Gyro',
displayname=ba.Lstr(
resource=f'{self._r}.disableCameraGyroscopeMotionText'
),
scale=1.0,
maxwidth=430,
)
self._always_use_internal_keyboard_check_box: ConfigCheckBox | None
if self._show_always_use_internal_keyboard:
v -= 42
self._always_use_internal_keyboard_check_box = ConfigCheckBox(
parent=self._subcontainer,
position=(50, v),
size=(self._sub_width - 100, 30),
configkey='Always Use Internal Keyboard',
autoselect=True,
displayname=ba.Lstr(
resource=f'{self._r}.alwaysUseInternalKeyboardText'
),
scale=1.0,
maxwidth=430,
)
ba.textwidget(
parent=self._subcontainer,
position=(90, v - 10),
size=(0, 0),
text=ba.Lstr(
resource=(
f'{self._r}.alwaysUseInternalKeyboardDescriptionText'
)
),
maxwidth=400,
flatness=1.0,
scale=0.65,
color=(0.4, 0.9, 0.4, 0.8),
h_align='left',
v_align='center',
)
v -= 20
else:
self._always_use_internal_keyboard_check_box = None
v -= self._spacing * 2.1
this_button_width = 410
self._modding_guide_button = ba.buttonwidget(
parent=self._subcontainer,
position=(self._sub_width / 2 - this_button_width / 2, v - 10),
size=(this_button_width, 60),
autoselect=True,
label=ba.Lstr(resource=f'{self._r}.moddingGuideText'),
text_scale=1.0,
on_activate_call=ba.Call(
ba.open_url, 'https://ballistica.net/wiki/modding-guide'
),
)
if self._show_always_use_internal_keyboard:
assert self._always_use_internal_keyboard_check_box is not None
ba.widget(
edit=self._always_use_internal_keyboard_check_box.widget,
down_widget=self._modding_guide_button,
)
ba.widget(
edit=self._modding_guide_button,
up_widget=self._always_use_internal_keyboard_check_box.widget,
)
else:
ba.widget(
edit=self._modding_guide_button,
up_widget=self._kick_idle_players_check_box.widget,
)
ba.widget(
edit=self._kick_idle_players_check_box.widget,
down_widget=self._modding_guide_button,
)
v -= self._spacing * 2.0
self._show_user_mods_button = ba.buttonwidget(
parent=self._subcontainer,
position=(self._sub_width / 2 - this_button_width / 2, v - 10),
size=(this_button_width, 60),
autoselect=True,
label=ba.Lstr(resource=f'{self._r}.showUserModsText'),
text_scale=1.0,
on_activate_call=show_user_scripts,
)
v -= self._spacing * 2.0
self._plugins_button = ba.buttonwidget(
parent=self._subcontainer,
position=(self._sub_width / 2 - this_button_width / 2, v - 10),
size=(this_button_width, 60),
autoselect=True,
label=ba.Lstr(resource='pluginsText'),
text_scale=1.0,
on_activate_call=self._on_plugins_button_press,
)
v -= self._spacing * 0.6
self._vr_test_button: ba.Widget | None
if self._do_vr_test_button:
v -= self._extra_button_spacing
self._vr_test_button = ba.buttonwidget(
parent=self._subcontainer,
position=(self._sub_width / 2 - this_button_width / 2, v - 14),
size=(this_button_width, 60),
autoselect=True,
label=ba.Lstr(resource=f'{self._r}.vrTestingText'),
text_scale=1.0,
on_activate_call=self._on_vr_test_press,
)
else:
self._vr_test_button = None
self._net_test_button: ba.Widget | None
if self._do_net_test_button:
v -= self._extra_button_spacing
self._net_test_button = ba.buttonwidget(
parent=self._subcontainer,
position=(self._sub_width / 2 - this_button_width / 2, v - 14),
size=(this_button_width, 60),
autoselect=True,
label=ba.Lstr(resource=f'{self._r}.netTestingText'),
text_scale=1.0,
on_activate_call=self._on_net_test_press,
)
else:
self._net_test_button = None
v -= 70
self._benchmarks_button = ba.buttonwidget(
parent=self._subcontainer,
position=(self._sub_width / 2 - this_button_width / 2, v - 14),
size=(this_button_width, 60),
autoselect=True,
label=ba.Lstr(resource=f'{self._r}.benchmarksText'),
text_scale=1.0,
on_activate_call=self._on_benchmark_press,
)
for child in self._subcontainer.get_children():
ba.widget(edit=child, show_buffer_bottom=30, show_buffer_top=20)
if ba.app.ui.use_toolbars:
pbtn = ba.internal.get_special_widget('party_button')
ba.widget(edit=self._scrollwidget, right_widget=pbtn)
if self._back_button is None:
ba.widget(
edit=self._scrollwidget,
left_widget=ba.internal.get_special_widget('back_button'),
)
self._restore_state()
def _show_restart_needed(self, value: Any) -> None:
del value # Unused.
ba.screenmessage(
ba.Lstr(resource=f'{self._r}.mustRestartText'), color=(1, 1, 0)
)
def _on_lang_inform_value_change(self, val: bool) -> None:
ba.internal.add_transaction(
{'type': 'SET_MISC_VAL', 'name': 'langInform', 'value': val}
)
ba.internal.run_transactions()
def _on_vr_test_press(self) -> None:
from bastd.ui.settings.vrtesting import VRTestingWindow
self._save_state()
ba.containerwidget(edit=self._root_widget, transition='out_left')
ba.app.ui.set_main_menu_window(
VRTestingWindow(transition='in_right').get_root_widget()
)
def _on_net_test_press(self) -> None:
from bastd.ui.settings.nettesting import NetTestingWindow
# Net-testing requires a signed in v1 account.
if ba.internal.get_v1_account_state() != 'signed_in':
ba.screenmessage(
ba.Lstr(resource='notSignedInErrorText'), color=(1, 0, 0)
)
ba.playsound(ba.getsound('error'))
return
self._save_state()
ba.containerwidget(edit=self._root_widget, transition='out_left')
ba.app.ui.set_main_menu_window(
NetTestingWindow(transition='in_right').get_root_widget()
)
def _on_friend_promo_code_press(self) -> None:
from bastd.ui import appinvite
from bastd.ui import account
if ba.internal.get_v1_account_state() != 'signed_in':
account.show_sign_in_prompt()
return
appinvite.handle_app_invites_press()
def _on_plugins_button_press(self) -> None:
from bastd.ui.settings.plugins import PluginWindow
self._save_state()
ba.containerwidget(edit=self._root_widget, transition='out_left')
ba.app.ui.set_main_menu_window(
PluginWindow(origin_widget=self._plugins_button).get_root_widget()
)
def _on_promo_code_press(self) -> None:
from bastd.ui.promocode import PromoCodeWindow
from bastd.ui.account import show_sign_in_prompt
# We have to be logged in for promo-codes to work.
if ba.internal.get_v1_account_state() != 'signed_in':
show_sign_in_prompt()
return
self._save_state()
ba.containerwidget(edit=self._root_widget, transition='out_left')
ba.app.ui.set_main_menu_window(
PromoCodeWindow(
origin_widget=self._promo_code_button
).get_root_widget()
)
def _on_benchmark_press(self) -> None:
from bastd.ui.debug import DebugWindow
self._save_state()
ba.containerwidget(edit=self._root_widget, transition='out_left')
ba.app.ui.set_main_menu_window(
DebugWindow(transition='in_right').get_root_widget()
)
def _save_state(self) -> None:
# pylint: disable=too-many-branches
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._promo_code_button:
sel_name = 'PromoCode'
elif sel == self._benchmarks_button:
sel_name = 'Benchmarks'
elif sel == self._kick_idle_players_check_box.widget:
sel_name = 'KickIdlePlayers'
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._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._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}\'')
ba.app.ui.window_states[type(self)] = {'sel_name': sel_name}
except Exception:
ba.print_exception(f'Error saving state for {self.__class__}')
def _restore_state(self) -> None:
# pylint: disable=too-many-branches
try:
sel_name = ba.app.ui.window_states.get(type(self), {}).get(
'sel_name'
)
if sel_name == 'Back':
sel = self._back_button
else:
ba.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 == 'PromoCode':
sel = self._promo_code_button
elif sel_name == 'Benchmarks':
sel = self._benchmarks_button
elif sel_name == 'KickIdlePlayers':
sel = self._kick_idle_players_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 == '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 == 'ModdingGuide':
sel = self._modding_guide_button
elif sel_name == 'LangInform':
sel = self._language_inform_checkbox
else:
sel = None
if sel is not None:
ba.containerwidget(
edit=self._subcontainer,
selected_child=sel,
visible_child=sel,
)
except Exception:
ba.print_exception(f'Error restoring state for {self.__class__}')
def _on_menu_open(self) -> None:
self._menu_open = True
def _on_menu_close(self) -> None:
self._menu_open = False
def _on_menu_choice(self, choice: str) -> None:
ba.app.lang.setlanguage(None if choice == 'Auto' else choice)
self._save_state()
ba.timer(0.1, ba.WeakCall(self._rebuild), timetype=ba.TimeType.REAL)
def _completed_langs_cb(self, results: dict[str, Any] | None) -> None:
if results is not None and results['langs'] is not None:
self._complete_langs_list = results['langs']
self._complete_langs_error = False
else:
self._complete_langs_list = None
self._complete_langs_error = True
ba.timer(
0.001,
ba.WeakCall(self._update_lang_status),
timetype=ba.TimeType.REAL,
)
def _do_back(self) -> None:
from bastd.ui.settings.allsettings import AllSettingsWindow
self._save_state()
ba.containerwidget(
edit=self._root_widget, transition=self._transition_out
)
ba.app.ui.set_main_menu_window(
AllSettingsWindow(transition='in_left').get_root_widget()
)

View file

@ -0,0 +1,331 @@
# Released under the MIT License. See LICENSE for details.
#
"""UI for top level settings categories."""
from __future__ import annotations
from typing import TYPE_CHECKING
import ba
import ba.internal
if TYPE_CHECKING:
pass
class AllSettingsWindow(ba.Window):
"""Window for selecting a settings category."""
def __init__(
self,
transition: str = 'in_right',
origin_widget: ba.Widget | None = None,
):
# pylint: disable=too-many-statements
# pylint: disable=too-many-locals
import threading
# Preload some modules we use in a background thread so we won't
# have a visual hitch when the user taps them.
threading.Thread(target=self._preload_modules).start()
ba.set_analytics_screen('Settings Window')
scale_origin: tuple[float, float] | None
if origin_widget is not None:
self._transition_out = 'out_scale'
scale_origin = origin_widget.get_screen_space_center()
transition = 'in_scale'
else:
self._transition_out = 'out_right'
scale_origin = None
uiscale = ba.app.ui.uiscale
width = 900 if uiscale is ba.UIScale.SMALL else 580
x_inset = 75 if uiscale is ba.UIScale.SMALL else 0
height = 435
self._r = 'settingsWindow'
top_extra = 20 if uiscale is ba.UIScale.SMALL else 0
uiscale = ba.app.ui.uiscale
super().__init__(
root_widget=ba.containerwidget(
size=(width, height + top_extra),
transition=transition,
toolbar_visibility='menu_minimal',
scale_origin_stack_offset=scale_origin,
scale=(
1.75
if uiscale is ba.UIScale.SMALL
else 1.35
if uiscale is ba.UIScale.MEDIUM
else 1.0
),
stack_offset=(0, -8) if uiscale is ba.UIScale.SMALL else (0, 0),
)
)
if ba.app.ui.use_toolbars and uiscale is ba.UIScale.SMALL:
self._back_button = None
ba.containerwidget(
edit=self._root_widget, on_cancel_call=self._do_back
)
else:
self._back_button = btn = ba.buttonwidget(
parent=self._root_widget,
autoselect=True,
position=(40 + x_inset, height - 55),
size=(130, 60),
scale=0.8,
text_scale=1.2,
label=ba.Lstr(resource='backText'),
button_type='back',
on_activate_call=self._do_back,
)
ba.containerwidget(edit=self._root_widget, cancel_button=btn)
ba.textwidget(
parent=self._root_widget,
position=(0, height - 44),
size=(width, 25),
text=ba.Lstr(resource=self._r + '.titleText'),
color=ba.app.ui.title_color,
h_align='center',
v_align='center',
maxwidth=130,
)
if self._back_button is not None:
ba.buttonwidget(
edit=self._back_button,
button_type='backSmall',
size=(60, 60),
label=ba.charstr(ba.SpecialChar.BACK),
)
v = height - 80
v -= 145
basew = 280 if uiscale is ba.UIScale.SMALL else 230
baseh = 170
x_offs = (
x_inset + (105 if uiscale is ba.UIScale.SMALL else 72) - basew
) # now unused
x_offs2 = x_offs + basew - 7
x_offs3 = x_offs + 2 * (basew - 7)
x_offs4 = x_offs2
x_offs5 = x_offs3
def _b_title(
x: float, y: float, button: ba.Widget, text: str | ba.Lstr
) -> None:
ba.textwidget(
parent=self._root_widget,
text=text,
position=(x + basew * 0.47, y + baseh * 0.22),
maxwidth=basew * 0.7,
size=(0, 0),
h_align='center',
v_align='center',
draw_controller=button,
color=(0.7, 0.9, 0.7, 1.0),
)
ctb = self._controllers_button = ba.buttonwidget(
parent=self._root_widget,
autoselect=True,
position=(x_offs2, v),
size=(basew, baseh),
button_type='square',
label='',
on_activate_call=self._do_controllers,
)
if ba.app.ui.use_toolbars and self._back_button is None:
bbtn = ba.internal.get_special_widget('back_button')
ba.widget(edit=ctb, left_widget=bbtn)
_b_title(
x_offs2, v, ctb, ba.Lstr(resource=self._r + '.controllersText')
)
imgw = imgh = 130
ba.imagewidget(
parent=self._root_widget,
position=(x_offs2 + basew * 0.49 - imgw * 0.5, v + 35),
size=(imgw, imgh),
texture=ba.gettexture('controllerIcon'),
draw_controller=ctb,
)
gfxb = self._graphics_button = ba.buttonwidget(
parent=self._root_widget,
autoselect=True,
position=(x_offs3, v),
size=(basew, baseh),
button_type='square',
label='',
on_activate_call=self._do_graphics,
)
if ba.app.ui.use_toolbars:
pbtn = ba.internal.get_special_widget('party_button')
ba.widget(edit=gfxb, up_widget=pbtn, right_widget=pbtn)
_b_title(x_offs3, v, gfxb, ba.Lstr(resource=self._r + '.graphicsText'))
imgw = imgh = 110
ba.imagewidget(
parent=self._root_widget,
position=(x_offs3 + basew * 0.49 - imgw * 0.5, v + 42),
size=(imgw, imgh),
texture=ba.gettexture('graphicsIcon'),
draw_controller=gfxb,
)
v -= baseh - 5
abtn = self._audio_button = ba.buttonwidget(
parent=self._root_widget,
autoselect=True,
position=(x_offs4, v),
size=(basew, baseh),
button_type='square',
label='',
on_activate_call=self._do_audio,
)
_b_title(x_offs4, v, abtn, ba.Lstr(resource=self._r + '.audioText'))
imgw = imgh = 120
ba.imagewidget(
parent=self._root_widget,
position=(x_offs4 + basew * 0.49 - imgw * 0.5 + 5, v + 35),
size=(imgw, imgh),
color=(1, 1, 0),
texture=ba.gettexture('audioIcon'),
draw_controller=abtn,
)
avb = self._advanced_button = ba.buttonwidget(
parent=self._root_widget,
autoselect=True,
position=(x_offs5, v),
size=(basew, baseh),
button_type='square',
label='',
on_activate_call=self._do_advanced,
)
_b_title(x_offs5, v, avb, ba.Lstr(resource=self._r + '.advancedText'))
imgw = imgh = 120
ba.imagewidget(
parent=self._root_widget,
position=(x_offs5 + basew * 0.49 - imgw * 0.5 + 5, v + 35),
size=(imgw, imgh),
color=(0.8, 0.95, 1),
texture=ba.gettexture('advancedIcon'),
draw_controller=avb,
)
self._restore_state()
# noinspection PyUnresolvedReferences
@staticmethod
def _preload_modules() -> None:
"""Preload modules we use (called in bg thread)."""
import bastd.ui.mainmenu as _unused1
import bastd.ui.settings.controls as _unused2
import bastd.ui.settings.graphics as _unused3
import bastd.ui.settings.audio as _unused4
import bastd.ui.settings.advanced as _unused5
def _do_back(self) -> None:
# pylint: disable=cyclic-import
from bastd.ui.mainmenu import MainMenuWindow
self._save_state()
ba.containerwidget(
edit=self._root_widget, transition=self._transition_out
)
ba.app.ui.set_main_menu_window(
MainMenuWindow(transition='in_left').get_root_widget()
)
def _do_controllers(self) -> None:
# pylint: disable=cyclic-import
from bastd.ui.settings.controls import ControlsSettingsWindow
self._save_state()
ba.containerwidget(edit=self._root_widget, transition='out_left')
ba.app.ui.set_main_menu_window(
ControlsSettingsWindow(
origin_widget=self._controllers_button
).get_root_widget()
)
def _do_graphics(self) -> None:
# pylint: disable=cyclic-import
from bastd.ui.settings.graphics import GraphicsSettingsWindow
self._save_state()
ba.containerwidget(edit=self._root_widget, transition='out_left')
ba.app.ui.set_main_menu_window(
GraphicsSettingsWindow(
origin_widget=self._graphics_button
).get_root_widget()
)
def _do_audio(self) -> None:
# pylint: disable=cyclic-import
from bastd.ui.settings.audio import AudioSettingsWindow
self._save_state()
ba.containerwidget(edit=self._root_widget, transition='out_left')
ba.app.ui.set_main_menu_window(
AudioSettingsWindow(
origin_widget=self._audio_button
).get_root_widget()
)
def _do_advanced(self) -> None:
# pylint: disable=cyclic-import
from bastd.ui.settings.advanced import AdvancedSettingsWindow
self._save_state()
ba.containerwidget(edit=self._root_widget, transition='out_left')
ba.app.ui.set_main_menu_window(
AdvancedSettingsWindow(
origin_widget=self._advanced_button
).get_root_widget()
)
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}\'')
ba.app.ui.window_states[type(self)] = {'sel_name': sel_name}
except Exception:
ba.print_exception(f'Error saving state for {self}.')
def _restore_state(self) -> None:
try:
sel_name = ba.app.ui.window_states.get(type(self), {}).get(
'sel_name'
)
sel: ba.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:
ba.containerwidget(edit=self._root_widget, selected_child=sel)
except Exception:
ba.print_exception(f'Error restoring state for {self}.')

View file

@ -0,0 +1,318 @@
# Released under the MIT License. See LICENSE for details.
#
"""Provides audio settings UI."""
from __future__ import annotations
from typing import TYPE_CHECKING
import ba
import ba.internal
if TYPE_CHECKING:
pass
class AudioSettingsWindow(ba.Window):
"""Window for editing audio settings."""
def __init__(
self,
transition: str = 'in_right',
origin_widget: ba.Widget | None = None,
):
# pylint: disable=too-many-statements
# pylint: disable=too-many-locals
# pylint: disable=cyclic-import
from bastd.ui.popup import PopupMenu
from bastd.ui.config import ConfigNumberEdit
music = ba.app.music
# If they provided an origin-widget, scale up from that.
scale_origin: tuple[float, float] | None
if origin_widget is not None:
self._transition_out = 'out_scale'
scale_origin = origin_widget.get_screen_space_center()
transition = 'in_scale'
else:
self._transition_out = 'out_right'
scale_origin = None
self._r = 'audioSettingsWindow'
spacing = 50.0
width = 460.0
height = 210.0
# Update: hard-coding head-relative audio to true now,
# so not showing options.
# show_vr_head_relative_audio = True if ba.app.vr_mode else False
show_vr_head_relative_audio = False
if show_vr_head_relative_audio:
height += 70
show_soundtracks = False
if music.have_music_player():
show_soundtracks = True
height += spacing * 2.0
uiscale = ba.app.ui.uiscale
base_scale = (
2.05
if uiscale is ba.UIScale.SMALL
else 1.6
if uiscale is ba.UIScale.MEDIUM
else 1.0
)
popup_menu_scale = base_scale * 1.2
super().__init__(
root_widget=ba.containerwidget(
size=(width, height),
transition=transition,
scale=base_scale,
scale_origin_stack_offset=scale_origin,
stack_offset=(0, -20)
if uiscale is ba.UIScale.SMALL
else (0, 0),
)
)
self._back_button = back_button = btn = ba.buttonwidget(
parent=self._root_widget,
position=(35, height - 55),
size=(120, 60),
scale=0.8,
text_scale=1.2,
label=ba.Lstr(resource='backText'),
button_type='back',
on_activate_call=self._back,
autoselect=True,
)
ba.containerwidget(edit=self._root_widget, cancel_button=btn)
v = height - 60
v -= spacing * 1.0
ba.textwidget(
parent=self._root_widget,
position=(width * 0.5, height - 32),
size=(0, 0),
text=ba.Lstr(resource=self._r + '.titleText'),
color=ba.app.ui.title_color,
maxwidth=180,
h_align='center',
v_align='center',
)
ba.buttonwidget(
edit=self._back_button,
button_type='backSmall',
size=(60, 60),
label=ba.charstr(ba.SpecialChar.BACK),
)
self._sound_volume_numedit = svne = ConfigNumberEdit(
parent=self._root_widget,
position=(40, v),
xoffset=10,
configkey='Sound Volume',
displayname=ba.Lstr(resource=self._r + '.soundVolumeText'),
minval=0.0,
maxval=1.0,
increment=0.1,
)
if ba.app.ui.use_toolbars:
ba.widget(
edit=svne.plusbutton,
right_widget=ba.internal.get_special_widget('party_button'),
)
v -= spacing
self._music_volume_numedit = ConfigNumberEdit(
parent=self._root_widget,
position=(40, v),
xoffset=10,
configkey='Music Volume',
displayname=ba.Lstr(resource=self._r + '.musicVolumeText'),
minval=0.0,
maxval=1.0,
increment=0.1,
callback=music.music_volume_changed,
changesound=False,
)
v -= 0.5 * spacing
self._vr_head_relative_audio_button: ba.Widget | None
if show_vr_head_relative_audio:
v -= 40
ba.textwidget(
parent=self._root_widget,
position=(40, v + 24),
size=(0, 0),
text=ba.Lstr(resource=self._r + '.headRelativeVRAudioText'),
color=(0.8, 0.8, 0.8),
maxwidth=230,
h_align='left',
v_align='center',
)
popup = PopupMenu(
parent=self._root_widget,
position=(290, v),
width=120,
button_size=(135, 50),
scale=popup_menu_scale,
choices=['Auto', 'On', 'Off'],
choices_display=[
ba.Lstr(resource='autoText'),
ba.Lstr(resource='onText'),
ba.Lstr(resource='offText'),
],
current_choice=ba.app.config.resolve('VR Head Relative Audio'),
on_value_change_call=self._set_vr_head_relative_audio,
)
self._vr_head_relative_audio_button = popup.get_button()
ba.textwidget(
parent=self._root_widget,
position=(width * 0.5, v - 11),
size=(0, 0),
text=ba.Lstr(resource=self._r + '.headRelativeVRAudioInfoText'),
scale=0.5,
color=(0.7, 0.8, 0.7),
maxwidth=400,
flatness=1.0,
h_align='center',
v_align='center',
)
v -= 30
else:
self._vr_head_relative_audio_button = None
self._soundtrack_button: ba.Widget | None
if show_soundtracks:
v -= 1.2 * spacing
self._soundtrack_button = ba.buttonwidget(
parent=self._root_widget,
position=((width - 310) / 2, v),
size=(310, 50),
autoselect=True,
label=ba.Lstr(resource=self._r + '.soundtrackButtonText'),
on_activate_call=self._do_soundtracks,
)
v -= spacing * 0.5
ba.textwidget(
parent=self._root_widget,
position=(0, v),
size=(width, 20),
text=ba.Lstr(resource=self._r + '.soundtrackDescriptionText'),
flatness=1.0,
h_align='center',
scale=0.5,
color=(0.7, 0.8, 0.7, 1.0),
maxwidth=400,
)
else:
self._soundtrack_button = None
# Tweak a few navigation bits.
try:
ba.widget(edit=back_button, down_widget=svne.minusbutton)
except Exception:
ba.print_exception('Error wiring AudioSettingsWindow.')
self._restore_state()
def _set_vr_head_relative_audio(self, val: str) -> None:
cfg = ba.app.config
cfg['VR Head Relative Audio'] = val
cfg.apply_and_commit()
def _do_soundtracks(self) -> None:
# pylint: disable=cyclic-import
from bastd.ui.soundtrack import browser as stb
# We require disk access for soundtracks;
# if we don't have it, request it.
if not ba.internal.have_permission(ba.Permission.STORAGE):
ba.playsound(ba.getsound('ding'))
ba.screenmessage(
ba.Lstr(resource='storagePermissionAccessText'),
color=(0.5, 1, 0.5),
)
ba.timer(
1.0,
ba.Call(ba.internal.request_permission, ba.Permission.STORAGE),
timetype=ba.TimeType.REAL,
)
return
self._save_state()
ba.containerwidget(edit=self._root_widget, transition='out_left')
ba.app.ui.set_main_menu_window(
stb.SoundtrackBrowserWindow(
origin_widget=self._soundtrack_button
).get_root_widget()
)
def _back(self) -> None:
# pylint: disable=cyclic-import
from bastd.ui.settings import allsettings
self._save_state()
ba.containerwidget(
edit=self._root_widget, transition=self._transition_out
)
ba.app.ui.set_main_menu_window(
allsettings.AllSettingsWindow(
transition='in_left'
).get_root_widget()
)
def _save_state(self) -> None:
try:
sel = self._root_widget.get_selected_child()
if sel == self._sound_volume_numedit.minusbutton:
sel_name = 'SoundMinus'
elif sel == self._sound_volume_numedit.plusbutton:
sel_name = 'SoundPlus'
elif sel == self._music_volume_numedit.minusbutton:
sel_name = 'MusicMinus'
elif sel == self._music_volume_numedit.plusbutton:
sel_name = 'MusicPlus'
elif sel == self._soundtrack_button:
sel_name = 'Soundtrack'
elif sel == self._back_button:
sel_name = 'Back'
elif sel == self._vr_head_relative_audio_button:
sel_name = 'VRHeadRelative'
else:
raise ValueError(f'unrecognized selection \'{sel}\'')
ba.app.ui.window_states[type(self)] = sel_name
except Exception:
ba.print_exception(f'Error saving state for {self.__class__}.')
def _restore_state(self) -> None:
try:
sel_name = ba.app.ui.window_states.get(type(self))
sel: ba.Widget | None
if sel_name == 'SoundMinus':
sel = self._sound_volume_numedit.minusbutton
elif sel_name == 'SoundPlus':
sel = self._sound_volume_numedit.plusbutton
elif sel_name == 'MusicMinus':
sel = self._music_volume_numedit.minusbutton
elif sel_name == 'MusicPlus':
sel = self._music_volume_numedit.plusbutton
elif sel_name == 'VRHeadRelative':
sel = self._vr_head_relative_audio_button
elif sel_name == 'Soundtrack':
sel = self._soundtrack_button
elif sel_name == 'Back':
sel = self._back_button
else:
sel = self._back_button
if sel:
ba.containerwidget(edit=self._root_widget, selected_child=sel)
except Exception:
ba.print_exception(f'Error restoring state for {self.__class__}.')

View file

@ -0,0 +1,476 @@
# Released under the MIT License. See LICENSE for details.
#
"""Provides a top level control settings window."""
from __future__ import annotations
from typing import TYPE_CHECKING
import ba
import ba.internal
if TYPE_CHECKING:
pass
class ControlsSettingsWindow(ba.Window):
"""Top level control settings window."""
def __init__(
self,
transition: str = 'in_right',
origin_widget: ba.Widget | None = None,
):
# FIXME: should tidy up here.
# pylint: disable=too-many-statements
# pylint: disable=too-many-branches
# pylint: disable=too-many-locals
# pylint: disable=cyclic-import
from bastd.ui import popup as popup_ui
self._have_selected_child = False
scale_origin: tuple[float, float] | None
# If they provided an origin-widget, scale up from that.
if origin_widget is not None:
self._transition_out = 'out_scale'
scale_origin = origin_widget.get_screen_space_center()
transition = 'in_scale'
else:
self._transition_out = 'out_right'
scale_origin = None
self._r = 'configControllersWindow'
app = ba.app
# is_fire_tv = ba.internal.is_running_on_fire_tv()
spacing = 50.0
button_width = 350.0
width = 460.0
height = 130.0
space_height = spacing * 0.3
# FIXME: should create vis settings in platform for these,
# not hard code them here.
show_gamepads = False
platform = app.platform
subplatform = app.subplatform
non_vr_windows = platform == 'windows' and (
subplatform != 'oculus' or not app.vr_mode
)
if platform in ('linux', 'android', 'mac') or non_vr_windows:
show_gamepads = True
height += spacing
show_touch = False
if ba.internal.have_touchscreen_input():
show_touch = True
height += spacing
show_space_1 = False
if show_gamepads or show_touch:
show_space_1 = True
height += space_height
show_keyboard = False
if (
ba.internal.getinputdevice('Keyboard', '#1', doraise=False)
is not None
):
show_keyboard = True
height += spacing
show_keyboard_p2 = False if app.vr_mode else show_keyboard
if show_keyboard_p2:
height += spacing
show_space_2 = False
if show_keyboard:
show_space_2 = True
height += space_height
if bool(True):
show_remote = True
height += spacing
else:
show_remote = False
# On windows (outside of oculus/vr), show an option to disable xinput.
show_xinput_toggle = False
if platform == 'windows' and not app.vr_mode:
show_xinput_toggle = True
# On mac builds, show an option to switch between generic and
# made-for-iOS/Mac systems
# (we can run into problems where devices register as one of each
# type otherwise)..
show_mac_controller_subsystem = False
if platform == 'mac' and ba.internal.is_xcode_build():
show_mac_controller_subsystem = True
if show_mac_controller_subsystem:
height += spacing * 1.5
if show_xinput_toggle:
height += spacing
uiscale = ba.app.ui.uiscale
smallscale = 1.7 if show_keyboard else 2.2
super().__init__(
root_widget=ba.containerwidget(
size=(width, height),
transition=transition,
scale_origin_stack_offset=scale_origin,
stack_offset=(
(0, -10) if uiscale is ba.UIScale.SMALL else (0, 0)
),
scale=(
smallscale
if uiscale is ba.UIScale.SMALL
else 1.5
if uiscale is ba.UIScale.MEDIUM
else 1.0
),
)
)
self._back_button = btn = ba.buttonwidget(
parent=self._root_widget,
position=(35, height - 60),
size=(140, 65),
scale=0.8,
text_scale=1.2,
autoselect=True,
label=ba.Lstr(resource='backText'),
button_type='back',
on_activate_call=self._back,
)
ba.containerwidget(edit=self._root_widget, cancel_button=btn)
# We need these vars to exist even if the buttons don't.
self._gamepads_button: ba.Widget | None = None
self._touch_button: ba.Widget | None = None
self._keyboard_button: ba.Widget | None = None
self._keyboard_2_button: ba.Widget | None = None
self._idevices_button: ba.Widget | None = None
ba.textwidget(
parent=self._root_widget,
position=(0, height - 49),
size=(width, 25),
text=ba.Lstr(resource=self._r + '.titleText'),
color=ba.app.ui.title_color,
h_align='center',
v_align='top',
)
ba.buttonwidget(
edit=btn,
button_type='backSmall',
size=(60, 60),
label=ba.charstr(ba.SpecialChar.BACK),
)
v = height - 75
v -= spacing
if show_touch:
self._touch_button = btn = ba.buttonwidget(
parent=self._root_widget,
position=((width - button_width) / 2, v),
size=(button_width, 43),
autoselect=True,
label=ba.Lstr(resource=self._r + '.configureTouchText'),
on_activate_call=self._do_touchscreen,
)
if ba.app.ui.use_toolbars:
ba.widget(
edit=btn,
right_widget=ba.internal.get_special_widget('party_button'),
)
if not self._have_selected_child:
ba.containerwidget(
edit=self._root_widget, selected_child=self._touch_button
)
ba.widget(
edit=self._back_button, down_widget=self._touch_button
)
self._have_selected_child = True
v -= spacing
if show_gamepads:
self._gamepads_button = btn = ba.buttonwidget(
parent=self._root_widget,
position=((width - button_width) / 2 - 7, v),
size=(button_width, 43),
autoselect=True,
label=ba.Lstr(resource=self._r + '.configureControllersText'),
on_activate_call=self._do_gamepads,
)
if ba.app.ui.use_toolbars:
ba.widget(
edit=btn,
right_widget=ba.internal.get_special_widget('party_button'),
)
if not self._have_selected_child:
ba.containerwidget(
edit=self._root_widget, selected_child=self._gamepads_button
)
ba.widget(
edit=self._back_button, down_widget=self._gamepads_button
)
self._have_selected_child = True
v -= spacing
else:
self._gamepads_button = None
if show_space_1:
v -= space_height
if show_keyboard:
self._keyboard_button = btn = ba.buttonwidget(
parent=self._root_widget,
position=((width - button_width) / 2 + 5, v),
size=(button_width, 43),
autoselect=True,
label=ba.Lstr(resource=self._r + '.configureKeyboardText'),
on_activate_call=self._config_keyboard,
)
if ba.app.ui.use_toolbars:
ba.widget(
edit=btn,
right_widget=ba.internal.get_special_widget('party_button'),
)
if not self._have_selected_child:
ba.containerwidget(
edit=self._root_widget, selected_child=self._keyboard_button
)
ba.widget(
edit=self._back_button, down_widget=self._keyboard_button
)
self._have_selected_child = True
v -= spacing
if show_keyboard_p2:
self._keyboard_2_button = ba.buttonwidget(
parent=self._root_widget,
position=((width - button_width) / 2 - 3, v),
size=(button_width, 43),
autoselect=True,
label=ba.Lstr(resource=self._r + '.configureKeyboard2Text'),
on_activate_call=self._config_keyboard2,
)
v -= spacing
if show_space_2:
v -= space_height
if show_remote:
self._idevices_button = btn = ba.buttonwidget(
parent=self._root_widget,
position=((width - button_width) / 2 - 5, v),
size=(button_width, 43),
autoselect=True,
label=ba.Lstr(resource=self._r + '.configureMobileText'),
on_activate_call=self._do_mobile_devices,
)
if ba.app.ui.use_toolbars:
ba.widget(
edit=btn,
right_widget=ba.internal.get_special_widget('party_button'),
)
if not self._have_selected_child:
ba.containerwidget(
edit=self._root_widget, selected_child=self._idevices_button
)
ba.widget(
edit=self._back_button, down_widget=self._idevices_button
)
self._have_selected_child = True
v -= spacing
if show_xinput_toggle:
def do_toggle(value: bool) -> None:
ba.screenmessage(
ba.Lstr(resource='settingsWindowAdvanced.mustRestartText'),
color=(1, 1, 0),
)
ba.playsound(ba.getsound('gunCocking'))
ba.internal.set_low_level_config_value(
'enablexinput', not value
)
ba.checkboxwidget(
parent=self._root_widget,
position=(100, v + 3),
size=(120, 30),
value=(
not ba.internal.get_low_level_config_value(
'enablexinput', 1
)
),
maxwidth=200,
on_value_change_call=do_toggle,
text=ba.Lstr(resource='disableXInputText'),
autoselect=True,
)
ba.textwidget(
parent=self._root_widget,
position=(width * 0.5, v - 5),
size=(0, 0),
text=ba.Lstr(resource='disableXInputDescriptionText'),
scale=0.5,
h_align='center',
v_align='center',
color=ba.app.ui.infotextcolor,
maxwidth=width * 0.8,
)
v -= spacing
if show_mac_controller_subsystem:
popup_ui.PopupMenu(
parent=self._root_widget,
position=(260, v - 10),
width=160,
button_size=(150, 50),
scale=1.5,
choices=['Classic', 'MFi', 'Both'],
choices_display=[
ba.Lstr(resource='macControllerSubsystemClassicText'),
ba.Lstr(resource='macControllerSubsystemMFiText'),
ba.Lstr(resource='macControllerSubsystemBothText'),
],
current_choice=ba.app.config.resolve(
'Mac Controller Subsystem'
),
on_value_change_call=self._set_mac_controller_subsystem,
)
ba.textwidget(
parent=self._root_widget,
position=(245, v + 13),
size=(0, 0),
text=ba.Lstr(resource='macControllerSubsystemTitleText'),
scale=1.0,
h_align='right',
v_align='center',
color=ba.app.ui.infotextcolor,
maxwidth=180,
)
ba.textwidget(
parent=self._root_widget,
position=(width * 0.5, v - 20),
size=(0, 0),
text=ba.Lstr(resource='macControllerSubsystemDescriptionText'),
scale=0.5,
h_align='center',
v_align='center',
color=ba.app.ui.infotextcolor,
maxwidth=width * 0.8,
)
v -= spacing * 1.5
self._restore_state()
def _set_mac_controller_subsystem(self, val: str) -> None:
cfg = ba.app.config
cfg['Mac Controller Subsystem'] = val
cfg.apply_and_commit()
def _config_keyboard(self) -> None:
# pylint: disable=cyclic-import
from bastd.ui.settings.keyboard import ConfigKeyboardWindow
self._save_state()
ba.containerwidget(edit=self._root_widget, transition='out_left')
ba.app.ui.set_main_menu_window(
ConfigKeyboardWindow(
ba.internal.getinputdevice('Keyboard', '#1')
).get_root_widget()
)
def _config_keyboard2(self) -> None:
# pylint: disable=cyclic-import
from bastd.ui.settings.keyboard import ConfigKeyboardWindow
self._save_state()
ba.containerwidget(edit=self._root_widget, transition='out_left')
ba.app.ui.set_main_menu_window(
ConfigKeyboardWindow(
ba.internal.getinputdevice('Keyboard', '#2')
).get_root_widget()
)
def _do_mobile_devices(self) -> None:
# pylint: disable=cyclic-import
from bastd.ui.settings.remoteapp import RemoteAppSettingsWindow
self._save_state()
ba.containerwidget(edit=self._root_widget, transition='out_left')
ba.app.ui.set_main_menu_window(
RemoteAppSettingsWindow().get_root_widget()
)
def _do_gamepads(self) -> None:
# pylint: disable=cyclic-import
from bastd.ui.settings.gamepadselect import GamepadSelectWindow
self._save_state()
ba.containerwidget(edit=self._root_widget, transition='out_left')
ba.app.ui.set_main_menu_window(GamepadSelectWindow().get_root_widget())
def _do_touchscreen(self) -> None:
# pylint: disable=cyclic-import
from bastd.ui.settings.touchscreen import TouchscreenSettingsWindow
self._save_state()
ba.containerwidget(edit=self._root_widget, transition='out_left')
ba.app.ui.set_main_menu_window(
TouchscreenSettingsWindow().get_root_widget()
)
def _save_state(self) -> None:
sel = self._root_widget.get_selected_child()
if sel == self._gamepads_button:
sel_name = 'GamePads'
elif sel == self._touch_button:
sel_name = 'Touch'
elif sel == self._keyboard_button:
sel_name = 'Keyboard'
elif sel == self._keyboard_2_button:
sel_name = 'Keyboard2'
elif sel == self._idevices_button:
sel_name = 'iDevices'
else:
sel_name = 'Back'
ba.app.ui.window_states[type(self)] = sel_name
def _restore_state(self) -> None:
sel_name = ba.app.ui.window_states.get(type(self))
if sel_name == 'GamePads':
sel = self._gamepads_button
elif sel_name == 'Touch':
sel = self._touch_button
elif sel_name == 'Keyboard':
sel = self._keyboard_button
elif sel_name == 'Keyboard2':
sel = self._keyboard_2_button
elif sel_name == 'iDevices':
sel = self._idevices_button
elif sel_name == 'Back':
sel = self._back_button
else:
sel = (
self._gamepads_button
if self._gamepads_button is not None
else self._back_button
)
ba.containerwidget(edit=self._root_widget, selected_child=sel)
def _back(self) -> None:
# pylint: disable=cyclic-import
from bastd.ui.settings.allsettings import AllSettingsWindow
self._save_state()
ba.containerwidget(
edit=self._root_widget, transition=self._transition_out
)
ba.app.ui.set_main_menu_window(
AllSettingsWindow(transition='in_left').get_root_widget()
)

View file

@ -0,0 +1,973 @@
# Released under the MIT License. See LICENSE for details.
#
"""Settings UI functionality related to gamepads."""
from __future__ import annotations
from typing import TYPE_CHECKING
import ba
import ba.internal
if TYPE_CHECKING:
from typing import Any, Callable
class GamepadSettingsWindow(ba.Window):
"""Window for configuring a gamepad."""
def __init__(
self,
gamepad: ba.InputDevice,
is_main_menu: bool = True,
transition: str = 'in_right',
transition_out: str = 'out_right',
settings: dict | None = None,
):
self._input = gamepad
# If our input-device went away, just return an empty zombie.
if not self._input:
return
self._name = self._input.name
self._r = 'configGamepadWindow'
self._settings = settings
self._transition_out = transition_out
# We're a secondary gamepad if supplied with settings.
self._is_secondary = settings is not None
self._ext = '_B' if self._is_secondary else ''
self._is_main_menu = is_main_menu
self._displayname = self._name
self._width = 700 if self._is_secondary else 730
self._height = 440 if self._is_secondary else 450
self._spacing = 40
uiscale = ba.app.ui.uiscale
super().__init__(
root_widget=ba.containerwidget(
size=(self._width, self._height),
scale=(
1.63
if uiscale is ba.UIScale.SMALL
else 1.35
if uiscale is ba.UIScale.MEDIUM
else 1.0
),
stack_offset=(-20, -16)
if uiscale is ba.UIScale.SMALL
else (0, 0),
transition=transition,
)
)
# Don't ask to config joysticks while we're in here.
self._rebuild_ui()
def _rebuild_ui(self) -> None:
# pylint: disable=too-many-statements
# pylint: disable=too-many-locals
from ba.internal import get_device_value
# Clear existing UI.
for widget in self._root_widget.get_children():
widget.delete()
self._textwidgets: dict[str, ba.Widget] = {}
# If we were supplied with settings, we're a secondary joystick and
# just operate on that. in the other (normal) case we make our own.
if not self._is_secondary:
# Fill our temp config with present values (for our primary and
# secondary controls).
self._settings = {}
for skey in [
'buttonJump',
'buttonJump_B',
'buttonPunch',
'buttonPunch_B',
'buttonBomb',
'buttonBomb_B',
'buttonPickUp',
'buttonPickUp_B',
'buttonStart',
'buttonStart_B',
'buttonStart2',
'buttonStart2_B',
'buttonUp',
'buttonUp_B',
'buttonDown',
'buttonDown_B',
'buttonLeft',
'buttonLeft_B',
'buttonRight',
'buttonRight_B',
'buttonRun1',
'buttonRun1_B',
'buttonRun2',
'buttonRun2_B',
'triggerRun1',
'triggerRun1_B',
'triggerRun2',
'triggerRun2_B',
'buttonIgnored',
'buttonIgnored_B',
'buttonIgnored2',
'buttonIgnored2_B',
'buttonIgnored3',
'buttonIgnored3_B',
'buttonIgnored4',
'buttonIgnored4_B',
'buttonVRReorient',
'buttonVRReorient_B',
'analogStickDeadZone',
'analogStickDeadZone_B',
'dpad',
'dpad_B',
'unassignedButtonsRun',
'unassignedButtonsRun_B',
'startButtonActivatesDefaultWidget',
'startButtonActivatesDefaultWidget_B',
'uiOnly',
'uiOnly_B',
'ignoreCompletely',
'ignoreCompletely_B',
'autoRecalibrateAnalogStick',
'autoRecalibrateAnalogStick_B',
'analogStickLR',
'analogStickLR_B',
'analogStickUD',
'analogStickUD_B',
'enableSecondary',
]:
val = get_device_value(self._input, skey)
if val != -1:
self._settings[skey] = val
back_button: ba.Widget | None
if self._is_secondary:
back_button = ba.buttonwidget(
parent=self._root_widget,
position=(self._width - 180, self._height - 65),
autoselect=True,
size=(160, 60),
label=ba.Lstr(resource='doneText'),
scale=0.9,
on_activate_call=self._save,
)
ba.containerwidget(
edit=self._root_widget,
start_button=back_button,
on_cancel_call=back_button.activate,
)
cancel_button = None
else:
cancel_button = ba.buttonwidget(
parent=self._root_widget,
position=(51, self._height - 65),
autoselect=True,
size=(160, 60),
label=ba.Lstr(resource='cancelText'),
scale=0.9,
on_activate_call=self._cancel,
)
ba.containerwidget(
edit=self._root_widget, cancel_button=cancel_button
)
save_button: ba.Widget | None
if not self._is_secondary:
save_button = ba.buttonwidget(
parent=self._root_widget,
position=(
self._width - (165 if self._is_secondary else 195),
self._height - 65,
),
size=((160 if self._is_secondary else 180), 60),
autoselect=True,
label=ba.Lstr(resource='doneText')
if self._is_secondary
else ba.Lstr(resource='saveText'),
scale=0.9,
on_activate_call=self._save,
)
ba.containerwidget(edit=self._root_widget, start_button=save_button)
else:
save_button = None
if not self._is_secondary:
v = self._height - 59
ba.textwidget(
parent=self._root_widget,
position=(0, v + 5),
size=(self._width, 25),
text=ba.Lstr(resource=self._r + '.titleText'),
color=ba.app.ui.title_color,
maxwidth=310,
h_align='center',
v_align='center',
)
v -= 48
ba.textwidget(
parent=self._root_widget,
position=(0, v + 3),
size=(self._width, 25),
text=self._name,
color=ba.app.ui.infotextcolor,
maxwidth=self._width * 0.9,
h_align='center',
v_align='center',
)
v -= self._spacing * 1
ba.textwidget(
parent=self._root_widget,
position=(50, v + 10),
size=(self._width - 100, 30),
text=ba.Lstr(resource=self._r + '.appliesToAllText'),
maxwidth=330,
scale=0.65,
color=(0.5, 0.6, 0.5, 1.0),
h_align='center',
v_align='center',
)
v -= 70
self._enable_check_box = None
else:
v = self._height - 49
ba.textwidget(
parent=self._root_widget,
position=(0, v + 5),
size=(self._width, 25),
text=ba.Lstr(resource=self._r + '.secondaryText'),
color=ba.app.ui.title_color,
maxwidth=300,
h_align='center',
v_align='center',
)
v -= self._spacing * 1
ba.textwidget(
parent=self._root_widget,
position=(50, v + 10),
size=(self._width - 100, 30),
text=ba.Lstr(resource=self._r + '.secondHalfText'),
maxwidth=300,
scale=0.65,
color=(0.6, 0.8, 0.6, 1.0),
h_align='center',
)
self._enable_check_box = ba.checkboxwidget(
parent=self._root_widget,
position=(self._width * 0.5 - 80, v - 73),
value=self.get_enable_secondary_value(),
autoselect=True,
on_value_change_call=self._enable_check_box_changed,
size=(200, 30),
text=ba.Lstr(resource=self._r + '.secondaryEnableText'),
scale=1.2,
)
v = self._height - 205
h_offs = 160
dist = 70
d_color = (0.4, 0.4, 0.8)
sclx = 1.2
scly = 0.98
dpm = ba.Lstr(resource=self._r + '.pressAnyButtonOrDpadText')
dpm2 = ba.Lstr(resource=self._r + '.ifNothingHappensTryAnalogText')
self._capture_button(
pos=(h_offs, v + scly * dist),
color=d_color,
button='buttonUp' + self._ext,
texture=ba.gettexture('upButton'),
scale=1.0,
message=dpm,
message2=dpm2,
)
self._capture_button(
pos=(h_offs - sclx * dist, v),
color=d_color,
button='buttonLeft' + self._ext,
texture=ba.gettexture('leftButton'),
scale=1.0,
message=dpm,
message2=dpm2,
)
self._capture_button(
pos=(h_offs + sclx * dist, v),
color=d_color,
button='buttonRight' + self._ext,
texture=ba.gettexture('rightButton'),
scale=1.0,
message=dpm,
message2=dpm2,
)
self._capture_button(
pos=(h_offs, v - scly * dist),
color=d_color,
button='buttonDown' + self._ext,
texture=ba.gettexture('downButton'),
scale=1.0,
message=dpm,
message2=dpm2,
)
dpm3 = ba.Lstr(resource=self._r + '.ifNothingHappensTryDpadText')
self._capture_button(
pos=(h_offs + 130, v - 125),
color=(0.4, 0.4, 0.6),
button='analogStickLR' + self._ext,
maxwidth=140,
texture=ba.gettexture('analogStick'),
scale=1.2,
message=ba.Lstr(resource=self._r + '.pressLeftRightText'),
message2=dpm3,
)
self._capture_button(
pos=(self._width * 0.5, v),
color=(0.4, 0.4, 0.6),
button='buttonStart' + self._ext,
texture=ba.gettexture('startButton'),
scale=0.7,
)
h_offs = self._width - 160
self._capture_button(
pos=(h_offs, v + scly * dist),
color=(0.6, 0.4, 0.8),
button='buttonPickUp' + self._ext,
texture=ba.gettexture('buttonPickUp'),
scale=1.0,
)
self._capture_button(
pos=(h_offs - sclx * dist, v),
color=(0.7, 0.5, 0.1),
button='buttonPunch' + self._ext,
texture=ba.gettexture('buttonPunch'),
scale=1.0,
)
self._capture_button(
pos=(h_offs + sclx * dist, v),
color=(0.5, 0.2, 0.1),
button='buttonBomb' + self._ext,
texture=ba.gettexture('buttonBomb'),
scale=1.0,
)
self._capture_button(
pos=(h_offs, v - scly * dist),
color=(0.2, 0.5, 0.2),
button='buttonJump' + self._ext,
texture=ba.gettexture('buttonJump'),
scale=1.0,
)
self._advanced_button = ba.buttonwidget(
parent=self._root_widget,
autoselect=True,
label=ba.Lstr(resource=self._r + '.advancedText'),
text_scale=0.9,
color=(0.45, 0.4, 0.5),
textcolor=(0.65, 0.6, 0.7),
position=(self._width - 300, 30),
size=(130, 40),
on_activate_call=self._do_advanced,
)
try:
if cancel_button is not None and save_button is not None:
ba.widget(edit=cancel_button, right_widget=save_button)
ba.widget(edit=save_button, left_widget=cancel_button)
except Exception:
ba.print_exception('Error wiring up gamepad config window.')
def get_r(self) -> str:
"""(internal)"""
return self._r
def get_advanced_button(self) -> ba.Widget:
"""(internal)"""
return self._advanced_button
def get_is_secondary(self) -> bool:
"""(internal)"""
return self._is_secondary
def get_settings(self) -> dict[str, Any]:
"""(internal)"""
assert self._settings is not None
return self._settings
def get_ext(self) -> str:
"""(internal)"""
return self._ext
def get_input(self) -> ba.InputDevice:
"""(internal)"""
return self._input
def _do_advanced(self) -> None:
# pylint: disable=cyclic-import
from bastd.ui.settings import gamepadadvanced
gamepadadvanced.GamepadAdvancedSettingsWindow(self)
def _enable_check_box_changed(self, value: bool) -> None:
assert self._settings is not None
if value:
self._settings['enableSecondary'] = 1
else:
# Just clear since this is default.
if 'enableSecondary' in self._settings:
del self._settings['enableSecondary']
def get_unassigned_buttons_run_value(self) -> bool:
"""(internal)"""
assert self._settings is not None
return self._settings.get('unassignedButtonsRun', True)
def set_unassigned_buttons_run_value(self, value: bool) -> None:
"""(internal)"""
assert self._settings is not None
if value:
if 'unassignedButtonsRun' in self._settings:
# Clear since this is default.
del self._settings['unassignedButtonsRun']
return
self._settings['unassignedButtonsRun'] = False
def get_start_button_activates_default_widget_value(self) -> bool:
"""(internal)"""
assert self._settings is not None
return self._settings.get('startButtonActivatesDefaultWidget', True)
def set_start_button_activates_default_widget_value(
self, value: bool
) -> None:
"""(internal)"""
assert self._settings is not None
if value:
if 'startButtonActivatesDefaultWidget' in self._settings:
# Clear since this is default.
del self._settings['startButtonActivatesDefaultWidget']
return
self._settings['startButtonActivatesDefaultWidget'] = False
def get_ui_only_value(self) -> bool:
"""(internal)"""
assert self._settings is not None
return self._settings.get('uiOnly', False)
def set_ui_only_value(self, value: bool) -> None:
"""(internal)"""
assert self._settings is not None
if not value:
if 'uiOnly' in self._settings:
# Clear since this is default.
del self._settings['uiOnly']
return
self._settings['uiOnly'] = True
def get_ignore_completely_value(self) -> bool:
"""(internal)"""
assert self._settings is not None
return self._settings.get('ignoreCompletely', False)
def set_ignore_completely_value(self, value: bool) -> None:
"""(internal)"""
assert self._settings is not None
if not value:
if 'ignoreCompletely' in self._settings:
# Clear since this is default.
del self._settings['ignoreCompletely']
return
self._settings['ignoreCompletely'] = True
def get_auto_recalibrate_analog_stick_value(self) -> bool:
"""(internal)"""
assert self._settings is not None
return self._settings.get('autoRecalibrateAnalogStick', False)
def set_auto_recalibrate_analog_stick_value(self, value: bool) -> None:
"""(internal)"""
assert self._settings is not None
if not value:
if 'autoRecalibrateAnalogStick' in self._settings:
# Clear since this is default.
del self._settings['autoRecalibrateAnalogStick']
else:
self._settings['autoRecalibrateAnalogStick'] = True
def get_enable_secondary_value(self) -> bool:
"""(internal)"""
assert self._settings is not None
if not self._is_secondary:
raise Exception('enable value only applies to secondary editor')
return self._settings.get('enableSecondary', False)
def show_secondary_editor(self) -> None:
"""(internal)"""
GamepadSettingsWindow(
self._input,
is_main_menu=False,
settings=self._settings,
transition='in_scale',
transition_out='out_scale',
)
def get_control_value_name(self, control: str) -> str | ba.Lstr:
"""(internal)"""
# pylint: disable=too-many-return-statements
assert self._settings is not None
if control == 'analogStickLR' + self._ext:
# This actually shows both LR and UD.
sval1 = (
self._settings['analogStickLR' + self._ext]
if 'analogStickLR' + self._ext in self._settings
else 5
if self._is_secondary
else 1
)
sval2 = (
self._settings['analogStickUD' + self._ext]
if 'analogStickUD' + self._ext in self._settings
else 6
if self._is_secondary
else 2
)
return (
self._input.get_axis_name(sval1)
+ ' / '
+ self._input.get_axis_name(sval2)
)
# If they're looking for triggers.
if control in ['triggerRun1' + self._ext, 'triggerRun2' + self._ext]:
if control in self._settings:
return self._input.get_axis_name(self._settings[control])
return ba.Lstr(resource=self._r + '.unsetText')
# Dead-zone.
if control == 'analogStickDeadZone' + self._ext:
if control in self._settings:
return str(self._settings[control])
return str(1.0)
# For dpad buttons: show individual buttons if any are set.
# Otherwise show whichever dpad is set (defaulting to 1).
dpad_buttons = [
'buttonLeft' + self._ext,
'buttonRight' + self._ext,
'buttonUp' + self._ext,
'buttonDown' + self._ext,
]
if control in dpad_buttons:
# If *any* dpad buttons are assigned, show only button assignments.
if any(b in self._settings for b in dpad_buttons):
if control in self._settings:
return self._input.get_button_name(self._settings[control])
return ba.Lstr(resource=self._r + '.unsetText')
# No dpad buttons - show the dpad number for all 4.
return ba.Lstr(
value='${A} ${B}',
subs=[
('${A}', ba.Lstr(resource=self._r + '.dpadText')),
(
'${B}',
str(
self._settings['dpad' + self._ext]
if 'dpad' + self._ext in self._settings
else 2
if self._is_secondary
else 1
),
),
],
)
# other buttons..
if control in self._settings:
return self._input.get_button_name(self._settings[control])
return ba.Lstr(resource=self._r + '.unsetText')
def _gamepad_event(
self,
control: str,
event: dict[str, Any],
dialog: AwaitGamepadInputWindow,
) -> None:
# pylint: disable=too-many-nested-blocks
# pylint: disable=too-many-branches
# pylint: disable=too-many-statements
assert self._settings is not None
ext = self._ext
# For our dpad-buttons we're looking for either a button-press or a
# hat-switch press.
if control in [
'buttonUp' + ext,
'buttonLeft' + ext,
'buttonDown' + ext,
'buttonRight' + ext,
]:
if event['type'] in ['BUTTONDOWN', 'HATMOTION']:
# If its a button-down.
if event['type'] == 'BUTTONDOWN':
value = event['button']
self._settings[control] = value
# If its a dpad.
elif event['type'] == 'HATMOTION':
# clear out any set dir-buttons
for btn in [
'buttonUp' + ext,
'buttonLeft' + ext,
'buttonRight' + ext,
'buttonDown' + ext,
]:
if btn in self._settings:
del self._settings[btn]
if event['hat'] == (2 if self._is_secondary else 1):
# Exclude value in default case.
if 'dpad' + ext in self._settings:
del self._settings['dpad' + ext]
else:
self._settings['dpad' + ext] = event['hat']
# Update the 4 dpad button txt widgets.
ba.textwidget(
edit=self._textwidgets['buttonUp' + ext],
text=self.get_control_value_name('buttonUp' + ext),
)
ba.textwidget(
edit=self._textwidgets['buttonLeft' + ext],
text=self.get_control_value_name('buttonLeft' + ext),
)
ba.textwidget(
edit=self._textwidgets['buttonRight' + ext],
text=self.get_control_value_name('buttonRight' + ext),
)
ba.textwidget(
edit=self._textwidgets['buttonDown' + ext],
text=self.get_control_value_name('buttonDown' + ext),
)
ba.playsound(ba.getsound('gunCocking'))
dialog.die()
elif control == 'analogStickLR' + ext:
if event['type'] == 'AXISMOTION':
# Ignore small values or else we might get triggered by noise.
if abs(event['value']) > 0.5:
axis = event['axis']
if axis == (5 if self._is_secondary else 1):
# Exclude value in default case.
if 'analogStickLR' + ext in self._settings:
del self._settings['analogStickLR' + ext]
else:
self._settings['analogStickLR' + ext] = axis
ba.textwidget(
edit=self._textwidgets['analogStickLR' + ext],
text=self.get_control_value_name('analogStickLR' + ext),
)
ba.playsound(ba.getsound('gunCocking'))
dialog.die()
# Now launch the up/down listener.
AwaitGamepadInputWindow(
self._input,
'analogStickUD' + ext,
self._gamepad_event,
ba.Lstr(resource=self._r + '.pressUpDownText'),
)
elif control == 'analogStickUD' + ext:
if event['type'] == 'AXISMOTION':
# Ignore small values or else we might get triggered by noise.
if abs(event['value']) > 0.5:
axis = event['axis']
# Ignore our LR axis.
if 'analogStickLR' + ext in self._settings:
lr_axis = self._settings['analogStickLR' + ext]
else:
lr_axis = 5 if self._is_secondary else 1
if axis != lr_axis:
if axis == (6 if self._is_secondary else 2):
# Exclude value in default case.
if 'analogStickUD' + ext in self._settings:
del self._settings['analogStickUD' + ext]
else:
self._settings['analogStickUD' + ext] = axis
ba.textwidget(
edit=self._textwidgets['analogStickLR' + ext],
text=self.get_control_value_name(
'analogStickLR' + ext
),
)
ba.playsound(ba.getsound('gunCocking'))
dialog.die()
else:
# For other buttons we just want a button-press.
if event['type'] == 'BUTTONDOWN':
value = event['button']
self._settings[control] = value
# Update the button's text widget.
ba.textwidget(
edit=self._textwidgets[control],
text=self.get_control_value_name(control),
)
ba.playsound(ba.getsound('gunCocking'))
dialog.die()
def _capture_button(
self,
pos: tuple[float, float],
color: tuple[float, float, float],
texture: ba.Texture,
button: str,
scale: float = 1.0,
message: ba.Lstr | None = None,
message2: ba.Lstr | None = None,
maxwidth: float = 80.0,
) -> ba.Widget:
if message is None:
message = ba.Lstr(resource=self._r + '.pressAnyButtonText')
base_size = 79
btn = ba.buttonwidget(
parent=self._root_widget,
position=(
pos[0] - base_size * 0.5 * scale,
pos[1] - base_size * 0.5 * scale,
),
autoselect=True,
size=(base_size * scale, base_size * scale),
texture=texture,
label='',
color=color,
)
# Make this in a timer so that it shows up on top of all other buttons.
def doit() -> None:
uiscale = 0.9 * scale
txt = ba.textwidget(
parent=self._root_widget,
position=(pos[0] + 0.0 * scale, pos[1] - 58.0 * scale),
color=(1, 1, 1, 0.3),
size=(0, 0),
h_align='center',
v_align='center',
scale=uiscale,
text=self.get_control_value_name(button),
maxwidth=maxwidth,
)
self._textwidgets[button] = txt
ba.buttonwidget(
edit=btn,
on_activate_call=ba.Call(
AwaitGamepadInputWindow,
self._input,
button,
self._gamepad_event,
message,
message2,
),
)
ba.timer(0, doit, timetype=ba.TimeType.REAL)
return btn
def _cancel(self) -> None:
from bastd.ui.settings.controls import ControlsSettingsWindow
ba.containerwidget(
edit=self._root_widget, transition=self._transition_out
)
if self._is_main_menu:
ba.app.ui.set_main_menu_window(
ControlsSettingsWindow(transition='in_left').get_root_widget()
)
def _save(self) -> None:
from ba.internal import (
master_server_post,
get_input_device_config,
get_input_map_hash,
should_submit_debug_info,
)
ba.containerwidget(
edit=self._root_widget, transition=self._transition_out
)
# If we're a secondary editor we just go away (we were editing our
# parent's settings dict).
if self._is_secondary:
return
assert self._settings is not None
if self._input:
dst = get_input_device_config(self._input, default=True)
dst2: dict[str, Any] = dst[0][dst[1]]
dst2.clear()
# Store any values that aren't -1.
for key, val in list(self._settings.items()):
if val != -1:
dst2[key] = val
# If we're allowed to phone home, send this config so we can
# generate more defaults in the future.
inputhash = get_input_map_hash(self._input)
if should_submit_debug_info():
master_server_post(
'controllerConfig',
{
'ua': ba.app.user_agent_string,
'b': ba.app.build_number,
'name': self._name,
'inputMapHash': inputhash,
'config': dst2,
'v': 2,
},
)
ba.app.config.apply_and_commit()
ba.playsound(ba.getsound('gunCocking'))
else:
ba.playsound(ba.getsound('error'))
if self._is_main_menu:
from bastd.ui.settings.controls import ControlsSettingsWindow
ba.app.ui.set_main_menu_window(
ControlsSettingsWindow(transition='in_left').get_root_widget()
)
class AwaitGamepadInputWindow(ba.Window):
"""Window for capturing a gamepad button press."""
def __init__(
self,
gamepad: ba.InputDevice,
button: str,
callback: Callable[[str, dict[str, Any], AwaitGamepadInputWindow], Any],
message: ba.Lstr | None = None,
message2: ba.Lstr | None = None,
):
if message is None:
print('AwaitGamepadInputWindow message is None!')
# Shouldn't get here.
message = ba.Lstr(value='Press any button...')
self._callback = callback
self._input = gamepad
self._capture_button = button
width = 400
height = 150
uiscale = ba.app.ui.uiscale
super().__init__(
root_widget=ba.containerwidget(
scale=(
2.0
if uiscale is ba.UIScale.SMALL
else 1.9
if uiscale is ba.UIScale.MEDIUM
else 1.0
),
size=(width, height),
transition='in_scale',
),
)
ba.textwidget(
parent=self._root_widget,
position=(0, (height - 60) if message2 is None else (height - 50)),
size=(width, 25),
text=message,
maxwidth=width * 0.9,
h_align='center',
v_align='center',
)
if message2 is not None:
ba.textwidget(
parent=self._root_widget,
position=(width * 0.5, height - 60),
size=(0, 0),
text=message2,
maxwidth=width * 0.9,
scale=0.47,
color=(0.7, 1.0, 0.7, 0.6),
h_align='center',
v_align='center',
)
self._counter = 5
self._count_down_text = ba.textwidget(
parent=self._root_widget,
h_align='center',
position=(0, height - 110),
size=(width, 25),
color=(1, 1, 1, 0.3),
text=str(self._counter),
)
self._decrement_timer: ba.Timer | None = ba.Timer(
1.0,
ba.Call(self._decrement),
repeat=True,
timetype=ba.TimeType.REAL,
)
ba.internal.capture_gamepad_input(ba.WeakCall(self._event_callback))
def __del__(self) -> None:
pass
def die(self) -> None:
"""Kill the window."""
# This strong-refs us; killing it allow us to die now.
self._decrement_timer = None
ba.internal.release_gamepad_input()
if self._root_widget:
ba.containerwidget(edit=self._root_widget, transition='out_scale')
def _event_callback(self, event: dict[str, Any]) -> None:
input_device = event['input_device']
assert isinstance(input_device, ba.InputDevice)
# Update - we now allow *any* input device of this type.
if (
self._input
and input_device
and input_device.name == self._input.name
):
self._callback(self._capture_button, event, self)
def _decrement(self) -> None:
self._counter -= 1
if self._counter >= 1:
if self._count_down_text:
ba.textwidget(
edit=self._count_down_text, text=str(self._counter)
)
else:
ba.playsound(ba.getsound('error'))
self.die()

View file

@ -0,0 +1,575 @@
# Released under the MIT License. See LICENSE for details.
#
"""UI functionality related to advanced gamepad configuring."""
from __future__ import annotations
from typing import TYPE_CHECKING
import ba
if TYPE_CHECKING:
from typing import Any
from bastd.ui.settings import gamepad as gpsui
class GamepadAdvancedSettingsWindow(ba.Window):
"""Window for advanced gamepad configuration."""
def __init__(self, parent_window: gpsui.GamepadSettingsWindow):
# pylint: disable=too-many-statements
# pylint: disable=too-many-locals
self._parent_window = parent_window
app = ba.app
self._r = parent_window.get_r()
uiscale = ba.app.ui.uiscale
self._width = 900 if uiscale is ba.UIScale.SMALL else 700
self._x_inset = x_inset = 100 if uiscale is ba.UIScale.SMALL else 0
self._height = 402 if uiscale is ba.UIScale.SMALL else 512
self._textwidgets: dict[str, ba.Widget] = {}
advb = parent_window.get_advanced_button()
super().__init__(
root_widget=ba.containerwidget(
transition='in_scale',
size=(self._width, self._height),
scale=1.06
* (
1.85
if uiscale is ba.UIScale.SMALL
else 1.35
if uiscale is ba.UIScale.MEDIUM
else 1.0
),
stack_offset=(0, -25)
if uiscale is ba.UIScale.SMALL
else (0, 0),
scale_origin_stack_offset=(advb.get_screen_space_center()),
)
)
ba.textwidget(
parent=self._root_widget,
position=(
self._width * 0.5,
self._height - (40 if uiscale is ba.UIScale.SMALL else 34),
),
size=(0, 0),
text=ba.Lstr(resource=self._r + '.advancedTitleText'),
maxwidth=320,
color=ba.app.ui.title_color,
h_align='center',
v_align='center',
)
back_button = btn = ba.buttonwidget(
parent=self._root_widget,
autoselect=True,
position=(
self._width - (176 + x_inset),
self._height - (60 if uiscale is ba.UIScale.SMALL else 55),
),
size=(120, 48),
text_scale=0.8,
label=ba.Lstr(resource='doneText'),
on_activate_call=self._done,
)
ba.containerwidget(
edit=self._root_widget,
start_button=btn,
on_cancel_call=btn.activate,
)
self._scroll_width = self._width - (100 + 2 * x_inset)
self._scroll_height = self._height - 110
self._sub_width = self._scroll_width - 20
self._sub_height = (
940 if self._parent_window.get_is_secondary() else 1040
)
if app.vr_mode:
self._sub_height += 50
self._scrollwidget = ba.scrollwidget(
parent=self._root_widget,
position=(
(self._width - self._scroll_width) * 0.5,
self._height - 65 - self._scroll_height,
),
size=(self._scroll_width, self._scroll_height),
claims_left_right=True,
claims_tab=True,
selection_loops_to_parent=True,
)
self._subcontainer = ba.containerwidget(
parent=self._scrollwidget,
size=(self._sub_width, self._sub_height),
background=False,
claims_left_right=True,
claims_tab=True,
selection_loops_to_parent=True,
)
ba.containerwidget(
edit=self._root_widget, selected_child=self._scrollwidget
)
h = 30
v = self._sub_height - 10
h2 = h + 12
# don't allow secondary joysticks to handle unassigned buttons
if not self._parent_window.get_is_secondary():
v -= 40
cb1 = ba.checkboxwidget(
parent=self._subcontainer,
position=(h + 70, v),
size=(500, 30),
text=ba.Lstr(resource=self._r + '.unassignedButtonsRunText'),
textcolor=(0.8, 0.8, 0.8),
maxwidth=330,
scale=1.0,
on_value_change_call=(
self._parent_window.set_unassigned_buttons_run_value
),
autoselect=True,
value=self._parent_window.get_unassigned_buttons_run_value(),
)
ba.widget(edit=cb1, up_widget=back_button)
v -= 60
capb = self._capture_button(
pos=(h2, v),
name=ba.Lstr(resource=self._r + '.runButton1Text'),
control='buttonRun1' + self._parent_window.get_ext(),
)
if self._parent_window.get_is_secondary():
for widget in capb:
ba.widget(edit=widget, up_widget=back_button)
v -= 42
self._capture_button(
pos=(h2, v),
name=ba.Lstr(resource=self._r + '.runButton2Text'),
control='buttonRun2' + self._parent_window.get_ext(),
)
ba.textwidget(
parent=self._subcontainer,
position=(self._sub_width * 0.5, v - 24),
size=(0, 0),
text=ba.Lstr(resource=self._r + '.runTriggerDescriptionText'),
color=(0.7, 1, 0.7, 0.6),
maxwidth=self._sub_width * 0.8,
scale=0.7,
h_align='center',
v_align='center',
)
v -= 85
self._capture_button(
pos=(h2, v),
name=ba.Lstr(resource=self._r + '.runTrigger1Text'),
control='triggerRun1' + self._parent_window.get_ext(),
message=ba.Lstr(resource=self._r + '.pressAnyAnalogTriggerText'),
)
v -= 42
self._capture_button(
pos=(h2, v),
name=ba.Lstr(resource=self._r + '.runTrigger2Text'),
control='triggerRun2' + self._parent_window.get_ext(),
message=ba.Lstr(resource=self._r + '.pressAnyAnalogTriggerText'),
)
# in vr mode, allow assigning a reset-view button
if app.vr_mode:
v -= 50
self._capture_button(
pos=(h2, v),
name=ba.Lstr(resource=self._r + '.vrReorientButtonText'),
control='buttonVRReorient' + self._parent_window.get_ext(),
)
v -= 60
self._capture_button(
pos=(h2, v),
name=ba.Lstr(resource=self._r + '.extraStartButtonText'),
control='buttonStart2' + self._parent_window.get_ext(),
)
v -= 60
self._capture_button(
pos=(h2, v),
name=ba.Lstr(resource=self._r + '.ignoredButton1Text'),
control='buttonIgnored' + self._parent_window.get_ext(),
)
v -= 42
self._capture_button(
pos=(h2, v),
name=ba.Lstr(resource=self._r + '.ignoredButton2Text'),
control='buttonIgnored2' + self._parent_window.get_ext(),
)
v -= 42
self._capture_button(
pos=(h2, v),
name=ba.Lstr(resource=self._r + '.ignoredButton3Text'),
control='buttonIgnored3' + self._parent_window.get_ext(),
)
v -= 42
self._capture_button(
pos=(h2, v),
name=ba.Lstr(resource=self._r + '.ignoredButton4Text'),
control='buttonIgnored4' + self._parent_window.get_ext(),
)
ba.textwidget(
parent=self._subcontainer,
position=(self._sub_width * 0.5, v - 14),
size=(0, 0),
text=ba.Lstr(resource=self._r + '.ignoredButtonDescriptionText'),
color=(0.7, 1, 0.7, 0.6),
scale=0.8,
maxwidth=self._sub_width * 0.8,
h_align='center',
v_align='center',
)
v -= 80
pwin = self._parent_window
ba.checkboxwidget(
parent=self._subcontainer,
autoselect=True,
position=(h + 50, v),
size=(400, 30),
text=ba.Lstr(resource=self._r + '.startButtonActivatesDefaultText'),
textcolor=(0.8, 0.8, 0.8),
maxwidth=450,
scale=0.9,
on_value_change_call=(
pwin.set_start_button_activates_default_widget_value
),
value=pwin.get_start_button_activates_default_widget_value(),
)
ba.textwidget(
parent=self._subcontainer,
position=(self._sub_width * 0.5, v - 12),
size=(0, 0),
text=ba.Lstr(
resource=self._r + '.startButtonActivatesDefaultDescriptionText'
),
color=(0.7, 1, 0.7, 0.6),
maxwidth=self._sub_width * 0.8,
scale=0.7,
h_align='center',
v_align='center',
)
v -= 80
ba.checkboxwidget(
parent=self._subcontainer,
autoselect=True,
position=(h + 50, v),
size=(400, 30),
text=ba.Lstr(resource=self._r + '.uiOnlyText'),
textcolor=(0.8, 0.8, 0.8),
maxwidth=450,
scale=0.9,
on_value_change_call=self._parent_window.set_ui_only_value,
value=self._parent_window.get_ui_only_value(),
)
ba.textwidget(
parent=self._subcontainer,
position=(self._sub_width * 0.5, v - 12),
size=(0, 0),
text=ba.Lstr(resource=self._r + '.uiOnlyDescriptionText'),
color=(0.7, 1, 0.7, 0.6),
maxwidth=self._sub_width * 0.8,
scale=0.7,
h_align='center',
v_align='center',
)
v -= 80
ba.checkboxwidget(
parent=self._subcontainer,
autoselect=True,
position=(h + 50, v),
size=(400, 30),
text=ba.Lstr(resource=self._r + '.ignoreCompletelyText'),
textcolor=(0.8, 0.8, 0.8),
maxwidth=450,
scale=0.9,
on_value_change_call=pwin.set_ignore_completely_value,
value=self._parent_window.get_ignore_completely_value(),
)
ba.textwidget(
parent=self._subcontainer,
position=(self._sub_width * 0.5, v - 12),
size=(0, 0),
text=ba.Lstr(resource=self._r + '.ignoreCompletelyDescriptionText'),
color=(0.7, 1, 0.7, 0.6),
maxwidth=self._sub_width * 0.8,
scale=0.7,
h_align='center',
v_align='center',
)
v -= 80
cb1 = ba.checkboxwidget(
parent=self._subcontainer,
autoselect=True,
position=(h + 50, v),
size=(400, 30),
text=ba.Lstr(resource=self._r + '.autoRecalibrateText'),
textcolor=(0.8, 0.8, 0.8),
maxwidth=450,
scale=0.9,
on_value_change_call=pwin.set_auto_recalibrate_analog_stick_value,
value=self._parent_window.get_auto_recalibrate_analog_stick_value(),
)
ba.textwidget(
parent=self._subcontainer,
position=(self._sub_width * 0.5, v - 12),
size=(0, 0),
text=ba.Lstr(resource=self._r + '.autoRecalibrateDescriptionText'),
color=(0.7, 1, 0.7, 0.6),
maxwidth=self._sub_width * 0.8,
scale=0.7,
h_align='center',
v_align='center',
)
v -= 80
buttons = self._config_value_editor(
ba.Lstr(resource=self._r + '.analogStickDeadZoneText'),
control=('analogStickDeadZone' + self._parent_window.get_ext()),
position=(h + 40, v),
min_val=0,
max_val=10.0,
increment=0.1,
x_offset=100,
)
ba.widget(edit=buttons[0], left_widget=cb1, up_widget=cb1)
ba.widget(edit=cb1, right_widget=buttons[0], down_widget=buttons[0])
ba.textwidget(
parent=self._subcontainer,
position=(self._sub_width * 0.5, v - 12),
size=(0, 0),
text=ba.Lstr(
resource=self._r + '.analogStickDeadZoneDescriptionText'
),
color=(0.7, 1, 0.7, 0.6),
maxwidth=self._sub_width * 0.8,
scale=0.7,
h_align='center',
v_align='center',
)
v -= 100
# child joysticks cant have child joysticks.. that's just
# crazy talk
if not self._parent_window.get_is_secondary():
ba.buttonwidget(
parent=self._subcontainer,
autoselect=True,
label=ba.Lstr(resource=self._r + '.twoInOneSetupText'),
position=(40, v),
size=(self._sub_width - 80, 50),
on_activate_call=self._parent_window.show_secondary_editor,
up_widget=buttons[0],
)
# set a bigger bottom show-buffer for the widgets we just made
# so we can see the text below them when navigating with
# a gamepad
for child in self._subcontainer.get_children():
ba.widget(edit=child, show_buffer_bottom=30, show_buffer_top=30)
def _capture_button(
self,
pos: tuple[float, float],
name: ba.Lstr,
control: str,
message: ba.Lstr | None = None,
) -> tuple[ba.Widget, ba.Widget]:
if message is None:
message = ba.Lstr(
resource=self._parent_window.get_r() + '.pressAnyButtonText'
)
btn = ba.buttonwidget(
parent=self._subcontainer,
autoselect=True,
position=(pos[0], pos[1]),
label=name,
size=(250, 60),
scale=0.7,
)
btn2 = ba.buttonwidget(
parent=self._subcontainer,
autoselect=True,
position=(pos[0] + 400, pos[1] + 2),
left_widget=btn,
color=(0.45, 0.4, 0.5),
textcolor=(0.65, 0.6, 0.7),
label=ba.Lstr(resource=self._r + '.clearText'),
size=(110, 50),
scale=0.7,
on_activate_call=ba.Call(self._clear_control, control),
)
ba.widget(edit=btn, right_widget=btn2)
# make this in a timer so that it shows up on top of all
# other buttons
def doit() -> None:
from bastd.ui.settings import gamepad
txt = ba.textwidget(
parent=self._subcontainer,
position=(pos[0] + 285, pos[1] + 20),
color=(1, 1, 1, 0.3),
size=(0, 0),
h_align='center',
v_align='center',
scale=0.7,
text=self._parent_window.get_control_value_name(control),
maxwidth=200,
)
self._textwidgets[control] = txt
ba.buttonwidget(
edit=btn,
on_activate_call=ba.Call(
gamepad.AwaitGamepadInputWindow,
self._parent_window.get_input(),
control,
self._gamepad_event,
message,
),
)
ba.timer(0, doit, timetype=ba.TimeType.REAL)
return btn, btn2
def _inc(
self, control: str, min_val: float, max_val: float, inc: float
) -> None:
val = self._parent_window.get_settings().get(control, 1.0)
val = min(max_val, max(min_val, val + inc))
if abs(1.0 - val) < 0.001:
if control in self._parent_window.get_settings():
del self._parent_window.get_settings()[control]
else:
self._parent_window.get_settings()[control] = round(val, 1)
ba.textwidget(
edit=self._textwidgets[control],
text=self._parent_window.get_control_value_name(control),
)
def _config_value_editor(
self,
name: ba.Lstr,
control: str,
position: tuple[float, float],
min_val: float = 0.0,
max_val: float = 100.0,
increment: float = 1.0,
change_sound: bool = True,
x_offset: float = 0.0,
displayname: ba.Lstr | None = None,
) -> tuple[ba.Widget, ba.Widget]:
if displayname is None:
displayname = name
ba.textwidget(
parent=self._subcontainer,
position=position,
size=(100, 30),
text=displayname,
color=(0.8, 0.8, 0.8, 1.0),
h_align='left',
v_align='center',
scale=1.0,
maxwidth=280,
)
self._textwidgets[control] = ba.textwidget(
parent=self._subcontainer,
position=(246.0 + x_offset, position[1]),
size=(60, 28),
editable=False,
color=(0.3, 1.0, 0.3, 1.0),
h_align='right',
v_align='center',
text=self._parent_window.get_control_value_name(control),
padding=2,
)
btn = ba.buttonwidget(
parent=self._subcontainer,
autoselect=True,
position=(330 + x_offset, position[1] + 4),
size=(28, 28),
label='-',
on_activate_call=ba.Call(
self._inc, control, min_val, max_val, -increment
),
repeat=True,
enable_sound=(change_sound is True),
)
btn2 = ba.buttonwidget(
parent=self._subcontainer,
autoselect=True,
position=(380 + x_offset, position[1] + 4),
size=(28, 28),
label='+',
on_activate_call=ba.Call(
self._inc, control, min_val, max_val, increment
),
repeat=True,
enable_sound=(change_sound is True),
)
return btn, btn2
def _clear_control(self, control: str) -> None:
if control in self._parent_window.get_settings():
del self._parent_window.get_settings()[control]
ba.textwidget(
edit=self._textwidgets[control],
text=self._parent_window.get_control_value_name(control),
)
def _gamepad_event(
self,
control: str,
event: dict[str, Any],
dialog: gpsui.AwaitGamepadInputWindow,
) -> None:
ext = self._parent_window.get_ext()
if control in ['triggerRun1' + ext, 'triggerRun2' + ext]:
if event['type'] == 'AXISMOTION':
# ignore small values or else we might get triggered
# by noise
if abs(event['value']) > 0.5:
self._parent_window.get_settings()[control] = event['axis']
# update the button's text widget
if self._textwidgets[control]:
ba.textwidget(
edit=self._textwidgets[control],
text=self._parent_window.get_control_value_name(
control
),
)
ba.playsound(ba.getsound('gunCocking'))
dialog.die()
else:
if event['type'] == 'BUTTONDOWN':
value = event['button']
self._parent_window.get_settings()[control] = value
# update the button's text widget
if self._textwidgets[control]:
ba.textwidget(
edit=self._textwidgets[control],
text=self._parent_window.get_control_value_name(
control
),
)
ba.playsound(ba.getsound('gunCocking'))
dialog.die()
def _done(self) -> None:
ba.containerwidget(edit=self._root_widget, transition='out_scale')

View file

@ -0,0 +1,192 @@
# Released under the MIT License. See LICENSE for details.
#
"""Settings UI related to gamepad functionality."""
from __future__ import annotations
from typing import TYPE_CHECKING
import ba
import ba.internal
if TYPE_CHECKING:
from typing import Any
def gamepad_configure_callback(event: dict[str, Any]) -> None:
"""Respond to a gamepad button press during config selection."""
from ba.internal import get_remote_app_name
from bastd.ui.settings import gamepad
# Ignore all but button-presses.
if event['type'] not in ['BUTTONDOWN', 'HATMOTION']:
return
ba.internal.release_gamepad_input()
try:
ba.app.ui.clear_main_menu_window(transition='out_left')
except Exception:
ba.print_exception('Error transitioning out main_menu_window.')
ba.playsound(ba.getsound('activateBeep'))
ba.playsound(ba.getsound('swish'))
inputdevice = event['input_device']
assert isinstance(inputdevice, ba.InputDevice)
if inputdevice.allows_configuring:
ba.app.ui.set_main_menu_window(
gamepad.GamepadSettingsWindow(inputdevice).get_root_widget()
)
else:
width = 700
height = 200
button_width = 100
uiscale = ba.app.ui.uiscale
dlg = ba.containerwidget(
scale=(
1.7
if uiscale is ba.UIScale.SMALL
else 1.4
if uiscale is ba.UIScale.MEDIUM
else 1.0
),
size=(width, height),
transition='in_right',
)
ba.app.ui.set_main_menu_window(dlg)
device_name = inputdevice.name
if device_name == 'iDevice':
msg = ba.Lstr(
resource='bsRemoteConfigureInAppText',
subs=[('${REMOTE_APP_NAME}', get_remote_app_name())],
)
else:
msg = ba.Lstr(
resource='cantConfigureDeviceText',
subs=[('${DEVICE}', device_name)],
)
ba.textwidget(
parent=dlg,
position=(0, height - 80),
size=(width, 25),
text=msg,
scale=0.8,
h_align='center',
v_align='top',
)
def _ok() -> None:
from bastd.ui.settings import controls
ba.containerwidget(edit=dlg, transition='out_right')
ba.app.ui.set_main_menu_window(
controls.ControlsSettingsWindow(
transition='in_left'
).get_root_widget()
)
ba.buttonwidget(
parent=dlg,
position=((width - button_width) / 2, 20),
size=(button_width, 60),
label=ba.Lstr(resource='okText'),
on_activate_call=_ok,
)
class GamepadSelectWindow(ba.Window):
"""Window for selecting a gamepad to configure."""
def __init__(self) -> None:
from typing import cast
width = 480
height = 170
spacing = 40
self._r = 'configGamepadSelectWindow'
uiscale = ba.app.ui.uiscale
super().__init__(
root_widget=ba.containerwidget(
scale=(
2.3
if uiscale is ba.UIScale.SMALL
else 1.5
if uiscale is ba.UIScale.MEDIUM
else 1.0
),
size=(width, height),
transition='in_right',
)
)
btn = ba.buttonwidget(
parent=self._root_widget,
position=(20, height - 60),
size=(130, 60),
label=ba.Lstr(resource='backText'),
button_type='back',
scale=0.8,
on_activate_call=self._back,
)
# Let's not have anything selected by default; its misleading looking
# for the controller getting configured.
ba.containerwidget(
edit=self._root_widget,
cancel_button=btn,
selected_child=cast(ba.Widget, 0),
)
ba.textwidget(
parent=self._root_widget,
position=(20, height - 50),
size=(width, 25),
text=ba.Lstr(resource=self._r + '.titleText'),
maxwidth=250,
color=ba.app.ui.title_color,
h_align='center',
v_align='center',
)
ba.buttonwidget(
edit=btn,
button_type='backSmall',
size=(60, 60),
label=ba.charstr(ba.SpecialChar.BACK),
)
v: float = height - 60
v -= spacing
ba.textwidget(
parent=self._root_widget,
position=(15, v),
size=(width - 30, 30),
scale=0.8,
text=ba.Lstr(resource=self._r + '.pressAnyButtonText'),
maxwidth=width * 0.95,
color=ba.app.ui.infotextcolor,
h_align='center',
v_align='top',
)
v -= spacing * 1.24
if ba.app.platform == 'android':
ba.textwidget(
parent=self._root_widget,
position=(15, v),
size=(width - 30, 30),
scale=0.46,
text=ba.Lstr(resource=self._r + '.androidNoteText'),
maxwidth=width * 0.95,
color=(0.7, 0.9, 0.7, 0.5),
h_align='center',
v_align='top',
)
ba.internal.capture_gamepad_input(gamepad_configure_callback)
def _back(self) -> None:
from bastd.ui.settings import controls
ba.internal.release_gamepad_input()
ba.containerwidget(edit=self._root_widget, transition='out_right')
ba.app.ui.set_main_menu_window(
controls.ControlsSettingsWindow(
transition='in_left'
).get_root_widget()
)

View file

@ -0,0 +1,484 @@
# Released under the MIT License. See LICENSE for details.
#
"""Provides UI for graphics settings."""
from __future__ import annotations
from typing import TYPE_CHECKING
import ba
import ba.internal
if TYPE_CHECKING:
pass
class GraphicsSettingsWindow(ba.Window):
"""Window for graphics settings."""
def __init__(
self,
transition: str = 'in_right',
origin_widget: ba.Widget | None = None,
):
# pylint: disable=too-many-locals
# pylint: disable=too-many-branches
# pylint: disable=too-many-statements
from bastd.ui import popup
from bastd.ui.config import ConfigCheckBox, ConfigNumberEdit
# if they provided an origin-widget, scale up from that
scale_origin: tuple[float, float] | None
if origin_widget is not None:
self._transition_out = 'out_scale'
scale_origin = origin_widget.get_screen_space_center()
transition = 'in_scale'
else:
self._transition_out = 'out_right'
scale_origin = None
self._r = 'graphicsSettingsWindow'
app = ba.app
spacing = 32
self._have_selected_child = False
uiscale = app.ui.uiscale
width = 450.0
height = 302.0
self._show_fullscreen = False
fullscreen_spacing_top = spacing * 0.2
fullscreen_spacing = spacing * 1.2
if uiscale == ba.UIScale.LARGE and app.platform != 'android':
self._show_fullscreen = True
height += fullscreen_spacing + fullscreen_spacing_top
show_gamma = False
gamma_spacing = spacing * 1.3
if ba.internal.has_gamma_control():
show_gamma = True
height += gamma_spacing
show_vsync = False
if app.platform == 'mac':
show_vsync = True
show_resolution = True
if app.vr_mode:
show_resolution = (
app.platform == 'android' and app.subplatform == 'cardboard'
)
uiscale = ba.app.ui.uiscale
base_scale = (
2.4
if uiscale is ba.UIScale.SMALL
else 1.5
if uiscale is ba.UIScale.MEDIUM
else 1.0
)
popup_menu_scale = base_scale * 1.2
v = height - 50
v -= spacing * 1.15
super().__init__(
root_widget=ba.containerwidget(
size=(width, height),
transition=transition,
scale_origin_stack_offset=scale_origin,
scale=base_scale,
stack_offset=(0, -30)
if uiscale is ba.UIScale.SMALL
else (0, 0),
)
)
btn = ba.buttonwidget(
parent=self._root_widget,
position=(35, height - 50),
size=(120, 60),
scale=0.8,
text_scale=1.2,
autoselect=True,
label=ba.Lstr(resource='backText'),
button_type='back',
on_activate_call=self._back,
)
ba.containerwidget(edit=self._root_widget, cancel_button=btn)
ba.textwidget(
parent=self._root_widget,
position=(0, height - 44),
size=(width, 25),
text=ba.Lstr(resource=self._r + '.titleText'),
color=ba.app.ui.title_color,
h_align='center',
v_align='top',
)
ba.buttonwidget(
edit=btn,
button_type='backSmall',
size=(60, 60),
label=ba.charstr(ba.SpecialChar.BACK),
)
self._fullscreen_checkbox: ba.Widget | None
if self._show_fullscreen:
v -= fullscreen_spacing_top
self._fullscreen_checkbox = ConfigCheckBox(
parent=self._root_widget,
position=(100, v),
maxwidth=200,
size=(300, 30),
configkey='Fullscreen',
displayname=ba.Lstr(
resource=self._r
+ (
'.fullScreenCmdText'
if app.platform == 'mac'
else '.fullScreenCtrlText'
)
),
).widget
if not self._have_selected_child:
ba.containerwidget(
edit=self._root_widget,
selected_child=self._fullscreen_checkbox,
)
self._have_selected_child = True
v -= fullscreen_spacing
else:
self._fullscreen_checkbox = None
self._gamma_controls: ConfigNumberEdit | None
if show_gamma:
self._gamma_controls = gmc = ConfigNumberEdit(
parent=self._root_widget,
position=(90, v),
configkey='Screen Gamma',
displayname=ba.Lstr(resource=self._r + '.gammaText'),
minval=0.1,
maxval=2.0,
increment=0.1,
xoffset=-70,
textscale=0.85,
)
if ba.app.ui.use_toolbars:
ba.widget(
edit=gmc.plusbutton,
right_widget=ba.internal.get_special_widget('party_button'),
)
if not self._have_selected_child:
ba.containerwidget(
edit=self._root_widget, selected_child=gmc.minusbutton
)
self._have_selected_child = True
v -= gamma_spacing
else:
self._gamma_controls = None
self._selected_color = (0.5, 1, 0.5, 1)
self._unselected_color = (0.7, 0.7, 0.7, 1)
# quality
ba.textwidget(
parent=self._root_widget,
position=(60, v),
size=(160, 25),
text=ba.Lstr(resource=self._r + '.visualsText'),
color=ba.app.ui.heading_color,
scale=0.65,
maxwidth=150,
h_align='center',
v_align='center',
)
popup.PopupMenu(
parent=self._root_widget,
position=(60, v - 50),
width=150,
scale=popup_menu_scale,
choices=['Auto', 'Higher', 'High', 'Medium', 'Low'],
choices_disabled=['Higher', 'High']
if ba.internal.get_max_graphics_quality() == 'Medium'
else [],
choices_display=[
ba.Lstr(resource='autoText'),
ba.Lstr(resource=self._r + '.higherText'),
ba.Lstr(resource=self._r + '.highText'),
ba.Lstr(resource=self._r + '.mediumText'),
ba.Lstr(resource=self._r + '.lowText'),
],
current_choice=ba.app.config.resolve('Graphics Quality'),
on_value_change_call=self._set_quality,
)
# texture controls
ba.textwidget(
parent=self._root_widget,
position=(230, v),
size=(160, 25),
text=ba.Lstr(resource=self._r + '.texturesText'),
color=ba.app.ui.heading_color,
scale=0.65,
maxwidth=150,
h_align='center',
v_align='center',
)
textures_popup = popup.PopupMenu(
parent=self._root_widget,
position=(230, v - 50),
width=150,
scale=popup_menu_scale,
choices=['Auto', 'High', 'Medium', 'Low'],
choices_display=[
ba.Lstr(resource='autoText'),
ba.Lstr(resource=self._r + '.highText'),
ba.Lstr(resource=self._r + '.mediumText'),
ba.Lstr(resource=self._r + '.lowText'),
],
current_choice=ba.app.config.resolve('Texture Quality'),
on_value_change_call=self._set_textures,
)
if ba.app.ui.use_toolbars:
ba.widget(
edit=textures_popup.get_button(),
right_widget=ba.internal.get_special_widget('party_button'),
)
v -= 80
h_offs = 0
if show_resolution:
# resolution
ba.textwidget(
parent=self._root_widget,
position=(h_offs + 60, v),
size=(160, 25),
text=ba.Lstr(resource=self._r + '.resolutionText'),
color=ba.app.ui.heading_color,
scale=0.65,
maxwidth=150,
h_align='center',
v_align='center',
)
# on standard android we have 'Auto', 'Native', and a few
# HD standards
if app.platform == 'android':
# on cardboard/daydream android we have a few
# render-target-scale options
if app.subplatform == 'cardboard':
current_res_cardboard = (
str(
min(
100,
max(
10,
int(
round(
ba.app.config.resolve(
'GVR Render Target Scale'
)
* 100.0
)
),
),
)
)
+ '%'
) # yapf: disable
popup.PopupMenu(
parent=self._root_widget,
position=(h_offs + 60, v - 50),
width=120,
scale=popup_menu_scale,
choices=['100%', '75%', '50%', '35%'],
current_choice=current_res_cardboard,
on_value_change_call=self._set_gvr_render_target_scale,
)
else:
native_res = ba.internal.get_display_resolution()
assert native_res is not None
choices = ['Auto', 'Native']
choices_display = [
ba.Lstr(resource='autoText'),
ba.Lstr(resource='nativeText'),
]
for res in [1440, 1080, 960, 720, 480]:
# nav bar is 72px so lets allow for that in what
# choices we show
if native_res[1] >= res - 72:
res_str = str(res) + 'p'
choices.append(res_str)
choices_display.append(ba.Lstr(value=res_str))
current_res_android = ba.app.config.resolve(
'Resolution (Android)'
)
popup.PopupMenu(
parent=self._root_widget,
position=(h_offs + 60, v - 50),
width=120,
scale=popup_menu_scale,
choices=choices,
choices_display=choices_display,
current_choice=current_res_android,
on_value_change_call=self._set_android_res,
)
else:
# if we're on a system that doesn't allow setting resolution,
# set pixel-scale instead
current_res = ba.internal.get_display_resolution()
if current_res is None:
current_res2 = (
str(
min(
100,
max(
10,
int(
round(
ba.app.config.resolve(
'Screen Pixel Scale'
)
* 100.0
)
),
),
)
)
+ '%'
) # yapf: disable
popup.PopupMenu(
parent=self._root_widget,
position=(h_offs + 60, v - 50),
width=120,
scale=popup_menu_scale,
choices=['100%', '88%', '75%', '63%', '50%'],
current_choice=current_res2,
on_value_change_call=self._set_pixel_scale,
)
else:
raise Exception(
'obsolete path; discrete resolutions'
' no longer supported'
)
# vsync
if show_vsync:
ba.textwidget(
parent=self._root_widget,
position=(230, v),
size=(160, 25),
text=ba.Lstr(resource=self._r + '.verticalSyncText'),
color=ba.app.ui.heading_color,
scale=0.65,
maxwidth=150,
h_align='center',
v_align='center',
)
popup.PopupMenu(
parent=self._root_widget,
position=(230, v - 50),
width=150,
scale=popup_menu_scale,
choices=['Auto', 'Always', 'Never'],
choices_display=[
ba.Lstr(resource='autoText'),
ba.Lstr(resource=self._r + '.alwaysText'),
ba.Lstr(resource=self._r + '.neverText'),
],
current_choice=ba.app.config.resolve('Vertical Sync'),
on_value_change_call=self._set_vsync,
)
v -= 90
fpsc = ConfigCheckBox(
parent=self._root_widget,
position=(69, v - 6),
size=(210, 30),
scale=0.86,
configkey='Show FPS',
displayname=ba.Lstr(resource=self._r + '.showFPSText'),
maxwidth=130,
)
# (tv mode doesnt apply to vr)
if not ba.app.vr_mode:
tvc = ConfigCheckBox(
parent=self._root_widget,
position=(240, v - 6),
size=(210, 30),
scale=0.86,
configkey='TV Border',
displayname=ba.Lstr(resource=self._r + '.tvBorderText'),
maxwidth=130,
)
# grumble..
ba.widget(edit=fpsc.widget, right_widget=tvc.widget)
try:
pass
except Exception:
ba.print_exception('Exception wiring up graphics settings UI:')
v -= spacing
# make a timer to update our controls in case the config changes
# under us
self._update_timer = ba.Timer(
0.25,
ba.WeakCall(self._update_controls),
repeat=True,
timetype=ba.TimeType.REAL,
)
def _back(self) -> None:
from bastd.ui.settings import allsettings
ba.containerwidget(
edit=self._root_widget, transition=self._transition_out
)
ba.app.ui.set_main_menu_window(
allsettings.AllSettingsWindow(
transition='in_left'
).get_root_widget()
)
def _set_quality(self, quality: str) -> None:
cfg = ba.app.config
cfg['Graphics Quality'] = quality
cfg.apply_and_commit()
def _set_textures(self, val: str) -> None:
cfg = ba.app.config
cfg['Texture Quality'] = val
cfg.apply_and_commit()
def _set_android_res(self, val: str) -> None:
cfg = ba.app.config
cfg['Resolution (Android)'] = val
cfg.apply_and_commit()
def _set_pixel_scale(self, res: str) -> None:
cfg = ba.app.config
cfg['Screen Pixel Scale'] = float(res[:-1]) / 100.0
cfg.apply_and_commit()
def _set_gvr_render_target_scale(self, res: str) -> None:
cfg = ba.app.config
cfg['GVR Render Target Scale'] = float(res[:-1]) / 100.0
cfg.apply_and_commit()
def _set_vsync(self, val: str) -> None:
cfg = ba.app.config
cfg['Vertical Sync'] = val
cfg.apply_and_commit()
def _update_controls(self) -> None:
if self._show_fullscreen:
ba.checkboxwidget(
edit=self._fullscreen_checkbox,
value=ba.app.config.resolve('Fullscreen'),
)

View file

@ -0,0 +1,381 @@
# Released under the MIT License. See LICENSE for details.
#
"""Keyboard settings related UI functionality."""
from __future__ import annotations
from typing import TYPE_CHECKING
import ba
import ba.internal
if TYPE_CHECKING:
from typing import Any
class ConfigKeyboardWindow(ba.Window):
"""Window for configuring keyboards."""
def __init__(self, c: ba.InputDevice, transition: str = 'in_right'):
self._r = 'configKeyboardWindow'
self._input = c
self._name = self._input.name
self._unique_id = self._input.unique_identifier
dname_raw = self._name
if self._unique_id != '#1':
dname_raw += ' ' + self._unique_id.replace('#', 'P')
self._displayname = ba.Lstr(translate=('inputDeviceNames', dname_raw))
self._width = 700
if self._unique_id != '#1':
self._height = 480
else:
self._height = 375
self._spacing = 40
uiscale = ba.app.ui.uiscale
super().__init__(
root_widget=ba.containerwidget(
size=(self._width, self._height),
scale=(
1.6
if uiscale is ba.UIScale.SMALL
else 1.3
if uiscale is ba.UIScale.MEDIUM
else 1.0
),
stack_offset=(0, 5) if uiscale is ba.UIScale.SMALL else (0, 0),
transition=transition,
)
)
self._rebuild_ui()
def _rebuild_ui(self) -> None:
from ba.internal import get_device_value
for widget in self._root_widget.get_children():
widget.delete()
# Fill our temp config with present values.
self._settings: dict[str, int] = {}
for button in [
'buttonJump',
'buttonPunch',
'buttonBomb',
'buttonPickUp',
'buttonStart',
'buttonStart2',
'buttonUp',
'buttonDown',
'buttonLeft',
'buttonRight',
]:
self._settings[button] = get_device_value(self._input, button)
cancel_button = ba.buttonwidget(
parent=self._root_widget,
autoselect=True,
position=(38, self._height - 85),
size=(170, 60),
label=ba.Lstr(resource='cancelText'),
scale=0.9,
on_activate_call=self._cancel,
)
save_button = ba.buttonwidget(
parent=self._root_widget,
autoselect=True,
position=(self._width - 190, self._height - 85),
size=(180, 60),
label=ba.Lstr(resource='saveText'),
scale=0.9,
text_scale=0.9,
on_activate_call=self._save,
)
ba.containerwidget(
edit=self._root_widget,
cancel_button=cancel_button,
start_button=save_button,
)
ba.widget(edit=cancel_button, right_widget=save_button)
ba.widget(edit=save_button, left_widget=cancel_button)
v = self._height - 74.0
ba.textwidget(
parent=self._root_widget,
position=(self._width * 0.5, v + 15),
size=(0, 0),
text=ba.Lstr(
resource=self._r + '.configuringText',
subs=[('${DEVICE}', self._displayname)],
),
color=ba.app.ui.title_color,
h_align='center',
v_align='center',
maxwidth=270,
scale=0.83,
)
v -= 20
if self._unique_id != '#1':
v -= 20
v -= self._spacing
ba.textwidget(
parent=self._root_widget,
position=(0, v + 19),
size=(self._width, 50),
text=ba.Lstr(resource=self._r + '.keyboard2NoteText'),
scale=0.7,
maxwidth=self._width * 0.75,
max_height=110,
color=ba.app.ui.infotextcolor,
h_align='center',
v_align='top',
)
v -= 40
v -= 10
v -= self._spacing * 2.2
v += 25
v -= 42
h_offs = 160
dist = 70
d_color = (0.4, 0.4, 0.8)
self._capture_button(
pos=(h_offs, v + 0.95 * dist),
color=d_color,
button='buttonUp',
texture=ba.gettexture('upButton'),
scale=1.0,
)
self._capture_button(
pos=(h_offs - 1.2 * dist, v),
color=d_color,
button='buttonLeft',
texture=ba.gettexture('leftButton'),
scale=1.0,
)
self._capture_button(
pos=(h_offs + 1.2 * dist, v),
color=d_color,
button='buttonRight',
texture=ba.gettexture('rightButton'),
scale=1.0,
)
self._capture_button(
pos=(h_offs, v - 0.95 * dist),
color=d_color,
button='buttonDown',
texture=ba.gettexture('downButton'),
scale=1.0,
)
if self._unique_id == '#2':
self._capture_button(
pos=(self._width * 0.5, v + 0.1 * dist),
color=(0.4, 0.4, 0.6),
button='buttonStart',
texture=ba.gettexture('startButton'),
scale=0.8,
)
h_offs = self._width - 160
self._capture_button(
pos=(h_offs, v + 0.95 * dist),
color=(0.6, 0.4, 0.8),
button='buttonPickUp',
texture=ba.gettexture('buttonPickUp'),
scale=1.0,
)
self._capture_button(
pos=(h_offs - 1.2 * dist, v),
color=(0.7, 0.5, 0.1),
button='buttonPunch',
texture=ba.gettexture('buttonPunch'),
scale=1.0,
)
self._capture_button(
pos=(h_offs + 1.2 * dist, v),
color=(0.5, 0.2, 0.1),
button='buttonBomb',
texture=ba.gettexture('buttonBomb'),
scale=1.0,
)
self._capture_button(
pos=(h_offs, v - 0.95 * dist),
color=(0.2, 0.5, 0.2),
button='buttonJump',
texture=ba.gettexture('buttonJump'),
scale=1.0,
)
def _capture_button(
self,
pos: tuple[float, float],
color: tuple[float, float, float],
texture: ba.Texture,
button: str,
scale: float = 1.0,
) -> None:
base_size = 79
btn = ba.buttonwidget(
parent=self._root_widget,
autoselect=True,
position=(
pos[0] - base_size * 0.5 * scale,
pos[1] - base_size * 0.5 * scale,
),
size=(base_size * scale, base_size * scale),
texture=texture,
label='',
color=color,
)
# Do this deferred so it shows up on top of other buttons. (ew.)
def doit() -> None:
if not self._root_widget:
return
uiscale = 0.66 * scale * 2.0
maxwidth = 76.0 * scale
txt = ba.textwidget(
parent=self._root_widget,
position=(pos[0] + 0.0 * scale, pos[1] - (57.0 - 18.0) * scale),
color=(1, 1, 1, 0.3),
size=(0, 0),
h_align='center',
v_align='top',
scale=uiscale,
maxwidth=maxwidth,
text=self._input.get_button_name(self._settings[button]),
)
ba.buttonwidget(
edit=btn,
autoselect=True,
on_activate_call=ba.Call(
AwaitKeyboardInputWindow, button, txt, self._settings
),
)
ba.pushcall(doit)
def _cancel(self) -> None:
from bastd.ui.settings.controls import ControlsSettingsWindow
ba.containerwidget(edit=self._root_widget, transition='out_right')
ba.app.ui.set_main_menu_window(
ControlsSettingsWindow(transition='in_left').get_root_widget()
)
def _save(self) -> None:
from bastd.ui.settings.controls import ControlsSettingsWindow
from ba.internal import (
get_input_device_config,
should_submit_debug_info,
master_server_post,
)
ba.containerwidget(edit=self._root_widget, transition='out_right')
ba.playsound(ba.getsound('gunCocking'))
# There's a chance the device disappeared; handle that gracefully.
if not self._input:
return
dst = get_input_device_config(self._input, default=False)
dst2: dict[str, Any] = dst[0][dst[1]]
dst2.clear()
# Store any values that aren't -1.
for key, val in list(self._settings.items()):
if val != -1:
dst2[key] = val
# If we're allowed to phone home, send this config so we can generate
# more defaults in the future.
if should_submit_debug_info():
master_server_post(
'controllerConfig',
{
'ua': ba.app.user_agent_string,
'name': self._name,
'b': ba.app.build_number,
'config': dst2,
'v': 2,
},
)
ba.app.config.apply_and_commit()
ba.app.ui.set_main_menu_window(
ControlsSettingsWindow(transition='in_left').get_root_widget()
)
class AwaitKeyboardInputWindow(ba.Window):
"""Window for capturing a keypress."""
def __init__(self, button: str, ui: ba.Widget, settings: dict):
self._capture_button = button
self._capture_key_ui = ui
self._settings = settings
width = 400
height = 150
uiscale = ba.app.ui.uiscale
super().__init__(
root_widget=ba.containerwidget(
size=(width, height),
transition='in_right',
scale=(
2.0
if uiscale is ba.UIScale.SMALL
else 1.5
if uiscale is ba.UIScale.MEDIUM
else 1.0
),
)
)
ba.textwidget(
parent=self._root_widget,
position=(0, height - 60),
size=(width, 25),
text=ba.Lstr(resource='pressAnyKeyText'),
h_align='center',
v_align='top',
)
self._counter = 5
self._count_down_text = ba.textwidget(
parent=self._root_widget,
h_align='center',
position=(0, height - 110),
size=(width, 25),
color=(1, 1, 1, 0.3),
text=str(self._counter),
)
self._decrement_timer: ba.Timer | None = ba.Timer(
1.0, self._decrement, repeat=True, timetype=ba.TimeType.REAL
)
ba.internal.capture_keyboard_input(ba.WeakCall(self._button_callback))
def __del__(self) -> None:
ba.internal.release_keyboard_input()
def _die(self) -> None:
# This strong-refs us; killing it allows us to die now.
self._decrement_timer = None
if self._root_widget:
ba.containerwidget(edit=self._root_widget, transition='out_left')
def _button_callback(self, event: dict[str, Any]) -> None:
self._settings[self._capture_button] = event['button']
if event['type'] == 'BUTTONDOWN':
bname = event['input_device'].get_button_name(event['button'])
ba.textwidget(edit=self._capture_key_ui, text=bname)
ba.playsound(ba.getsound('gunCocking'))
self._die()
def _decrement(self) -> None:
self._counter -= 1
if self._counter >= 1:
ba.textwidget(edit=self._count_down_text, text=str(self._counter))
else:
self._die()

View file

@ -0,0 +1,448 @@
# Released under the MIT License. See LICENSE for details.
#
"""Provides ui for network related testing."""
from __future__ import annotations
import time
import copy
import weakref
from threading import Thread
from typing import TYPE_CHECKING
from efro.error import CleanError
import ba
import ba.internal
from bastd.ui.settings.testing import TestingWindow
if TYPE_CHECKING:
from typing import Callable, Any
# We generally want all net tests to timeout on their own, but we add
# sort of sane max in case they don't.
MAX_TEST_SECONDS = 60 * 2
class NetTestingWindow(ba.Window):
"""Window that runs a networking test suite to help diagnose issues."""
def __init__(self, transition: str = 'in_right'):
self._width = 820
self._height = 500
self._printed_lines: list[str] = []
uiscale = ba.app.ui.uiscale
super().__init__(
root_widget=ba.containerwidget(
size=(self._width, self._height),
scale=(
1.56
if uiscale is ba.UIScale.SMALL
else 1.2
if uiscale is ba.UIScale.MEDIUM
else 0.8
),
stack_offset=(0.0, -7 if uiscale is ba.UIScale.SMALL else 0.0),
transition=transition,
)
)
self._done_button = ba.buttonwidget(
parent=self._root_widget,
position=(40, self._height - 77),
size=(120, 60),
scale=0.8,
autoselect=True,
label=ba.Lstr(resource='doneText'),
on_activate_call=self._done,
)
self._copy_button = ba.buttonwidget(
parent=self._root_widget,
position=(self._width - 200, self._height - 77),
size=(100, 60),
scale=0.8,
autoselect=True,
label=ba.Lstr(resource='copyText'),
on_activate_call=self._copy,
)
self._settings_button = ba.buttonwidget(
parent=self._root_widget,
position=(self._width - 100, self._height - 77),
size=(60, 60),
scale=0.8,
autoselect=True,
label=ba.Lstr(value='...'),
on_activate_call=self._show_val_testing,
)
twidth = self._width - 450
ba.textwidget(
parent=self._root_widget,
position=(self._width * 0.5, self._height - 55),
size=(0, 0),
text=ba.Lstr(resource='settingsWindowAdvanced.netTestingText'),
color=(0.8, 0.8, 0.8, 1.0),
h_align='center',
v_align='center',
maxwidth=twidth,
)
self._scroll = ba.scrollwidget(
parent=self._root_widget,
position=(50, 50),
size=(self._width - 100, self._height - 140),
capture_arrows=True,
autoselect=True,
)
self._rows = ba.columnwidget(parent=self._scroll)
ba.containerwidget(
edit=self._root_widget, cancel_button=self._done_button
)
# Now kick off the tests.
# Pass a weak-ref to this window so we don't keep it alive
# if we back out before it completes. Also set is as daemon
# so it doesn't keep the app running if the user is trying to quit.
Thread(
daemon=True,
target=ba.Call(_run_diagnostics, weakref.ref(self)),
).start()
def print(self, text: str, color: tuple[float, float, float]) -> None:
"""Print text to our console thingie."""
for line in text.splitlines():
txt = ba.textwidget(
parent=self._rows,
color=color,
text=line,
scale=0.75,
flatness=1.0,
shadow=0.0,
size=(0, 20),
)
ba.containerwidget(edit=self._rows, visible_child=txt)
self._printed_lines.append(line)
def _copy(self) -> None:
if not ba.clipboard_is_supported():
ba.screenmessage(
'Clipboard not supported on this platform.', color=(1, 0, 0)
)
return
ba.clipboard_set_text('\n'.join(self._printed_lines))
ba.screenmessage(f'{len(self._printed_lines)} lines copied.')
def _show_val_testing(self) -> None:
ba.app.ui.set_main_menu_window(NetValTestingWindow().get_root_widget())
ba.containerwidget(edit=self._root_widget, transition='out_left')
def _done(self) -> None:
# pylint: disable=cyclic-import
from bastd.ui.settings.advanced import AdvancedSettingsWindow
ba.app.ui.set_main_menu_window(
AdvancedSettingsWindow(transition='in_left').get_root_widget()
)
ba.containerwidget(edit=self._root_widget, transition='out_right')
def _run_diagnostics(weakwin: weakref.ref[NetTestingWindow]) -> None:
# pylint: disable=too-many-statements
from efro.util import utc_now
have_error = [False]
# We're running in a background thread but UI stuff needs to run
# in the logic thread; give ourself a way to pass stuff to it.
def _print(
text: str, color: tuple[float, float, float] | None = None
) -> None:
def _print_in_logic_thread() -> None:
win = weakwin()
if win is not None:
win.print(text, (1.0, 1.0, 1.0) if color is None else color)
ba.pushcall(_print_in_logic_thread, from_other_thread=True)
def _print_test_results(call: Callable[[], Any]) -> bool:
"""Run the provided call, print result, & return success."""
starttime = time.monotonic()
try:
call()
duration = time.monotonic() - starttime
_print(f'Succeeded in {duration:.2f}s.', color=(0, 1, 0))
return True
except Exception as exc:
import traceback
duration = time.monotonic() - starttime
msg = (
str(exc)
if isinstance(exc, CleanError)
else traceback.format_exc()
)
_print(msg, color=(1.0, 1.0, 0.3))
_print(f'Failed in {duration:.2f}s.', color=(1, 0, 0))
have_error[0] = True
return False
try:
_print(
f'Running network diagnostics...\n'
f'ua: {ba.app.user_agent_string}\n'
f'time: {utc_now()}.'
)
if bool(False):
_print('\nRunning dummy success test...')
_print_test_results(_dummy_success)
_print('\nRunning dummy fail test...')
_print_test_results(_dummy_fail)
# V1 ping
baseaddr = ba.internal.get_master_server_address(source=0, version=1)
_print(f'\nContacting V1 master-server src0 ({baseaddr})...')
v1worked = _print_test_results(lambda: _test_fetch(baseaddr))
# V1 alternate ping (only if primary fails since this often fails).
if v1worked:
_print('\nSkipping V1 master-server src1 test since src0 worked.')
else:
baseaddr = ba.internal.get_master_server_address(
source=1, version=1
)
_print(f'\nContacting V1 master-server src1 ({baseaddr})...')
_print_test_results(lambda: _test_fetch(baseaddr))
if 'none succeeded' in ba.app.net.v1_test_log:
_print(
f'\nV1-test-log failed: {ba.app.net.v1_test_log}',
color=(1, 0, 0),
)
have_error[0] = True
else:
_print(f'\nV1-test-log ok: {ba.app.net.v1_test_log}')
for srcid, result in sorted(ba.app.net.v1_ctest_results.items()):
_print(f'\nV1 src{srcid} result: {result}')
curv1addr = ba.internal.get_master_server_address(version=1)
_print(f'\nUsing V1 address: {curv1addr}')
_print('\nRunning V1 transaction...')
_print_test_results(_test_v1_transaction)
# V2 ping
baseaddr = ba.internal.get_master_server_address(version=2)
_print(f'\nContacting V2 master-server ({baseaddr})...')
_print_test_results(lambda: _test_fetch(baseaddr))
_print('\nComparing local time to V2 server...')
_print_test_results(_test_v2_time)
# Get V2 nearby zone
with ba.app.net.zone_pings_lock:
zone_pings = copy.deepcopy(ba.app.net.zone_pings)
nearest_zone = (
None
if not zone_pings
else sorted(zone_pings.items(), key=lambda i: i[1])[0]
)
if nearest_zone is not None:
nearstr = f'{nearest_zone[0]}: {nearest_zone[1]:.0f}ms'
else:
nearstr = '-'
_print(f'\nChecking nearest V2 zone ping ({nearstr})...')
_print_test_results(lambda: _test_nearby_zone_ping(nearest_zone))
_print('\nSending V2 cloud message...')
_print_test_results(_test_v2_cloud_message)
if have_error[0]:
_print(
'\nDiagnostics complete. Some diagnostics failed.',
color=(10, 0, 0),
)
else:
_print(
'\nDiagnostics complete. Everything looks good!',
color=(0, 1, 0),
)
except Exception:
import traceback
_print(
f'An unexpected error occurred during testing;'
f' please report this.\n'
f'{traceback.format_exc()}',
color=(1, 0, 0),
)
def _dummy_success() -> None:
"""Dummy success test."""
time.sleep(1.2)
def _dummy_fail() -> None:
"""Dummy fail test case."""
raise RuntimeError('fail-test')
def _test_v1_transaction() -> None:
"""Dummy fail test case."""
if ba.internal.get_v1_account_state() != 'signed_in':
raise RuntimeError('Not signed in.')
starttime = time.monotonic()
# Gets set to True on success or string on error.
results: list[Any] = [False]
def _cb(cbresults: Any) -> None:
# Simply set results here; our other thread acts on them.
if not isinstance(cbresults, dict) or 'party_code' not in cbresults:
results[0] = 'Unexpected transaction response'
return
results[0] = True # Success!
def _do_it() -> None:
# Fire off a transaction with a callback.
ba.internal.add_transaction(
{
'type': 'PRIVATE_PARTY_QUERY',
'expire_time': time.time() + 20,
},
callback=_cb,
)
ba.internal.run_transactions()
ba.pushcall(_do_it, from_other_thread=True)
while results[0] is False:
time.sleep(0.01)
if time.monotonic() - starttime > MAX_TEST_SECONDS:
raise RuntimeError(
f'test timed out after {MAX_TEST_SECONDS} seconds'
)
# If we got left a string, its an error.
if isinstance(results[0], str):
raise RuntimeError(results[0])
def _test_v2_cloud_message() -> None:
from dataclasses import dataclass
import bacommon.cloud
@dataclass
class _Results:
errstr: str | None = None
send_time: float | None = None
response_time: float | None = None
results = _Results()
def _cb(response: bacommon.cloud.PingResponse | Exception) -> None:
# Note: this runs in another thread so need to avoid exceptions.
results.response_time = time.monotonic()
if isinstance(response, Exception):
results.errstr = str(response)
if not isinstance(response, bacommon.cloud.PingResponse):
results.errstr = f'invalid response type: {type(response)}.'
def _send() -> None:
# Note: this runs in another thread so need to avoid exceptions.
results.send_time = time.monotonic()
ba.app.cloud.send_message_cb(bacommon.cloud.PingMessage(), _cb)
# This stuff expects to be run from the logic thread.
ba.pushcall(_send, from_other_thread=True)
wait_start_time = time.monotonic()
while True:
if results.response_time is not None:
break
time.sleep(0.01)
if time.monotonic() - wait_start_time > MAX_TEST_SECONDS:
raise RuntimeError(
f'Timeout ({MAX_TEST_SECONDS} seconds)'
f' waiting for cloud message response'
)
if results.errstr is not None:
raise RuntimeError(results.errstr)
def _test_v2_time() -> None:
offset = ba.app.net.server_time_offset_hours
if offset is None:
raise RuntimeError(
'no time offset found;'
' perhaps unable to communicate with v2 server?'
)
if abs(offset) >= 2.0:
raise CleanError(
f'Your device time is off from world time by {offset:.1f} hours.\n'
'This may cause network operations to fail due to your device\n'
' incorrectly treating SSL certificates as not-yet-valid, etc.\n'
'Check your device time and time-zone settings to fix this.\n'
)
def _test_fetch(baseaddr: str) -> None:
# pylint: disable=consider-using-with
import urllib.request
response = urllib.request.urlopen(
urllib.request.Request(
f'{baseaddr}/ping', None, {'User-Agent': ba.app.user_agent_string}
),
context=ba.app.net.sslcontext,
timeout=MAX_TEST_SECONDS,
)
if response.getcode() != 200:
raise RuntimeError(
f'Got unexpected response code {response.getcode()}.'
)
data = response.read()
if data != b'pong':
raise RuntimeError('Got unexpected response data.')
def _test_nearby_zone_ping(nearest_zone: tuple[str, float] | None) -> None:
"""Try to ping nearest v2 zone."""
if nearest_zone is None:
raise RuntimeError('No nearest zone.')
if nearest_zone[1] > 500:
raise RuntimeError('Ping too high.')
class NetValTestingWindow(TestingWindow):
"""Window to test network related settings."""
def __init__(self, transition: str = 'in_right'):
entries = [
{'name': 'bufferTime', 'label': 'Buffer Time', 'increment': 1.0},
{
'name': 'delaySampling',
'label': 'Delay Sampling',
'increment': 1.0,
},
{
'name': 'dynamicsSyncTime',
'label': 'Dynamics Sync Time',
'increment': 10,
},
{'name': 'showNetInfo', 'label': 'Show Net Info', 'increment': 1},
]
super().__init__(
title=ba.Lstr(resource='settingsWindowAdvanced.netTestingText'),
entries=entries,
transition=transition,
back_call=lambda: NetTestingWindow(transition='in_left'),
)

View file

@ -0,0 +1,264 @@
# Released under the MIT License. See LICENSE for details.
#
"""Plugin Window UI."""
from __future__ import annotations
from typing import TYPE_CHECKING
import ba
if TYPE_CHECKING:
pass
class PluginWindow(ba.Window):
"""Window for configuring plugins."""
def __init__(
self,
transition: str = 'in_right',
origin_widget: ba.Widget | None = None,
):
# pylint: disable=too-many-locals
# pylint: disable=too-many-statements
app = ba.app
# If they provided an origin-widget, scale up from that.
scale_origin: tuple[float, float] | None
if origin_widget is not None:
self._transition_out = 'out_scale'
scale_origin = origin_widget.get_screen_space_center()
transition = 'in_scale'
else:
self._transition_out = 'out_right'
scale_origin = None
uiscale = ba.app.ui.uiscale
self._width = 870.0 if uiscale is ba.UIScale.SMALL else 670.0
x_inset = 100 if uiscale is ba.UIScale.SMALL else 0
self._height = (
390.0
if uiscale is ba.UIScale.SMALL
else 450.0
if uiscale is ba.UIScale.MEDIUM
else 520.0
)
top_extra = 10 if uiscale is ba.UIScale.SMALL else 0
super().__init__(
root_widget=ba.containerwidget(
size=(self._width, self._height + top_extra),
transition=transition,
toolbar_visibility='menu_minimal',
scale_origin_stack_offset=scale_origin,
scale=(
2.06
if uiscale is ba.UIScale.SMALL
else 1.4
if uiscale is ba.UIScale.MEDIUM
else 1.0
),
stack_offset=(0, -25)
if uiscale is ba.UIScale.SMALL
else (0, 0),
)
)
self._scroll_width = self._width - (100 + 2 * x_inset)
self._scroll_height = self._height - 115.0
self._sub_width = self._scroll_width * 0.95
self._sub_height = 724.0
if app.ui.use_toolbars and uiscale is ba.UIScale.SMALL:
ba.containerwidget(
edit=self._root_widget, on_cancel_call=self._do_back
)
self._back_button = None
else:
self._back_button = ba.buttonwidget(
parent=self._root_widget,
position=(53 + x_inset, self._height - 60),
size=(140, 60),
scale=0.8,
autoselect=True,
label=ba.Lstr(resource='backText'),
button_type='back',
on_activate_call=self._do_back,
)
ba.containerwidget(
edit=self._root_widget, cancel_button=self._back_button
)
self._title_text = ba.textwidget(
parent=self._root_widget,
position=(0, self._height - 52),
size=(self._width, 25),
text=ba.Lstr(resource='pluginsText'),
color=app.ui.title_color,
h_align='center',
v_align='top',
)
if self._back_button is not None:
ba.buttonwidget(
edit=self._back_button,
button_type='backSmall',
size=(60, 60),
label=ba.charstr(ba.SpecialChar.BACK),
)
settings_button_x = 670 if uiscale is ba.UIScale.SMALL else 570
self._settings_button = ba.buttonwidget(
parent=self._root_widget,
position=(settings_button_x, self._height - 60),
size=(40, 40),
label='',
on_activate_call=self._open_settings,
)
ba.imagewidget(
parent=self._root_widget,
position=(settings_button_x + 3, self._height - 60),
size=(35, 35),
texture=ba.gettexture('settingsIcon'),
)
ba.widget(
edit=self._settings_button,
up_widget=self._settings_button,
right_widget=self._settings_button,
)
self._scrollwidget = ba.scrollwidget(
parent=self._root_widget,
position=(50 + x_inset, 50),
simple_culling_v=20.0,
highlight=False,
size=(self._scroll_width, self._scroll_height),
selection_loops_to_parent=True,
claims_left_right=True,
)
ba.widget(edit=self._scrollwidget, right_widget=self._scrollwidget)
if ba.app.meta.scanresults is None:
ba.screenmessage(
'Still scanning plugins; please try again.', color=(1, 0, 0)
)
ba.playsound(ba.getsound('error'))
pluglist = ba.app.plugins.potential_plugins
plugstates: dict[str, dict] = ba.app.config.setdefault('Plugins', {})
assert isinstance(plugstates, dict)
plug_line_height = 50
sub_width = self._scroll_width
sub_height = len(pluglist) * plug_line_height
self._subcontainer = ba.containerwidget(
parent=self._scrollwidget,
size=(sub_width, sub_height),
background=False,
)
for i, availplug in enumerate(pluglist):
plugin = ba.app.plugins.active_plugins.get(availplug.class_path)
active = plugin is not None
plugstate = plugstates.setdefault(availplug.class_path, {})
checked = plugstate.get('enabled', False)
assert isinstance(checked, bool)
item_y = sub_height - (i + 1) * plug_line_height
check = ba.checkboxwidget(
parent=self._subcontainer,
text=availplug.display_name,
autoselect=True,
value=checked,
maxwidth=self._scroll_width - 200,
position=(10, item_y),
size=(self._scroll_width - 40, 50),
on_value_change_call=ba.Call(
self._check_value_changed, availplug
),
textcolor=(
(0.8, 0.3, 0.3)
if not availplug.available
else (0, 1, 0)
if active
else (0.6, 0.6, 0.6)
),
)
if plugin is not None and plugin.has_settings_ui():
button = ba.buttonwidget(
parent=self._subcontainer,
label=ba.Lstr(resource='mainMenu.settingsText'),
autoselect=True,
size=(100, 40),
position=(sub_width - 130, item_y + 6),
)
ba.buttonwidget(
edit=button,
on_activate_call=ba.Call(plugin.show_settings_ui, button),
)
else:
button = None
# Allow getting back to back button.
if i == 0:
ba.widget(
edit=check,
up_widget=self._back_button,
left_widget=self._back_button,
right_widget=self._settings_button,
)
if button is not None:
ba.widget(edit=button, up_widget=self._back_button)
# Make sure we scroll all the way to the end when using
# keyboard/button nav.
ba.widget(edit=check, show_buffer_top=40, show_buffer_bottom=40)
ba.containerwidget(
edit=self._root_widget, selected_child=self._scrollwidget
)
self._restore_state()
def _check_value_changed(
self, plug: ba.PotentialPlugin, value: bool
) -> None:
ba.screenmessage(
ba.Lstr(resource='settingsWindowAdvanced.mustRestartText'),
color=(1.0, 0.5, 0.0),
)
plugstates: dict[str, dict] = ba.app.config.setdefault('Plugins', {})
assert isinstance(plugstates, dict)
plugstate = plugstates.setdefault(plug.class_path, {})
plugstate['enabled'] = value
ba.app.config.commit()
def _open_settings(self) -> None:
# pylint: disable=cyclic-import
from bastd.ui.settings.pluginsettings import PluginSettingsWindow
ba.playsound(ba.getsound('swish'))
ba.containerwidget(edit=self._root_widget, transition='out_left')
ba.app.ui.set_main_menu_window(
PluginSettingsWindow(transition='in_right').get_root_widget()
)
def _save_state(self) -> None:
pass
def _restore_state(self) -> None:
pass
def _do_back(self) -> None:
# pylint: disable=cyclic-import
from bastd.ui.settings.advanced import AdvancedSettingsWindow
self._save_state()
ba.containerwidget(
edit=self._root_widget, transition=self._transition_out
)
ba.app.ui.set_main_menu_window(
AdvancedSettingsWindow(transition='in_left').get_root_widget()
)

View file

@ -0,0 +1,174 @@
# Released under the MIT License. See LICENSE for details.
#
"""Plugin Settings UI."""
from __future__ import annotations
from typing import TYPE_CHECKING
import ba
from bastd.ui.confirm import ConfirmWindow
if TYPE_CHECKING:
pass
class PluginSettingsWindow(ba.Window):
"""Plugin Settings Window"""
def __init__(self, transition: str = 'in_right'):
scale_origin: tuple[float, float] | None
self._transition_out = 'out_right'
scale_origin = None
uiscale = ba.app.ui.uiscale
width = 470.0 if uiscale is ba.UIScale.SMALL else 470.0
height = (
365.0
if uiscale is ba.UIScale.SMALL
else 300.0
if uiscale is ba.UIScale.MEDIUM
else 370.0
)
top_extra = 10 if uiscale is ba.UIScale.SMALL else 0
super().__init__(
root_widget=ba.containerwidget(
size=(width, height + top_extra),
transition=transition,
toolbar_visibility='menu_minimal',
scale_origin_stack_offset=scale_origin,
scale=(
2.06
if uiscale is ba.UIScale.SMALL
else 1.4
if uiscale is ba.UIScale.MEDIUM
else 1.0
),
stack_offset=(0, -25)
if uiscale is ba.UIScale.SMALL
else (0, 0),
)
)
self._back_button = ba.buttonwidget(
parent=self._root_widget,
position=(53, height - 60),
size=(60, 60),
scale=0.8,
autoselect=True,
label=ba.charstr(ba.SpecialChar.BACK),
button_type='backSmall',
on_activate_call=self._do_back,
)
ba.containerwidget(
edit=self._root_widget, cancel_button=self._back_button
)
self._title_text = ba.textwidget(
parent=self._root_widget,
position=(0, height - 52),
size=(width, 25),
text=ba.Lstr(resource='pluginSettingsText'),
color=ba.app.ui.title_color,
h_align='center',
v_align='top',
)
self._y_position = 170 if uiscale is ba.UIScale.MEDIUM else 205
self._enable_plugins_button = ba.buttonwidget(
parent=self._root_widget,
position=(65, self._y_position),
size=(350, 60),
autoselect=True,
label=ba.Lstr(resource='pluginsEnableAllText'),
text_scale=1.0,
on_activate_call=lambda: ConfirmWindow(
action=self._enable_all_plugins,
),
)
self._y_position -= 70
self._disable_plugins_button = ba.buttonwidget(
parent=self._root_widget,
position=(65, self._y_position),
size=(350, 60),
autoselect=True,
label=ba.Lstr(resource='pluginsDisableAllText'),
text_scale=1.0,
on_activate_call=lambda: ConfirmWindow(
action=self._disable_all_plugins,
),
)
self._y_position -= 70
self._enable_new_plugins_check_box = ba.checkboxwidget(
parent=self._root_widget,
position=(65, self._y_position),
size=(350, 60),
value=ba.app.config.get(
ba.app.plugins.AUTO_ENABLE_NEW_PLUGINS_CONFIG_KEY,
ba.app.plugins.AUTO_ENABLE_NEW_PLUGINS_DEFAULT,
),
text=ba.Lstr(resource='pluginsAutoEnableNewText'),
scale=1.0,
maxwidth=308,
on_value_change_call=self._update_value,
)
ba.widget(
edit=self._back_button, down_widget=self._enable_plugins_button
)
ba.widget(
edit=self._disable_plugins_button,
left_widget=self._disable_plugins_button,
)
ba.widget(
edit=self._enable_new_plugins_check_box,
left_widget=self._enable_new_plugins_check_box,
right_widget=self._enable_new_plugins_check_box,
down_widget=self._enable_new_plugins_check_box,
)
def _enable_all_plugins(self) -> None:
cfg = ba.app.config
plugs: dict[str, dict] = cfg.setdefault('Plugins', {})
for plug in plugs.values():
plug['enabled'] = True
cfg.apply_and_commit()
ba.screenmessage(
ba.Lstr(resource='settingsWindowAdvanced.mustRestartText'),
color=(1.0, 0.5, 0.0),
)
def _disable_all_plugins(self) -> None:
cfg = ba.app.config
plugs: dict[str, dict] = cfg.setdefault('Plugins', {})
for plug in plugs.values():
plug['enabled'] = False
cfg.apply_and_commit()
ba.screenmessage(
ba.Lstr(resource='settingsWindowAdvanced.mustRestartText'),
color=(1.0, 0.5, 0.0),
)
def _update_value(self, val: bool) -> None:
cfg = ba.app.config
cfg[ba.app.plugins.AUTO_ENABLE_NEW_PLUGINS_CONFIG_KEY] = val
cfg.apply_and_commit()
def _do_back(self) -> None:
# pylint: disable=cyclic-import
from bastd.ui.settings.plugins import PluginWindow
ba.containerwidget(
edit=self._root_widget, transition=self._transition_out
)
ba.app.ui.set_main_menu_window(
PluginWindow(transition='in_left').get_root_widget()
)

View file

@ -0,0 +1,107 @@
# Released under the MIT License. See LICENSE for details.
#
"""Settings UI related to PS3 controllers."""
from __future__ import annotations
import _ba
import ba
class PS3ControllerSettingsWindow(ba.Window):
"""UI showing info about using PS3 controllers."""
def __init__(self) -> None:
width = 760
height = 330 if _ba.is_running_on_fire_tv() else 540
spacing = 40
self._r = 'ps3ControllersWindow'
uiscale = ba.app.ui.uiscale
super().__init__(root_widget=ba.containerwidget(
size=(width, height),
transition='in_right',
scale=(1.35 if uiscale is ba.UIScale.SMALL else
1.3 if uiscale is ba.UIScale.MEDIUM else 1.0)))
btn = ba.buttonwidget(parent=self._root_widget,
position=(37, height - 73),
size=(135, 65),
scale=0.85,
label=ba.Lstr(resource='backText'),
button_type='back',
autoselect=True,
on_activate_call=self._back)
ba.containerwidget(edit=self._root_widget, cancel_button=btn)
ba.textwidget(parent=self._root_widget,
position=(width * 0.5, height - 46),
size=(0, 0),
maxwidth=410,
text=ba.Lstr(resource=self._r + '.titleText',
subs=[('${APP_NAME}',
ba.Lstr(resource='titleText'))]),
color=ba.app.ui.title_color,
h_align='center',
v_align='center')
ba.buttonwidget(edit=btn,
button_type='backSmall',
size=(60, 60),
label=ba.charstr(ba.SpecialChar.BACK))
v = height - 90
v -= spacing
if _ba.is_running_on_fire_tv():
ba.textwidget(parent=self._root_widget,
position=(width * 0.5, height * 0.45),
size=(0, 0),
color=(0.7, 0.9, 0.7, 1.0),
maxwidth=width * 0.95,
max_height=height * 0.8,
scale=1.0,
text=ba.Lstr(resource=self._r +
'.ouyaInstructionsText'),
h_align='center',
v_align='center')
else:
txts = ba.Lstr(resource=self._r +
'.macInstructionsText').evaluate().split('\n\n\n')
ba.textwidget(parent=self._root_widget,
position=(width * 0.5, v - 29),
size=(0, 0),
color=(0.7, 0.9, 0.7, 1.0),
maxwidth=width * 0.95,
max_height=170,
scale=1.0,
text=txts[0].strip(),
h_align='center',
v_align='center')
if txts:
ba.textwidget(parent=self._root_widget,
position=(width * 0.5, v - 280),
size=(0, 0),
color=(0.7, 0.9, 0.7, 1.0),
maxwidth=width * 0.95,
max_height=170,
scale=1.0,
text=txts[1].strip(),
h_align='center',
v_align='center')
ba.buttonwidget(parent=self._root_widget,
position=(225, v - 176),
size=(300, 40),
label=ba.Lstr(resource=self._r +
'.pairingTutorialText'),
autoselect=True,
on_activate_call=ba.Call(
ba.open_url, 'http://www.youtube.com/watch'
'?v=IlR_HxeOQpI&feature=related'))
def _back(self) -> None:
from bastd.ui.settings import controls
ba.containerwidget(edit=self._root_widget, transition='out_right')
ba.app.ui.set_main_menu_window(
controls.ControlsSettingsWindow(
transition='in_left').get_root_widget())

View file

@ -0,0 +1,147 @@
# Released under the MIT License. See LICENSE for details.
#
"""Settings UI functionality related to the remote app."""
from __future__ import annotations
import ba
class RemoteAppSettingsWindow(ba.Window):
"""Window showing info/settings related to the remote app."""
def __init__(self) -> None:
from ba.internal import get_remote_app_name
self._r = 'connectMobileDevicesWindow'
width = 700
height = 390
spacing = 40
uiscale = ba.app.ui.uiscale
super().__init__(
root_widget=ba.containerwidget(
size=(width, height),
transition='in_right',
scale=(
1.85
if uiscale is ba.UIScale.SMALL
else 1.3
if uiscale is ba.UIScale.MEDIUM
else 1.0
),
stack_offset=(-10, 0)
if uiscale is ba.UIScale.SMALL
else (0, 0),
)
)
btn = ba.buttonwidget(
parent=self._root_widget,
position=(40, height - 67),
size=(140, 65),
scale=0.8,
label=ba.Lstr(resource='backText'),
button_type='back',
text_scale=1.1,
autoselect=True,
on_activate_call=self._back,
)
ba.containerwidget(edit=self._root_widget, cancel_button=btn)
ba.textwidget(
parent=self._root_widget,
position=(width * 0.5, height - 42),
size=(0, 0),
text=ba.Lstr(resource=self._r + '.titleText'),
maxwidth=370,
color=ba.app.ui.title_color,
scale=0.8,
h_align='center',
v_align='center',
)
ba.buttonwidget(
edit=btn,
button_type='backSmall',
size=(60, 60),
label=ba.charstr(ba.SpecialChar.BACK),
)
v = height - 70.0
v -= spacing * 1.2
ba.textwidget(
parent=self._root_widget,
position=(15, v - 26),
size=(width - 30, 30),
maxwidth=width * 0.95,
color=(0.7, 0.9, 0.7, 1.0),
scale=0.8,
text=ba.Lstr(
resource=self._r + '.explanationText',
subs=[
('${APP_NAME}', ba.Lstr(resource='titleText')),
('${REMOTE_APP_NAME}', get_remote_app_name()),
],
),
max_height=100,
h_align='center',
v_align='center',
)
v -= 90
# hmm the itms:// version doesnt bounce through safari but is kinda
# apple-specific-ish
# Update: now we just show link to the remote webpage.
ba.textwidget(
parent=self._root_widget,
position=(width * 0.5, v + 5),
size=(0, 0),
color=(0.7, 0.9, 0.7, 1.0),
scale=1.4,
text='bombsquadgame.com/remote',
maxwidth=width * 0.95,
max_height=60,
h_align='center',
v_align='center',
)
v -= 30
ba.textwidget(
parent=self._root_widget,
position=(width * 0.5, v - 35),
size=(0, 0),
color=(0.7, 0.9, 0.7, 0.8),
scale=0.65,
text=ba.Lstr(resource=self._r + '.bestResultsText'),
maxwidth=width * 0.95,
max_height=height * 0.19,
h_align='center',
v_align='center',
)
ba.checkboxwidget(
parent=self._root_widget,
position=(width * 0.5 - 150, v - 116),
size=(300, 30),
maxwidth=300,
scale=0.8,
value=not ba.app.config.resolve('Enable Remote App'),
autoselect=True,
text=ba.Lstr(resource='disableRemoteAppConnectionsText'),
on_value_change_call=self._on_check_changed,
)
def _on_check_changed(self, value: bool) -> None:
cfg = ba.app.config
cfg['Enable Remote App'] = not value
cfg.apply_and_commit()
def _back(self) -> None:
from bastd.ui.settings import controls
ba.containerwidget(edit=self._root_widget, transition='out_right')
ba.app.ui.set_main_menu_window(
controls.ControlsSettingsWindow(
transition='in_left'
).get_root_widget()
)

View file

@ -0,0 +1,224 @@
# Released under the MIT License. See LICENSE for details.
#
"""Provides UI for test settings."""
from __future__ import annotations
import copy
from typing import TYPE_CHECKING
import ba
import ba.internal
if TYPE_CHECKING:
from typing import Any, Callable
class TestingWindow(ba.Window):
"""Window for conveniently testing various settings."""
def __init__(
self,
title: ba.Lstr,
entries: list[dict[str, Any]],
transition: str = 'in_right',
back_call: Callable[[], ba.Window] | None = None,
):
uiscale = ba.app.ui.uiscale
self._width = 600
self._height = 324 if uiscale is ba.UIScale.SMALL else 400
self._entries = copy.deepcopy(entries)
self._back_call = back_call
super().__init__(
root_widget=ba.containerwidget(
size=(self._width, self._height),
transition=transition,
scale=(
2.5
if uiscale is ba.UIScale.SMALL
else 1.2
if uiscale is ba.UIScale.MEDIUM
else 1.0
),
stack_offset=(0, -28)
if uiscale is ba.UIScale.SMALL
else (0, 0),
)
)
self._back_button = btn = ba.buttonwidget(
parent=self._root_widget,
autoselect=True,
position=(65, self._height - 59),
size=(130, 60),
scale=0.8,
text_scale=1.2,
label=ba.Lstr(resource='backText'),
button_type='back',
on_activate_call=self._do_back,
)
ba.textwidget(
parent=self._root_widget,
position=(self._width * 0.5, self._height - 35),
size=(0, 0),
color=ba.app.ui.title_color,
h_align='center',
v_align='center',
maxwidth=245,
text=title,
)
ba.buttonwidget(
edit=self._back_button,
button_type='backSmall',
size=(60, 60),
label=ba.charstr(ba.SpecialChar.BACK),
)
ba.textwidget(
parent=self._root_widget,
position=(self._width * 0.5, self._height - 75),
size=(0, 0),
color=ba.app.ui.infotextcolor,
h_align='center',
v_align='center',
maxwidth=self._width * 0.75,
text=ba.Lstr(resource='settingsWindowAdvanced.forTestingText'),
)
ba.containerwidget(edit=self._root_widget, cancel_button=btn)
self._scroll_width = self._width - 130
self._scroll_height = self._height - 140
self._scrollwidget = ba.scrollwidget(
parent=self._root_widget,
size=(self._scroll_width, self._scroll_height),
highlight=False,
position=((self._width - self._scroll_width) * 0.5, 40),
)
ba.containerwidget(edit=self._scrollwidget, claims_left_right=True)
self._spacing = 50
self._sub_width = self._scroll_width * 0.95
self._sub_height = 50 + len(self._entries) * self._spacing + 60
self._subcontainer = ba.containerwidget(
parent=self._scrollwidget,
size=(self._sub_width, self._sub_height),
background=False,
)
h = 230
v = self._sub_height - 48
for i, entry in enumerate(self._entries):
entry_name = entry['name']
# If we haven't yet, record the default value for this name so
# we can reset if we want..
if entry_name not in ba.app.value_test_defaults:
ba.app.value_test_defaults[entry_name] = ba.internal.value_test(
entry_name
)
ba.textwidget(
parent=self._subcontainer,
position=(h, v),
size=(0, 0),
h_align='right',
v_align='center',
maxwidth=200,
text=entry['label'],
)
btn = ba.buttonwidget(
parent=self._subcontainer,
position=(h + 20, v - 19),
size=(40, 40),
autoselect=True,
repeat=True,
left_widget=self._back_button,
button_type='square',
label='-',
on_activate_call=ba.Call(self._on_minus_press, entry['name']),
)
if i == 0:
ba.widget(edit=btn, up_widget=self._back_button)
# pylint: disable=consider-using-f-string
entry['widget'] = ba.textwidget(
parent=self._subcontainer,
position=(h + 100, v),
size=(0, 0),
h_align='center',
v_align='center',
maxwidth=60,
text='%.4g' % ba.internal.value_test(entry_name),
)
btn = ba.buttonwidget(
parent=self._subcontainer,
position=(h + 140, v - 19),
size=(40, 40),
autoselect=True,
repeat=True,
button_type='square',
label='+',
on_activate_call=ba.Call(self._on_plus_press, entry['name']),
)
if i == 0:
ba.widget(edit=btn, up_widget=self._back_button)
v -= self._spacing
v -= 35
ba.buttonwidget(
parent=self._subcontainer,
autoselect=True,
size=(200, 50),
position=(self._sub_width * 0.5 - 100, v),
label=ba.Lstr(resource='settingsWindowAdvanced.resetText'),
right_widget=btn,
on_activate_call=self._on_reset_press,
)
def _get_entry(self, name: str) -> dict[str, Any]:
for entry in self._entries:
if entry['name'] == name:
return entry
raise ba.NotFoundError(f'Entry not found: {name}')
def _on_reset_press(self) -> None:
for entry in self._entries:
ba.internal.value_test(
entry['name'],
absolute=ba.app.value_test_defaults[entry['name']],
)
# pylint: disable=consider-using-f-string
ba.textwidget(
edit=entry['widget'],
text='%.4g' % ba.internal.value_test(entry['name']),
)
def _on_minus_press(self, entry_name: str) -> None:
entry = self._get_entry(entry_name)
ba.internal.value_test(entry['name'], change=-entry['increment'])
# pylint: disable=consider-using-f-string
ba.textwidget(
edit=entry['widget'],
text='%.4g' % ba.internal.value_test(entry['name']),
)
def _on_plus_press(self, entry_name: str) -> None:
entry = self._get_entry(entry_name)
ba.internal.value_test(entry['name'], change=entry['increment'])
# pylint: disable=consider-using-f-string
ba.textwidget(
edit=entry['widget'],
text='%.4g' % ba.internal.value_test(entry['name']),
)
def _do_back(self) -> None:
# pylint: disable=cyclic-import
from bastd.ui.settings.advanced import AdvancedSettingsWindow
ba.containerwidget(edit=self._root_widget, transition='out_right')
backwin = (
self._back_call()
if self._back_call is not None
else AdvancedSettingsWindow(transition='in_left')
)
ba.app.ui.set_main_menu_window(backwin.get_root_widget())

View file

@ -0,0 +1,283 @@
# Released under the MIT License. See LICENSE for details.
#
"""UI settings functionality related to touchscreens."""
from __future__ import annotations
import ba
import ba.internal
class TouchscreenSettingsWindow(ba.Window):
"""Settings window for touchscreens."""
def __del__(self) -> None:
# Note - this happens in 'back' too;
# we just do it here too in case the window is closed by other means.
# FIXME: Could switch to a UI destroy callback now that those are a
# thing that exists.
ba.internal.set_touchscreen_editing(False)
def __init__(self) -> None:
self._width = 650
self._height = 380
self._spacing = 40
self._r = 'configTouchscreenWindow'
ba.internal.set_touchscreen_editing(True)
uiscale = ba.app.ui.uiscale
super().__init__(
root_widget=ba.containerwidget(
size=(self._width, self._height),
transition='in_right',
scale=(
1.9
if uiscale is ba.UIScale.SMALL
else 1.55
if uiscale is ba.UIScale.MEDIUM
else 1.2
),
)
)
btn = ba.buttonwidget(
parent=self._root_widget,
position=(55, self._height - 60),
size=(120, 60),
label=ba.Lstr(resource='backText'),
button_type='back',
scale=0.8,
on_activate_call=self._back,
)
ba.containerwidget(edit=self._root_widget, cancel_button=btn)
ba.textwidget(
parent=self._root_widget,
position=(25, self._height - 50),
size=(self._width, 25),
text=ba.Lstr(resource=self._r + '.titleText'),
color=ba.app.ui.title_color,
maxwidth=280,
h_align='center',
v_align='center',
)
ba.buttonwidget(
edit=btn,
button_type='backSmall',
size=(60, 60),
label=ba.charstr(ba.SpecialChar.BACK),
)
self._scroll_width = self._width - 100
self._scroll_height = self._height - 110
self._sub_width = self._scroll_width - 20
self._sub_height = 360
self._scrollwidget = ba.scrollwidget(
parent=self._root_widget,
position=(
(self._width - self._scroll_width) * 0.5,
self._height - 65 - self._scroll_height,
),
size=(self._scroll_width, self._scroll_height),
claims_left_right=True,
claims_tab=True,
selection_loops_to_parent=True,
)
self._subcontainer = ba.containerwidget(
parent=self._scrollwidget,
size=(self._sub_width, self._sub_height),
background=False,
claims_left_right=True,
claims_tab=True,
selection_loops_to_parent=True,
)
self._build_gui()
def _build_gui(self) -> None:
from bastd.ui.config import ConfigNumberEdit, ConfigCheckBox
from bastd.ui.radiogroup import make_radio_group
# Clear anything already there.
children = self._subcontainer.get_children()
for child in children:
child.delete()
h = 30
v = self._sub_height - 85
clr = (0.8, 0.8, 0.8, 1.0)
clr2 = (0.8, 0.8, 0.8)
ba.textwidget(
parent=self._subcontainer,
position=(-10, v + 43),
size=(self._sub_width, 25),
text=ba.Lstr(resource=self._r + '.swipeInfoText'),
flatness=1.0,
color=(0, 0.9, 0.1, 0.7),
maxwidth=self._sub_width * 0.9,
scale=0.55,
h_align='center',
v_align='center',
)
cur_val = ba.app.config.get('Touch Movement Control Type', 'swipe')
ba.textwidget(
parent=self._subcontainer,
position=(h, v - 2),
size=(0, 30),
text=ba.Lstr(resource=self._r + '.movementText'),
maxwidth=190,
color=clr,
v_align='center',
)
cb1 = ba.checkboxwidget(
parent=self._subcontainer,
position=(h + 220, v),
size=(170, 30),
text=ba.Lstr(resource=self._r + '.joystickText'),
maxwidth=100,
textcolor=clr2,
scale=0.9,
)
cb2 = ba.checkboxwidget(
parent=self._subcontainer,
position=(h + 357, v),
size=(170, 30),
text=ba.Lstr(resource=self._r + '.swipeText'),
maxwidth=100,
textcolor=clr2,
value=False,
scale=0.9,
)
make_radio_group(
(cb1, cb2), ('joystick', 'swipe'), cur_val, self._movement_changed
)
v -= 50
ConfigNumberEdit(
parent=self._subcontainer,
position=(h, v),
xoffset=65,
configkey='Touch Controls Scale Movement',
displayname=ba.Lstr(resource=self._r + '.movementControlScaleText'),
changesound=False,
minval=0.1,
maxval=4.0,
increment=0.1,
)
v -= 50
cur_val = ba.app.config.get('Touch Action Control Type', 'buttons')
ba.textwidget(
parent=self._subcontainer,
position=(h, v - 2),
size=(0, 30),
text=ba.Lstr(resource=self._r + '.actionsText'),
maxwidth=190,
color=clr,
v_align='center',
)
cb1 = ba.checkboxwidget(
parent=self._subcontainer,
position=(h + 220, v),
size=(170, 30),
text=ba.Lstr(resource=self._r + '.buttonsText'),
maxwidth=100,
textcolor=clr2,
scale=0.9,
)
cb2 = ba.checkboxwidget(
parent=self._subcontainer,
position=(h + 357, v),
size=(170, 30),
text=ba.Lstr(resource=self._r + '.swipeText'),
maxwidth=100,
textcolor=clr2,
scale=0.9,
)
make_radio_group(
(cb1, cb2), ('buttons', 'swipe'), cur_val, self._actions_changed
)
v -= 50
ConfigNumberEdit(
parent=self._subcontainer,
position=(h, v),
xoffset=65,
configkey='Touch Controls Scale Actions',
displayname=ba.Lstr(resource=self._r + '.actionControlScaleText'),
changesound=False,
minval=0.1,
maxval=4.0,
increment=0.1,
)
v -= 50
ConfigCheckBox(
parent=self._subcontainer,
position=(h, v),
size=(400, 30),
maxwidth=400,
configkey='Touch Controls Swipe Hidden',
displayname=ba.Lstr(resource=self._r + '.swipeControlsHiddenText'),
)
v -= 65
ba.buttonwidget(
parent=self._subcontainer,
position=(self._sub_width * 0.5 - 70, v),
size=(170, 60),
label=ba.Lstr(resource=self._r + '.resetText'),
scale=0.75,
on_activate_call=self._reset,
)
ba.textwidget(
parent=self._root_widget,
position=(self._width * 0.5, 38),
size=(0, 0),
h_align='center',
text=ba.Lstr(resource=self._r + '.dragControlsText'),
maxwidth=self._width * 0.8,
scale=0.65,
color=(1, 1, 1, 0.4),
)
def _actions_changed(self, v: str) -> None:
cfg = ba.app.config
cfg['Touch Action Control Type'] = v
cfg.apply_and_commit()
def _movement_changed(self, v: str) -> None:
cfg = ba.app.config
cfg['Touch Movement Control Type'] = v
cfg.apply_and_commit()
def _reset(self) -> None:
cfg = ba.app.config
cfgkeys = [
'Touch Movement Control Type',
'Touch Action Control Type',
'Touch Controls Scale',
'Touch Controls Scale Movement',
'Touch Controls Scale Actions',
'Touch Controls Swipe Hidden',
'Touch DPad X',
'Touch DPad Y',
'Touch Buttons X',
'Touch Buttons Y',
]
for cfgkey in cfgkeys:
if cfgkey in cfg:
del cfg[cfgkey]
cfg.apply_and_commit()
ba.timer(0, self._build_gui, timetype=ba.TimeType.REAL)
def _back(self) -> None:
from bastd.ui.settings import controls
ba.containerwidget(edit=self._root_widget, transition='out_right')
ba.app.ui.set_main_menu_window(
controls.ControlsSettingsWindow(
transition='in_left'
).get_root_widget()
)
ba.internal.set_touchscreen_editing(False)

View file

@ -0,0 +1,90 @@
# Released under the MIT License. See LICENSE for details.
#
"""Provides UI for testing vr settings."""
from __future__ import annotations
from typing import TYPE_CHECKING
import ba
from bastd.ui.settings.testing import TestingWindow
if TYPE_CHECKING:
from typing import Any
class VRTestingWindow(TestingWindow):
"""Window for testing vr settings."""
def __init__(self, transition: str = 'in_right'):
entries: list[dict[str, Any]] = []
app = ba.app
# these are gear-vr only
if app.platform == 'android' and app.subplatform == 'oculus':
entries += [
{
'name': 'timeWarpDebug',
'label': 'Time Warp Debug',
'increment': 1.0,
},
{
'name': 'chromaticAberrationCorrection',
'label': 'Chromatic Aberration Correction',
'increment': 1.0,
},
{
'name': 'vrMinimumVSyncs',
'label': 'Minimum Vsyncs',
'increment': 1.0,
},
# {'name':'eyeOffsX','label':'Eye IPD','increment':0.001}
]
# cardboard/gearvr get eye offset controls..
# if app.platform == 'android':
# entries += [
# {'name':'eyeOffsY','label':'Eye Offset Y','increment':0.01},
# {'name':'eyeOffsZ','label':'Eye Offset Z','increment':0.005}]
# everyone gets head-scale
entries += [
{'name': 'headScale', 'label': 'Head Scale', 'increment': 1.0}
]
# and everyone gets all these..
entries += [
{
'name': 'vrCamOffsetY',
'label': 'In-Game Cam Offset Y',
'increment': 0.1,
},
{
'name': 'vrCamOffsetZ',
'label': 'In-Game Cam Offset Z',
'increment': 0.1,
},
{
'name': 'vrOverlayScale',
'label': 'Overlay Scale',
'increment': 0.025,
},
{
'name': 'allowCameraMovement',
'label': 'Allow Camera Movement',
'increment': 1.0,
},
{
'name': 'cameraPanSpeedScale',
'label': 'Camera Movement Speed',
'increment': 0.1,
},
{
'name': 'showOverlayBounds',
'label': 'Show Overlay Bounds',
'increment': 1,
},
]
super().__init__(
ba.Lstr(resource='settingsWindowAdvanced.vrTestingText'),
entries,
transition,
)

View file

@ -0,0 +1,251 @@
# Released under the MIT License. See LICENSE for details.
#
"""Settings UI functionality related to wiimote support."""
from __future__ import annotations
import _ba
import ba
class WiimoteSettingsWindow(ba.Window):
"""Window for setting up Wiimotes."""
def __init__(self) -> None:
self._r = 'wiimoteSetupWindow'
width = 600
height = 480
spacing = 40
super().__init__(root_widget=ba.containerwidget(size=(width, height),
transition='in_right'))
btn = ba.buttonwidget(parent=self._root_widget,
position=(55, height - 50),
size=(120, 60),
scale=0.8,
autoselect=True,
label=ba.Lstr(resource='backText'),
button_type='back',
on_activate_call=self._back)
ba.containerwidget(edit=self._root_widget, cancel_button=btn)
ba.textwidget(parent=self._root_widget,
position=(width * 0.5, height - 28),
size=(0, 0),
text=ba.Lstr(resource=self._r + '.titleText'),
maxwidth=270,
color=ba.app.ui.title_color,
h_align='center',
v_align='center')
ba.buttonwidget(edit=btn,
button_type='backSmall',
size=(60, 60),
label=ba.charstr(ba.SpecialChar.BACK))
v = height - 60.0
v -= spacing
ba.textwidget(parent=self._root_widget,
position=(width * 0.5, v - 80),
size=(0, 0),
color=(0.7, 0.9, 0.7, 1.0),
scale=0.75,
text=ba.Lstr(resource=self._r + '.macInstructionsText'),
maxwidth=width * 0.95,
max_height=height * 0.5,
h_align='center',
v_align='center')
v -= 230
button_width = 200
v -= 30
btn = ba.buttonwidget(parent=self._root_widget,
position=(width / 2 - button_width / 2, v + 1),
autoselect=True,
size=(button_width, 50),
label=ba.Lstr(resource=self._r + '.listenText'),
on_activate_call=WiimoteListenWindow)
ba.containerwidget(edit=self._root_widget, start_button=btn)
v -= spacing * 1.1
ba.textwidget(parent=self._root_widget,
position=(width * 0.5, v),
size=(0, 0),
color=(0.7, 0.9, 0.7, 1.0),
scale=0.8,
maxwidth=width * 0.95,
text=ba.Lstr(resource=self._r + '.thanksText'),
h_align='center',
v_align='center')
v -= 30
this_button_width = 200
ba.buttonwidget(parent=self._root_widget,
position=(width / 2 - this_button_width / 2, v - 14),
color=(0.45, 0.4, 0.5),
autoselect=True,
size=(this_button_width, 15),
label=ba.Lstr(resource=self._r + '.copyrightText'),
textcolor=(0.55, 0.5, 0.6),
text_scale=0.6,
on_activate_call=WiimoteLicenseWindow)
def _back(self) -> None:
from bastd.ui.settings import controls
ba.containerwidget(edit=self._root_widget, transition='out_right')
ba.app.ui.set_main_menu_window(
controls.ControlsSettingsWindow(
transition='in_left').get_root_widget())
class WiimoteListenWindow(ba.Window):
"""Window shown while listening for a wiimote connection."""
def __init__(self) -> None:
self._r = 'wiimoteListenWindow'
width = 650
height = 210
super().__init__(root_widget=ba.containerwidget(size=(width, height),
transition='in_right'))
btn = ba.buttonwidget(parent=self._root_widget,
position=(35, height - 60),
size=(140, 60),
autoselect=True,
label=ba.Lstr(resource='cancelText'),
scale=0.8,
on_activate_call=self._dismiss)
ba.containerwidget(edit=self._root_widget, cancel_button=btn)
_ba.start_listening_for_wii_remotes()
self._wiimote_connect_counter = 15
ba.app.ui.dismiss_wii_remotes_window_call = ba.WeakCall(self._dismiss)
ba.textwidget(parent=self._root_widget,
position=(15, height - 55),
size=(width - 30, 30),
text=ba.Lstr(resource=self._r + '.listeningText'),
color=ba.app.ui.title_color,
maxwidth=320,
h_align='center',
v_align='center')
ba.textwidget(parent=self._root_widget,
position=(15, height - 110),
size=(width - 30, 30),
scale=1.0,
text=ba.Lstr(resource=self._r + '.pressText'),
maxwidth=width * 0.9,
color=(0.7, 0.9, 0.7, 1.0),
h_align='center',
v_align='center')
ba.textwidget(parent=self._root_widget,
position=(15, height - 140),
size=(width - 30, 30),
color=(0.7, 0.9, 0.7, 1.0),
scale=0.55,
text=ba.Lstr(resource=self._r + '.pressText2'),
maxwidth=width * 0.95,
h_align='center',
v_align='center')
self._counter_text = ba.textwidget(parent=self._root_widget,
position=(15, 23),
size=(width - 30, 30),
scale=1.2,
text='15',
h_align='center',
v_align='top')
for i in range(1, 15):
ba.timer(1.0 * i,
ba.WeakCall(self._decrement),
timetype=ba.TimeType.REAL)
ba.timer(15.0, ba.WeakCall(self._dismiss), timetype=ba.TimeType.REAL)
def _decrement(self) -> None:
self._wiimote_connect_counter -= 1
ba.textwidget(edit=self._counter_text,
text=str(self._wiimote_connect_counter))
def _dismiss(self) -> None:
ba.containerwidget(edit=self._root_widget, transition='out_left')
_ba.stop_listening_for_wii_remotes()
class WiimoteLicenseWindow(ba.Window):
"""Window displaying the Darwiinremote software license."""
def __init__(self) -> None:
self._r = 'wiimoteLicenseWindow'
width = 750
height = 550
super().__init__(root_widget=ba.containerwidget(size=(width, height),
transition='in_right'))
btn = ba.buttonwidget(parent=self._root_widget,
position=(65, height - 50),
size=(120, 60),
scale=0.8,
autoselect=True,
label=ba.Lstr(resource='backText'),
button_type='back',
on_activate_call=self._close)
ba.containerwidget(edit=self._root_widget, cancel_button=btn)
ba.textwidget(parent=self._root_widget,
position=(0, height - 48),
size=(width, 30),
text=ba.Lstr(resource=self._r + '.titleText'),
h_align='center',
color=ba.app.ui.title_color,
v_align='center')
license_text = (
'Copyright (c) 2007, DarwiinRemote Team\n'
'All rights reserved.\n'
'\n'
' Redistribution and use in source and binary forms, with or '
'without modification,\n'
' are permitted provided that'
' the following conditions are met:\n'
'\n'
'1. Redistributions of source code must retain the above copyright'
' notice, this\n'
' list of conditions and the following disclaimer.\n'
'2. Redistributions in binary form must reproduce the above'
' copyright notice, this\n'
' list of conditions and the following disclaimer in the'
' documentation and/or other\n'
' materials provided with the distribution.\n'
'3. Neither the name of this project nor the names of its'
' contributors may be used to\n'
' endorse or promote products derived from this software'
' without specific prior\n'
' written permission.\n'
'\n'
'THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND'
' CONTRIBUTORS "AS IS"\n'
'AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT'
' LIMITED TO, THE\n'
'IMPLIED WARRANTIES OF MERCHANTABILITY'
' AND FITNESS FOR A PARTICULAR'
' PURPOSE\n'
'ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR'
' CONTRIBUTORS BE\n'
'LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,'
' OR\n'
'CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT'
' OF\n'
' SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;'
' OR BUSINESS\n'
'INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,'
' WHETHER IN\n'
'CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR'
' OTHERWISE)\n'
'ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF'
' ADVISED OF THE\n'
'POSSIBILITY OF SUCH DAMAGE.\n')
license_text_scale = 0.62
ba.textwidget(parent=self._root_widget,
position=(100, height * 0.45),
size=(0, 0),
h_align='left',
v_align='center',
padding=4,
color=(0.7, 0.9, 0.7, 1.0),
scale=license_text_scale,
maxwidth=width * 0.9 - 100,
max_height=height * 0.85,
text=license_text)
def _close(self) -> None:
ba.containerwidget(edit=self._root_widget, transition='out_right')

View file

@ -0,0 +1,138 @@
# Released under the MIT License. See LICENSE for details.
#
"""UI functionality related to using xbox360 controllers."""
from __future__ import annotations
from typing import TYPE_CHECKING
import ba
import ba.internal
if TYPE_CHECKING:
pass
class XBox360ControllerSettingsWindow(ba.Window):
"""UI showing info about xbox 360 controllers."""
def __init__(self) -> None:
self._r = 'xbox360ControllersWindow'
width = 700
height = 300 if ba.internal.is_running_on_fire_tv() else 485
spacing = 40
uiscale = ba.app.ui.uiscale
super().__init__(
root_widget=ba.containerwidget(
size=(width, height),
transition='in_right',
scale=(
1.4
if uiscale is ba.UIScale.SMALL
else 1.4
if uiscale is ba.UIScale.MEDIUM
else 1.0
),
)
)
btn = ba.buttonwidget(
parent=self._root_widget,
position=(35, height - 65),
size=(120, 60),
scale=0.84,
label=ba.Lstr(resource='backText'),
button_type='back',
autoselect=True,
on_activate_call=self._back,
)
ba.containerwidget(edit=self._root_widget, cancel_button=btn)
ba.textwidget(
parent=self._root_widget,
position=(width * 0.5, height - 42),
size=(0, 0),
scale=0.85,
text=ba.Lstr(
resource=self._r + '.titleText',
subs=[('${APP_NAME}', ba.Lstr(resource='titleText'))],
),
color=ba.app.ui.title_color,
maxwidth=400,
h_align='center',
v_align='center',
)
ba.buttonwidget(
edit=btn,
button_type='backSmall',
size=(60, 60),
label=ba.charstr(ba.SpecialChar.BACK),
)
v = height - 70
v -= spacing
if ba.internal.is_running_on_fire_tv():
ba.textwidget(
parent=self._root_widget,
position=(width * 0.5, height * 0.47),
size=(0, 0),
color=(0.7, 0.9, 0.7, 1.0),
maxwidth=width * 0.95,
max_height=height * 0.75,
scale=0.7,
text=ba.Lstr(resource=self._r + '.ouyaInstructionsText'),
h_align='center',
v_align='center',
)
else:
ba.textwidget(
parent=self._root_widget,
position=(width * 0.5, v - 1),
size=(0, 0),
color=(0.7, 0.9, 0.7, 1.0),
maxwidth=width * 0.95,
max_height=height * 0.22,
text=ba.Lstr(resource=self._r + '.macInstructionsText'),
scale=0.7,
h_align='center',
v_align='center',
)
v -= 90
b_width = 300
btn = ba.buttonwidget(
parent=self._root_widget,
position=((width - b_width) * 0.5, v - 10),
size=(b_width, 50),
label=ba.Lstr(resource=self._r + '.getDriverText'),
autoselect=True,
on_activate_call=ba.Call(
ba.open_url,
'https://github.com/360Controller/360Controller/releases',
),
)
ba.containerwidget(edit=self._root_widget, start_button=btn)
v -= 60
ba.textwidget(
parent=self._root_widget,
position=(width * 0.5, v - 85),
size=(0, 0),
color=(0.7, 0.9, 0.7, 1.0),
maxwidth=width * 0.95,
max_height=height * 0.46,
scale=0.7,
text=ba.Lstr(resource=self._r + '.macInstructions2Text'),
h_align='center',
v_align='center',
)
def _back(self) -> None:
from bastd.ui.settings import controls
ba.containerwidget(edit=self._root_widget, transition='out_right')
ba.app.ui.set_main_menu_window(
controls.ControlsSettingsWindow(
transition='in_left'
).get_root_widget()
)