bauiv1lib.settings.gamepad

Settings UI functionality related to gamepads.

   1# Released under the MIT License. See LICENSE for details.
   2#
   3# pylint: disable=too-many-lines
   4"""Settings UI functionality related to gamepads."""
   5
   6from __future__ import annotations
   7
   8import logging
   9from typing import TYPE_CHECKING, override
  10
  11from bauiv1lib.popup import PopupMenuWindow
  12import bascenev1 as bs
  13import bauiv1 as bui
  14
  15if TYPE_CHECKING:
  16    from typing import Any, Callable
  17
  18    from bauiv1lib.popup import PopupWindow
  19
  20
  21class GamepadSettingsWindow(bui.MainWindow):
  22    """Window for configuring a gamepad."""
  23
  24    # pylint: disable=too-many-public-methods
  25
  26    def __init__(
  27        self,
  28        inputdevice: bs.InputDevice,
  29        *,
  30        modal: bool = False,
  31        transition: str | None = 'in_right',
  32        transition_out: str = 'out_right',
  33        origin_widget: bui.Widget | None = None,
  34        settings: dict | None = None,
  35    ):
  36        self._inputdevice = inputdevice
  37
  38        # If our input-device went away, just return an empty zombie.
  39        if not self._inputdevice:
  40            return
  41
  42        self._name = self._inputdevice.name
  43
  44        self._r = 'configGamepadWindow'
  45        self._transition_out = transition_out
  46
  47        # We're a secondary gamepad if supplied with settings.
  48        self._is_secondary = settings is not None
  49        self._ext = '_B' if self._is_secondary else ''
  50        self._modal = modal
  51        self._displayname = self._name
  52        self._width = 700 if self._is_secondary else 730
  53        self._height = 440 if self._is_secondary else 450
  54        self._spacing = 40
  55        assert bui.app.classic is not None
  56        uiscale = bui.app.ui_v1.uiscale
  57        super().__init__(
  58            root_widget=bui.containerwidget(
  59                size=(self._width, self._height),
  60                scale=(
  61                    1.4
  62                    if uiscale is bui.UIScale.SMALL
  63                    else 1.3 if uiscale is bui.UIScale.MEDIUM else 1.0
  64                ),
  65                stack_offset=(
  66                    (0, -10) if uiscale is bui.UIScale.SMALL else (0, 0)
  67                ),
  68            ),
  69            transition=transition,
  70            origin_widget=origin_widget,
  71        )
  72
  73        self._settings: dict[str, int] = {}
  74        if not self._is_secondary:
  75            self._get_config_mapping()
  76
  77        # Don't ask to config joysticks while we're in here.
  78        self._rebuild_ui()
  79
  80    @override
  81    def get_main_window_state(self) -> bui.MainWindowState:
  82        # Support recreating our window for back/refresh purposes.
  83        cls = type(self)
  84
  85        # Pull stuff out of self here; if we do it in the lambda we keep
  86        # self alive which we don't want.
  87        assert not self._is_secondary
  88        assert not self._modal
  89
  90        inputdevice = self._inputdevice
  91
  92        return bui.BasicMainWindowState(
  93            create_call=lambda transition, origin_widget: cls(
  94                inputdevice=inputdevice,
  95                transition=transition,
  96                origin_widget=origin_widget,
  97            )
  98        )
  99
 100    def _get_config_mapping(self, default: bool = False) -> None:
 101        for button in [
 102            'buttonJump',
 103            'buttonJump_B',
 104            'buttonPunch',
 105            'buttonPunch_B',
 106            'buttonBomb',
 107            'buttonBomb_B',
 108            'buttonPickUp',
 109            'buttonPickUp_B',
 110            'buttonStart',
 111            'buttonStart_B',
 112            'buttonStart2',
 113            'buttonStart2_B',
 114            'buttonUp',
 115            'buttonUp_B',
 116            'buttonDown',
 117            'buttonDown_B',
 118            'buttonLeft',
 119            'buttonLeft_B',
 120            'buttonRight',
 121            'buttonRight_B',
 122            'buttonRun1',
 123            'buttonRun1_B',
 124            'buttonRun2',
 125            'buttonRun2_B',
 126            'triggerRun1',
 127            'triggerRun1_B',
 128            'triggerRun2',
 129            'triggerRun2_B',
 130            'buttonIgnored',
 131            'buttonIgnored_B',
 132            'buttonIgnored2',
 133            'buttonIgnored2_B',
 134            'buttonIgnored3',
 135            'buttonIgnored3_B',
 136            'buttonIgnored4',
 137            'buttonIgnored4_B',
 138            'buttonVRReorient',
 139            'buttonVRReorient_B',
 140            'analogStickDeadZone',
 141            'analogStickDeadZone_B',
 142            'dpad',
 143            'dpad_B',
 144            'unassignedButtonsRun',
 145            'unassignedButtonsRun_B',
 146            'startButtonActivatesDefaultWidget',
 147            'startButtonActivatesDefaultWidget_B',
 148            'uiOnly',
 149            'uiOnly_B',
 150            'ignoreCompletely',
 151            'ignoreCompletely_B',
 152            'autoRecalibrateAnalogStick',
 153            'autoRecalibrateAnalogStick_B',
 154            'analogStickLR',
 155            'analogStickLR_B',
 156            'analogStickUD',
 157            'analogStickUD_B',
 158            'enableSecondary',
 159        ]:
 160            assert bui.app.classic is not None
 161            val = bui.app.classic.get_input_device_mapped_value(
 162                self._inputdevice, button, default
 163            )
 164            if val != -1:
 165                self._settings[button] = val
 166
 167    def _rebuild_ui(self, is_reset: bool = False) -> None:
 168        # pylint: disable=too-many-statements
 169
 170        assert bui.app.classic is not None
 171
 172        # Clear existing UI.
 173        for widget in self._root_widget.get_children():
 174            widget.delete()
 175
 176        self._textwidgets: dict[str, bui.Widget] = {}
 177
 178        back_button: bui.Widget | None
 179
 180        if self._is_secondary:
 181            back_button = bui.buttonwidget(
 182                parent=self._root_widget,
 183                position=(self._width - 180, self._height - 65),
 184                autoselect=True,
 185                size=(160, 60),
 186                label=bui.Lstr(resource='doneText'),
 187                scale=0.9,
 188                on_activate_call=self._save,
 189            )
 190            bui.containerwidget(
 191                edit=self._root_widget,
 192                start_button=back_button,
 193                on_cancel_call=back_button.activate,
 194            )
 195            cancel_button = None
 196        else:
 197            cancel_button = bui.buttonwidget(
 198                parent=self._root_widget,
 199                position=(51, self._height - 65),
 200                autoselect=True,
 201                size=(160, 60),
 202                label=bui.Lstr(resource='cancelText'),
 203                scale=0.9,
 204                on_activate_call=self._cancel,
 205            )
 206            bui.containerwidget(
 207                edit=self._root_widget, cancel_button=cancel_button
 208            )
 209
 210        save_button: bui.Widget | None
 211        if not self._is_secondary:
 212            save_button = bui.buttonwidget(
 213                parent=self._root_widget,
 214                position=(self._width - 195, self._height - 65),
 215                size=(180, 60),
 216                autoselect=True,
 217                label=bui.Lstr(resource='saveText'),
 218                scale=0.9,
 219                on_activate_call=self._save,
 220            )
 221            bui.containerwidget(
 222                edit=self._root_widget, start_button=save_button
 223            )
 224        else:
 225            save_button = None
 226
 227        if not self._is_secondary:
 228            v = self._height - 59
 229            bui.textwidget(
 230                parent=self._root_widget,
 231                position=(0, v + 5),
 232                size=(self._width, 25),
 233                text=bui.Lstr(resource=f'{self._r}.titleText'),
 234                color=bui.app.ui_v1.title_color,
 235                maxwidth=310,
 236                h_align='center',
 237                v_align='center',
 238            )
 239            v -= 48
 240
 241            bui.textwidget(
 242                parent=self._root_widget,
 243                position=(0, v + 3),
 244                size=(self._width, 25),
 245                text=self._name,
 246                color=bui.app.ui_v1.infotextcolor,
 247                maxwidth=self._width * 0.9,
 248                h_align='center',
 249                v_align='center',
 250            )
 251            v -= self._spacing * 1
 252
 253            bui.textwidget(
 254                parent=self._root_widget,
 255                position=(50, v + 10),
 256                size=(self._width - 100, 30),
 257                text=bui.Lstr(resource=f'{self._r}.appliesToAllText'),
 258                maxwidth=330,
 259                scale=0.65,
 260                color=(0.5, 0.6, 0.5, 1.0),
 261                h_align='center',
 262                v_align='center',
 263            )
 264            v -= 70
 265            self._enable_check_box = None
 266        else:
 267            v = self._height - 49
 268            bui.textwidget(
 269                parent=self._root_widget,
 270                position=(0, v + 5),
 271                size=(self._width, 25),
 272                text=bui.Lstr(resource=f'{self._r}.secondaryText'),
 273                color=bui.app.ui_v1.title_color,
 274                maxwidth=300,
 275                h_align='center',
 276                v_align='center',
 277            )
 278            v -= self._spacing * 1
 279
 280            bui.textwidget(
 281                parent=self._root_widget,
 282                position=(50, v + 10),
 283                size=(self._width - 100, 30),
 284                text=bui.Lstr(resource=f'{self._r}.secondHalfText'),
 285                maxwidth=300,
 286                scale=0.65,
 287                color=(0.6, 0.8, 0.6, 1.0),
 288                h_align='center',
 289            )
 290            self._enable_check_box = bui.checkboxwidget(
 291                parent=self._root_widget,
 292                position=(self._width * 0.5 - 80, v - 73),
 293                value=self.get_enable_secondary_value(),
 294                autoselect=True,
 295                on_value_change_call=self._enable_check_box_changed,
 296                size=(200, 30),
 297                text=bui.Lstr(resource=f'{self._r}.secondaryEnableText'),
 298                scale=1.2,
 299            )
 300            v = self._height - 205
 301
 302        h_offs = 160
 303        dist = 70
 304        d_color = (0.4, 0.4, 0.8)
 305        sclx = 1.2
 306        scly = 0.98
 307        dpm = bui.Lstr(resource=f'{self._r}.pressAnyButtonOrDpadText')
 308        dpm2 = bui.Lstr(resource=f'{self._r}.ifNothingHappensTryAnalogText')
 309        self._capture_button(
 310            pos=(h_offs, v + scly * dist),
 311            color=d_color,
 312            button='buttonUp' + self._ext,
 313            texture=bui.gettexture('upButton'),
 314            scale=1.0,
 315            message=dpm,
 316            message2=dpm2,
 317        )
 318        self._capture_button(
 319            pos=(h_offs - sclx * dist, v),
 320            color=d_color,
 321            button='buttonLeft' + self._ext,
 322            texture=bui.gettexture('leftButton'),
 323            scale=1.0,
 324            message=dpm,
 325            message2=dpm2,
 326        )
 327        self._capture_button(
 328            pos=(h_offs + sclx * dist, v),
 329            color=d_color,
 330            button='buttonRight' + self._ext,
 331            texture=bui.gettexture('rightButton'),
 332            scale=1.0,
 333            message=dpm,
 334            message2=dpm2,
 335        )
 336        self._capture_button(
 337            pos=(h_offs, v - scly * dist),
 338            color=d_color,
 339            button='buttonDown' + self._ext,
 340            texture=bui.gettexture('downButton'),
 341            scale=1.0,
 342            message=dpm,
 343            message2=dpm2,
 344        )
 345
 346        dpm3 = bui.Lstr(resource=f'{self._r}.ifNothingHappensTryDpadText')
 347        self._capture_button(
 348            pos=(h_offs + 130, v - 125),
 349            color=(0.4, 0.4, 0.6),
 350            button='analogStickLR' + self._ext,
 351            maxwidth=140,
 352            texture=bui.gettexture('analogStick'),
 353            scale=1.2,
 354            message=bui.Lstr(resource=f'{self._r}.pressLeftRightText'),
 355            message2=dpm3,
 356        )
 357
 358        self._capture_button(
 359            pos=(self._width * 0.5, v),
 360            color=(0.4, 0.4, 0.6),
 361            button='buttonStart' + self._ext,
 362            texture=bui.gettexture('startButton'),
 363            scale=0.7,
 364        )
 365
 366        h_offs = self._width - 160
 367
 368        self._capture_button(
 369            pos=(h_offs, v + scly * dist),
 370            color=(0.6, 0.4, 0.8),
 371            button='buttonPickUp' + self._ext,
 372            texture=bui.gettexture('buttonPickUp'),
 373            scale=1.0,
 374        )
 375        self._capture_button(
 376            pos=(h_offs - sclx * dist, v),
 377            color=(0.7, 0.5, 0.1),
 378            button='buttonPunch' + self._ext,
 379            texture=bui.gettexture('buttonPunch'),
 380            scale=1.0,
 381        )
 382        self._capture_button(
 383            pos=(h_offs + sclx * dist, v),
 384            color=(0.5, 0.2, 0.1),
 385            button='buttonBomb' + self._ext,
 386            texture=bui.gettexture('buttonBomb'),
 387            scale=1.0,
 388        )
 389        self._capture_button(
 390            pos=(h_offs, v - scly * dist),
 391            color=(0.2, 0.5, 0.2),
 392            button='buttonJump' + self._ext,
 393            texture=bui.gettexture('buttonJump'),
 394            scale=1.0,
 395        )
 396
 397        self._more_button = bui.buttonwidget(
 398            parent=self._root_widget,
 399            autoselect=True,
 400            label='...',
 401            text_scale=0.9,
 402            color=(0.45, 0.4, 0.5),
 403            textcolor=(0.65, 0.6, 0.7),
 404            position=(self._width - 300, 30),
 405            size=(130, 40),
 406            on_activate_call=self._do_more,
 407        )
 408
 409        try:
 410            if cancel_button is not None and save_button is not None:
 411                bui.widget(edit=cancel_button, right_widget=save_button)
 412                bui.widget(edit=save_button, left_widget=cancel_button)
 413                if is_reset:
 414                    bui.containerwidget(
 415                        edit=self._root_widget,
 416                        selected_child=self._more_button,
 417                    )
 418        except Exception:
 419            logging.exception('Error wiring up gamepad config window.')
 420
 421    def get_r(self) -> str:
 422        """(internal)"""
 423        return self._r
 424
 425    def get_advanced_button(self) -> bui.Widget:
 426        """(internal)"""
 427        return self._more_button
 428
 429    def get_is_secondary(self) -> bool:
 430        """(internal)"""
 431        return self._is_secondary
 432
 433    def get_settings(self) -> dict[str, Any]:
 434        """(internal)"""
 435        assert self._settings is not None
 436        return self._settings
 437
 438    def get_ext(self) -> str:
 439        """(internal)"""
 440        return self._ext
 441
 442    def get_input(self) -> bs.InputDevice:
 443        """(internal)"""
 444        return self._inputdevice
 445
 446    def _do_advanced(self) -> None:
 447        # pylint: disable=cyclic-import
 448        from bauiv1lib.settings import gamepadadvanced
 449
 450        gamepadadvanced.GamepadAdvancedSettingsWindow(self)
 451
 452    def _enable_check_box_changed(self, value: bool) -> None:
 453        assert self._settings is not None
 454        if value:
 455            self._settings['enableSecondary'] = 1
 456        else:
 457            # Just clear since this is default.
 458            if 'enableSecondary' in self._settings:
 459                del self._settings['enableSecondary']
 460
 461    def get_unassigned_buttons_run_value(self) -> bool:
 462        """(internal)"""
 463        assert self._settings is not None
 464        val = self._settings.get('unassignedButtonsRun', True)
 465        assert isinstance(val, bool)
 466        return val
 467
 468    def set_unassigned_buttons_run_value(self, value: bool) -> None:
 469        """(internal)"""
 470        assert self._settings is not None
 471        if value:
 472            if 'unassignedButtonsRun' in self._settings:
 473                # Clear since this is default.
 474                del self._settings['unassignedButtonsRun']
 475                return
 476        self._settings['unassignedButtonsRun'] = False
 477
 478    def get_start_button_activates_default_widget_value(self) -> bool:
 479        """(internal)"""
 480        assert self._settings is not None
 481        val = self._settings.get('startButtonActivatesDefaultWidget', True)
 482        assert isinstance(val, bool)
 483        return val
 484
 485    def set_start_button_activates_default_widget_value(
 486        self, value: bool
 487    ) -> None:
 488        """(internal)"""
 489        assert self._settings is not None
 490        if value:
 491            if 'startButtonActivatesDefaultWidget' in self._settings:
 492                # Clear since this is default.
 493                del self._settings['startButtonActivatesDefaultWidget']
 494                return
 495        self._settings['startButtonActivatesDefaultWidget'] = False
 496
 497    def get_ui_only_value(self) -> bool:
 498        """(internal)"""
 499        assert self._settings is not None
 500        val = self._settings.get('uiOnly', False)
 501        assert isinstance(val, bool)
 502        return val
 503
 504    def set_ui_only_value(self, value: bool) -> None:
 505        """(internal)"""
 506        assert self._settings is not None
 507        if not value:
 508            if 'uiOnly' in self._settings:
 509                # Clear since this is default.
 510                del self._settings['uiOnly']
 511                return
 512        self._settings['uiOnly'] = True
 513
 514    def get_ignore_completely_value(self) -> bool:
 515        """(internal)"""
 516        assert self._settings is not None
 517        val = self._settings.get('ignoreCompletely', False)
 518        assert isinstance(val, bool)
 519        return val
 520
 521    def set_ignore_completely_value(self, value: bool) -> None:
 522        """(internal)"""
 523        assert self._settings is not None
 524        if not value:
 525            if 'ignoreCompletely' in self._settings:
 526                # Clear since this is default.
 527                del self._settings['ignoreCompletely']
 528                return
 529        self._settings['ignoreCompletely'] = True
 530
 531    def get_auto_recalibrate_analog_stick_value(self) -> bool:
 532        """(internal)"""
 533        assert self._settings is not None
 534        val = self._settings.get('autoRecalibrateAnalogStick', False)
 535        assert isinstance(val, bool)
 536        return val
 537
 538    def set_auto_recalibrate_analog_stick_value(self, value: bool) -> None:
 539        """(internal)"""
 540        assert self._settings is not None
 541        if not value:
 542            if 'autoRecalibrateAnalogStick' in self._settings:
 543                # Clear since this is default.
 544                del self._settings['autoRecalibrateAnalogStick']
 545        else:
 546            self._settings['autoRecalibrateAnalogStick'] = True
 547
 548    def get_enable_secondary_value(self) -> bool:
 549        """(internal)"""
 550        assert self._settings is not None
 551        if not self._is_secondary:
 552            raise RuntimeError('Enable value only applies to secondary editor.')
 553        val = self._settings.get('enableSecondary', False)
 554        assert isinstance(val, bool)
 555        return val
 556
 557    def show_secondary_editor(self) -> None:
 558        """(internal)"""
 559        GamepadSettingsWindow(
 560            self._inputdevice,
 561            modal=True,
 562            settings=self._settings,
 563            transition='in_scale',
 564            transition_out='out_scale',
 565        )
 566
 567    def get_control_value_name(self, control: str) -> str | bui.Lstr:
 568        """(internal)"""
 569        # pylint: disable=too-many-return-statements
 570        assert self._settings is not None
 571        if control == 'analogStickLR' + self._ext:
 572            # This actually shows both LR and UD.
 573            sval1 = (
 574                self._settings['analogStickLR' + self._ext]
 575                if 'analogStickLR' + self._ext in self._settings
 576                else 5 if self._is_secondary else None
 577            )
 578            sval2 = (
 579                self._settings['analogStickUD' + self._ext]
 580                if 'analogStickUD' + self._ext in self._settings
 581                else 6 if self._is_secondary else None
 582            )
 583            assert isinstance(sval1, (int, type(None)))
 584            assert isinstance(sval2, (int, type(None)))
 585            if sval1 is not None and sval2 is not None:
 586                return (
 587                    self._inputdevice.get_axis_name(sval1)
 588                    + ' / '
 589                    + self._inputdevice.get_axis_name(sval2)
 590                )
 591            return bui.Lstr(resource=f'{self._r}.unsetText')
 592
 593        # If they're looking for triggers.
 594        if control in ['triggerRun1' + self._ext, 'triggerRun2' + self._ext]:
 595            if control in self._settings:
 596                return self._inputdevice.get_axis_name(self._settings[control])
 597            return bui.Lstr(resource=f'{self._r}.unsetText')
 598
 599        # Dead-zone.
 600        if control == 'analogStickDeadZone' + self._ext:
 601            if control in self._settings:
 602                return str(self._settings[control])
 603            return str(1.0)
 604
 605        # For dpad buttons: show individual buttons if any are set.
 606        # Otherwise show whichever dpad is set.
 607        dpad_buttons = [
 608            'buttonLeft' + self._ext,
 609            'buttonRight' + self._ext,
 610            'buttonUp' + self._ext,
 611            'buttonDown' + self._ext,
 612        ]
 613        if control in dpad_buttons:
 614            # If *any* dpad buttons are assigned, show only button assignments.
 615            if any(b in self._settings for b in dpad_buttons):
 616                if control in self._settings:
 617                    return self._inputdevice.get_button_name(
 618                        self._settings[control]
 619                    )
 620                return bui.Lstr(resource=f'{self._r}.unsetText')
 621
 622            # No dpad buttons - show the dpad number for all 4.
 623            dpadnum = (
 624                self._settings['dpad' + self._ext]
 625                if 'dpad' + self._ext in self._settings
 626                else 2 if self._is_secondary else None
 627            )
 628            assert isinstance(dpadnum, (int, type(None)))
 629            if dpadnum is not None:
 630                return bui.Lstr(
 631                    value='${A} ${B}',
 632                    subs=[
 633                        ('${A}', bui.Lstr(resource=f'{self._r}.dpadText')),
 634                        (
 635                            '${B}',
 636                            str(dpadnum),
 637                        ),
 638                    ],
 639                )
 640            return bui.Lstr(resource=f'{self._r}.unsetText')
 641
 642        # Other buttons.
 643        if control in self._settings:
 644            return self._inputdevice.get_button_name(self._settings[control])
 645        return bui.Lstr(resource=f'{self._r}.unsetText')
 646
 647    def _gamepad_event(
 648        self,
 649        control: str,
 650        event: dict[str, Any],
 651        dialog: AwaitGamepadInputWindow,
 652    ) -> None:
 653        # pylint: disable=too-many-branches
 654        assert self._settings is not None
 655        ext = self._ext
 656
 657        # For our dpad-buttons we're looking for either a button-press or a
 658        # hat-switch press.
 659        if control in [
 660            'buttonUp' + ext,
 661            'buttonLeft' + ext,
 662            'buttonDown' + ext,
 663            'buttonRight' + ext,
 664        ]:
 665            if event['type'] in ['BUTTONDOWN', 'HATMOTION']:
 666                # If its a button-down.
 667                if event['type'] == 'BUTTONDOWN':
 668                    value = event['button']
 669                    self._settings[control] = value
 670
 671                # If its a dpad.
 672                elif event['type'] == 'HATMOTION':
 673                    # clear out any set dir-buttons
 674                    for btn in [
 675                        'buttonUp' + ext,
 676                        'buttonLeft' + ext,
 677                        'buttonRight' + ext,
 678                        'buttonDown' + ext,
 679                    ]:
 680                        if btn in self._settings:
 681                            del self._settings[btn]
 682                    if event['hat'] == (2 if self._is_secondary else 1):
 683                        self._settings['dpad' + ext] = event['hat']
 684
 685                # Update the 4 dpad button txt widgets.
 686                bui.textwidget(
 687                    edit=self._textwidgets['buttonUp' + ext],
 688                    text=self.get_control_value_name('buttonUp' + ext),
 689                )
 690                bui.textwidget(
 691                    edit=self._textwidgets['buttonLeft' + ext],
 692                    text=self.get_control_value_name('buttonLeft' + ext),
 693                )
 694                bui.textwidget(
 695                    edit=self._textwidgets['buttonRight' + ext],
 696                    text=self.get_control_value_name('buttonRight' + ext),
 697                )
 698                bui.textwidget(
 699                    edit=self._textwidgets['buttonDown' + ext],
 700                    text=self.get_control_value_name('buttonDown' + ext),
 701                )
 702                bui.getsound('gunCocking').play()
 703                dialog.die()
 704
 705        elif control == 'analogStickLR' + ext:
 706            if event['type'] == 'AXISMOTION':
 707                # Ignore small values or else we might get triggered by noise.
 708                if abs(event['value']) > 0.5:
 709                    axis = event['axis']
 710                    if axis == (5 if self._is_secondary else 1):
 711                        self._settings['analogStickLR' + ext] = axis
 712                    bui.textwidget(
 713                        edit=self._textwidgets['analogStickLR' + ext],
 714                        text=self.get_control_value_name('analogStickLR' + ext),
 715                    )
 716                    bui.getsound('gunCocking').play()
 717                    dialog.die()
 718
 719                    # Now launch the up/down listener.
 720                    AwaitGamepadInputWindow(
 721                        self._inputdevice,
 722                        'analogStickUD' + ext,
 723                        self._gamepad_event,
 724                        bui.Lstr(resource=f'{self._r}.pressUpDownText'),
 725                    )
 726
 727        elif control == 'analogStickUD' + ext:
 728            if event['type'] == 'AXISMOTION':
 729                # Ignore small values or else we might get triggered by noise.
 730                if abs(event['value']) > 0.5:
 731                    axis = event['axis']
 732
 733                    # Ignore our LR axis.
 734                    if 'analogStickLR' + ext in self._settings:
 735                        lr_axis = self._settings['analogStickLR' + ext]
 736                    else:
 737                        lr_axis = 5 if self._is_secondary else 1
 738                    if axis != lr_axis:
 739                        if axis == (6 if self._is_secondary else 2):
 740                            self._settings['analogStickUD' + ext] = axis
 741                        bui.textwidget(
 742                            edit=self._textwidgets['analogStickLR' + ext],
 743                            text=self.get_control_value_name(
 744                                'analogStickLR' + ext
 745                            ),
 746                        )
 747                        bui.getsound('gunCocking').play()
 748                        dialog.die()
 749        else:
 750            # For other buttons we just want a button-press.
 751            if event['type'] == 'BUTTONDOWN':
 752                value = event['button']
 753                self._settings[control] = value
 754
 755                # Update the button's text widget.
 756                bui.textwidget(
 757                    edit=self._textwidgets[control],
 758                    text=self.get_control_value_name(control),
 759                )
 760                bui.getsound('gunCocking').play()
 761                dialog.die()
 762
 763    def _capture_button(
 764        self,
 765        pos: tuple[float, float],
 766        color: tuple[float, float, float],
 767        texture: bui.Texture,
 768        button: str,
 769        *,
 770        scale: float = 1.0,
 771        message: bui.Lstr | None = None,
 772        message2: bui.Lstr | None = None,
 773        maxwidth: float = 80.0,
 774    ) -> bui.Widget:
 775        if message is None:
 776            message = bui.Lstr(resource=f'{self._r}.pressAnyButtonText')
 777        base_size = 79
 778        btn = bui.buttonwidget(
 779            parent=self._root_widget,
 780            position=(
 781                pos[0] - base_size * 0.5 * scale,
 782                pos[1] - base_size * 0.5 * scale,
 783            ),
 784            autoselect=True,
 785            size=(base_size * scale, base_size * scale),
 786            texture=texture,
 787            label='',
 788            color=color,
 789        )
 790
 791        # Make this in a timer so that it shows up on top of all other buttons.
 792
 793        def doit() -> None:
 794            uiscale = 0.9 * scale
 795            txt = bui.textwidget(
 796                parent=self._root_widget,
 797                position=(pos[0] + 0.0 * scale, pos[1] - 58.0 * scale),
 798                color=(1, 1, 1, 0.3),
 799                size=(0, 0),
 800                h_align='center',
 801                v_align='center',
 802                scale=uiscale,
 803                text=self.get_control_value_name(button),
 804                maxwidth=maxwidth,
 805            )
 806            self._textwidgets[button] = txt
 807            bui.buttonwidget(
 808                edit=btn,
 809                on_activate_call=bui.Call(
 810                    AwaitGamepadInputWindow,
 811                    self._inputdevice,
 812                    button,
 813                    self._gamepad_event,
 814                    message,
 815                    message2,
 816                ),
 817            )
 818
 819        bui.pushcall(doit)
 820        return btn
 821
 822    def _cancel(self) -> None:
 823
 824        if self._modal:
 825            # no-op if our underlying widget is dead or on its way out.
 826            if not self._root_widget or self._root_widget.transitioning_out:
 827                return
 828            bui.containerwidget(
 829                edit=self._root_widget, transition=self._transition_out
 830            )
 831        else:
 832            self.main_window_back()
 833
 834    def _reset(self) -> None:
 835        from bauiv1lib.confirm import ConfirmWindow
 836
 837        assert bui.app.classic is not None
 838
 839        # efro note: I think it's ok to reset without a confirm here
 840        # because the user can see pretty clearly what changes and can
 841        # cancel out of the settings window without saving if they want.
 842        if bool(False):
 843            ConfirmWindow(
 844                # TODO: Implement a translation string for this!
 845                'Are you sure you want to reset your button mapping?\n'
 846                'This will also reset your advanced mappings\n'
 847                'and secondary controller button mappings.',
 848                self._do_reset,
 849                width=490,
 850                height=150,
 851            )
 852        else:
 853            self._do_reset()
 854
 855    def _do_reset(self) -> None:
 856        """Resets the input's mapping settings."""
 857        from babase import InputDeviceNotFoundError
 858
 859        self._settings = {}
 860        # Unplugging the controller while performing a
 861        # mapping reset makes things go bonkers a little.
 862        try:
 863            self._get_config_mapping(default=True)
 864        except InputDeviceNotFoundError:
 865            pass
 866
 867        self._rebuild_ui(is_reset=True)
 868        bui.getsound('gunCocking').play()
 869
 870    def _do_more(self) -> None:
 871        """Show a burger menu with extra settings."""
 872        # pylint: disable=cyclic-import
 873        choices: list[str] = [
 874            'advanced',
 875            'reset',
 876        ]
 877        choices_display: list[bui.Lstr] = [
 878            bui.Lstr(resource=f'{self._r}.advancedText'),
 879            bui.Lstr(resource='settingsWindowAdvanced.resetText'),
 880        ]
 881
 882        uiscale = bui.app.ui_v1.uiscale
 883        PopupMenuWindow(
 884            position=self._more_button.get_screen_space_center(),
 885            scale=(
 886                2.3
 887                if uiscale is bui.UIScale.SMALL
 888                else 1.65 if uiscale is bui.UIScale.MEDIUM else 1.23
 889            ),
 890            width=150,
 891            choices=choices,
 892            choices_display=choices_display,
 893            current_choice='advanced',
 894            delegate=self,
 895        )
 896
 897    def popup_menu_selected_choice(
 898        self, popup_window: PopupMenuWindow, choice: str
 899    ) -> None:
 900        """Called when a choice is selected in the popup."""
 901        del popup_window  # unused
 902        if choice == 'reset':
 903            self._reset()
 904        elif choice == 'advanced':
 905            self._do_advanced()
 906        else:
 907            print(f'invalid choice: {choice}')
 908
 909    def popup_menu_closing(self, popup_window: PopupWindow) -> None:
 910        """Called when the popup is closing."""
 911
 912    def _save(self) -> None:
 913        classic = bui.app.classic
 914        assert classic is not None
 915
 916        # no-op if our underlying widget is dead or on its way out.
 917        if not self._root_widget or self._root_widget.transitioning_out:
 918            return
 919
 920        # If we're a secondary editor we just go away (we were editing our
 921        # parent's settings dict).
 922        if self._is_secondary:
 923            assert self._modal
 924            bui.containerwidget(
 925                edit=self._root_widget, transition=self._transition_out
 926            )
 927            return
 928
 929        assert self._settings is not None
 930        if self._inputdevice:
 931            dst = classic.get_input_device_config(
 932                self._inputdevice, default=True
 933            )
 934            dst2: dict[str, Any] = dst[0][dst[1]]
 935            dst2.clear()
 936
 937            # Store any values that aren't -1.
 938            for key, val in list(self._settings.items()):
 939                if val != -1:
 940                    dst2[key] = val
 941
 942            # If we're allowed to phone home, send this config so we can
 943            # generate more defaults in the future.
 944            inputhash = classic.get_input_device_map_hash(self._inputdevice)
 945            classic.master_server_v1_post(
 946                'controllerConfig',
 947                {
 948                    'ua': classic.legacy_user_agent_string,
 949                    'b': bui.app.env.engine_build_number,
 950                    'name': self._name,
 951                    'inputMapHash': inputhash,
 952                    'config': dst2,
 953                    'v': 2,
 954                },
 955            )
 956            bui.app.config.apply_and_commit()
 957            bui.getsound('gunCocking').play()
 958        else:
 959            bui.getsound('error').play()
 960
 961        if self._modal:
 962            bui.containerwidget(
 963                edit=self._root_widget, transition=self._transition_out
 964            )
 965        else:
 966            assert self.main_window_has_control()
 967            self.main_window_back()
 968
 969
 970class AwaitGamepadInputWindow(bui.Window):
 971    """Window for capturing a gamepad button press."""
 972
 973    def __init__(
 974        self,
 975        gamepad: bs.InputDevice,
 976        button: str,
 977        callback: Callable[[str, dict[str, Any], AwaitGamepadInputWindow], Any],
 978        message: bui.Lstr | None = None,
 979        message2: bui.Lstr | None = None,
 980    ):
 981        # pylint: disable=too-many-positional-arguments
 982        if message is None:
 983            print('AwaitGamepadInputWindow message is None!')
 984            # Shouldn't get here.
 985            message = bui.Lstr(value='Press any button...')
 986        self._callback = callback
 987        self._input = gamepad
 988        self._capture_button = button
 989        width = 400
 990        height = 150
 991        assert bui.app.classic is not None
 992        uiscale = bui.app.ui_v1.uiscale
 993        super().__init__(
 994            root_widget=bui.containerwidget(
 995                scale=(
 996                    2.0
 997                    if uiscale is bui.UIScale.SMALL
 998                    else 1.9 if uiscale is bui.UIScale.MEDIUM else 1.0
 999                ),
1000                size=(width, height),
1001                transition='in_scale',
1002            ),
1003        )
1004        bui.textwidget(
1005            parent=self._root_widget,
1006            position=(0, (height - 60) if message2 is None else (height - 50)),
1007            size=(width, 25),
1008            text=message,
1009            maxwidth=width * 0.9,
1010            h_align='center',
1011            v_align='center',
1012        )
1013        if message2 is not None:
1014            bui.textwidget(
1015                parent=self._root_widget,
1016                position=(width * 0.5, height - 60),
1017                size=(0, 0),
1018                text=message2,
1019                maxwidth=width * 0.9,
1020                scale=0.47,
1021                color=(0.7, 1.0, 0.7, 0.6),
1022                h_align='center',
1023                v_align='center',
1024            )
1025        self._counter = 5
1026        self._count_down_text = bui.textwidget(
1027            parent=self._root_widget,
1028            h_align='center',
1029            position=(0, height - 110),
1030            size=(width, 25),
1031            color=(1, 1, 1, 0.3),
1032            text=str(self._counter),
1033        )
1034        self._decrement_timer: bui.AppTimer | None = bui.AppTimer(
1035            1.0, bui.Call(self._decrement), repeat=True
1036        )
1037        bs.capture_gamepad_input(bui.WeakCall(self._event_callback))
1038
1039    def __del__(self) -> None:
1040        pass
1041
1042    def die(self) -> None:
1043        """Kill the window."""
1044
1045        # This strong-refs us; killing it allow us to die now.
1046        self._decrement_timer = None
1047        bs.release_gamepad_input()
1048        if self._root_widget:
1049            bui.containerwidget(edit=self._root_widget, transition='out_scale')
1050
1051    def _event_callback(self, event: dict[str, Any]) -> None:
1052        input_device = event['input_device']
1053        assert isinstance(input_device, bs.InputDevice)
1054
1055        # Update - we now allow *any* input device of this type.
1056        if (
1057            self._input
1058            and input_device
1059            and input_device.name == self._input.name
1060        ):
1061            self._callback(self._capture_button, event, self)
1062
1063    def _decrement(self) -> None:
1064        self._counter -= 1
1065        if self._counter >= 1:
1066            if self._count_down_text:
1067                bui.textwidget(
1068                    edit=self._count_down_text, text=str(self._counter)
1069                )
1070        else:
1071            bui.getsound('error').play()
1072            self.die()
class GamepadSettingsWindow(bauiv1._uitypes.MainWindow):
 22class GamepadSettingsWindow(bui.MainWindow):
 23    """Window for configuring a gamepad."""
 24
 25    # pylint: disable=too-many-public-methods
 26
 27    def __init__(
 28        self,
 29        inputdevice: bs.InputDevice,
 30        *,
 31        modal: bool = False,
 32        transition: str | None = 'in_right',
 33        transition_out: str = 'out_right',
 34        origin_widget: bui.Widget | None = None,
 35        settings: dict | None = None,
 36    ):
 37        self._inputdevice = inputdevice
 38
 39        # If our input-device went away, just return an empty zombie.
 40        if not self._inputdevice:
 41            return
 42
 43        self._name = self._inputdevice.name
 44
 45        self._r = 'configGamepadWindow'
 46        self._transition_out = transition_out
 47
 48        # We're a secondary gamepad if supplied with settings.
 49        self._is_secondary = settings is not None
 50        self._ext = '_B' if self._is_secondary else ''
 51        self._modal = modal
 52        self._displayname = self._name
 53        self._width = 700 if self._is_secondary else 730
 54        self._height = 440 if self._is_secondary else 450
 55        self._spacing = 40
 56        assert bui.app.classic is not None
 57        uiscale = bui.app.ui_v1.uiscale
 58        super().__init__(
 59            root_widget=bui.containerwidget(
 60                size=(self._width, self._height),
 61                scale=(
 62                    1.4
 63                    if uiscale is bui.UIScale.SMALL
 64                    else 1.3 if uiscale is bui.UIScale.MEDIUM else 1.0
 65                ),
 66                stack_offset=(
 67                    (0, -10) if uiscale is bui.UIScale.SMALL else (0, 0)
 68                ),
 69            ),
 70            transition=transition,
 71            origin_widget=origin_widget,
 72        )
 73
 74        self._settings: dict[str, int] = {}
 75        if not self._is_secondary:
 76            self._get_config_mapping()
 77
 78        # Don't ask to config joysticks while we're in here.
 79        self._rebuild_ui()
 80
 81    @override
 82    def get_main_window_state(self) -> bui.MainWindowState:
 83        # Support recreating our window for back/refresh purposes.
 84        cls = type(self)
 85
 86        # Pull stuff out of self here; if we do it in the lambda we keep
 87        # self alive which we don't want.
 88        assert not self._is_secondary
 89        assert not self._modal
 90
 91        inputdevice = self._inputdevice
 92
 93        return bui.BasicMainWindowState(
 94            create_call=lambda transition, origin_widget: cls(
 95                inputdevice=inputdevice,
 96                transition=transition,
 97                origin_widget=origin_widget,
 98            )
 99        )
