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

Window for configuring a gamepad.

GamepadSettingsWindow( gamepad: _bascenev1.InputDevice, is_main_menu: bool = True, transition: str = 'in_right', transition_out: str = 'out_right', settings: dict | None = None)
26    def __init__(
27        self,
28        gamepad: bs.InputDevice,
29        is_main_menu: bool = True,
30        transition: str = 'in_right',
31        transition_out: str = 'out_right',
32        settings: dict | None = None,
33    ):
34        self._input = gamepad
35
36        # If our input-device went away, just return an empty zombie.
37        if not self._input:
38            return
39
40        self._name = self._input.name
41
42        self._r = 'configGamepadWindow'
43        self._transition_out = transition_out
44
45        # We're a secondary gamepad if supplied with settings.
46        self._is_secondary = settings is not None
47        self._ext = '_B' if self._is_secondary else ''
48        self._is_main_menu = is_main_menu
49        self._displayname = self._name
50        self._width = 700 if self._is_secondary else 730
51        self._height = 440 if self._is_secondary else 450
52        self._spacing = 40
53        assert bui.app.classic is not None
54        uiscale = bui.app.ui_v1.uiscale
55        super().__init__(
56            root_widget=bui.containerwidget(
57                size=(self._width, self._height),
58                scale=(
59                    1.63
60                    if uiscale is bui.UIScale.SMALL
61                    else 1.35 if uiscale is bui.UIScale.MEDIUM else 1.0
62                ),
63                stack_offset=(
64                    (-20, -16) if uiscale is bui.UIScale.SMALL else (0, 0)
65                ),
66                transition=transition,
67            )
68        )
69
70        self._settings: dict[str, int] = {}
71        if not self._is_secondary:
72            self._get_config_mapping()
73        # Don't ask to config joysticks while we're in here.
74        self._rebuild_ui()
def popup_menu_selected_choice(self, popup_window: bauiv1lib.popup.PopupMenuWindow, choice: str) -> None:
875    def popup_menu_selected_choice(
876        self, popup_window: PopupMenuWindow, choice: str
877    ) -> None:
878        """Called when a choice is selected in the popup."""
879        del popup_window  # unused
880        if choice == 'reset':
881            self._reset()
882        elif choice == 'advanced':
883            self._do_advanced()
884        else:
885            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:
887    def popup_menu_closing(self, popup_window: PopupWindow) -> None:
888        """Called when the popup is closing."""

Called when the popup is closing.

