bauiv1lib.settings.keyboard

Keyboard settings related UI functionality.

  1# Released under the MIT License. See LICENSE for details.
  2#
  3"""Keyboard settings related UI functionality."""
  4
  5from __future__ import annotations
  6
  7from typing import TYPE_CHECKING, override
  8
  9from bauiv1lib.popup import PopupMenuWindow
 10import bauiv1 as bui
 11import bascenev1 as bs
 12
 13if TYPE_CHECKING:
 14    from typing import Any
 15
 16    from bauiv1lib.popup import PopupWindow
 17
 18
 19class ConfigKeyboardWindow(bui.MainWindow):
 20    """Window for configuring keyboards."""
 21
 22    def __init__(
 23        self,
 24        c: bs.InputDevice,
 25        transition: str | None = 'in_right',
 26        origin_widget: bui.Widget | None = None,
 27    ):
 28        self._r = 'configKeyboardWindow'
 29        self._input = c
 30        self._name = self._input.name
 31        self._unique_id = self._input.unique_identifier
 32        dname_raw = self._name
 33        if self._unique_id != '#1':
 34            dname_raw += ' ' + self._unique_id.replace('#', 'P')
 35        self._displayname = bui.Lstr(translate=('inputDeviceNames', dname_raw))
 36        self._width = 700
 37        if self._unique_id != '#1':
 38            self._height = 480
 39        else:
 40            self._height = 375
 41        self._spacing = 40
 42        assert bui.app.classic is not None
 43        uiscale = bui.app.ui_v1.uiscale
 44        super().__init__(
 45            root_widget=bui.containerwidget(
 46                size=(self._width, self._height),
 47                scale=(
 48                    1.4
 49                    if uiscale is bui.UIScale.SMALL
 50                    else 1.3 if uiscale is bui.UIScale.MEDIUM else 1.0
 51                ),
 52                stack_offset=(0, 5) if uiscale is bui.UIScale.SMALL else (0, 0),
 53                transition=transition,
 54            ),
 55            transition=transition,
 56            origin_widget=origin_widget,
 57        )
 58
 59        self._settings: dict[str, int] = {}
 60        self._get_config_mapping()
 61
 62        self._rebuild_ui()
 63
 64    @override
 65    def get_main_window_state(self) -> bui.MainWindowState:
 66        # Support recreating our window for back/refresh purposes.
 67        cls = type(self)
 68
 69        # Pull things from self here; if we do it within the lambda
 70        # we'll keep self alive which is bad.
 71        inputdevice = self._input
 72
 73        return bui.BasicMainWindowState(
 74            create_call=lambda transition, origin_widget: cls(
 75                transition=transition,
 76                origin_widget=origin_widget,
 77                c=inputdevice,
 78            )
 79        )
 80
 81    def _get_config_mapping(self, default: bool = False) -> None:
 82        for button in [
 83            'buttonJump',
 84            'buttonPunch',
 85            'buttonBomb',
 86            'buttonPickUp',
 87            'buttonStart',
 88            'buttonStart2',
 89            'buttonUp',
 90            'buttonDown',
 91            'buttonLeft',
 92            'buttonRight',
 93        ]:
 94            assert bui.app.classic is not None
 95            self._settings[button] = (
 96                bui.app.classic.get_input_device_mapped_value(
 97                    self._input, button, default
 98                )
 99            )
