# 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 bauiv1 as bui
if TYPE_CHECKING:
from typing import Any
from bauiv1lib.settings.gamepad import (
GamepadSettingsWindow,
AwaitGamepadInputWindow,
)
[docs]
class GamepadAdvancedSettingsWindow(bui.Window):
"""Window for advanced gamepad configuration."""
def __init__(self, parent_window: GamepadSettingsWindow):
# pylint: disable=too-many-statements
# pylint: disable=too-many-locals
self._parent_window = parent_window
app = bui.app
self._r = parent_window.get_r()
assert bui.app.classic is not None
uiscale = bui.app.ui_v1.uiscale
self._width = 900 if uiscale is bui.UIScale.SMALL else 700
self._x_inset = x_inset = 100 if uiscale is bui.UIScale.SMALL else 0
self._height = 402 if uiscale is bui.UIScale.SMALL else 512
self._textwidgets: dict[str, bui.Widget] = {}
advb = parent_window.get_advanced_button()
super().__init__(
root_widget=bui.containerwidget(
transition='in_scale',
size=(self._width, self._height),
scale=1.06
* (
1.6
if uiscale is bui.UIScale.SMALL
else 1.35 if uiscale is bui.UIScale.MEDIUM else 1.0
),
stack_offset=(
(0, -25) if uiscale is bui.UIScale.SMALL else (0, 0)
),
scale_origin_stack_offset=(advb.get_screen_space_center()),
)
)
bui.textwidget(
parent=self._root_widget,
position=(
self._width * 0.5,
self._height - (40 if uiscale is bui.UIScale.SMALL else 34),
),
size=(0, 0),
text=bui.Lstr(resource=f'{self._r}.advancedTitleText'),
maxwidth=320,
color=bui.app.ui_v1.title_color,
h_align='center',
v_align='center',
)
back_button = btn = bui.buttonwidget(
parent=self._root_widget,
autoselect=True,
position=(
self._width - (176 + x_inset),
self._height - (60 if uiscale is bui.UIScale.SMALL else 55),
),
size=(120, 48),
text_scale=0.8,
label=bui.Lstr(resource='doneText'),
on_activate_call=self._done,
)
bui.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.env.vr:
self._sub_height += 50
self._scrollwidget = bui.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,
selection_loops_to_parent=True,
)
self._subcontainer = bui.containerwidget(
parent=self._scrollwidget,
size=(self._sub_width, self._sub_height),
background=False,
claims_left_right=True,
selection_loops_to_parent=True,
)
bui.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 = bui.checkboxwidget(
parent=self._subcontainer,
position=(h + 70, v),
size=(500, 30),
text=bui.Lstr(resource=f'{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(),
)
bui.widget(edit=cb1, up_widget=back_button)
v -= 60
capb = self._capture_button(
pos=(h2, v),
name=bui.Lstr(resource=f'{self._r}.runButton1Text'),
control='buttonRun1' + self._parent_window.get_ext(),
)
if self._parent_window.get_is_secondary():
for widget in capb:
bui.widget(edit=widget, up_widget=back_button)
v -= 42
self._capture_button(
pos=(h2, v),
name=bui.Lstr(resource=f'{self._r}.runButton2Text'),
control='buttonRun2' + self._parent_window.get_ext(),
)
bui.textwidget(
parent=self._subcontainer,
position=(self._sub_width * 0.5, v - 24),
size=(0, 0),
text=bui.Lstr(resource=f'{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=bui.Lstr(resource=f'{self._r}.runTrigger1Text'),
control='triggerRun1' + self._parent_window.get_ext(),
message=bui.Lstr(resource=f'{self._r}.pressAnyAnalogTriggerText'),
)
v -= 42
self._capture_button(
pos=(h2, v),
name=bui.Lstr(resource=f'{self._r}.runTrigger2Text'),
control='triggerRun2' + self._parent_window.get_ext(),
message=bui.Lstr(resource=f'{self._r}.pressAnyAnalogTriggerText'),
)
# in vr mode, allow assigning a reset-view button
if app.env.vr:
v -= 50
self._capture_button(
pos=(h2, v),
name=bui.Lstr(resource=f'{self._r}.vrReorientButtonText'),
control='buttonVRReorient' + self._parent_window.get_ext(),
)
v -= 60
self._capture_button(
pos=(h2, v),
name=bui.Lstr(resource=f'{self._r}.extraStartButtonText'),
control='buttonStart2' + self._parent_window.get_ext(),
)
v -= 60
self._capture_button(
pos=(h2, v),
name=bui.Lstr(resource=f'{self._r}.ignoredButton1Text'),
control='buttonIgnored' + self._parent_window.get_ext(),
)
v -= 42
self._capture_button(
pos=(h2, v),
name=bui.Lstr(resource=f'{self._r}.ignoredButton2Text'),
control='buttonIgnored2' + self._parent_window.get_ext(),
)
v -= 42
self._capture_button(
pos=(h2, v),
name=bui.Lstr(resource=f'{self._r}.ignoredButton3Text'),
control='buttonIgnored3' + self._parent_window.get_ext(),
)
v -= 42
self._capture_button(
pos=(h2, v),
name=bui.Lstr(resource=f'{self._r}.ignoredButton4Text'),
control='buttonIgnored4' + self._parent_window.get_ext(),
)
bui.textwidget(
parent=self._subcontainer,
position=(self._sub_width * 0.5, v - 14),
size=(0, 0),
text=bui.Lstr(resource=f'{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
bui.checkboxwidget(
parent=self._subcontainer,
autoselect=True,
position=(h + 50, v),
size=(400, 30),
text=bui.Lstr(
resource=f'{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(),
)
bui.textwidget(
parent=self._subcontainer,
position=(self._sub_width * 0.5, v - 12),
size=(0, 0),
text=bui.Lstr(
resource=f'{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
bui.checkboxwidget(
parent=self._subcontainer,
autoselect=True,
position=(h + 50, v),
size=(400, 30),
text=bui.Lstr(resource=f'{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(),
)
bui.textwidget(
parent=self._subcontainer,
position=(self._sub_width * 0.5, v - 12),
size=(0, 0),
text=bui.Lstr(resource=f'{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
bui.checkboxwidget(
parent=self._subcontainer,
autoselect=True,
position=(h + 50, v),
size=(400, 30),
text=bui.Lstr(resource=f'{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(),
)
bui.textwidget(
parent=self._subcontainer,
position=(self._sub_width * 0.5, v - 12),
size=(0, 0),
text=bui.Lstr(
resource=f'{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 = bui.checkboxwidget(
parent=self._subcontainer,
autoselect=True,
position=(h + 50, v),
size=(400, 30),
text=bui.Lstr(resource=f'{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(),
)
bui.textwidget(
parent=self._subcontainer,
position=(self._sub_width * 0.5, v - 12),
size=(0, 0),
text=bui.Lstr(resource=f'{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(
bui.Lstr(resource=f'{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,
)
bui.widget(edit=buttons[0], left_widget=cb1, up_widget=cb1)
bui.widget(edit=cb1, right_widget=buttons[0], down_widget=buttons[0])
bui.textwidget(
parent=self._subcontainer,
position=(self._sub_width * 0.5, v - 12),
size=(0, 0),
text=bui.Lstr(
resource=f'{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():
bui.buttonwidget(
parent=self._subcontainer,
autoselect=True,
label=bui.Lstr(resource=f'{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():
bui.widget(edit=child, show_buffer_bottom=30, show_buffer_top=30)
def _capture_button(
self,
pos: tuple[float, float],
name: bui.Lstr,
control: str,
message: bui.Lstr | None = None,
) -> tuple[bui.Widget, bui.Widget]:
if message is None:
message = bui.Lstr(
resource=self._parent_window.get_r() + '.pressAnyButtonText'
)
btn = bui.buttonwidget(
parent=self._subcontainer,
autoselect=True,
position=(pos[0], pos[1]),
label=name,
size=(250, 60),
scale=0.7,
)
btn2 = bui.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=bui.Lstr(resource=f'{self._r}.clearText'),
size=(110, 50),
scale=0.7,
on_activate_call=bui.Call(self._clear_control, control),
)
bui.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 bauiv1lib.settings.gamepad import AwaitGamepadInputWindow
txt = bui.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
bui.buttonwidget(
edit=btn,
on_activate_call=bui.Call(
AwaitGamepadInputWindow,
self._parent_window.get_input(),
control,
self._gamepad_event,
message,
),
)
bui.pushcall(doit)
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)
bui.textwidget(
edit=self._textwidgets[control],
text=self._parent_window.get_control_value_name(control),
)
def _config_value_editor(
self,
name: bui.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: bui.Lstr | None = None,
) -> tuple[bui.Widget, bui.Widget]:
if displayname is None:
displayname = name
bui.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] = bui.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 = bui.buttonwidget(
parent=self._subcontainer,
autoselect=True,
position=(330 + x_offset, position[1] + 4),
size=(28, 28),
label='-',
on_activate_call=bui.Call(
self._inc, control, min_val, max_val, -increment
),
repeat=True,
enable_sound=(change_sound is True),
)
btn2 = bui.buttonwidget(
parent=self._subcontainer,
autoselect=True,
position=(380 + x_offset, position[1] + 4),
size=(28, 28),
label='+',
on_activate_call=bui.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]
bui.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: 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]:
bui.textwidget(
edit=self._textwidgets[control],
text=self._parent_window.get_control_value_name(
control
),
)
bui.getsound('gunCocking').play()
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]:
bui.textwidget(
edit=self._textwidgets[control],
text=self._parent_window.get_control_value_name(
control
),
)
bui.getsound('gunCocking').play()
dialog.die()
def _done(self) -> None:
bui.containerwidget(edit=self._root_widget, transition='out_scale')
# Docs-generation hack; import some stuff that we likely only forward-declared
# in our actual source code so that docs tools can find it.
from typing import (Coroutine, Any, Literal, Callable,
Generator, Awaitable, Sequence, Self)
import asyncio
from concurrent.futures import Future
from pathlib import Path
from enum import Enum