Source code for bauiv1lib.settings.keyboard

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

from __future__ import annotations  # Docs-generation hack.

from typing import TYPE_CHECKING, override

from bauiv1lib.popup import PopupMenuWindow
import bauiv1 as bui
from bauiv1 import _commonassets, _classicassets
from bauiv1 import _uiv1assets
import bascenev1 as bs

if TYPE_CHECKING:
    from typing import Any

    from bauiv1lib.popup import PopupWindow


_kbstrs = _classicassets.strings.settings.controllers.keyboard
_gpstrs = _classicassets.strings.settings.controllers.gamepad


[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 # Room at the bottom for keyboard 1's enable-P2 checkbox (under # the '...' button). self._bottom_extra = 0 if self._unique_id != '#1' else 45 if self._unique_id != '#1': self._height = 480 else: self._height = 375 + self._bottom_extra self._spacing = 40 assert bui.app.classic is not None uiscale = bui.app.ui_v1.uiscale self._base_scale = ( 1.4 if uiscale is bui.UIScale.SMALL else 0.91 if uiscale is bui.UIScale.MEDIUM else 0.7 ) super().__init__( root_widget=bui.containerwidget( size=(self._width, self._height), scale=self._base_scale, 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, ) )
[docs] @override def main_window_should_preserve_selection(self) -> bool: return False
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=_commonassets.strings.actions.cancel, 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=_commonassets.strings.actions.save, 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), # Still legacy: the device display-name is an # inputDeviceNames translate (those port in the deferred # translate-category phase; see strings-asset-migration # D35), so the whole composition stays Lstr until then. text=bui.Lstr( resource='configKeyboardWindow.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=_kbstrs.keyboard2_note, 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) d_up = self._capture_button( pos=(h_offs, v + 0.95 * dist), color=d_color, button='buttonUp', texture=_classicassets.textures.up_button.get(), scale=1.0, ) d_left = self._capture_button( pos=(h_offs - 1.2 * dist, v), color=d_color, button='buttonLeft', texture=_classicassets.textures.left_button.get(), scale=1.0, ) d_right = self._capture_button( pos=(h_offs + 1.2 * dist, v), color=d_color, button='buttonRight', texture=_classicassets.textures.right_button.get(), scale=1.0, ) d_down = self._capture_button( pos=(h_offs, v - 0.95 * dist), color=d_color, button='buttonDown', texture=_classicassets.textures.down_button.get(), 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=_uiv1assets.textures.start_button.get(), scale=0.8, ) h_offs = self._width - 160 a_pickup = self._capture_button( pos=(h_offs, v + 0.95 * dist), color=(0.6, 0.4, 0.8), button='buttonPickUp', texture=_classicassets.textures.button_pick_up.get(), scale=1.0, ) a_punch = self._capture_button( pos=(h_offs - 1.2 * dist, v), color=(0.7, 0.5, 0.1), button='buttonPunch', texture=_classicassets.textures.button_punch.get(), scale=1.0, ) a_bomb = self._capture_button( pos=(h_offs + 1.2 * dist, v), color=(0.5, 0.2, 0.1), button='buttonBomb', texture=_classicassets.textures.button_bomb.get(), scale=1.0, ) a_jump = self._capture_button( pos=(h_offs, v - 0.95 * dist), color=(0.2, 0.5, 0.2), button='buttonJump', texture=_classicassets.textures.button_jump.get(), scale=1.0, ) # Autoselect guesses poorly around the side buttons of each # cluster; up/down from them should hit the top/bottom one. for widget in (d_left, d_right): bui.widget(edit=widget, up_widget=d_up, down_widget=d_down) for widget in (a_punch, a_bomb): bui.widget(edit=widget, up_widget=a_pickup, down_widget=a_jump) 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 + self._bottom_extra), size=(130, 40), on_activate_call=self._do_more, ) bui.widget(edit=d_down, down_widget=self._more_button) # Keyboard 1 hosts keyboard P2 (a second player on the same # keyboard), which only gets keys when enabled here. if self._unique_id == '#1': # pylint: disable=cyclic-import from bauiv1lib.config import ConfigCheckBox p2check = ConfigCheckBox( parent=self._root_widget, configkey='Keyboard P2 Enabled', position=(self._width * 0.5 - 110, 28), size=(220, 30), displayname=_kbstrs.enable_keyboard_p2, scale=0.8, maxwidth=200, ) # Purple, like the '...' button above it. bui.checkboxwidget( edit=p2check.widget, color=(0.45, 0.4, 0.5), textcolor=(0.65, 0.6, 0.7), ) bui.widget(edit=p2check.widget, up_widget=self._more_button) if is_reset: bui.containerwidget( edit=self._root_widget, selected_child=self._more_button, ) def _pretty_button_name(self, button_name: str) -> bui.LangStr: button_id = self._settings[button_name] if button_id == -1: return _gpstrs.unset 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, ) -> bui.Widget: 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.CallStrict( AwaitKeyboardInputWindow, button, txt, self._settings ), ) bui.pushcall(doit) return btn 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) _commonassets.audio.gun_cocking.get().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.LangStr] = [ _commonassets.strings.actions.reset, ] PopupMenuWindow( position=self._more_button.get_screen_space_center(), # Sized relative to the window so it looks the same at any # ui scale. scale=1.25 * self._base_scale, 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 _commonassets.audio.gun_cocking.get().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=_kbstrs.press_any_key, 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.WeakCallPartial(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) _commonassets.audio.gun_cocking.get().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, TypeIs) import asyncio from concurrent.futures import Future from pathlib import Path from enum import Enum