100
101    def _get_config_mapping(self, default: bool = False) -> None:
102        for button in [
103            'buttonJump',
104            'buttonJump_B',
105            'buttonPunch',
106            'buttonPunch_B',
107            'buttonBomb',
108            'buttonBomb_B',
109            'buttonPickUp',
110            'buttonPickUp_B',
111            'buttonStart',
112            'buttonStart_B',
113            'buttonStart2',
114            'buttonStart2_B',
115            'buttonUp',
116            'buttonUp_B',
117            'buttonDown',
118            'buttonDown_B',
119            'buttonLeft',
120            'buttonLeft_B',
121            'buttonRight',
122            'buttonRight_B',
123            'buttonRun1',
124            'buttonRun1_B',
125            'buttonRun2',
126            'buttonRun2_B',
127            'triggerRun1',
128            'triggerRun1_B',
129            'triggerRun2',
130            'triggerRun2_B',
131            'buttonIgnored',
132            'buttonIgnored_B',
133            'buttonIgnored2',
134            'buttonIgnored2_B',
135            'buttonIgnored3',
136            'buttonIgnored3_B',
137            'buttonIgnored4',
138            'buttonIgnored4_B',
139            'buttonVRReorient',
140            'buttonVRReorient_B',
141            'analogStickDeadZone',
142            'analogStickDeadZone_B',
143            'dpad',
144            'dpad_B',
145            'unassignedButtonsRun',
146            'unassignedButtonsRun_B',
147            'startButtonActivatesDefaultWidget',
148            'startButtonActivatesDefaultWidget_B',
149            'uiOnly',
150            'uiOnly_B',
151            'ignoreCompletely',
152            'ignoreCompletely_B',
153            'autoRecalibrateAnalogStick',
154            'autoRecalibrateAnalogStick_B',
155            'analogStickLR',
156            'analogStickLR_B',
157            'analogStickUD',
158            'analogStickUD_B',
159            'enableSecondary',
160        ]:
161            assert bui.app.classic is not None
162            val = bui.app.classic.get_input_device_mapped_value(
163                self._inputdevice, button, default
164            )
165            if val != -1:
166                self._settings[button] = val
167
168    def _rebuild_ui(self, is_reset: bool = False) -> None:
169        # pylint: disable=too-many-statements
170
171        assert bui.app.classic is not None
172
173        # Clear existing UI.
174        for widget in self._root_widget.get_children():
175            widget.delete()
176
177        self._textwidgets: dict[str, bui.Widget] = {}
178
179        back_button: bui.Widget | None
180
181        if self._is_secondary:
182            back_button = bui.buttonwidget(
183                parent=self._root_widget,
184                position=(self._width - 180, self._height - 65),
185                autoselect=True,
186                size=(160, 60),
187                label=bui.Lstr(resource='doneText'),
188                scale=0.9,
189                on_activate_call=self._save,
190            )
191            bui.containerwidget(
192                edit=self._root_widget,
193                start_button=back_button,
194                on_cancel_call=back_button.activate,
195            )
196            cancel_button = None
197        else:
198            cancel_button = bui.buttonwidget(
199                parent=self._root_widget,
200                position=(51, self._height - 65),
201                autoselect=True,
202                size=(160, 60),
203                label=bui.Lstr(resource='cancelText'),
204                scale=0.9,
205                on_activate_call=self._cancel,
206            )
207            bui.containerwidget(
208                edit=self._root_widget, cancel_button=cancel_button
209            )
210
211        save_button: bui.Widget | None
212        if not self._is_secondary:
213            save_button = bui.buttonwidget(
214                parent=self._root_widget,
215                position=(self._width - 195, self._height - 65),
216                size=(180, 60),
217                autoselect=True,
218                label=bui.Lstr(resource='saveText'),
219                scale=0.9,
220                on_activate_call=self._save,
221            )
222            bui.containerwidget(
223                edit=self._root_widget, start_button=save_button
224            )
225        else:
226            save_button = None
227
228        if not self._is_secondary:
229            v = self._height - 59
230            bui.textwidget(
231                parent=self._root_widget,
232                position=(0, v + 5),
233                size=(self._width, 25),
234                text=bui.Lstr(resource=f'{self._r}.titleText'),
235                color=bui.app.ui_v1.title_color,
236                maxwidth=310,
237                h_align='center',
238                v_align='center',
239            )
240            v -= 48
241
242            bui.textwidget(
243                parent=self._root_widget,
244                position=(0, v + 3),
245                size=(self._width, 25),
246                text=self._name,
247                color=bui.app.ui_v1.infotextcolor,
248                maxwidth=self._width * 0.9,
249                h_align='center',
250                v_align='center',
251            )
252            v -= self._spacing * 1
253
254            bui.textwidget(
255                parent=self._root_widget,
256                position=(50, v + 10),
257                size=(self._width - 100, 30),
258                text=bui.Lstr(resource=f'{self._r}.appliesToAllText'),
259                maxwidth=330,
260                scale=0.65,
261                color=(0.5, 0.6, 0.5, 1.0),
262                h_align='center',
263                v_align='center',
264            )
265            v -= 70
266            self._enable_check_box = None
267        else:
268            v = self._height - 49
269            bui.textwidget(
270                parent=self._root_widget,
271                position=(0, v + 5),
272                size=(self._width, 25),
273                text=bui.Lstr(resource=f'{self._r}.secondaryText'),
274                color=bui.app.ui_v1.title_color,
275                maxwidth=300,
276                h_align='center',
277                v_align='center',
278            )
279            v -= self._spacing * 1
280
281            bui.textwidget(
282                parent=self._root_widget,
283                position=(50, v + 10),
284                size=(self._width - 100, 30),
285                text=bui.Lstr(resource=f'{self._r}.secondHalfText'),
286                maxwidth=300,
287                scale=0.65,
288                color=(0.6, 0.8, 0.6, 1.0),
289                h_align='center',
290            )
291            self._enable_check_box = bui.checkboxwidget(
292                parent=self._root_widget,
293                position=(self._width * 0.5 - 80, v - 73),
294                value=self.get_enable_secondary_value(),
295                autoselect=True,
296                on_value_change_call=self._enable_check_box_changed,
297                size=(200, 30),
298                text=bui.Lstr(resource=f'{self._r}.secondaryEnableText'),
299                scale=1.2,
300            )
301            v = self._height - 205
302
303        h_offs = 160
304        dist = 70
305        d_color = (0.4, 0.4, 0.8)
306        sclx = 1.2
307        scly = 0.98
308        dpm = bui.Lstr(resource=f'{self._r}.pressAnyButtonOrDpadText')
309        dpm2 = bui.Lstr(resource=f'{self._r}.ifNothingHappensTryAnalogText')
310        self._capture_button(
311            pos=(h_offs, v + scly * dist),
312            color=d_color,
313            button='buttonUp' + self._ext,
314            texture=bui.gettexture('upButton'),
315            scale=1.0,
316            message=dpm,
317            message2=dpm2,
318        )
319        self._capture_button(
320            pos=(h_offs - sclx * dist, v),
321            color=d_color,
322            button='buttonLeft' + self._ext,
323            texture=bui.gettexture('leftButton'),
324            scale=1.0,
325            message=dpm,
326            message2=dpm2,
327        )
328        self._capture_button(
329            pos=(h_offs + sclx * dist, v),
330            color=d_color,
331            button='buttonRight' + self._ext,
332            texture=bui.gettexture('rightButton'),
333            scale=1.0,
334            message=dpm,
335            message2=dpm2,
336        )
337        self._capture_button(
338            pos=(h_offs, v - scly * dist),
339            color=d_color,
340            button='buttonDown' + self._ext,
341            texture=bui.gettexture('downButton'),
342            scale=1.0,
343            message=dpm,
344            message2=dpm2,
345        )
346
347        dpm3 = bui.Lstr(resource=f'{self._r}.ifNothingHappensTryDpadText')
348        self._capture_button(
349            pos=(h_offs + 130, v - 125),
350            color=(0.4, 0.4, 0.6),
351            button='analogStickLR' + self._ext,
352            maxwidth=140,
353            texture=bui.gettexture('analogStick'),
354            scale=1.2,
355            message=bui.Lstr(resource=f'{self._r}.pressLeftRightText'),
356            message2=dpm3,
357        )
358
359        self._capture_button(
360            pos=(self._width * 0.5, v),
361            color=(0.4, 0.4, 0.6),
362            button='buttonStart' + self._ext,
363            texture=bui.gettexture('startButton'),
364            scale=0.7,
365        )
366
367        h_offs = self._width - 160
368
369        self._capture_button(
370            pos=(h_offs, v + scly * dist),
371            color=(0.6, 0.4, 0.8),
372            button='buttonPickUp' + self._ext,
373            texture=bui.gettexture('buttonPickUp'),
374            scale=1.0,
375        )
376        self._capture_button(
377            pos=(h_offs - sclx * dist, v),
378            color=(0.7, 0.5, 0.1),
379            button='buttonPunch' + self._ext,
380            texture=bui.gettexture('buttonPunch'),
381            scale=1.0,
382        )
383        self._capture_button(
384            pos=(h_offs + sclx * dist, v),
385            color=(0.5, 0.2, 0.1),
386            button='buttonBomb' + self._ext,
387            texture=bui.gettexture('buttonBomb'),
388            scale=1.0,
389        )
390        self._capture_button(
391            pos=(h_offs, v - scly * dist),
392            color=(0.2, 0.5, 0.2),
393            button='buttonJump' + self._ext,
394            texture=bui.gettexture('buttonJump'),
395            scale=1.0,
396        )
397
398        self._more_button = bui.buttonwidget(
399            parent=self._root_widget,
400            autoselect=True,
401            label='...',
402            text_scale=0.9,
403            color=(0.45, 0.4, 0.5),
404            textcolor=(0.65, 0.6, 0.7),
405            position=(self._width - 300, 30),
406            size=(130, 40),
407            on_activate_call=self._do_more,
408        )
409
410        try:
411            if cancel_button is not None and save_button is not None:
412                bui.widget(edit=cancel_button, right_widget=save_button)
413                bui.widget(edit=save_button, left_widget=cancel_button)
414                if is_reset:
415                    bui.containerwidget(
416                        edit=self._root_widget,
417                        selected_child=self._more_button,
418                    )
419        except Exception:
420            logging.exception('Error wiring up gamepad config window.')
421
422    def get_r(self) -> str:
423        """(internal)"""
424        return self._r
425
426    def get_advanced_button(self) -> bui.Widget:
427        """(internal)"""
428        return self._more_button
429
430    def get_is_secondary(self) -> bool:
431        """(internal)"""
432        return self._is_secondary
433
434    def get_settings(self) -> dict[str, Any]:
435        """(internal)"""
436        assert self._settings is not None
437        return self._settings
438
439    def get_ext(self) -> str:
440        """(internal)"""
441        return self._ext
442
443    def get_input(self) -> bs.InputDevice:
444        """(internal)"""
445        return self._inputdevice
446
447    def _do_advanced(self) -> None:
448        # pylint: disable=cyclic-import
449        from bauiv1lib.settings import gamepadadvanced
450
451        gamepadadvanced.GamepadAdvancedSettingsWindow(self)
452
453    def _enable_check_box_changed(self, value: bool) -> None:
454        assert self._settings is not None
455        if value:
456            self._settings['enableSecondary'] = 1
457        else:
458            # Just clear since this is default.
459            if 'enableSecondary' in self._settings:
460                del self._settings['enableSecondary']
461
462    def get_unassigned_buttons_run_value(self) -> bool:
463        """(internal)"""
464        assert self._settings is not None
465        val = self._settings.get('unassignedButtonsRun', True)
466        assert isinstance(val, bool)
467        return val
468
469    def set_unassigned_buttons_run_value(self, value: bool) -> None:
470        """(internal)"""
471        assert self._settings is not None
472        if value:
473            if 'unassignedButtonsRun' in self._settings:
474                # Clear since this is default.
475                del self._settings['unassignedButtonsRun']
476                return
477        self._settings['unassignedButtonsRun'] = False
478
479    def get_start_button_activates_default_widget_value(self) -> bool:
480        """(internal)"""
481        assert self._settings is not None
482        val = self._settings.get('startButtonActivatesDefaultWidget', True)
483        assert isinstance(val, bool)
484        return val
485
486    def set_start_button_activates_default_widget_value(
487        self, value: bool
488    ) -> None:
489        """(internal)"""
490        assert self._settings is not None
491        if value:
492            if 'startButtonActivatesDefaultWidget' in self._settings:
493                # Clear since this is default.
494                del self._settings['startButtonActivatesDefaultWidget']
495                return
496        self._settings['startButtonActivatesDefaultWidget'] = False
497
498    def get_ui_only_value(self) -> bool:
499        """(internal)"""
500        assert self._settings is not None
501        val = self._settings.get('uiOnly', False)
502        assert isinstance(val, bool)
503        return val
504
505    def set_ui_only_value(self, value: bool) -> None:
506        """(internal)"""
507        assert self._settings is not None
508        if not value:
509            if 'uiOnly' in self._settings:
510                # Clear since this is default.
511                del self._settings['uiOnly']
512                return
513        self._settings['uiOnly'] = True
514
515    def get_ignore_completely_value(self) -> bool:
516        """(internal)"""
517        assert self._settings is not None
518        val = self._settings.get('ignoreCompletely', False)
519        assert isinstance(val, bool)
520        return val
521
522    def set_ignore_completely_value(self, value: bool) -> None:
523        """(internal)"""
524        assert self._settings is not None
525        if not value:
526            if 'ignoreCompletely' in self._settings:
527                # Clear since this is default.
528                del self._settings['ignoreCompletely']
529                return
530        self._settings['ignoreCompletely'] = True
531
532    def get_auto_recalibrate_analog_stick_value(self) -> bool:
533        """(internal)"""
534        assert self._settings is not None
535        val = self._settings.get('autoRecalibrateAnalogStick', False)
536        assert isinstance(val, bool)
537        return val
538
539    def set_auto_recalibrate_analog_stick_value(self, value: bool) -> None:
540        """(internal)"""
541        assert self._settings is not None
542        if not value:
543            if 'autoRecalibrateAnalogStick' in self._settings:
544                # Clear since this is default.
545                del self._settings['autoRecalibrateAnalogStick']
546        else:
547            self._settings['autoRecalibrateAnalogStick'] = True
548
549    def get_enable_secondary_value(self) -> bool:
550        """(internal)"""
551        assert self._settings is not None
552        if not self._is_secondary:
553            raise RuntimeError('Enable value only applies to secondary editor.')
554        val = self._settings.get('enableSecondary', False)
555        assert isinstance(val, bool)
556        return val
557
558    def show_secondary_editor(self) -> None:
559        """(internal)"""
560        GamepadSettingsWindow(
561            self._inputdevice,
562            modal=True,
563            settings=self._settings,
564            transition='in_scale',
565            transition_out='out_scale',
566        )
567
568    def get_control_value_name(self, control: str) -> str | bui.Lstr:
569        """(internal)"""
570        # pylint: disable=too-many-return-statements
571        assert self._settings is not None
572        if control == 'analogStickLR' + self._ext:
573            # This actually shows both LR and UD.
574            sval1 = (
575                self._settings['analogStickLR' + self._ext]
576                if 'analogStickLR' + self._ext in self._settings
577                else 5 if self._is_secondary else None
578            )
579            sval2 = (
580                self._settings['analogStickUD' + self._ext]
581                if 'analogStickUD' + self._ext in self._settings
582                else 6 if self._is_secondary else None
583            )
584            assert isinstance(sval1, (int, type(None)))
585            assert isinstance(sval2, (int, type(None)))
586            if sval1 is not None and sval2 is not None:
587                return (
588                    self._inputdevice.get_axis_name(sval1)
589                    + ' / '
590                    + self._inputdevice.get_axis_name(sval2)
591                )
592            return bui.Lstr(resource=f'{self._r}.unsetText')
593
594        # If they're looking for triggers.
595        if control in ['triggerRun1' + self._ext, 'triggerRun2' + self._ext]:
596            if control in self._settings:
597                return self._inputdevice.get_axis_name(self._settings[control])
598            return bui.Lstr(resource=f'{self._r}.unsetText')
599
600        # Dead-zone.
601        if control == 'analogStickDeadZone' + self._ext:
602            if control in self._settings:
603                return str(self._settings[control])
604            return str(1.0)
605
606        # For dpad buttons: show individual buttons if any are set.
607        # Otherwise show whichever dpad is set.
608        dpad_buttons = [
609            'buttonLeft' + self._ext,
610            'buttonRight' + self._ext,
611            'buttonUp' + self._ext,
612            'buttonDown' + self._ext,
613        ]
614        if control in dpad_buttons:
615            # If *any* dpad buttons are assigned, show only button assignments.
616            if any(b in self._settings for b in dpad_buttons):
617                if control in self._settings:
618                    return self._inputdevice.get_button_name(
619                        self._settings[control]
620                    )
621                return bui.Lstr(resource=f'{self._r}.unsetText')
622
623            # No dpad buttons - show the dpad number for all 4.
624            dpadnum = (
625                self._settings['dpad' + self._ext]
626                if 'dpad' + self._ext in self._settings
627                else 2 if self._is_secondary else None
628            )
629            assert isinstance(dpadnum, (int, type(None)))
630            if dpadnum is not None:
631                return bui.Lstr(
632                    value='${A} ${B}',
633                    subs=[
634                        ('${A}', bui.Lstr(resource=f'{self._r}.dpadText')),
635                        (
636                            '${B}',
637                            str(dpadnum),
638                        ),
639                    ],
640                )
641            return bui.Lstr(resource=f'{self._r}.unsetText')
642
643        # Other buttons.
644        if control in self._settings:
645            return self._inputdevice.get_button_name(self._settings[control])
646        return bui.Lstr(resource=f'{self._r}.unsetText')
647
648    def _gamepad_event(
649        self,
650        control: str,
651        event: dict[str, Any],
652        dialog: AwaitGamepadInputWindow,
653    ) -> None:
654        # pylint: disable=too-many-branches
655        assert self._settings is not None
656        ext = self._ext
657
658        # For our dpad-buttons we're looking for either a button-press or a
659        # hat-switch press.
660        if control in [
661            'buttonUp' + ext,
662            'buttonLeft' + ext,
663            'buttonDown' + ext,
664            'buttonRight' + ext,
665        ]:
666            if event['type'] in ['BUTTONDOWN', 'HATMOTION']:
667                # If its a button-down.
668                if event['type'] == 'BUTTONDOWN':
669                    value = event['button']
670                    self._settings[control] = value
671
672                # If its a dpad.
673                elif event['type'] == 'HATMOTION':
674                    # clear out any set dir-buttons
675                    for btn in [
676                        'buttonUp' + ext,
677                        'buttonLeft' + ext,
678                        'buttonRight' + ext,
679                        'buttonDown' + ext,
680                    ]:
681                        if btn in self._settings:
682                            del self._settings[btn]
683                    if event['hat'] == (2 if self._is_secondary else 1):
684                        self._settings['dpad' + ext] = event['hat']
685
686                # Update the 4 dpad button txt widgets.
687                bui.textwidget(
688                    edit=self._textwidgets['buttonUp' + ext],
689                    text=self.get_control_value_name('buttonUp' + ext),
690                )
691                bui.textwidget(
692                    edit=self._textwidgets['buttonLeft' + ext],
693                    text=self.get_control_value_name('buttonLeft' + ext),
694                )
695                bui.textwidget(
696                    edit=self._textwidgets['buttonRight' + ext],
697                    text=self.get_control_value_name('buttonRight' + ext),
698                )
699                bui.textwidget(
700                    edit=self._textwidgets['buttonDown' + ext],
701                    text=self.get_control_value_name('buttonDown' + ext),
702                )
703                bui.getsound('gunCocking').play()
704                dialog.die()
705
706        elif control == 'analogStickLR' + ext:
707            if event['type'] == 'AXISMOTION':
708                # Ignore small values or else we might get triggered by noise.
709                if abs(event['value']) > 0.5:
710                    axis = event['axis']
711                    if axis == (5 if self._is_secondary else 1):
712                        self._settings['analogStickLR' + ext] = axis
713                    bui.textwidget(
714                        edit=self._textwidgets['analogStickLR' + ext],
715                        text=self.get_control_value_name('analogStickLR' + ext),
716                    )
717                    bui.getsound('gunCocking').play()
718                    dialog.die()
719
720                    # Now launch the up/down listener.
721                    AwaitGamepadInputWindow(
722                        self._inputdevice,
723                        'analogStickUD' + ext,
724                        self._gamepad_event,
725                        bui.Lstr(resource=f'{self._r}.pressUpDownText'),
726                    )
727
728        elif control == 'analogStickUD' + ext:
729            if event['type'] == 'AXISMOTION':
730                # Ignore small values or else we might get triggered by noise.
731                if abs(event['value']) > 0.5:
732                    axis = event['axis']
733
734                    # Ignore our LR axis.
735                    if 'analogStickLR' + ext in self._settings:
736                        lr_axis = self._settings['analogStickLR' + ext]
737                    else:
738                        lr_axis = 5 if self._is_secondary else 1
739                    if axis != lr_axis:
740                        if axis == (6 if self._is_secondary else 2):
741                            self._settings['analogStickUD' + ext] = axis
742                        bui.textwidget(
743                            edit=self._textwidgets['analogStickLR' + ext],
744                            text=self.get_control_value_name(
745                                'analogStickLR' + ext
746                            ),
747                        )
748                        bui.getsound('gunCocking').play()
749                        dialog.die()
750        else:
751            # For other buttons we just want a button-press.
752            if event['type'] == 'BUTTONDOWN':
753                value = event['button']
754                self._settings[control] = value
755
756                # Update the button's text widget.
757                bui.textwidget(
758                    edit=self._textwidgets[control],
759                    text=self.get_control_value_name(control),
760                )
761                bui.getsound('gunCocking').play()
762                dialog.die()
763
764    def _capture_button(
765        self,
766        pos: tuple[float, float],
767        color: tuple[float, float, float],
768        texture: bui.Texture,
769        button: str,
770        *,
771        scale: float = 1.0,
772        message: bui.Lstr | None = None,
773        message2: bui.Lstr | None = None,
774        maxwidth: float = 80.0,
775    ) -> bui.Widget:
776        if message is None:
777            message = bui.Lstr(resource=f'{self._r}.pressAnyButtonText')
778        base_size = 79
779        btn = bui.buttonwidget(
780            parent=self._root_widget,
781            position=(
782                pos[0] - base_size * 0.5 * scale,
783                pos[1] - base_size * 0.5 * scale,
784            ),
785            autoselect=True,
786            size=(base_size * scale, base_size * scale),
787            texture=texture,
788            label='',
789            color=color,
790        )
791
792        # Make this in a timer so that it shows up on top of all other buttons.
793
794        def doit() -> None:
795            uiscale = 0.9 * scale
796            txt = bui.textwidget(
797                parent=self._root_widget,
798                position=(pos[0] + 0.0 * scale, pos[1] - 58.0 * scale),
799                color=(1, 1, 1, 0.3),
800                size=(0, 0),
801                h_align='center',
802                v_align='center',
803                scale=uiscale,
804                text=self.get_control_value_name(button),
805                maxwidth=maxwidth,
806            )
807            self._textwidgets[button] = txt
808            bui.buttonwidget(
809                edit=btn,
810                on_activate_call=bui.Call(
811                    AwaitGamepadInputWindow,
812                    self._inputdevice,
813                    button,
814                    self._gamepad_event,
815                    message,
816                    message2,
817                ),
818            )
819
820        bui.pushcall(doit)
821        return btn
822
823    def _cancel(self) -> None:
824
825        if self._modal:
826            # no-op if our underlying widget is dead or on its way out.
827            if not self._root_widget or self._root_widget.transitioning_out:
828                return
829            bui.containerwidget(
830                edit=self._root_widget, transition=self._transition_out
831            )
832        else:
833            self.main_window_back()
834
835    def _reset(self) -> None:
836        from bauiv1lib.confirm import ConfirmWindow
837
838        assert bui.app.classic is not None
839
840        # efro note: I think it's ok to reset without a confirm here
841        # because the user can see pretty clearly what changes and can
842        # cancel out of the settings window without saving if they want.
843        if bool(False):
844            ConfirmWindow(
845                # TODO: Implement a translation string for this!
846                'Are you sure you want to reset your button mapping?\n'
847                'This will also reset your advanced mappings\n'
848                'and secondary controller button mappings.',
849                self._do_reset,
850                width=490,
851                height=150,
852            )
853        else:
854            self._do_reset()
855
856    def _do_reset(self) -> None:
857        """Resets the input's mapping settings."""
858        from babase import InputDeviceNotFoundError
859
860        self._settings = {}
861        # Unplugging the controller while performing a
862        # mapping reset makes things go bonkers a little.
863        try:
864            self._get_config_mapping(default=True)
865        except InputDeviceNotFoundError:
866            pass
867
868        self._rebuild_ui(is_reset=True)
869        bui.getsound('gunCocking').play()
870
871    def _do_more(self) -> None:
872        """Show a burger menu with extra settings."""
873        # pylint: disable=cyclic-import
874        choices: list[str] = [
875            'advanced',
876            'reset',
877        ]
878        choices_display: list[bui.Lstr] = [
879            bui.Lstr(resource=f'{self._r}.advancedText'),
880            bui.Lstr(resource='settingsWindowAdvanced.resetText'),
881        ]
882
883        uiscale = bui.app.ui_v1.uiscale
884        PopupMenuWindow(
885            position=self._more_button.get_screen_space_center(),
886            scale=(
887                2.3
888                if uiscale is bui.UIScale.SMALL
889                else 1.65 if uiscale is bui.UIScale.MEDIUM else 1.23
890            ),
891            width=150,
892            choices=choices,
893            choices_display=choices_display,
894            current_choice='advanced',
895            delegate=self,
896        )
897
898    def popup_menu_selected_choice(
899        self, popup_window: PopupMenuWindow, choice: str
900    ) -> None:
901        """Called when a choice is selected in the popup."""
902        del popup_window  # unused
903        if choice == 'reset':
904            self._reset()
905        elif choice == 'advanced':
906            self._do_advanced()
907        else:
908            print(f'invalid choice: {choice}')
909
910    def popup_menu_closing(self, popup_window: PopupWindow) -> None:
911        """Called when the popup is closing."""
912
913    def _save(self) -> None:
914        classic = bui.app.classic
915        assert classic is not None
916
917        # no-op if our underlying widget is dead or on its way out.
918        if not self._root_widget or self._root_widget.transitioning_out:
919            return
920
921        # If we're a secondary editor we just go away (we were editing our
922        # parent's settings dict).
923        if self._is_secondary:
924            assert self._modal
925            bui.containerwidget(
926                edit=self._root_widget, transition=self._transition_out
927            )
928            return
929
930        assert self._settings is not None
931        if self._inputdevice:
932            dst = classic.get_input_device_config(
933                self._inputdevice, default=True
934            )
935            dst2: dict[str, Any] = dst[0][dst[1]]
936            dst2.clear()
937
938            # Store any values that aren't -1.
939            for key, val in list(self._settings.items()):
940                if val != -1:
941                    dst2[key] = val
942
943            # If we're allowed to phone home, send this config so we can
944            # generate more defaults in the future.
945            inputhash = classic.get_input_device_map_hash(self._inputdevice)
946            classic.master_server_v1_post(
947                'controllerConfig',
948                {
949                    'ua': classic.legacy_user_agent_string,
950                    'b': bui.app.env.engine_build_number,
951                    'name': self._name,
952                    'inputMapHash': inputhash,
953                    'config': dst2,
954                    'v': 2,
955                },
956            )
957            bui.app.config.apply_and_commit()
958            bui.getsound('gunCocking').play()
959        else:
960            bui.getsound('error').play()
961
962        if self._modal:
963            bui.containerwidget(
964                edit=self._root_widget, transition=self._transition_out
965            )
966        else:
967            assert self.main_window_has_control()
968            self.main_window_back()