100
101    def _rebuild_ui(self, is_reset: bool = False) -> None:
102        assert bui.app.classic is not None
103
104        for widget in self._root_widget.get_children():
105            widget.delete()
106
107        # b_off = 0 if self._unique_id != '#1' else 9
108        cancel_button = bui.buttonwidget(
109            parent=self._root_widget,
110            autoselect=True,
111            position=(38, self._height - 85),
112            size=(170, 60),
113            label=bui.Lstr(resource='cancelText'),
114            scale=0.9,
115            on_activate_call=self.main_window_back,
116        )
117        save_button = bui.buttonwidget(
118            parent=self._root_widget,
119            autoselect=True,
120            position=(self._width - 190, self._height - 85),
121            size=(180, 60),
122            label=bui.Lstr(resource='saveText'),
123            scale=0.9,
124            text_scale=0.9,
125            on_activate_call=self._save,
126        )
127        bui.containerwidget(
128            edit=self._root_widget,
129            cancel_button=cancel_button,
130            start_button=save_button,
131        )
132
133        v = self._height - 74.0
134        bui.textwidget(
135            parent=self._root_widget,
136            position=(self._width * 0.5, v + 15),
137            size=(0, 0),
138            text=bui.Lstr(
139                resource=f'{self._r}.configuringText',
140                subs=[('${DEVICE}', self._displayname)],
141            ),
142            color=bui.app.ui_v1.title_color,
143            h_align='center',
144            v_align='center',
145            maxwidth=270,
146            scale=0.83,
147        )
148        v -= 20
149
150        if self._unique_id != '#1':
151            v -= 20
152            v -= self._spacing
153            bui.textwidget(
154                parent=self._root_widget,
155                position=(0, v + 19),
156                size=(self._width, 50),
157                text=bui.Lstr(resource=f'{self._r}.keyboard2NoteText'),
158                scale=0.7,
159                maxwidth=self._width * 0.75,
160                max_height=110,
161                color=bui.app.ui_v1.infotextcolor,
162                h_align='center',
163                v_align='top',
164            )
165            v -= 40
166        v -= 10
167        v -= self._spacing * 2.2
168        v += 25
169        v -= 42
170        h_offs = 160
171        dist = 70
172        d_color = (0.4, 0.4, 0.8)
173        self._capture_button(
174            pos=(h_offs, v + 0.95 * dist),
175            color=d_color,
176            button='buttonUp',
177            texture=bui.gettexture('upButton'),
178            scale=1.0,
179        )
180        self._capture_button(
181            pos=(h_offs - 1.2 * dist, v),
182            color=d_color,
183            button='buttonLeft',
184            texture=bui.gettexture('leftButton'),
185            scale=1.0,
186        )
187        self._capture_button(
188            pos=(h_offs + 1.2 * dist, v),
189            color=d_color,
190            button='buttonRight',
191            texture=bui.gettexture('rightButton'),
192            scale=1.0,
193        )
194        self._capture_button(
195            pos=(h_offs, v - 0.95 * dist),
196            color=d_color,
197            button='buttonDown',
198            texture=bui.gettexture('downButton'),
199            scale=1.0,
200        )
201
202        if self._unique_id == '#2':
203            self._capture_button(
204                pos=(self._width * 0.5, v + 0.1 * dist),
205                color=(0.4, 0.4, 0.6),
206                button='buttonStart',
207                texture=bui.gettexture('startButton'),
208                scale=0.8,
209            )
210
211        h_offs = self._width - 160
212
213        self._capture_button(
214            pos=(h_offs, v + 0.95 * dist),
215            color=(0.6, 0.4, 0.8),
216            button='buttonPickUp',
217            texture=bui.gettexture('buttonPickUp'),
218            scale=1.0,
219        )
220        self._capture_button(
221            pos=(h_offs - 1.2 * dist, v),
222            color=(0.7, 0.5, 0.1),
223            button='buttonPunch',
224            texture=bui.gettexture('buttonPunch'),
225            scale=1.0,
226        )
227        self._capture_button(
228            pos=(h_offs + 1.2 * dist, v),
229            color=(0.5, 0.2, 0.1),
230            button='buttonBomb',
231            texture=bui.gettexture('buttonBomb'),
232            scale=1.0,
233        )
234        self._capture_button(
235            pos=(h_offs, v - 0.95 * dist),
236            color=(0.2, 0.5, 0.2),
237            button='buttonJump',
238            texture=bui.gettexture('buttonJump'),
239            scale=1.0,
240        )
241
242        self._more_button = bui.buttonwidget(
243            parent=self._root_widget,
244            autoselect=True,
245            label='...',
246            text_scale=0.9,
247            color=(0.45, 0.4, 0.5),
248            textcolor=(0.65, 0.6, 0.7),
249            position=(self._width * 0.5 - 65, 30),
250            size=(130, 40),
251            on_activate_call=self._do_more,
252        )
253
254        if is_reset:
255            bui.containerwidget(
256                edit=self._root_widget,
257                selected_child=self._more_button,
258            )
259
260    def _pretty_button_name(self, button_name: str) -> bui.Lstr:
261        button_id = self._settings[button_name]
262        if button_id == -1:
263            return bs.Lstr(resource='configGamepadWindow.unsetText')
264        return self._input.get_button_name(button_id)
265
266    def _capture_button(
267        self,
268        pos: tuple[float, float],
269        color: tuple[float, float, float],
270        texture: bui.Texture,
271        button: str,
272        scale: float = 1.0,
273    ) -> None:
274        # pylint: disable=too-many-positional-arguments
275        base_size = 79
276        btn = bui.buttonwidget(
277            parent=self._root_widget,
278            autoselect=True,
279            position=(
280                pos[0] - base_size * 0.5 * scale,
281                pos[1] - base_size * 0.5 * scale,
282            ),
283            size=(base_size * scale, base_size * scale),
284            texture=texture,
285            label='',
286            color=color,
287        )
288
289        # Do this deferred so it shows up on top of other buttons. (ew.)
290        def doit() -> None:
291            if not self._root_widget:
292                return
293            uiscale = 0.66 * scale * 2.0
294            maxwidth = 76.0 * scale
295            txt = bui.textwidget(
296                parent=self._root_widget,
297                position=(pos[0] + 0.0 * scale, pos[1] - (57.0 - 18.0) * scale),
298                color=(1, 1, 1, 0.3),
299                size=(0, 0),
300                h_align='center',
301                v_align='top',
302                scale=uiscale,
303                maxwidth=maxwidth,
304                text=self._pretty_button_name(button),
305            )
306            bui.buttonwidget(
307                edit=btn,
308                autoselect=True,
309                on_activate_call=bui.Call(
310                    AwaitKeyboardInputWindow, button, txt, self._settings
311                ),
312            )
313
314        bui.pushcall(doit)
315
316    def _reset(self) -> None:
317        from bauiv1lib.confirm import ConfirmWindow
318
319        assert bui.app.classic is not None
320
321        # efro note: I think it's ok to reset without a confirm here
322        # because the user can see pretty clearly what changes and can
323        # cancel out of the keyboard settings edit if they want.
324        if bool(False):
325            ConfirmWindow(
326                # TODO: Implement a translation string for this!
327                'Are you sure you want to reset your button mapping?',
328                self._do_reset,
329                width=480,
330                height=95,
331            )
332        else:
333            self._do_reset()
334
335    def _do_reset(self) -> None:
336        """Resets the input's mapping settings."""
337        self._settings = {}
338        self._get_config_mapping(default=True)
339        self._rebuild_ui(is_reset=True)
340        bui.getsound('gunCocking').play()
341
342    def _do_more(self) -> None:
343        """Show a burger menu with extra settings."""
344        # pylint: disable=cyclic-import
345        choices: list[str] = [
346            'reset',
347        ]
348        choices_display: list[bui.Lstr] = [
349            bui.Lstr(resource='settingsWindowAdvanced.resetText'),
350        ]
351
352        uiscale = bui.app.ui_v1.uiscale
353        PopupMenuWindow(
354            position=self._more_button.get_screen_space_center(),
355            scale=(
356                2.3
357                if uiscale is bui.UIScale.SMALL
358                else 1.65 if uiscale is bui.UIScale.MEDIUM else 1.23
359            ),
360            width=150,
361            choices=choices,
362            choices_display=choices_display,
363            current_choice='reset',
364            delegate=self,
365        )
366
367    def popup_menu_selected_choice(
368        self, popup_window: PopupMenuWindow, choice: str
369    ) -> None:
370        """Called when a choice is selected in the popup."""
371        del popup_window  # unused
372        if choice == 'reset':
373            self._reset()
374        else:
375            print(f'invalid choice: {choice}')
376
377    def popup_menu_closing(self, popup_window: PopupWindow) -> None:
378        """Called when the popup is closing."""
379
380    def _save(self) -> None:
381
382        # no-op if we're not in control.
383        if not self.main_window_has_control():
384            return
385
386        assert bui.app.classic is not None
387        bui.getsound('gunCocking').play()
388
389        # There's a chance the device disappeared; handle that
390        # gracefully.
391        if not self._input:
392            return
393
394        dst = bui.app.classic.get_input_device_config(
395            self._input, default=False
396        )
397        dst2: dict[str, Any] = dst[0][dst[1]]
398        dst2.clear()
399
400        # Store any values that aren't -1.
401        for key, val in list(self._settings.items()):
402            if val != -1:
403                dst2[key] = val
404
405        # Send this config to the master-server so we can generate more
406        # defaults in the future.
407        if bui.app.classic is not None:
408            bui.app.classic.master_server_v1_post(
409                'controllerConfig',
410                {
411                    'ua': bui.app.classic.legacy_user_agent_string,
412                    'name': self._name,
413                    'b': bui.app.env.engine_build_number,
414                    'config': dst2,
415                    'v': 2,
416                },
417            )
418        bui.app.config.apply_and_commit()
419
420        self.main_window_back()
421
422
423class AwaitKeyboardInputWindow(bui.Window):
424    """Window for capturing a keypress."""
425
426    def __init__(self, button: str, ui: bui.Widget, settings: dict):
427        self._capture_button = button
428        self._capture_key_ui = ui
429        self._settings = settings
430
431        width = 400
432        height = 150
433        assert bui.app.classic is not None
434        uiscale = bui.app.ui_v1.uiscale
435        super().__init__(
436            root_widget=bui.containerwidget(
437                size=(width, height),
438                transition='in_right',
439                scale=(
440                    2.0
441                    if uiscale is bui.UIScale.SMALL
442                    else 1.5 if uiscale is bui.UIScale.MEDIUM else 1.0
443                ),
444            )
445        )
446        bui.textwidget(
447            parent=self._root_widget,
448            position=(0, height - 60),
449            size=(width, 25),
450            text=bui.Lstr(resource='pressAnyKeyText'),
451            h_align='center',
452            v_align='top',
453        )
454
455        self._counter = 5
456        self._count_down_text = bui.textwidget(
457            parent=self._root_widget,
458            h_align='center',
459            position=(0, height - 110),
460            size=(width, 25),
461            color=(1, 1, 1, 0.3),
462            text=str(self._counter),
463        )
464        self._decrement_timer: bui.AppTimer | None = bui.AppTimer(
465            1.0, self._decrement, repeat=True
466        )
467        bs.capture_keyboard_input(bui.WeakCall(self._button_callback))
468
469    def __del__(self) -> None:
470        bs.release_keyboard_input()
471
472    def _die(self) -> None:
473        # This strong-refs us; killing it allows us to die now.
474        self._decrement_timer = None
475        if self._root_widget:
476            bui.containerwidget(edit=self._root_widget, transition='out_left')
477
478    def _button_callback(self, event: dict[str, Any]) -> None:
479        self._settings[self._capture_button] = event['button']
480        if event['type'] == 'BUTTONDOWN':
481            bname = event['input_device'].get_button_name(event['button'])
482            bui.textwidget(edit=self._capture_key_ui, text=bname)
483            bui.getsound('gunCocking').play()
484            self._die()
485
486    def _decrement(self) -> None:
487        self._counter -= 1
488        if self._counter >= 1:
489            bui.textwidget(edit=self._count_down_text, text=str(self._counter))
490        else:
491            self._die()
class ConfigKeyboardWindow(bauiv1._uitypes.MainWindow):
 20class ConfigKeyboardWindow(bui.MainWindow):
 21    """Window for configuring keyboards."""
 22
 23    def __init__(
 24        self,
 25        c: bs.InputDevice,
 26        transition: str | None = 'in_right',
 27        origin_widget: bui.Widget | None = None,
 28    ):
 29        self._r = 'configKeyboardWindow'
 30        self._input = c
 31        self._name = self._input.name
 32        self._unique_id = self._input.unique_identifier
 33        dname_raw = self._name
 34        if self._unique_id != '#1':
 35            dname_raw += ' ' + self._unique_id.replace('#', 'P')
 36        self._displayname = bui.Lstr(translate=('inputDeviceNames', dname_raw))
 37        self._width = 700
 38        if self._unique_id != '#1':
 39            self._height = 480
 40        else:
 41            self._height = 375
 42        self._spacing = 40
 43        assert bui.app.classic is not None
 44        uiscale = bui.app.ui_v1.uiscale
 45        super().__init__(
 46            root_widget=bui.containerwidget(
 47                size=(self._width, self._height),
 48                scale=(
 49                    1.4
 50                    if uiscale is bui.UIScale.SMALL
 51                    else 1.3 if uiscale is bui.UIScale.MEDIUM else 1.0
 52                ),
 53                stack_offset=(0, 5) if uiscale is bui.UIScale.SMALL else (0, 0),
 54                transition=transition,
 55            ),
 56            transition=transition,
 57            origin_widget=origin_widget,
 58        )
 59
 60        self._settings: dict[str, int] = {}
 61        self._get_config_mapping()
 62
 63        self._rebuild_ui()
 64
 65    @override
 66    def get_main_window_state(self) -> bui.MainWindowState:
 67        # Support recreating our window for back/refresh purposes.
 68        cls = type(self)
 69
 70        # Pull things from self here; if we do it within the lambda
 71        # we'll keep self alive which is bad.
 72        inputdevice = self._input
 73
 74        return bui.BasicMainWindowState(
 75            create_call=lambda transition, origin_widget: cls(
 76                transition=transition,
 77                origin_widget=origin_widget,
 78                c=inputdevice,
 79            )
 80        )
 81
 82    def _get_config_mapping(self, default: bool = False) -> None:
 83        for button in [
 84            'buttonJump',
 85            'buttonPunch',
 86            'buttonBomb',
 87            'buttonPickUp',
 88            'buttonStart',
 89            'buttonStart2',
 90            'buttonUp',
 91            'buttonDown',
 92            'buttonLeft',
 93            'buttonRight',
 94        ]:
 95            assert bui.app.classic is not None
 96            self._settings[button] = (
 97                bui.app.classic.get_input_device_mapped_value(
 98                    self._input, button, default
 99                )
100            )
101
102    def _rebuild_ui(self, is_reset: bool = False) -> None:
103        assert bui.app.classic is not None
104
105        for widget in self._root_widget.get_children():
106            widget.delete()
107
108        # b_off = 0 if self._unique_id != '#1' else 9
109        cancel_button = bui.buttonwidget(
110            parent=self._root_widget,
111            autoselect=True,
112            position=(38, self._height - 85),
113            size=(170, 60),
114            label=bui.Lstr(resource='cancelText'),
115            scale=0.9,
116            on_activate_call=self.main_window_back,
117        )
118        save_button = bui.buttonwidget(
119            parent=self._root_widget,
120            autoselect=True,
121            position=(self._width - 190, self._height - 85),
122            size=(180, 60),
123            label=bui.Lstr(resource='saveText'),
124            scale=0.9,
125            text_scale=0.9,
126            on_activate_call=self._save,
127        )
128        bui.containerwidget(
129            edit=self._root_widget,
130            cancel_button=cancel_button,
131            start_button=save_button,
132        )
133
134        v = self._height - 74.0
135        bui.textwidget(
136            parent=self._root_widget,
137            position=(self._width * 0.5, v + 15),
138            size=(0, 0),
139            text=bui.Lstr(
140                resource=f'{self._r}.configuringText',
141                subs=[('${DEVICE}', self._displayname)],
142            ),
143            color=bui.app.ui_v1.title_color,
144            h_align='center',
145            v_align='center',
146            maxwidth=270,
147            scale=0.83,
148        )
149        v -= 20
150
151        if self._unique_id != '#1':
152            v -= 20
153            v -= self._spacing
154            bui.textwidget(
155                parent=self._root_widget,
156                position=(0, v + 19),
157                size=(self._width, 50),
158                text=bui.Lstr(resource=f'{self._r}.keyboard2NoteText'),
159                scale=0.7,
160                maxwidth=self._width * 0.75,
161                max_height=110,
162                color=bui.app.ui_v1.infotextcolor,
163                h_align='center',
164                v_align='top',
165            )
166            v -= 40
167        v -= 10
168        v -= self._spacing * 2.2
169        v += 25
170        v -= 42
171        h_offs = 160
172        dist = 70
173        d_color = (0.4, 0.4, 0.8)
174        self._capture_button(
175            pos=(h_offs, v + 0.95 * dist),
176            color=d_color,
177            button='buttonUp',
178            texture=bui.gettexture('upButton'),
179            scale=1.0,
180        )
181        self._capture_button(
182            pos=(h_offs - 1.2 * dist, v),
183            color=d_color,
184            button='buttonLeft',
185            texture=bui.gettexture('leftButton'),
186            scale=1.0,
187        )
188        self._capture_button(
189            pos=(h_offs + 1.2 * dist, v),
190            color=d_color,
191            button='buttonRight',
192            texture=bui.gettexture('rightButton'),
193            scale=1.0,
194        )
195        self._capture_button(
196            pos=(h_offs, v - 0.95 * dist),
197            color=d_color,
198            button='buttonDown',
199            texture=bui.gettexture('downButton'),
200            scale=1.0,
201        )
202
203        if self._unique_id == '#2':
204            self._capture_button(
205                pos=(self._width * 0.5, v + 0.1 * dist),
206                color=(0.4, 0.4, 0.6),
207                button='buttonStart',
208                texture=bui.gettexture('startButton'),
209                scale=0.8,
210            )
211
212        h_offs = self._width - 160
213
214        self._capture_button(
215            pos=(h_offs, v + 0.95 * dist),
216            color=(0.6, 0.4, 0.8),
217            button='buttonPickUp',
218            texture=bui.gettexture('buttonPickUp'),
219            scale=1.0,
220        )
221        self._capture_button(
222            pos=(h_offs - 1.2 * dist, v),
223            color=(0.7, 0.5, 0.1),
224            button='buttonPunch',
225            texture=bui.gettexture('buttonPunch'),
226            scale=1.0,
227        )
228        self._capture_button(
229            pos=(h_offs + 1.2 * dist, v),
230            color=(0.5, 0.2, 0.1),
231            button='buttonBomb',
232            texture=bui.gettexture('buttonBomb'),
233            scale=1.0,
234        )
235        self._capture_button(
236            pos=(h_offs, v - 0.95 * dist),
237            color=(0.2, 0.5, 0.2),
238            button='buttonJump',
239            texture=bui.gettexture('buttonJump'),
240            scale=1.0,
241        )
242
243        self._more_button = bui.buttonwidget(
244            parent=self._root_widget,
245            autoselect=True,
246            label='...',
247            text_scale=0.9,
248            color=(0.45, 0.4, 0.5),
249            textcolor=(0.65, 0.6, 0.7),
250            position=(self._width * 0.5 - 65, 30),
251            size=(130, 40),
252            on_activate_call=self._do_more,
253        )
254
255        if is_reset:
256            bui.containerwidget(
257                edit=self._root_widget,
258                selected_child=self._more_button,
259            )
260
261    def _pretty_button_name(self, button_name: str) -> bui.Lstr:
262        button_id = self._settings[button_name]
263        if button_id == -1:
264            return bs.Lstr(resource='configGamepadWindow.unsetText')
265        return self._input.get_button_name(button_id)
266
267    def _capture_button(
268        self,
269        pos: tuple[float, float],
270        color: tuple[float, float, float],
271        texture: bui.Texture,
272        button: str,
273        scale: float = 1.0,
274    ) -> None:
275        # pylint: disable=too-many-positional-arguments
276        base_size = 79
277        btn = bui.buttonwidget(
278            parent=self._root_widget,
279            autoselect=True,
280            position=(
281                pos[0] - base_size * 0.5 * scale,
282                pos[1] - base_size * 0.5 * scale,
283            ),
284            size=(base_size * scale, base_size * scale),
285            texture=texture,
286            label='',
287            color=color,
288        )
289
290        # Do this deferred so it shows up on top of other buttons. (ew.)
291        def doit() -> None:
292            if not self._root_widget:
293                return
294            uiscale = 0.66 * scale * 2.0
295            maxwidth = 76.0 * scale
296            txt = bui.textwidget(
297                parent=self._root_widget,
298                position=(pos[0] + 0.0 * scale, pos[1] - (57.0 - 18.0) * scale),
299                color=(1, 1, 1, 0.3),
300                size=(0, 0),
301                h_align='center',
302                v_align='top',
303                scale=uiscale,
304                maxwidth=maxwidth,
305                text=self._pretty_button_name(button),
306            )
307            bui.buttonwidget(
308                edit=btn,
309                autoselect=True,
310                on_activate_call=bui.Call(
311                    AwaitKeyboardInputWindow, button, txt, self._settings
312                ),
313            )
314
315        bui.pushcall(doit)
316
317    def _reset(self) -> None:
318        from bauiv1lib.confirm import ConfirmWindow
319
320        assert bui.app.classic is not None
321
322        # efro note: I think it's ok to reset without a confirm here
323        # because the user can see pretty clearly what changes and can
324        # cancel out of the keyboard settings edit if they want.
325        if bool(False):
326            ConfirmWindow(
327                # TODO: Implement a translation string for this!
328                'Are you sure you want to reset your button mapping?',
329                self._do_reset,
330                width=480,
331                height=95,
332            )
333        else:
334            self._do_reset()
335
336    def _do_reset(self) -> None:
337        """Resets the input's mapping settings."""
338        self._settings = {}
339        self._get_config_mapping(default=True)
340        self._rebuild_ui(is_reset=True)
341        bui.getsound('gunCocking').play()
342
343    def _do_more(self) -> None:
344        """Show a burger menu with extra settings."""
345        # pylint: disable=cyclic-import
346        choices: list[str] = [
347            'reset',
348        ]
349        choices_display: list[bui.Lstr] = [
350            bui.Lstr(resource='settingsWindowAdvanced.resetText'),
351        ]
352
353        uiscale = bui.app.ui_v1.uiscale
354        PopupMenuWindow(
355            position=self._more_button.get_screen_space_center(),
356            scale=(
357                2.3
358                if uiscale is bui.UIScale.SMALL
359                else 1.65 if uiscale is bui.UIScale.MEDIUM else 1.23
360            ),
361            width=150,
362            choices=choices,
363            choices_display=choices_display,
364            current_choice='reset',
365            delegate=self,
366        )
367
368    def popup_menu_selected_choice(
369        self, popup_window: PopupMenuWindow, choice: str
370    ) -> None:
371        """Called when a choice is selected in the popup."""
372        del popup_window  # unused
373        if choice == 'reset':
374            self._reset()
375        else:
376            print(f'invalid choice: {choice}')
377
378    def popup_menu_closing(self, popup_window: PopupWindow) -> None:
379        """Called when the popup is closing."""
380
381    def _save(self) -> None:
382
383        # no-op if we're not in control.
384        if not self.main_window_has_control():
385            return
386
387        assert bui.app.classic is not None
388        bui.getsound('gunCocking').play()
389
390        # There's a chance the device disappeared; handle that
391        # gracefully.
392        if not self._input:
393            return
394
395        dst = bui.app.classic.get_input_device_config(
396            self._input, default=False
397        )
398        dst2: dict[str, Any] = dst[0][dst[1]]
399        dst2.clear()
400
401        # Store any values that aren't -1.
402        for key, val in list(self._settings.items()):
403            if val != -1:
404                dst2[key] = val
405
406        # Send this config to the master-server so we can generate more
407        # defaults in the future.
408        if bui.app.classic is not None:
409            bui.app.classic.master_server_v1_post(
410                'controllerConfig',
411                {
412                    'ua': bui.app.classic.legacy_user_agent_string,
413                    'name': self._name,
414                    'b': bui.app.env.engine_build_number,
415                    'config': dst2,
416                    'v': 2,
417                },
418            )
419        bui.app.config.apply_and_commit()
420
421        self.main_window_back()

