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        gamepad: bs.InputDevice,
  28        is_main_menu: bool = True,
  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._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            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        # 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._input, 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._input
 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._input,
 539            is_main_menu=False,
 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._input.get_axis_name(sval1)
 566                    + ' / '
 567                    + self._input.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._input.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._input.get_button_name(self._settings[control])
 596                return bui.Lstr(resource=f'{self._r}.unsetText')
 597
 598            # No dpad buttons - show the dpad number for all 4.
 599            dpadnum = (
 600                self._settings['dpad' + self._ext]
 601                if 'dpad' + self._ext in self._settings
 602                else 2 if self._is_secondary else None
 603            )
 604            assert isinstance(dpadnum, (int, type(None)))
 605            if dpadnum is not None:
 606                return bui.Lstr(
 607                    value='${A} ${B}',
 608                    subs=[
 609                        ('${A}', bui.Lstr(resource=f'{self._r}.dpadText')),
 610                        (
 611                            '${B}',
 612                            str(dpadnum),
 613                        ),
 614                    ],
 615                )
 616            return bui.Lstr(resource=f'{self._r}.unsetText')
 617
 618        # Other buttons.
 619        if control in self._settings:
 620            return self._input.get_button_name(self._settings[control])
 621        return bui.Lstr(resource=f'{self._r}.unsetText')
 622
 623    def _gamepad_event(
 624        self,
 625        control: str,
 626        event: dict[str, Any],
 627        dialog: AwaitGamepadInputWindow,
 628    ) -> None:
 629        # pylint: disable=too-many-branches
 630        assert self._settings is not None
 631        ext = self._ext
 632
 633        # For our dpad-buttons we're looking for either a button-press or a
 634        # hat-switch press.
 635        if control in [
 636            'buttonUp' + ext,
 637            'buttonLeft' + ext,
 638            'buttonDown' + ext,
 639            'buttonRight' + ext,
 640        ]:
 641            if event['type'] in ['BUTTONDOWN', 'HATMOTION']:
 642                # If its a button-down.
 643                if event['type'] == 'BUTTONDOWN':
 644                    value = event['button']
 645                    self._settings[control] = value
 646
 647                # If its a dpad.
 648                elif event['type'] == 'HATMOTION':
 649                    # clear out any set dir-buttons
 650                    for btn in [
 651                        'buttonUp' + ext,
 652                        'buttonLeft' + ext,
 653                        'buttonRight' + ext,
 654                        'buttonDown' + ext,
 655                    ]:
 656                        if btn in self._settings:
 657                            del self._settings[btn]
 658                    if event['hat'] == (2 if self._is_secondary else 1):
 659                        self._settings['dpad' + ext] = event['hat']
 660
 661                # Update the 4 dpad button txt widgets.
 662                bui.textwidget(
 663                    edit=self._textwidgets['buttonUp' + ext],
 664                    text=self.get_control_value_name('buttonUp' + ext),
 665                )
 666                bui.textwidget(
 667                    edit=self._textwidgets['buttonLeft' + ext],
 668                    text=self.get_control_value_name('buttonLeft' + ext),
 669                )
 670                bui.textwidget(
 671                    edit=self._textwidgets['buttonRight' + ext],
 672                    text=self.get_control_value_name('buttonRight' + ext),
 673                )
 674                bui.textwidget(
 675                    edit=self._textwidgets['buttonDown' + ext],
 676                    text=self.get_control_value_name('buttonDown' + ext),
 677                )
 678                bui.getsound('gunCocking').play()
 679                dialog.die()
 680
 681        elif control == 'analogStickLR' + ext:
 682            if event['type'] == 'AXISMOTION':
 683                # Ignore small values or else we might get triggered by noise.
 684                if abs(event['value']) > 0.5:
 685                    axis = event['axis']
 686                    if axis == (5 if self._is_secondary else 1):
 687                        self._settings['analogStickLR' + ext] = axis
 688                    bui.textwidget(
 689                        edit=self._textwidgets['analogStickLR' + ext],
 690                        text=self.get_control_value_name('analogStickLR' + ext),
 691                    )
 692                    bui.getsound('gunCocking').play()
 693                    dialog.die()
 694
 695                    # Now launch the up/down listener.
 696                    AwaitGamepadInputWindow(
 697                        self._input,
 698                        'analogStickUD' + ext,
 699                        self._gamepad_event,
 700                        bui.Lstr(resource=f'{self._r}.pressUpDownText'),
 701                    )
 702
 703        elif control == 'analogStickUD' + ext:
 704            if event['type'] == 'AXISMOTION':
 705                # Ignore small values or else we might get triggered by noise.
 706                if abs(event['value']) > 0.5:
 707                    axis = event['axis']
 708
 709                    # Ignore our LR axis.
 710                    if 'analogStickLR' + ext in self._settings:
 711                        lr_axis = self._settings['analogStickLR' + ext]
 712                    else:
 713                        lr_axis = 5 if self._is_secondary else 1
 714                    if axis != lr_axis:
 715                        if axis == (6 if self._is_secondary else 2):
 716                            self._settings['analogStickUD' + ext] = axis
 717                        bui.textwidget(
 718                            edit=self._textwidgets['analogStickLR' + ext],
 719                            text=self.get_control_value_name(
 720                                'analogStickLR' + ext
 721                            ),
 722                        )
 723                        bui.getsound('gunCocking').play()
 724                        dialog.die()
 725        else:
 726            # For other buttons we just want a button-press.
 727            if event['type'] == 'BUTTONDOWN':
 728                value = event['button']
 729                self._settings[control] = value
 730
 731                # Update the button's text widget.
 732                bui.textwidget(
 733                    edit=self._textwidgets[control],
 734                    text=self.get_control_value_name(control),
 735                )
 736                bui.getsound('gunCocking').play()
 737                dialog.die()
 738
 739    def _capture_button(
 740        self,
 741        pos: tuple[float, float],
 742        color: tuple[float, float, float],
 743        texture: bui.Texture,
 744        button: str,
 745        scale: float = 1.0,
 746        message: bui.Lstr | None = None,
 747        message2: bui.Lstr | None = None,
 748        maxwidth: float = 80.0,
 749    ) -> bui.Widget:
 750        if message is None:
 751            message = bui.Lstr(resource=f'{self._r}.pressAnyButtonText')
 752        base_size = 79
 753        btn = bui.buttonwidget(
 754            parent=self._root_widget,
 755            position=(
 756                pos[0] - base_size * 0.5 * scale,
 757                pos[1] - base_size * 0.5 * scale,
 758            ),
 759            autoselect=True,
 760            size=(base_size * scale, base_size * scale),
 761            texture=texture,
 762            label='',
 763            color=color,
 764        )
 765
 766        # Make this in a timer so that it shows up on top of all other buttons.
 767
 768        def doit() -> None:
 769            uiscale = 0.9 * scale
 770            txt = bui.textwidget(
 771                parent=self._root_widget,
 772                position=(pos[0] + 0.0 * scale, pos[1] - 58.0 * scale),
 773                color=(1, 1, 1, 0.3),
 774                size=(0, 0),
 775                h_align='center',
 776                v_align='center',
 777                scale=uiscale,
 778                text=self.get_control_value_name(button),
 779                maxwidth=maxwidth,
 780            )
 781            self._textwidgets[button] = txt
 782            bui.buttonwidget(
 783                edit=btn,
 784                on_activate_call=bui.Call(
 785                    AwaitGamepadInputWindow,
 786                    self._input,
 787                    button,
 788                    self._gamepad_event,
 789                    message,
 790                    message2,
 791                ),
 792            )
 793
 794        bui.pushcall(doit)
 795        return btn
 796
 797    def _cancel(self) -> None:
 798        # from bauiv1lib.settings.controls import ControlsSettingsWindow
 799
 800        # no-op if our underlying widget is dead or on its way out.
 801        if not self._root_widget or self._root_widget.transitioning_out:
 802            return
 803
 804        bui.containerwidget(
 805            edit=self._root_widget, transition=self._transition_out
 806        )
 807        if self._is_main_menu:
 808            self.main_window_back()
 809            # assert bui.app.classic is not None
 810            # bui.app.ui_v1.set_main_window(
 811            #     ControlsSettingsWindow(transition='in_left'),
 812            #     from_window=self,
 813            #     is_back=True,
 814            # )
 815
 816    def _reset(self) -> None:
 817        from bauiv1lib.confirm import ConfirmWindow
 818
 819        assert bui.app.classic is not None
 820
 821        # efro note: I think it's ok to reset without a confirm here
 822        # because the user can see pretty clearly what changes and can
 823        # cancel out of the settings window without saving if they want.
 824        if bool(False):
 825            ConfirmWindow(
 826                # TODO: Implement a translation string for this!
 827                'Are you sure you want to reset your button mapping?\n'
 828                'This will also reset your advanced mappings\n'
 829                'and secondary controller button mappings.',
 830                self._do_reset,
 831                width=490,
 832                height=150,
 833            )
 834        else:
 835            self._do_reset()
 836
 837    def _do_reset(self) -> None:
 838        """Resets the input's mapping settings."""
 839        from babase import InputDeviceNotFoundError
 840
 841        self._settings = {}
 842        # Unplugging the controller while performing a
 843        # mapping reset makes things go bonkers a little.
 844        try:
 845            self._get_config_mapping(default=True)
 846        except InputDeviceNotFoundError:
 847            pass
 848
 849        self._rebuild_ui(is_reset=True)
 850        bui.getsound('gunCocking').play()
 851
 852    def _do_more(self) -> None:
 853        """Show a burger menu with extra settings."""
 854        # pylint: disable=cyclic-import
 855        choices: list[str] = [
 856            'advanced',
 857            'reset',
 858        ]
 859        choices_display: list[bui.Lstr] = [
 860            bui.Lstr(resource=f'{self._r}.advancedText'),
 861            bui.Lstr(resource='settingsWindowAdvanced.resetText'),
 862        ]
 863
 864        uiscale = bui.app.ui_v1.uiscale
 865        PopupMenuWindow(
 866            position=self._more_button.get_screen_space_center(),
 867            scale=(
 868                2.3
 869                if uiscale is bui.UIScale.SMALL
 870                else 1.65 if uiscale is bui.UIScale.MEDIUM else 1.23
 871            ),
 872            width=150,
 873            choices=choices,
 874            choices_display=choices_display,
 875            current_choice='advanced',
 876            delegate=self,
 877        )
 878
 879    def popup_menu_selected_choice(
 880        self, popup_window: PopupMenuWindow, choice: str
 881    ) -> None:
 882        """Called when a choice is selected in the popup."""
 883        del popup_window  # unused
 884        if choice == 'reset':
 885            self._reset()
 886        elif choice == 'advanced':
 887            self._do_advanced()
 888        else:
 889            print(f'invalid choice: {choice}')
 890
 891    def popup_menu_closing(self, popup_window: PopupWindow) -> None:
 892        """Called when the popup is closing."""
 893
 894    def _save(self) -> None:
 895        classic = bui.app.classic
 896        assert classic is not None
 897
 898        # no-op if our underlying widget is dead or on its way out.
 899        if not self._root_widget or self._root_widget.transitioning_out:
 900            return
 901
 902        bui.containerwidget(
 903            edit=self._root_widget, transition=self._transition_out
 904        )
 905
 906        # If we're a secondary editor we just go away (we were editing our
 907        # parent's settings dict).
 908        if self._is_secondary:
 909            return
 910
 911        assert self._settings is not None
 912        if self._input:
 913            dst = classic.get_input_device_config(self._input, default=True)
 914            dst2: dict[str, Any] = dst[0][dst[1]]
 915            dst2.clear()
 916
 917            # Store any values that aren't -1.
 918            for key, val in list(self._settings.items()):
 919                if val != -1:
 920                    dst2[key] = val
 921
 922            # If we're allowed to phone home, send this config so we can
 923            # generate more defaults in the future.
 924            inputhash = classic.get_input_device_map_hash(self._input)
 925            classic.master_server_v1_post(
 926                'controllerConfig',
 927                {
 928                    'ua': classic.legacy_user_agent_string,
 929                    'b': bui.app.env.engine_build_number,
 930                    'name': self._name,
 931                    'inputMapHash': inputhash,
 932                    'config': dst2,
 933                    'v': 2,
 934                },
 935            )
 936            bui.app.config.apply_and_commit()
 937            bui.getsound('gunCocking').play()
 938        else:
 939            bui.getsound('error').play()
 940
 941        if self._is_main_menu:
 942            from bauiv1lib.settings.controls import ControlsSettingsWindow
 943
 944            assert bui.app.classic is not None
 945            bui.app.ui_v1.set_main_window(
 946                ControlsSettingsWindow(transition='in_left'),
 947                from_window=self,
 948                is_back=True,
 949            )
 950
 951
 952class AwaitGamepadInputWindow(bui.Window):
 953    """Window for capturing a gamepad button press."""
 954
 955    def __init__(
 956        self,
 957        gamepad: bs.InputDevice,
 958        button: str,
 959        callback: Callable[[str, dict[str, Any], AwaitGamepadInputWindow], Any],
 960        message: bui.Lstr | None = None,
 961        message2: bui.Lstr | None = None,
 962    ):
 963        if message is None:
 964            print('AwaitGamepadInputWindow message is None!')
 965            # Shouldn't get here.
 966            message = bui.Lstr(value='Press any button...')
 967        self._callback = callback
 968        self._input = gamepad
 969        self._capture_button = button
 970        width = 400
 971        height = 150
 972        assert bui.app.classic is not None
 973        uiscale = bui.app.ui_v1.uiscale
 974        super().__init__(
 975            root_widget=bui.containerwidget(
 976                scale=(
 977                    2.0
 978                    if uiscale is bui.UIScale.SMALL
 979                    else 1.9 if uiscale is bui.UIScale.MEDIUM else 1.0
 980                ),
 981                size=(width, height),
 982                transition='in_scale',
 983            ),
 984        )
 985        bui.textwidget(
 986            parent=self._root_widget,
 987            position=(0, (height - 60) if message2 is None else (height - 50)),
 988            size=(width, 25),
 989            text=message,
 990            maxwidth=width * 0.9,
 991            h_align='center',
 992            v_align='center',
 993        )
 994        if message2 is not None:
 995            bui.textwidget(
 996                parent=self._root_widget,
 997                position=(width * 0.5, height - 60),
 998                size=(0, 0),
 999                text=message2,
1000                maxwidth=width * 0.9,
1001                scale=0.47,
1002                color=(0.7, 1.0, 0.7, 0.6),
1003                h_align='center',
1004                v_align='center',
1005            )
1006        self._counter = 5
1007        self._count_down_text = bui.textwidget(
1008            parent=self._root_widget,
1009            h_align='center',
1010            position=(0, height - 110),
1011            size=(width, 25),
1012            color=(1, 1, 1, 0.3),
1013            text=str(self._counter),
1014        )
1015        self._decrement_timer: bui.AppTimer | None = bui.AppTimer(
1016            1.0, bui.Call(self._decrement), repeat=True
1017        )
1018        bs.capture_gamepad_input(bui.WeakCall(self._event_callback))
1019
1020    def __del__(self) -> None:
1021        pass
1022
1023    def die(self) -> None:
1024        """Kill the window."""
1025
1026        # This strong-refs us; killing it allow us to die now.
1027        self._decrement_timer = None
1028        bs.release_gamepad_input()
1029        if self._root_widget:
1030            bui.containerwidget(edit=self._root_widget, transition='out_scale')
1031
1032    def _event_callback(self, event: dict[str, Any]) -> None:
1033        input_device = event['input_device']
1034        assert isinstance(input_device, bs.InputDevice)
1035
1036        # Update - we now allow *any* input device of this type.
1037        if (
1038            self._input
1039            and input_device
1040            and input_device.name == self._input.name
1041        ):
1042            self._callback(self._capture_button, event, self)
1043
1044    def _decrement(self) -> None:
1045        self._counter -= 1
1046        if self._counter >= 1:
1047            if self._count_down_text:
1048                bui.textwidget(
1049                    edit=self._count_down_text, text=str(self._counter)
1050                )
1051        else:
1052            bui.getsound('error').play()
1053            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        gamepad: bs.InputDevice,
 29        is_main_menu: bool = True,
 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._input = gamepad
 36
 37        # If our input-device went away, just return an empty zombie.
 38        if not self._input:
 39            return
 40
 41        self._name = self._input.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._is_main_menu = is_main_menu
 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.63
 61                    if uiscale is bui.UIScale.SMALL
 62                    else 1.35 if uiscale is bui.UIScale.MEDIUM else 1.0
 63                ),
 64                stack_offset=(
 65                    (-20, -16) if uiscale is bui.UIScale.SMALL else (0, 0)
 66                ),
 67                transition=transition,
 68            ),
 69            transition=transition,
 70            origin_widget=origin_widget,
 71        )
 72
 73        self._settings: dict[str, int] = {}
 74        if not self._is_secondary:
 75            self._get_config_mapping()
 76        # 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._input, 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._input
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._input,
540            is_main_menu=False,
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._input.get_axis_name(sval1)
567                    + ' / '
568                    + self._input.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._input.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._input.get_button_name(self._settings[control])
597                return bui.Lstr(resource=f'{self._r}.unsetText')
598
599            # No dpad buttons - show the dpad number for all 4.
600            dpadnum = (
601                self._settings['dpad' + self._ext]
602                if 'dpad' + self._ext in self._settings
603                else 2 if self._is_secondary else None
604            )
605            assert isinstance(dpadnum, (int, type(None)))
606            if dpadnum is not None:
607                return bui.Lstr(
608                    value='${A} ${B}',
609                    subs=[
610                        ('${A}', bui.Lstr(resource=f'{self._r}.dpadText')),
611                        (
612                            '${B}',
613                            str(dpadnum),
614                        ),
615                    ],
616                )
617            return bui.Lstr(resource=f'{self._r}.unsetText')
618
619        # Other buttons.
620        if control in self._settings:
621            return self._input.get_button_name(self._settings[control])
622        return bui.Lstr(resource=f'{self._r}.unsetText')
623
624    def _gamepad_event(
625        self,
626        control: str,
627        event: dict[str, Any],
628        dialog: AwaitGamepadInputWindow,
629    ) -> None:
630        # pylint: disable=too-many-branches
631        assert self._settings is not None
632        ext = self._ext
633
634        # For our dpad-buttons we're looking for either a button-press or a
635        # hat-switch press.
636        if control in [
637            'buttonUp' + ext,
638            'buttonLeft' + ext,
639            'buttonDown' + ext,
640            'buttonRight' + ext,
641        ]:
642            if event['type'] in ['BUTTONDOWN', 'HATMOTION']:
643                # If its a button-down.
644                if event['type'] == 'BUTTONDOWN':
645                    value = event['button']
646                    self._settings[control] = value
647
648                # If its a dpad.
649                elif event['type'] == 'HATMOTION':
650                    # clear out any set dir-buttons
651                    for btn in [
652                        'buttonUp' + ext,
653                        'buttonLeft' + ext,
654                        'buttonRight' + ext,
655                        'buttonDown' + ext,
656                    ]:
657                        if btn in self._settings:
658                            del self._settings[btn]
659                    if event['hat'] == (2 if self._is_secondary else 1):
660                        self._settings['dpad' + ext] = event['hat']
661
662                # Update the 4 dpad button txt widgets.
663                bui.textwidget(
664                    edit=self._textwidgets['buttonUp' + ext],
665                    text=self.get_control_value_name('buttonUp' + ext),
666                )
667                bui.textwidget(
668                    edit=self._textwidgets['buttonLeft' + ext],
669                    text=self.get_control_value_name('buttonLeft' + ext),
670                )
671                bui.textwidget(
672                    edit=self._textwidgets['buttonRight' + ext],
673                    text=self.get_control_value_name('buttonRight' + ext),
674                )
675                bui.textwidget(
676                    edit=self._textwidgets['buttonDown' + ext],
677                    text=self.get_control_value_name('buttonDown' + ext),
678                )
679                bui.getsound('gunCocking').play()
680                dialog.die()
681
682        elif control == 'analogStickLR' + ext:
683            if event['type'] == 'AXISMOTION':
684                # Ignore small values or else we might get triggered by noise.
685                if abs(event['value']) > 0.5:
686                    axis = event['axis']
687                    if axis == (5 if self._is_secondary else 1):
688                        self._settings['analogStickLR' + ext] = axis
689                    bui.textwidget(
690                        edit=self._textwidgets['analogStickLR' + ext],
691                        text=self.get_control_value_name('analogStickLR' + ext),
692                    )
693                    bui.getsound('gunCocking').play()
694                    dialog.die()
695
696                    # Now launch the up/down listener.
697                    AwaitGamepadInputWindow(
698                        self._input,
699                        'analogStickUD' + ext,
700                        self._gamepad_event,
701                        bui.Lstr(resource=f'{self._r}.pressUpDownText'),
702                    )
703
704        elif control == 'analogStickUD' + ext:
705            if event['type'] == 'AXISMOTION':
706                # Ignore small values or else we might get triggered by noise.
707                if abs(event['value']) > 0.5:
708                    axis = event['axis']
709
710                    # Ignore our LR axis.
711                    if 'analogStickLR' + ext in self._settings:
712                        lr_axis = self._settings['analogStickLR' + ext]
713                    else:
714                        lr_axis = 5 if self._is_secondary else 1
715                    if axis != lr_axis:
716                        if axis == (6 if self._is_secondary else 2):
717                            self._settings['analogStickUD' + ext] = axis
718                        bui.textwidget(
719                            edit=self._textwidgets['analogStickLR' + ext],
720                            text=self.get_control_value_name(
721                                'analogStickLR' + ext
722                            ),
723                        )
724                        bui.getsound('gunCocking').play()
725                        dialog.die()
726        else:
727            # For other buttons we just want a button-press.
728            if event['type'] == 'BUTTONDOWN':
729                value = event['button']
730                self._settings[control] = value
731
732                # Update the button's text widget.
733                bui.textwidget(
734                    edit=self._textwidgets[control],
735                    text=self.get_control_value_name(control),
736                )
737                bui.getsound('gunCocking').play()
738                dialog.die()
739
740    def _capture_button(
741        self,
742        pos: tuple[float, float],
743        color: tuple[float, float, float],
744        texture: bui.Texture,
745        button: str,
746        scale: float = 1.0,
747        message: bui.Lstr | None = None,
748        message2: bui.Lstr | None = None,
749        maxwidth: float = 80.0,
750    ) -> bui.Widget:
751        if message is None:
752            message = bui.Lstr(resource=f'{self._r}.pressAnyButtonText')
753        base_size = 79
754        btn = bui.buttonwidget(
755            parent=self._root_widget,
756            position=(
757                pos[0] - base_size * 0.5 * scale,
758                pos[1] - base_size * 0.5 * scale,
759            ),
760            autoselect=True,
761            size=(base_size * scale, base_size * scale),
762            texture=texture,
763            label='',
764            color=color,
765        )
766
767        # Make this in a timer so that it shows up on top of all other buttons.
768
769        def doit() -> None:
770            uiscale = 0.9 * scale
771            txt = bui.textwidget(
772                parent=self._root_widget,
773                position=(pos[0] + 0.0 * scale, pos[1] - 58.0 * scale),
774                color=(1, 1, 1, 0.3),
775                size=(0, 0),
776                h_align='center',
777                v_align='center',
778                scale=uiscale,
779                text=self.get_control_value_name(button),
780                maxwidth=maxwidth,
781            )
782            self._textwidgets[button] = txt
783            bui.buttonwidget(
784                edit=btn,
785                on_activate_call=bui.Call(
786                    AwaitGamepadInputWindow,
787                    self._input,
788                    button,
789                    self._gamepad_event,
790                    message,
791                    message2,
792                ),
793            )
794
795        bui.pushcall(doit)
796        return btn
797
798    def _cancel(self) -> None:
799        # from bauiv1lib.settings.controls import ControlsSettingsWindow
800
801        # no-op if our underlying widget is dead or on its way out.
802        if not self._root_widget or self._root_widget.transitioning_out:
803            return
804
805        bui.containerwidget(
806            edit=self._root_widget, transition=self._transition_out
807        )
808        if self._is_main_menu:
809            self.main_window_back()
810            # assert bui.app.classic is not None
811            # bui.app.ui_v1.set_main_window(
812            #     ControlsSettingsWindow(transition='in_left'),
813            #     from_window=self,
814            #     is_back=True,
815            # )
816
817    def _reset(self) -> None:
818        from bauiv1lib.confirm import ConfirmWindow
819
820        assert bui.app.classic is not None
821
822        # efro note: I think it's ok to reset without a confirm here
823        # because the user can see pretty clearly what changes and can
824        # cancel out of the settings window without saving if they want.
825        if bool(False):
826            ConfirmWindow(
827                # TODO: Implement a translation string for this!
828                'Are you sure you want to reset your button mapping?\n'
829                'This will also reset your advanced mappings\n'
830                'and secondary controller button mappings.',
831                self._do_reset,
832                width=490,
833                height=150,
834            )
835        else:
836            self._do_reset()
837
838    def _do_reset(self) -> None:
839        """Resets the input's mapping settings."""
840        from babase import InputDeviceNotFoundError
841
842        self._settings = {}
843        # Unplugging the controller while performing a
844        # mapping reset makes things go bonkers a little.
845        try:
846            self._get_config_mapping(default=True)
847        except InputDeviceNotFoundError:
848            pass
849
850        self._rebuild_ui(is_reset=True)
851        bui.getsound('gunCocking').play()
852
853    def _do_more(self) -> None:
854        """Show a burger menu with extra settings."""
855        # pylint: disable=cyclic-import
856        choices: list[str] = [
857            'advanced',
858            'reset',
859        ]
860        choices_display: list[bui.Lstr] = [
861            bui.Lstr(resource=f'{self._r}.advancedText'),
862            bui.Lstr(resource='settingsWindowAdvanced.resetText'),
863        ]
864
865        uiscale = bui.app.ui_v1.uiscale
866        PopupMenuWindow(
867            position=self._more_button.get_screen_space_center(),
868            scale=(
869                2.3
870                if uiscale is bui.UIScale.SMALL
871                else 1.65 if uiscale is bui.UIScale.MEDIUM else 1.23
872            ),
873            width=150,
874            choices=choices,
875            choices_display=choices_display,
876            current_choice='advanced',
877            delegate=self,
878        )
879
880    def popup_menu_selected_choice(
881        self, popup_window: PopupMenuWindow, choice: str
882    ) -> None:
883        """Called when a choice is selected in the popup."""
884        del popup_window  # unused
885        if choice == 'reset':
886            self._reset()
887        elif choice == 'advanced':
888            self._do_advanced()
889        else:
890            print(f'invalid choice: {choice}')
891
892    def popup_menu_closing(self, popup_window: PopupWindow) -> None:
893        """Called when the popup is closing."""
894
895    def _save(self) -> None:
896        classic = bui.app.classic
897        assert classic is not None
898
899        # no-op if our underlying widget is dead or on its way out.
900        if not self._root_widget or self._root_widget.transitioning_out:
901            return
902
903        bui.containerwidget(
904            edit=self._root_widget, transition=self._transition_out
905        )
906
907        # If we're a secondary editor we just go away (we were editing our
908        # parent's settings dict).
909        if self._is_secondary:
910            return
911
912        assert self._settings is not None
913        if self._input:
914            dst = classic.get_input_device_config(self._input, default=True)
915            dst2: dict[str, Any] = dst[0][dst[1]]
916            dst2.clear()
917
918            # Store any values that aren't -1.
919            for key, val in list(self._settings.items()):
920                if val != -1:
921                    dst2[key] = val
922
923            # If we're allowed to phone home, send this config so we can
924            # generate more defaults in the future.
925            inputhash = classic.get_input_device_map_hash(self._input)
926            classic.master_server_v1_post(
927                'controllerConfig',
928                {
929                    'ua': classic.legacy_user_agent_string,
930                    'b': bui.app.env.engine_build_number,
931                    'name': self._name,
932                    'inputMapHash': inputhash,
933                    'config': dst2,
934                    'v': 2,
935                },
936            )
937            bui.app.config.apply_and_commit()
938            bui.getsound('gunCocking').play()
939        else:
940            bui.getsound('error').play()
941
942        if self._is_main_menu:
943            from bauiv1lib.settings.controls import ControlsSettingsWindow
944
945            assert bui.app.classic is not None
946            bui.app.ui_v1.set_main_window(
947                ControlsSettingsWindow(transition='in_left'),
948                from_window=self,
949                is_back=True,
950            )

