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

Window for configuring a gamepad.

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

Window for capturing a gamepad button press.

AwaitGamepadInputWindow( gamepad: _bascenev1.InputDevice, button: str, callback: Callable[[str, dict[str, Any], AwaitGamepadInputWindow], Any], message: babase._language.Lstr | None = None, message2: babase._language.Lstr | None = None)
857    def __init__(
858        self,
859        gamepad: bs.InputDevice,
860        button: str,
861        callback: Callable[[str, dict[str, Any], AwaitGamepadInputWindow], Any],
862        message: bui.Lstr | None = None,
863        message2: bui.Lstr | None = None,
864    ):
865        if message is None:
866            print('AwaitGamepadInputWindow message is None!')
867            # Shouldn't get here.
868            message = bui.Lstr(value='Press any button...')
869        self._callback = callback
870        self._input = gamepad
871        self._capture_button = button
872        width = 400
873        height = 150
874        assert bui.app.classic is not None
875        uiscale = bui.app.ui_v1.uiscale
876        super().__init__(
877            root_widget=bui.containerwidget(
878                scale=(
879                    2.0
880                    if uiscale is bui.UIScale.SMALL
881                    else 1.9
882                    if uiscale is bui.UIScale.MEDIUM
883                    else 1.0
884                ),
885                size=(width, height),
886                transition='in_scale',
887            ),
888        )
889        bui.textwidget(
890            parent=self._root_widget,
891            position=(0, (height - 60) if message2 is None else (height - 50)),
892            size=(width, 25),
893            text=message,
894            maxwidth=width * 0.9,
895            h_align='center',
896            v_align='center',
897        )
898        if message2 is not None:
899            bui.textwidget(
900                parent=self._root_widget,
901                position=(width * 0.5, height - 60),
902                size=(0, 0),
903                text=message2,
904                maxwidth=width * 0.9,
905                scale=0.47,
906                color=(0.7, 1.0, 0.7, 0.6),
907                h_align='center',
908                v_align='center',
909            )
910        self._counter = 5
911        self._count_down_text = bui.textwidget(
912            parent=self._root_widget,
913            h_align='center',
914            position=(0, height - 110),
915            size=(width, 25),
916            color=(1, 1, 1, 0.3),
917            text=str(self._counter),
918        )
919        self._decrement_timer: bui.AppTimer | None = bui.AppTimer(
920            1.0, bui.Call(self._decrement), repeat=True
921        )
922        bs.capture_gamepad_input(bui.WeakCall(self._event_callback))
def die(self) -> None:
927    def die(self) -> None:
928        """Kill the window."""
929
930        # This strong-refs us; killing it allow us to die now.
931        self._decrement_timer = None
932        bs.release_gamepad_input()
933        if self._root_widget:
934            bui.containerwidget(edit=self._root_widget, transition='out_scale')

Kill the window.

Inherited Members
bauiv1._uitypes.Window
get_root_widget