Window for configuring keyboards.

ConfigKeyboardWindow( c: _bascenev1.InputDevice, transition: str | None = 'in_right', origin_widget: _bauiv1.Widget | None = None)
23    def __init__(
24        self,
25        c: bs.InputDevice,
26        transition: str | None = 'in_right',
27        origin_widget: bui.Widget | None = None,
28    ):
29        self._r = 'configKeyboardWindow'
30        self._input = c
31        self._name = self._input.name
32        self._unique_id = self._input.unique_identifier
33        dname_raw = self._name
34        if self._unique_id != '#1':
35            dname_raw += ' ' + self._unique_id.replace('#', 'P')
36        self._displayname = bui.Lstr(translate=('inputDeviceNames', dname_raw))
37        self._width = 700
38        if self._unique_id != '#1':
39            self._height = 480
40        else:
41            self._height = 375
42        self._spacing = 40
43        assert bui.app.classic is not None
44        uiscale = bui.app.ui_v1.uiscale
45        super().__init__(
46            root_widget=bui.containerwidget(
47                size=(self._width, self._height),
48                scale=(
49                    1.4
50                    if uiscale is bui.UIScale.SMALL
51                    else 1.3 if uiscale is bui.UIScale.MEDIUM else 1.0
52                ),
53                stack_offset=(0, 5) if uiscale is bui.UIScale.SMALL else (0, 0),
54                transition=transition,
55            ),
56            transition=transition,
57            origin_widget=origin_widget,
58        )
59
60        self._settings: dict[str, int] = {}
61        self._get_config_mapping()
62
63        self._rebuild_ui()