Window for configuring a gamepad.

GamepadSettingsWindow( gamepad: _bascenev1.InputDevice, is_main_menu: bool = True, 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        gamepad: bs.InputDevice,
29        is_main_menu: bool = True,
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._input = gamepad
36
37        # If our input-device went away, just return an empty zombie.
38        if not self._input:
39            return
40
41        self._name = self._input.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._is_main_menu = is_main_menu
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.63
61                    if uiscale is bui.UIScale.SMALL
62                    else 1.35 if uiscale is bui.UIScale.MEDIUM else 1.0
63                ),
64                stack_offset=(
65                    (-20, -16) if uiscale is bui.UIScale.SMALL else (0, 0)
66                ),
67                transition=transition,
68            ),
69            transition=transition,
70            origin_widget=origin_widget,
71        )
72
73        self._settings: dict[str, int] = {}
74        if not self._is_secondary:
75            self._get_config_mapping()
76        # 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:
880    def popup_menu_selected_choice(
881        self, popup_window: PopupMenuWindow, choice: str
882    ) -> None:
883        """Called when a choice is selected in the popup."""
884        del popup_window  # unused
885        if choice == 'reset':
886            self._reset()
887        elif choice == 'advanced':
888            self._do_advanced()
889        else:
890            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:
892    def popup_menu_closing(self, popup_window: PopupWindow) -> None:
893        """Called when the popup is closing."""

Called when the popup is closing.

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

Kill the window.

Inherited Members
bauiv1._uitypes.Window
get_root_widget