Window for configuring a gamepad.

GamepadSettingsWindow( inputdevice: _bascenev1.InputDevice, *, modal: bool = False, transition: str | None = 'in_right', transition_out: str = 'out_right', origin_widget: _bauiv1.Widget | None = None, settings: dict | None = None)
27    def __init__(
28        self,
29        inputdevice: bs.InputDevice,
30        *,
31        modal: bool = False,
32        transition: str | None = 'in_right',
33        transition_out: str = 'out_right',
34        origin_widget: bui.Widget | None = None,
35        settings: dict | None = None,
36    ):
37        self._inputdevice = inputdevice
38
39        # If our input-device went away, just return an empty zombie.
40        if not self._inputdevice:
41            return
42
43        self._name = self._inputdevice.name
44
45        self._r = 'configGamepadWindow'
46        self._transition_out = transition_out
47
48        # We're a secondary gamepad if supplied with settings.
49        self._is_secondary = settings is not None
50        self._ext = '_B' if self._is_secondary else ''
51        self._modal = modal
52        self._displayname = self._name
53        self._width = 700 if self._is_secondary else 730
54        self._height = 440 if self._is_secondary else 450
55        self._spacing = 40
56        assert bui.app.classic is not None
57        uiscale = bui.app.ui_v1.uiscale
58        super().__init__(
59            root_widget=bui.containerwidget(
60                size=(self._width, self._height),
61                scale=(
62                    1.4
63                    if uiscale is bui.UIScale.SMALL
64                    else 1.3 if uiscale is bui.UIScale.MEDIUM else 1.0
65                ),
66                stack_offset=(
67                    (0, -10) if uiscale is bui.UIScale.SMALL else (0, 0)
68                ),
69            ),
70            transition=transition,
71            origin_widget=origin_widget,
72        )
73
74        self._settings: dict[str, int] = {}
75        if not self._is_secondary:
76            self._get_config_mapping()
77
78        # Don't ask to config joysticks while we're in here.
79        self._rebuild_ui()