Create a MainWindow given a root widget and transition info.

Automatically handles in and out transitions on the provided widget, so there is no need to set transitions when creating it.

@override
def get_main_window_state(self) -> bauiv1.MainWindowState:
65    @override
66    def get_main_window_state(self) -> bui.MainWindowState:
67        # Support recreating our window for back/refresh purposes.
68        cls = type(self)
69
70        # Pull things from self here; if we do it within the lambda
71        # we'll keep self alive which is bad.
72        inputdevice = self._input
73
74        return bui.BasicMainWindowState(
75            create_call=lambda transition, origin_widget: cls(
76                transition=transition,
77                origin_widget=origin_widget,
78                c=inputdevice,
79            )
80        )

Return a WindowState to recreate this window, if supported.

def popup_menu_selected_choice(self, popup_window: bauiv1lib.popup.PopupMenuWindow, choice: str) -> None:
368    def popup_menu_selected_choice(
369        self, popup_window: PopupMenuWindow, choice: str
370    ) -> None:
371        """Called when a choice is selected in the popup."""
372        del popup_window  # unused
373        if choice == 'reset':
374            self._reset()
375        else:
376            print(f'invalid choice: {choice}')

Called when a choice is selected in the popup.

def popup_menu_closing(self, popup_window: bauiv1lib.popup.PopupWindow) -> None:
378    def popup_menu_closing(self, popup_window: PopupWindow) -> None:
379        """Called when the popup is closing."""

