bastd.ui.settings.gamepad

Settings UI functionality related to gamepads.

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

Window for configuring a gamepad.

GamepadSettingsWindow( gamepad: _ba.InputDevice, is_main_menu: bool = True, transition: str = 'in_right', transition_out: str = 'out_right', settings: dict | None = None)
20    def __init__(
21        self,
22        gamepad: ba.InputDevice,
23        is_main_menu: bool = True,
24        transition: str = 'in_right',
25        transition_out: str = 'out_right',
26        settings: dict | None = None,
27    ):
28        self._input = gamepad
29
30        # If our input-device went away, just return an empty zombie.
31        if not self._input:
32            return
33
34        self._name = self._input.name
35
36        self._r = 'configGamepadWindow'
37        self._settings = settings
38        self._transition_out = transition_out
39
40        # We're a secondary gamepad if supplied with settings.
41        self._is_secondary = settings is not None
42        self._ext = '_B' if self._is_secondary else ''
43        self._is_main_menu = is_main_menu
44        self._displayname = self._name
45        self._width = 700 if self._is_secondary else 730
46        self._height = 440 if self._is_secondary else 450
47        self._spacing = 40
48        uiscale = ba.app.ui.uiscale
49        super().__init__(
50            root_widget=ba.containerwidget(
51                size=(self._width, self._height),
52                scale=(
53                    1.63
54                    if uiscale is ba.UIScale.SMALL
55                    else 1.35
56                    if uiscale is ba.UIScale.MEDIUM
57                    else 1.0
58                ),
59                stack_offset=(-20, -16)
60                if uiscale is ba.UIScale.SMALL
61                else (0, 0),
62                transition=transition,
63            )
64        )
65
66        # Don't ask to config joysticks while we're in here.
67        self._rebuild_ui()
Inherited Members
ba.ui.Window
get_root_widget
class AwaitGamepadInputWindow(ba.ui.Window):
869class AwaitGamepadInputWindow(ba.Window):
870    """Window for capturing a gamepad button press."""
871
872    def __init__(
873        self,
874        gamepad: ba.InputDevice,
875        button: str,
876        callback: Callable[[str, dict[str, Any], AwaitGamepadInputWindow], Any],
877        message: ba.Lstr | None = None,
878        message2: ba.Lstr | None = None,
879    ):
880        if message is None:
881            print('AwaitGamepadInputWindow message is None!')
882            # Shouldn't get here.
883            message = ba.Lstr(value='Press any button...')
884        self._callback = callback
885        self._input = gamepad
886        self._capture_button = button
887        width = 400
888        height = 150
889        uiscale = ba.app.ui.uiscale
890        super().__init__(
891            root_widget=ba.containerwidget(
892                scale=(
893                    2.0
894                    if uiscale is ba.UIScale.SMALL
895                    else 1.9
896                    if uiscale is ba.UIScale.MEDIUM
897                    else 1.0
898                ),
899                size=(width, height),
900                transition='in_scale',
901            ),
902        )
903        ba.textwidget(
904            parent=self._root_widget,
905            position=(0, (height - 60) if message2 is None else (height - 50)),
906            size=(width, 25),
907            text=message,
908            maxwidth=width * 0.9,
909            h_align='center',
910            v_align='center',
911        )
912        if message2 is not None:
913            ba.textwidget(
914                parent=self._root_widget,
915                position=(width * 0.5, height - 60),
916                size=(0, 0),
917                text=message2,
918                maxwidth=width * 0.9,
919                scale=0.47,
920                color=(0.7, 1.0, 0.7, 0.6),
921                h_align='center',
922                v_align='center',
923            )
924        self._counter = 5
925        self._count_down_text = ba.textwidget(
926            parent=self._root_widget,
927            h_align='center',
928            position=(0, height - 110),
929            size=(width, 25),
930            color=(1, 1, 1, 0.3),
931            text=str(self._counter),
932        )
933        self._decrement_timer: ba.Timer | None = ba.Timer(
934            1.0,
935            ba.Call(self._decrement),
936            repeat=True,
937            timetype=ba.TimeType.REAL,
938        )
939        ba.internal.capture_gamepad_input(ba.WeakCall(self._event_callback))
940
941    def __del__(self) -> None:
942        pass
943
944    def die(self) -> None:
945        """Kill the window."""
946
947        # This strong-refs us; killing it allow us to die now.
948        self._decrement_timer = None
949        ba.internal.release_gamepad_input()
950        if self._root_widget:
951            ba.containerwidget(edit=self._root_widget, transition='out_scale')
952
953    def _event_callback(self, event: dict[str, Any]) -> None:
954        input_device = event['input_device']
955        assert isinstance(input_device, ba.InputDevice)
956
957        # Update - we now allow *any* input device of this type.
958        if (
959            self._input
960            and input_device
961            and input_device.name == self._input.name
962        ):
963            self._callback(self._capture_button, event, self)
964
965    def _decrement(self) -> None:
966        self._counter -= 1
967        if self._counter >= 1:
968            if self._count_down_text:
969                ba.textwidget(
970                    edit=self._count_down_text, text=str(self._counter)
971                )
972        else:
973            ba.playsound(ba.getsound('error'))
974            self.die()