Create a MainWindow given a root widget and transition info.

Automatically handles in and out transitions on the provided widget, so there is no need to set transitions when creating it.

@override
def get_main_window_state(self) -> bauiv1.MainWindowState:
81    @override
82    def get_main_window_state(self) -> bui.MainWindowState:
83        # Support recreating our window for back/refresh purposes.
84        cls = type(self)
85
86        # Pull stuff out of self here; if we do it in the lambda we keep
87        # self alive which we don't want.
88        assert not self._is_secondary
89        assert not self._modal
90
91        inputdevice = self._inputdevice
92
93        return bui.BasicMainWindowState(
94            create_call=lambda transition, origin_widget: cls(
95                inputdevice=inputdevice,
96                transition=transition,
97                origin_widget=origin_widget,
98            )
99        )

Return a WindowState to recreate this window, if supported.

def popup_menu_selected_choice(self, popup_window: bauiv1lib.popup.PopupMenuWindow, choice: str) -> None:
898    def popup_menu_selected_choice(
899        self, popup_window: PopupMenuWindow, choice: str
900    ) -> None:
901        """Called when a choice is selected in the popup."""
902        del popup_window  # unused
903        if choice == 'reset':
904            self._reset()
905        elif choice == 'advanced':
906            self._do_advanced()
907        else:
908            print(f'invalid choice: {choice}')

