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

Window for configuring a gamepad.

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

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

Kill the window.

Inherited Members
bauiv1._uitypes.Window
get_root_widget