Source code for bauiv1lib.settings.keyboard

# Released under the MIT License. See LICENSE for details.
#
"""Keyboard settings related UI functionality."""

from __future__ import annotations

from typing import TYPE_CHECKING, override

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


[docs] class ConfigKeyboardWindow(bui.MainWindow): """Window for configuring keyboards.""" def __init__( self, c: bs.InputDevice, transition: str | None = 'in_right', origin_widget: bui.Widget | None = None, ): 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 = bui.Lstr(translate=('inputDeviceNames', dname_raw)) self._width = 700 if self._unique_id != '#1': self._height = 480 else: self._height = 375 self._spacing = 40 assert bui.app.classic is not None uiscale = bui.app.ui_v1.uiscale super().__init__( root_widget=bui.containerwidget( size=(self._width, self._height), scale=( 1.4 if uiscale is bui.UIScale.SMALL else 1.3 if uiscale is bui.UIScale.MEDIUM else 1.0 ), stack_offset=(0, 5) if uiscale is bui.UIScale.SMALL else (0, 0), transition=transition, ), transition=transition, origin_widget=origin_widget, ) self._settings: dict[str, int] = {} self._get_config_mapping() self._rebuild_ui()
[docs] @override def get_main_window_state(self) -> bui.MainWindowState: # Support recreating our window for back/refresh purposes. cls = type(self) # Pull things from self here; if we do it within the lambda # we'll keep self alive which is bad. inputdevice = self._input return bui.BasicMainWindowState( create_call=lambda transition, origin_widget: cls( transition=transition, origin_widget=origin_widget, c=inputdevice, ) )
def _get_config_mapping(self, default: bool = False) -> None: for button in [ 'buttonJump', 'buttonPunch', 'buttonBomb', 'buttonPickUp', 'buttonStart', 'buttonStart2', 'buttonUp', 'buttonDown', 'buttonLeft', 'buttonRight', ]: 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, position=(38, self._height - 85), size=(170, 60), label=bui.Lstr(resource='cancelText'), scale=0.9, on_activate_call=self.main_window_back, ) save_button = bui.buttonwidget( parent=self._root_widget, autoselect=True, position=(self._width - 190, self._height - 85), size=(180, 60), label=bui.Lstr(resource='saveText'), scale=0.9, text_scale=0.9, on_activate_call=self._save, ) bui.containerwidget( edit=self._root_widget, cancel_button=cancel_button, start_button=save_button, ) v = self._height - 74.0 bui.textwidget( parent=self._root_widget, position=(self._width * 0.5, v + 15), size=(0, 0), text=bui.Lstr( resource=f'{self._r}.configuringText', subs=[('${DEVICE}', self._displayname)], ), color=bui.app.ui_v1.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 bui.textwidget( parent=self._root_widget, position=(0, v + 19), size=(self._width, 50), text=bui.Lstr(resource=f'{self._r}.keyboard2NoteText'), scale=0.7, maxwidth=self._width * 0.75, max_height=110, color=bui.app.ui_v1.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=bui.gettexture('upButton'), scale=1.0, ) self._capture_button( pos=(h_offs - 1.2 * dist, v), color=d_color, button='buttonLeft', texture=bui.gettexture('leftButton'), scale=1.0, ) self._capture_button( pos=(h_offs + 1.2 * dist, v), color=d_color, button='buttonRight', texture=bui.gettexture('rightButton'), scale=1.0, ) self._capture_button( pos=(h_offs, v - 0.95 * dist), color=d_color, button='buttonDown', texture=bui.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=bui.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=bui.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=bui.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=bui.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=bui.gettexture('buttonJump'), 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: return bs.Lstr(resource='configGamepadWindow.unsetText') return self._input.get_button_name(button_id) def _capture_button( self, pos: tuple[float, float], color: tuple[float, float, float], texture: bui.Texture, button: str, scale: float = 1.0, ) -> None: # pylint: disable=too-many-positional-arguments base_size = 79 btn = bui.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 = bui.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._pretty_button_name(button), ) bui.buttonwidget( edit=btn, autoselect=True, on_activate_call=bui.Call( AwaitKeyboardInputWindow, button, txt, self._settings ), ) bui.pushcall(doit) def _reset(self) -> None: from bauiv1lib.confirm import ConfirmWindow assert bui.app.classic is not None # efro note: I think it's ok to reset without a confirm here # because the user can see pretty clearly what changes and can # cancel out of the keyboard settings edit if they want. if bool(False): 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, ) else: self._do_reset() 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, )
[docs] 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}')
[docs] def popup_menu_closing(self, popup_window: PopupWindow) -> None: """Called when the popup is closing."""
def _save(self) -> None: # no-op if we're not in control. if not self.main_window_has_control(): return assert bui.app.classic is not None bui.getsound('gunCocking').play() # There's a chance the device disappeared; handle that # gracefully. if not self._input: return dst = bui.app.classic.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 # Send this config to the master-server so we can generate more # defaults in the future. if bui.app.classic is not None: bui.app.classic.master_server_v1_post( 'controllerConfig', { 'ua': bui.app.classic.legacy_user_agent_string, 'name': self._name, 'b': bui.app.env.engine_build_number, 'config': dst2, 'v': 2, }, ) bui.app.config.apply_and_commit() self.main_window_back()
[docs] class AwaitKeyboardInputWindow(bui.Window): """Window for capturing a keypress.""" def __init__(self, button: str, ui: bui.Widget, settings: dict): self._capture_button = button self._capture_key_ui = ui self._settings = settings width = 400 height = 150 assert bui.app.classic is not None uiscale = bui.app.ui_v1.uiscale super().__init__( root_widget=bui.containerwidget( size=(width, height), transition='in_right', scale=( 2.0 if uiscale is bui.UIScale.SMALL else 1.5 if uiscale is bui.UIScale.MEDIUM else 1.0 ), ) ) bui.textwidget( parent=self._root_widget, position=(0, height - 60), size=(width, 25), text=bui.Lstr(resource='pressAnyKeyText'), h_align='center', v_align='top', ) self._counter = 5 self._count_down_text = bui.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: bui.AppTimer | None = bui.AppTimer( 1.0, self._decrement, repeat=True ) bs.capture_keyboard_input(bui.WeakCall(self._button_callback)) def __del__(self) -> None: bs.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: bui.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']) bui.textwidget(edit=self._capture_key_ui, text=bname) bui.getsound('gunCocking').play() self._die() def _decrement(self) -> None: self._counter -= 1 if self._counter >= 1: bui.textwidget(edit=self._count_down_text, text=str(self._counter)) else: self._die()
# 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