Called when a choice is selected in the popup.

def popup_menu_closing(self, popup_window: bauiv1lib.popup.PopupWindow) -> None:
910    def popup_menu_closing(self, popup_window: PopupWindow) -> None:
911        """Called when the popup is closing."""

Called when the popup is closing.

class AwaitGamepadInputWindow(bauiv1._uitypes.Window):
 971class AwaitGamepadInputWindow(bui.Window):
 972    """Window for capturing a gamepad button press."""
 973
 974    def __init__(
 975        self,
 976        gamepad: bs.InputDevice,
 977        button: str,
 978        callback: Callable[[str, dict[str, Any], AwaitGamepadInputWindow], Any],
 979        message: bui.Lstr | None = None,
 980        message2: bui.Lstr | None = None,
 981    ):
 982        # pylint: disable=too-many-positional-arguments
 983        if message is None:
 984            print('AwaitGamepadInputWindow message is None!')
 985            # Shouldn't get here.
 986            message = bui.Lstr(value='Press any button...')
 987        self._callback = callback
 988        self._input = gamepad
 989        self._capture_button = button
 990        width = 400
 991        height = 150
 992        assert bui.app.classic is not None
 993        uiscale = bui.app.ui_v1.uiscale
 994        super().__init__(
 995            root_widget=bui.containerwidget(
 996                scale=(
 997                    2.0
 998                    if uiscale is bui.UIScale.SMALL
 999                    else 1.9 if uiscale is bui.UIScale.MEDIUM else 1.0
1000                ),
1001                size=(width, height),
1002                transition='in_scale',
1003            ),
1004        )
1005        bui.textwidget(
1006            parent=self._root_widget,
1007            position=(0, (height - 60) if message2 is None else (height - 50)),
1008            size=(width, 25),
1009            text=message,
1010            maxwidth=width * 0.9,
1011            h_align='center',
1012            v_align='center',
1013        )
1014        if message2 is not None:
1015            bui.textwidget(
1016                parent=self._root_widget,
1017                position=(width * 0.5, height - 60),
1018                size=(0, 0),
1019                text=message2,
1020                maxwidth=width * 0.9,
1021                scale=0.47,
1022                color=(0.7, 1.0, 0.7, 0.6),
1023                h_align='center',
1024                v_align='center',
1025            )
1026        self._counter = 5
1027        self._count_down_text = bui.textwidget(
1028            parent=self._root_widget,
1029            h_align='center',
1030            position=(0, height - 110),
1031            size=(width, 25),
1032            color=(1, 1, 1, 0.3),
1033            text=str(self._counter),
1034        )
1035        self._decrement_timer: bui.AppTimer | None = bui.AppTimer(
1036            1.0, bui.Call(self._decrement), repeat=True
1037        )
1038        bs.capture_gamepad_input(bui.WeakCall(self._event_callback))
1039
1040    def __del__(self) -> None:
1041        pass
1042
1043    def die(self) -> None:
1044        """Kill the window."""
1045
1046        # This strong-refs us; killing it allow us to die now.
1047        self._decrement_timer = None
1048        bs.release_gamepad_input()
1049        if self._root_widget:
1050            bui.containerwidget(edit=self._root_widget, transition='out_scale')
1051
1052    def _event_callback(self, event: dict[str, Any]) -> None:
1053        input_device = event['input_device']
1054        assert isinstance(input_device, bs.InputDevice)
1055
1056        # Update - we now allow *any* input device of this type.
1057        if (
1058            self._input
1059            and input_device
1060            and input_device.name == self._input.name
1061        ):
1062            self._callback(self._capture_button, event, self)
1063
1064    def _decrement(self) -> None:
1065        self._counter -= 1
1066        if self._counter >= 1:
1067            if self._count_down_text:
1068                bui.textwidget(
1069                    edit=self._count_down_text, text=str(self._counter)
1070                )
1071        else:
1072            bui.getsound('error').play()
1073            self.die()

