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

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__(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        self._input = gamepad
27
28        # If our input-device went away, just return an empty zombie.
29        if not self._input:
30            return
31
32        self._name = self._input.name
33
34        self._r = 'configGamepadWindow'
35        self._settings = settings
36        self._transition_out = transition_out
37
38        # We're a secondary gamepad if supplied with settings.
39        self._is_secondary = (settings is not None)
40        self._ext = '_B' if self._is_secondary else ''
41        self._is_main_menu = is_main_menu
42        self._displayname = self._name
43        self._width = 700 if self._is_secondary else 730
44        self._height = 440 if self._is_secondary else 450
45        self._spacing = 40
46        uiscale = ba.app.ui.uiscale
47        super().__init__(root_widget=ba.containerwidget(
48            size=(self._width, self._height),
49            scale=(1.63 if uiscale is ba.UIScale.SMALL else
50                   1.35 if uiscale is ba.UIScale.MEDIUM else 1.0),
51            stack_offset=(-20, -16) if uiscale is ba.UIScale.SMALL else (0, 0),
52            transition=transition))
53
54        # Don't ask to config joysticks while we're in here.
55        self._rebuild_ui()
Inherited Members
ba.ui.Window
get_root_widget
class AwaitGamepadInputWindow(ba.ui.Window):
749class AwaitGamepadInputWindow(ba.Window):
750    """Window for capturing a gamepad button press."""
751
752    def __init__(
753            self,
754            gamepad: ba.InputDevice,
755            button: str,
756            callback: Callable[[str, dict[str, Any], AwaitGamepadInputWindow],
757                               Any],
758            message: ba.Lstr | None = None,
759            message2: ba.Lstr | None = None):
760        if message is None:
761            print('AwaitGamepadInputWindow message is None!')
762            # Shouldn't get here.
763            message = ba.Lstr(value='Press any button...')
764        self._callback = callback
765        self._input = gamepad
766        self._capture_button = button
767        width = 400
768        height = 150
769        uiscale = ba.app.ui.uiscale
770        super().__init__(root_widget=ba.containerwidget(
771            scale=(2.0 if uiscale is ba.UIScale.SMALL else
772                   1.9 if uiscale is ba.UIScale.MEDIUM else 1.0),
773            size=(width, height),
774            transition='in_scale'), )
775        ba.textwidget(parent=self._root_widget,
776                      position=(0, (height - 60) if message2 is None else
777                                (height - 50)),
778                      size=(width, 25),
779                      text=message,
780                      maxwidth=width * 0.9,
781                      h_align='center',
782                      v_align='center')
783        if message2 is not None:
784            ba.textwidget(parent=self._root_widget,
785                          position=(width * 0.5, height - 60),
786                          size=(0, 0),
787                          text=message2,
788                          maxwidth=width * 0.9,
789                          scale=0.47,
790                          color=(0.7, 1.0, 0.7, 0.6),
791                          h_align='center',
792                          v_align='center')
793        self._counter = 5
794        self._count_down_text = ba.textwidget(parent=self._root_widget,
795                                              h_align='center',
796                                              position=(0, height - 110),
797                                              size=(width, 25),
798                                              color=(1, 1, 1, 0.3),
799                                              text=str(self._counter))
800        self._decrement_timer: ba.Timer | None = ba.Timer(
801            1.0,
802            ba.Call(self._decrement),
803            repeat=True,
804            timetype=ba.TimeType.REAL)
805        _ba.capture_gamepad_input(ba.WeakCall(self._event_callback))
806
807    def __del__(self) -> None:
808        pass
809
810    def die(self) -> None:
811        """Kill the window."""
812
813        # This strong-refs us; killing it allow us to die now.
814        self._decrement_timer = None
815        _ba.release_gamepad_input()
816        if self._root_widget:
817            ba.containerwidget(edit=self._root_widget, transition='out_scale')
818
819    def _event_callback(self, event: dict[str, Any]) -> None:
820        input_device = event['input_device']
821        assert isinstance(input_device, ba.InputDevice)
822
823        # Update - we now allow *any* input device of this type.
824        if (self._input and input_device
825                and input_device.name == self._input.name):
826            self._callback(self._capture_button, event, self)
827
828    def _decrement(self) -> None:
829        self._counter -= 1
830        if self._counter >= 1:
831            if self._count_down_text:
832                ba.textwidget(edit=self._count_down_text,
833                              text=str(self._counter))
834        else:
835            ba.playsound(ba.getsound('error'))
836            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)
752    def __init__(
753            self,
754            gamepad: ba.InputDevice,
755            button: str,
756            callback: Callable[[str, dict[str, Any], AwaitGamepadInputWindow],
757                               Any],
758            message: ba.Lstr | None = None,
759            message2: ba.Lstr | None = None):
760        if message is None:
761            print('AwaitGamepadInputWindow message is None!')
762            # Shouldn't get here.
763            message = ba.Lstr(value='Press any button...')
764        self._callback = callback
765        self._input = gamepad
766        self._capture_button = button
767        width = 400
768        height = 150
769        uiscale = ba.app.ui.uiscale
770        super().__init__(root_widget=ba.containerwidget(
771            scale=(2.0 if uiscale is ba.UIScale.SMALL else
772                   1.9 if uiscale is ba.UIScale.MEDIUM else 1.0),
773            size=(width, height),
774            transition='in_scale'), )
775        ba.textwidget(parent=self._root_widget,
776                      position=(0, (height - 60) if message2 is None else
777                                (height - 50)),
778                      size=(width, 25),
779                      text=message,
780                      maxwidth=width * 0.9,
781                      h_align='center',
782                      v_align='center')
783        if message2 is not None:
784            ba.textwidget(parent=self._root_widget,
785                          position=(width * 0.5, height - 60),
786                          size=(0, 0),
787                          text=message2,
788                          maxwidth=width * 0.9,
789                          scale=0.47,
790                          color=(0.7, 1.0, 0.7, 0.6),
791                          h_align='center',
792                          v_align='center')
793        self._counter = 5
794        self._count_down_text = ba.textwidget(parent=self._root_widget,
795                                              h_align='center',
796                                              position=(0, height - 110),
797                                              size=(width, 25),
798                                              color=(1, 1, 1, 0.3),
799                                              text=str(self._counter))
800        self._decrement_timer: ba.Timer | None = ba.Timer(
801            1.0,
802            ba.Call(self._decrement),
803            repeat=True,
804            timetype=ba.TimeType.REAL)
805        _ba.capture_gamepad_input(ba.WeakCall(self._event_callback))
def die(self) -> None:
810    def die(self) -> None:
811        """Kill the window."""
812
813        # This strong-refs us; killing it allow us to die now.
814        self._decrement_timer = None
815        _ba.release_gamepad_input()
816        if self._root_widget:
817            ba.containerwidget(edit=self._root_widget, transition='out_scale')

Kill the window.

Inherited Members
ba.ui.Window
get_root_widget