diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ed939f9..b8019ec3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ languages; I feel it helps keep logic more understandable and should help us catch problems where a base class changes or removes a method and child classes forget to adapt to the change. +- Added a reset button in the input mapping menu. (Thanks Temp!) - Respawn icons now have dotted steps showing decimal progress to assist players on calculating when they are gonna respawn. (Thanks 3alTemp!) - Replays now have rewind/fast-forward buttons!! (Thanks Dliwk, vishal332008!) diff --git a/src/assets/ba_data/python/baclassic/_input.py b/src/assets/ba_data/python/baclassic/_input.py index 5c934c32..0effcb5b 100644 --- a/src/assets/ba_data/python/baclassic/_input.py +++ b/src/assets/ba_data/python/baclassic/_input.py @@ -13,7 +13,10 @@ if TYPE_CHECKING: def get_input_device_mapped_value( - devicename: str, unique_id: str, name: str + devicename: str, + unique_id: str, + name: str, + default: bool = False, ) -> Any: """Returns a mapped value for an input device. @@ -30,8 +33,9 @@ def get_input_device_mapped_value( subplatform = app.classic.subplatform appconfig = babase.app.config - # If there's an entry in our config for this controller, use it. - if 'Controllers' in appconfig: + # If there's an entry in our config for this controller and + # we're not looking for our default mappings, use it. + if 'Controllers' in appconfig and not default: ccfgs = appconfig['Controllers'] if devicename in ccfgs: mapping = None diff --git a/src/assets/ba_data/python/baclassic/_subsystem.py b/src/assets/ba_data/python/baclassic/_subsystem.py index 51dd7679..4bdf1406 100644 --- a/src/assets/ba_data/python/baclassic/_subsystem.py +++ b/src/assets/ba_data/python/baclassic/_subsystem.py @@ -575,15 +575,18 @@ class ClassicSubsystem(babase.AppSubsystem): ) def get_input_device_mapped_value( - self, device: bascenev1.InputDevice, name: str + self, + device: bascenev1.InputDevice, + name: str, + default: bool = False, ) -> Any: - """Returns a mapped value for an input device. + """Return a mapped value for an input device. This checks the user config and falls back to default values where available. """ return _input.get_input_device_mapped_value( - device.name, device.unique_identifier, name + device.name, device.unique_identifier, name, default ) def get_input_device_map_hash( diff --git a/src/assets/ba_data/python/bauiv1lib/settings/gamepad.py b/src/assets/ba_data/python/bauiv1lib/settings/gamepad.py index 2ec2e37c..0474f32e 100644 --- a/src/assets/ba_data/python/bauiv1lib/settings/gamepad.py +++ b/src/assets/ba_data/python/bauiv1lib/settings/gamepad.py @@ -1,5 +1,6 @@ # Released under the MIT License. See LICENSE for details. # +# pylint: disable=too-many-lines """Settings UI functionality related to gamepads.""" from __future__ import annotations @@ -7,15 +8,18 @@ from __future__ import annotations import logging from typing import TYPE_CHECKING +from bauiv1lib.popup import PopupMenuWindow import bascenev1 as bs import bauiv1 as bui if TYPE_CHECKING: from typing import Any, Callable + from bauiv1lib.popup import PopupWindow class GamepadSettingsWindow(bui.Window): """Window for configuring a gamepad.""" + # pylint: disable=too-many-public-methods def __init__( self, @@ -34,7 +38,6 @@ class GamepadSettingsWindow(bui.Window): 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. @@ -62,12 +65,81 @@ class GamepadSettingsWindow(bui.Window): ) ) + self._settings: dict[str, int] = {} + if not self._is_secondary: + self._get_config_mapping() # Don't ask to config joysticks while we're in here. self._rebuild_ui() - def _rebuild_ui(self) -> None: + def _get_config_mapping(self, default: bool = False) -> None: + for button 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', + ]: + assert bui.app.classic is not None + val = bui.app.classic.get_input_device_mapped_value( + self._input, button, default + ) + if val != -1: + self._settings[button] = val + + def _rebuild_ui(self, is_reset: bool = False) -> None: # pylint: disable=too-many-statements - # pylint: disable=too-many-locals assert bui.app.classic is not None @@ -77,77 +149,6 @@ class GamepadSettingsWindow(bui.Window): self._textwidgets: dict[str, bui.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 = bui.app.classic.get_input_device_mapped_value( - self._input, skey - ) - if val != -1: - self._settings[skey] = val - back_button: bui.Widget | None if self._is_secondary: @@ -367,22 +368,27 @@ class GamepadSettingsWindow(bui.Window): scale=1.0, ) - self._advanced_button = bui.buttonwidget( + self._more_button = bui.buttonwidget( parent=self._root_widget, autoselect=True, - label=bui.Lstr(resource=self._r + '.advancedText'), + label='...', 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, + on_activate_call=self._do_more, ) try: if cancel_button is not None and save_button is not None: bui.widget(edit=cancel_button, right_widget=save_button) bui.widget(edit=save_button, left_widget=cancel_button) + if is_reset: + bui.containerwidget( + edit=self._root_widget, + selected_child=self._more_button, + ) except Exception: logging.exception('Error wiring up gamepad config window.') @@ -392,7 +398,7 @@ class GamepadSettingsWindow(bui.Window): def get_advanced_button(self) -> bui.Widget: """(internal)""" - return self._advanced_button + return self._more_button def get_is_secondary(self) -> bool: """(internal)""" @@ -801,6 +807,78 @@ class GamepadSettingsWindow(bui.Window): from_window=self._root_widget, ) + def _reset(self) -> None: + from bauiv1lib.confirm import ConfirmWindow + + assert bui.app.classic is not None + ConfirmWindow( + # TODO: Implement a translation string for this! + 'Are you sure you want to reset your button mapping?\n' + 'This will also reset your advanced mappings\n' + 'and secondary controller button mappings.', + self._do_reset, + width=490, + height=150, + ) + + def _do_reset(self) -> None: + """Resets the input's mapping settings.""" + from babase import InputDeviceNotFoundError + self._settings = {} + # Unplugging the controller while performing a + # mapping reset makes things go bonkers a little. + try: + self._get_config_mapping(default=True) + except InputDeviceNotFoundError: + pass + + self._rebuild_ui(is_reset=True) + bui.getsound('gunCocking').play() + + def _do_more(self) -> None: + """Show a burger menu with extra settings.""" + # pylint: disable=cyclic-import + choices: list[str] = [ + 'advanced', + 'reset', + ] + choices_display: list[bui.Lstr] = [ + bui.Lstr(resource=self._r + '.advancedText'), + bui.Lstr(resource='settingsWindowAdvanced.resetText'), + ] + + uiscale = bui.app.ui_v1.uiscale + PopupMenuWindow( + position=self._more_button.get_screen_space_center(), + scale=( + 2.3 + if uiscale is bui.UIScale.SMALL + else 1.65 + if uiscale is bui.UIScale.MEDIUM + else 1.23 + ), + width=150, + choices=choices, + choices_display=choices_display, + current_choice='advanced', + delegate=self, + ) + + def popup_menu_selected_choice( + self, popup_window: PopupMenuWindow, choice: str + ) -> None: + """Called when a choice is selected in the popup.""" + del popup_window # unused + if choice == 'reset': + self._reset() + elif choice == 'advanced': + self._do_advanced() + else: + print(f'invalid choice: {choice}') + + def popup_menu_closing(self, popup_window: PopupWindow) -> None: + """Called when the popup is closing.""" + def _save(self) -> None: classic = bui.app.classic assert classic is not None diff --git a/src/assets/ba_data/python/bauiv1lib/settings/keyboard.py b/src/assets/ba_data/python/bauiv1lib/settings/keyboard.py index 5984c985..2290faf5 100644 --- a/src/assets/ba_data/python/bauiv1lib/settings/keyboard.py +++ b/src/assets/ba_data/python/bauiv1lib/settings/keyboard.py @@ -6,11 +6,13 @@ from __future__ import annotations from typing import TYPE_CHECKING +from bauiv1lib.popup import PopupMenuWindow import bauiv1 as bui import bascenev1 as bs if TYPE_CHECKING: from typing import Any + from bauiv1lib.popup import PopupWindow class ConfigKeyboardWindow(bui.Window): @@ -46,16 +48,12 @@ class ConfigKeyboardWindow(bui.Window): ) ) + self._settings: dict[str, int] = {} + self._get_config_mapping() + self._rebuild_ui() - def _rebuild_ui(self) -> None: - assert bui.app.classic is not None - - for widget in self._root_widget.get_children(): - widget.delete() - - # Fill our temp config with present values. - self._settings: dict[str, int] = {} + def _get_config_mapping(self, default: bool = False) -> None: for button in [ 'buttonJump', 'buttonPunch', @@ -68,12 +66,20 @@ class ConfigKeyboardWindow(bui.Window): 'buttonLeft', 'buttonRight', ]: - self._settings[button] = ( - bui.app.classic.get_input_device_mapped_value( - self._input, button - ) + assert bui.app.classic is not None + self._settings[ + button + ] = bui.app.classic.get_input_device_mapped_value( + self._input, button, default ) + def _rebuild_ui(self, is_reset: bool = False) -> None: + assert bui.app.classic is not None + + for widget in self._root_widget.get_children(): + widget.delete() + + #b_off = 0 if self._unique_id != '#1' else 9 cancel_button = bui.buttonwidget( parent=self._root_widget, autoselect=True, @@ -99,9 +105,6 @@ class ConfigKeyboardWindow(bui.Window): start_button=save_button, ) - bui.widget(edit=cancel_button, right_widget=save_button) - bui.widget(edit=save_button, left_widget=cancel_button) - v = self._height - 74.0 bui.textwidget( parent=self._root_widget, @@ -211,6 +214,24 @@ class ConfigKeyboardWindow(bui.Window): scale=1.0, ) + self._more_button = bui.buttonwidget( + parent=self._root_widget, + autoselect=True, + label='...', + text_scale=0.9, + color=(0.45, 0.4, 0.5), + textcolor=(0.65, 0.6, 0.7), + position=(self._width * 0.5 - 65, 30), + size=(130, 40), + on_activate_call=self._do_more, + ) + + if is_reset: + bui.containerwidget( + edit=self._root_widget, + selected_child=self._more_button, + ) + def _pretty_button_name(self, button_name: str) -> bui.Lstr: button_id = self._settings[button_name] if button_id == -1: @@ -280,6 +301,65 @@ class ConfigKeyboardWindow(bui.Window): from_window=self._root_widget, ) + def _reset(self) -> None: + from bauiv1lib.confirm import ConfirmWindow + + assert bui.app.classic is not None + ConfirmWindow( + # TODO: Implement a translation string for this! + 'Are you sure you want to reset your button mapping?', + self._do_reset, + width=480, + height=95, + ) + + def _do_reset(self) -> None: + """Resets the input's mapping settings.""" + self._settings = {} + self._get_config_mapping(default=True) + self._rebuild_ui(is_reset=True) + bui.getsound('gunCocking').play() + + def _do_more(self) -> None: + """Show a burger menu with extra settings.""" + # pylint: disable=cyclic-import + choices: list[str] = [ + 'reset', + ] + choices_display: list[bui.Lstr] = [ + bui.Lstr(resource='settingsWindowAdvanced.resetText'), + ] + + uiscale = bui.app.ui_v1.uiscale + PopupMenuWindow( + position=self._more_button.get_screen_space_center(), + scale=( + 2.3 + if uiscale is bui.UIScale.SMALL + else 1.65 + if uiscale is bui.UIScale.MEDIUM + else 1.23 + ), + width=150, + choices=choices, + choices_display=choices_display, + current_choice='reset', + delegate=self, + ) + + def popup_menu_selected_choice( + self, popup_window: PopupMenuWindow, choice: str + ) -> None: + """Called when a choice is selected in the popup.""" + del popup_window # unused + if choice == 'reset': + self._reset() + else: + print(f'invalid choice: {choice}') + + def popup_menu_closing(self, popup_window: PopupWindow) -> None: + """Called when the popup is closing.""" + def _save(self) -> None: from bauiv1lib.settings.controls import ControlsSettingsWindow