Called when the popup is closing.

class AwaitKeyboardInputWindow(bauiv1._uitypes.Window):
424class AwaitKeyboardInputWindow(bui.Window):
425    """Window for capturing a keypress."""
426
427    def __init__(self, button: str, ui: bui.Widget, settings: dict):
428        self._capture_button = button
429        self._capture_key_ui = ui
430        self._settings = settings
431
432        width = 400
433        height = 150
434        assert bui.app.classic is not None
435        uiscale = bui.app.ui_v1.uiscale
436        super().__init__(
437            root_widget=bui.containerwidget(
438                size=(width, height),
439                transition='in_right',
440                scale=(
441                    2.0
442                    if uiscale is bui.UIScale.SMALL
443                    else 1.5 if uiscale is bui.UIScale.MEDIUM else 1.0
444                ),
445            )
446        )
447        bui.textwidget(
448            parent=self._root_widget,
449            position=(0, height - 60),
450            size=(width, 25),
451            text=bui.Lstr(resource='pressAnyKeyText'),
452            h_align='center',
453            v_align='top',
454        )
455
456        self._counter = 5
457        self._count_down_text = bui.textwidget(
458            parent=self._root_widget,
459            h_align='center',
460            position=(0, height - 110),
461            size=(width, 25),
462            color=(1, 1, 1, 0.3),
463            text=str(self._counter),
464        )
465        self._decrement_timer: bui.AppTimer | None = bui.AppTimer(
466            1.0, self._decrement, repeat=True
467        )
468        bs.capture_keyboard_input(bui.WeakCall(self._button_callback))
469
470    def __del__(self) -> None:
471        bs.release_keyboard_input()
472
473    def _die(self) -> None:
474        # This strong-refs us; killing it allows us to die now.
475        self._decrement_timer = None
476        if self._root_widget:
477            bui.containerwidget(edit=self._root_widget, transition='out_left')
478
479    def _button_callback(self, event: dict[str, Any]) -> None:
480        self._settings[self._capture_button] = event['button']
481        if event['type'] == 'BUTTONDOWN':
482            bname = event['input_device'].get_button_name(event['button'])
483            bui.textwidget(edit=self._capture_key_ui, text=bname)
484            bui.getsound('gunCocking').play()
485            self._die()
486
487    def _decrement(self) -> None:
488        self._counter -= 1
489        if self._counter >= 1:
490            bui.textwidget(edit=self._count_down_text, text=str(self._counter))
491        else:
492            self._die()