Window for capturing a gamepad button press.

AwaitGamepadInputWindow( gamepad: _bascenev1.InputDevice, button: str, callback: Callable[[str, dict[str, Any], AwaitGamepadInputWindow], Any], message: babase.Lstr | None = None, message2: babase.Lstr | None = None)
 974    def __init__(
 975        self,
 976        gamepad: bs.InputDevice,
 977        button: str,
 978        callback: Callable[[str, dict[str, Any], AwaitGamepadInputWindow], Any],
 979        message: bui.Lstr | None = None,
 980        message2: bui.Lstr | None = None,
 981    ):
 982        # pylint: disable=too-many-positional-arguments
 983        if message is None:
 984            print('AwaitGamepadInputWindow message is None!')
 985            # Shouldn't get here.
 986            message = bui.Lstr(value='Press any button...')
 987        self._callback = callback
 988        self._input = gamepad
 989        self._capture_button = button
 990        width = 400
 991        height = 150
 992        assert bui.app.classic is not None
 993        uiscale = bui.app.ui_v1.uiscale
 994        super().__init__(
 995            root_widget=bui.containerwidget(
 996                scale=(
 997                    2.0
 998                    if uiscale is bui.UIScale.SMALL
 999                    else 1.9 if uiscale is bui.UIScale.MEDIUM else 1.0
1000                ),
1001                size=(width, height),
1002                transition='in_scale',
1003            ),
1004        )
1005        bui.textwidget(
1006            parent=self._root_widget,
1007            position=(0, (height - 60) if message2 is None else (height - 50)),
1008            size=(width, 25),
1009            text=message,
1010            maxwidth=width * 0.9,
1011            h_align='center',
1012            v_align='center',
1013        )
1014        if message2 is not None:
1015            bui.textwidget(
1016                parent=self._root_widget,
1017                position=(width * 0.5, height - 60),
1018                size=(0, 0),
1019                text=message2,
1020                maxwidth=width * 0.9,
1021                scale=0.47,
1022                color=(0.7, 1.0, 0.7, 0.6),
1023                h_align='center',
1024                v_align='center',
1025            )
1026        self._counter = 5
1027        self._count_down_text = bui.textwidget(
1028            parent=self._root_widget,
1029            h_align='center',
1030            position=(0, height - 110),
1031            size=(width, 25),
1032            color=(1, 1, 1, 0.3),
1033            text=str(self._counter),
1034        )
1035        self._decrement_timer: bui.AppTimer | None = bui.AppTimer(
1036            1.0, bui.Call(self._decrement), repeat=True
1037        )
1038        bs.capture_gamepad_input(bui.WeakCall(self._event_callback))
def die(self) -> None:
1043    def die(self) -> None:
1044        """Kill the window."""
1045
1046        # This strong-refs us; killing it allow us to die now.
1047        self._decrement_timer = None
1048        bs.release_gamepad_input()
1049        if self._root_widget:
1050            bui.containerwidget(edit=self._root_widget, transition='out_scale')

Kill the window.