Inherited Members
bauiv1._uitypes.Window
get_root_widget
class AwaitGamepadInputWindow(bauiv1._uitypes.Window):
 947class AwaitGamepadInputWindow(bui.Window):
 948    """Window for capturing a gamepad button press."""
 949
 950    def __init__(
 951        self,
 952        gamepad: bs.InputDevice,
 953        button: str,
 954        callback: Callable[[str, dict[str, Any], AwaitGamepadInputWindow], Any],
 955        message: bui.Lstr | None = None,
 956        message2: bui.Lstr | None = None,
 957    ):
 958        if message is None:
 959            print('AwaitGamepadInputWindow message is None!')
 960            # Shouldn't get here.
 961            message = bui.Lstr(value='Press any button...')
 962        self._callback = callback
 963        self._input = gamepad
 964        self._capture_button = button
 965        width = 400
 966        height = 150
 967        assert bui.app.classic is not None
 968        uiscale = bui.app.ui_v1.uiscale
 969        super().__init__(
 970            root_widget=bui.containerwidget(
 971                scale=(
 972                    2.0
 973                    if uiscale is bui.UIScale.SMALL
 974                    else 1.9 if uiscale is bui.UIScale.MEDIUM else 1.0
 975                ),
 976                size=(width, height),
 977                transition='in_scale',
 978            ),
 979        )
 980        bui.textwidget(
 981            parent=self._root_widget,
 982            position=(0, (height - 60) if message2 is None else (height - 50)),
 983            size=(width, 25),
 984            text=message,
 985            maxwidth=width * 0.9,
 986            h_align='center',
 987            v_align='center',
 988        )
 989        if message2 is not None:
 990            bui.textwidget(
 991                parent=self._root_widget,
 992                position=(width * 0.5, height - 60),
 993                size=(0, 0),
 994                text=message2,
 995                maxwidth=width * 0.9,
 996                scale=0.47,
 997                color=(0.7, 1.0, 0.7, 0.6),
 998                h_align='center',
 999                v_align='center',
1000            )
1001        self._counter = 5
1002        self._count_down_text = bui.textwidget(
1003            parent=self._root_widget,
1004            h_align='center',
1005            position=(0, height - 110),
1006            size=(width, 25),
1007            color=(1, 1, 1, 0.3),
1008            text=str(self._counter),
1009        )
1010        self._decrement_timer: bui.AppTimer | None = bui.AppTimer(
1011            1.0, bui.Call(self._decrement), repeat=True
1012        )
1013        bs.capture_gamepad_input(bui.WeakCall(self._event_callback))
1014
1015    def __del__(self) -> None:
1016        pass
1017
1018    def die(self) -> None:
1019        """Kill the window."""
1020
1021        # This strong-refs us; killing it allow us to die now.
1022        self._decrement_timer = None
1023        bs.release_gamepad_input()
1024        if self._root_widget:
1025            bui.containerwidget(edit=self._root_widget, transition='out_scale')
1026
1027    def _event_callback(self, event: dict[str, Any]) -> None:
1028        input_device = event['input_device']
1029        assert isinstance(input_device, bs.InputDevice)
1030
1031        # Update - we now allow *any* input device of this type.
1032        if (
1033            self._input
1034            and input_device
1035            and input_device.name == self._input.name
1036        ):
1037            self._callback(self._capture_button, event, self)
1038
1039    def _decrement(self) -> None:
1040        self._counter -= 1
1041        if self._counter >= 1:
1042            if self._count_down_text:
1043                bui.textwidget(
1044                    edit=self._count_down_text, text=str(self._counter)
1045                )
1046        else:
1047            bui.getsound('error').play()
1048            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._language.Lstr | None = None, message2: babase._language.Lstr | None = None)
 950    def __init__(
 951        self,
 952        gamepad: bs.InputDevice,
 953        button: str,
 954        callback: Callable[[str, dict[str, Any], AwaitGamepadInputWindow], Any],
 955        message: bui.Lstr | None = None,
 956        message2: bui.Lstr | None = None,
 957    ):
 958        if message is None:
 959            print('AwaitGamepadInputWindow message is None!')
 960            # Shouldn't get here.
 961            message = bui.Lstr(value='Press any button...')
 962        self._callback = callback
 963        self._input = gamepad
 964        self._capture_button = button
 965        width = 400
 966        height = 150
 967        assert bui.app.classic is not None
 968        uiscale = bui.app.ui_v1.uiscale
 969        super().__init__(
 970            root_widget=bui.containerwidget(
 971                scale=(
 972                    2.0
 973                    if uiscale is bui.UIScale.SMALL
 974                    else 1.9 if uiscale is bui.UIScale.MEDIUM else 1.0
 975                ),
 976                size=(width, height),
 977                transition='in_scale',
 978            ),
 979        )
 980        bui.textwidget(
 981            parent=self._root_widget,
 982            position=(0, (height - 60) if message2 is None else (height - 50)),
 983            size=(width, 25),
 984            text=message,
 985            maxwidth=width * 0.9,
 986            h_align='center',
 987            v_align='center',
 988        )
 989        if message2 is not None:
 990            bui.textwidget(
 991                parent=self._root_widget,
 992                position=(width * 0.5, height - 60),
 993                size=(0, 0),
 994                text=message2,
 995                maxwidth=width * 0.9,
 996                scale=0.47,
 997                color=(0.7, 1.0, 0.7, 0.6),
 998                h_align='center',
 999                v_align='center',
1000            )
1001        self._counter = 5
1002        self._count_down_text = bui.textwidget(
1003            parent=self._root_widget,
1004            h_align='center',
1005            position=(0, height - 110),
1006            size=(width, 25),
1007            color=(1, 1, 1, 0.3),
1008            text=str(self._counter),
1009        )
1010        self._decrement_timer: bui.AppTimer | None = bui.AppTimer(
1011            1.0, bui.Call(self._decrement), repeat=True
1012        )
1013        bs.capture_gamepad_input(bui.WeakCall(self._event_callback))
def die(self) -> None:
1018    def die(self) -> None:
1019        """Kill the window."""
1020
1021        # This strong-refs us; killing it allow us to die now.
1022        self._decrement_timer = None
1023        bs.release_gamepad_input()
1024        if self._root_widget:
1025            bui.containerwidget(edit=self._root_widget, transition='out_scale')

Kill the window.

Inherited Members
bauiv1._uitypes.Window
get_root_widget