Window for capturing a keypress.

AwaitKeyboardInputWindow(button: str, ui: _bauiv1.Widget, settings: dict)
427    def __init__(self, button: str, ui: bui.Widget, settings: dict):
428        self._capture_button = button
429        self._capture_key_ui = ui
430        self._settings = settings
431
432        width = 400
433        height = 150
434        assert bui.app.classic is not None
435        uiscale = bui.app.ui_v1.uiscale
436        super().__init__(
437            root_widget=bui.containerwidget(
438                size=(width, height),
439                transition='in_right',
440                scale=(
441                    2.0
442                    if uiscale is bui.UIScale.SMALL
443                    else 1.5 if uiscale is bui.UIScale.MEDIUM else 1.0
444                ),
445            )
446        )
447        bui.textwidget(
448            parent=self._root_widget,
449            position=(0, height - 60),
450            size=(width, 25),
451            text=bui.Lstr(resource='pressAnyKeyText'),
452            h_align='center',
453            v_align='top',
454        )
455
456        self._counter = 5
457        self._count_down_text = bui.textwidget(
458            parent=self._root_widget,
459            h_align='center',
460            position=(0, height - 110),
461            size=(width, 25),
462            color=(1, 1, 1, 0.3),
463            text=str(self._counter),
464        )
465        self._decrement_timer: bui.AppTimer | None = bui.AppTimer(
466            1.0, self._decrement, repeat=True
467        )
468        bs.capture_keyboard_input(bui.WeakCall(self._button_callback))