Window for capturing a gamepad button press.

AwaitGamepadInputWindow( gamepad: _ba.InputDevice, button: str, callback: Callable[[str, dict[str, Any], bastd.ui.settings.gamepad.AwaitGamepadInputWindow], Any], message: ba._language.Lstr | None = None, message2: ba._language.Lstr | None = None)
872    def __init__(
873        self,
874        gamepad: ba.InputDevice,
875        button: str,
876        callback: Callable[[str, dict[str, Any], AwaitGamepadInputWindow], Any],
877        message: ba.Lstr | None = None,
878        message2: ba.Lstr | None = None,
879    ):
880        if message is None:
881            print('AwaitGamepadInputWindow message is None!')
882            # Shouldn't get here.
883            message = ba.Lstr(value='Press any button...')
884        self._callback = callback
885        self._input = gamepad
886        self._capture_button = button
887        width = 400
888        height = 150
889        uiscale = ba.app.ui.uiscale
890        super().__init__(
891            root_widget=ba.containerwidget(
892                scale=(
893                    2.0
894                    if uiscale is ba.UIScale.SMALL
895                    else 1.9
896                    if uiscale is ba.UIScale.MEDIUM
897                    else 1.0
898                ),
899                size=(width, height),
900                transition='in_scale',
901            ),
902        )
903        ba.textwidget(
904            parent=self._root_widget,
905            position=(0, (height - 60) if message2 is None else (height - 50)),
906            size=(width, 25),
907            text=message,
908            maxwidth=width * 0.9,
909            h_align='center',
910            v_align='center',
911        )
912        if message2 is not None:
913            ba.textwidget(
914                parent=self._root_widget,
915                position=(width * 0.5, height - 60),
916                size=(0, 0),
917                text=message2,
918                maxwidth=width * 0.9,
919                scale=0.47,
920                color=(0.7, 1.0, 0.7, 0.6),
921                h_align='center',
922                v_align='center',
923            )
924        self._counter = 5
925        self._count_down_text = ba.textwidget(
926            parent=self._root_widget,
927            h_align='center',
928            position=(0, height - 110),
929            size=(width, 25),
930            color=(1, 1, 1, 0.3),
931            text=str(self._counter),
932        )
933        self._decrement_timer: ba.Timer | None = ba.Timer(
934            1.0,
935            ba.Call(self._decrement),
936            repeat=True,
937            timetype=ba.TimeType.REAL,
938        )
939        ba.internal.capture_gamepad_input(ba.WeakCall(self._event_callback))
def die(self) -> None:
944    def die(self) -> None:
945        """Kill the window."""
946
947        # This strong-refs us; killing it allow us to die now.
948        self._decrement_timer = None
949        ba.internal.release_gamepad_input()
950        if self._root_widget:
951            ba.containerwidget(edit=self._root_widget, transition='out_scale')

Kill the window.

Inherited Members
ba.ui.Window
get_root_widget