bauiv1lib.playlist.editgame

Provides UI for editing a game config.

  1# Released under the MIT License. See LICENSE for details.
  2#
  3"""Provides UI for editing a game config."""
  4
  5from __future__ import annotations
  6
  7import copy
  8import random
  9import logging
 10from typing import TYPE_CHECKING, cast, override
 11
 12import bascenev1 as bs
 13import bauiv1 as bui
 14
 15if TYPE_CHECKING:
 16    from typing import Any, Callable
 17
 18
 19class PlaylistEditGameWindow(bui.MainWindow):
 20    """Window for editing a game config."""
 21
 22    def __init__(
 23        self,
 24        gametype: type[bs.GameActivity],
 25        sessiontype: type[bs.Session],
 26        config: dict[str, Any] | None,
 27        completion_call: Callable[[dict[str, Any] | None, bui.MainWindow], Any],
 28        default_selection: str | None = None,
 29        transition: str | None = 'in_right',
 30        origin_widget: bui.Widget | None = None,
 31        edit_info: dict[str, Any] | None = None,
 32    ):
 33        # pylint: disable=too-many-branches
 34        # pylint: disable=too-many-positional-arguments
 35        # pylint: disable=too-many-statements
 36        # pylint: disable=too-many-locals
 37        from bascenev1 import (
 38            get_filtered_map_name,
 39            get_map_class,
 40            get_map_display_string,
 41        )
 42
 43        assert bui.app.classic is not None
 44        store = bui.app.classic.store
 45
 46        self._gametype = gametype
 47        self._sessiontype = sessiontype
 48
 49        # If we're within an editing session we get passed edit_info
 50        # (returning from map selection window, etc).
 51        if edit_info is not None:
 52            self._edit_info = edit_info
 53
 54        # ..otherwise determine whether we're adding or editing a game based
 55        # on whether an existing config was passed to us.
 56        else:
 57            if config is None:
 58                self._edit_info = {'editType': 'add'}
 59            else:
 60                self._edit_info = {'editType': 'edit'}
 61
 62        self._r = 'gameSettingsWindow'
 63
 64        valid_maps = gametype.get_supported_maps(sessiontype)
 65        if not valid_maps:
 66            bui.screenmessage(bui.Lstr(resource='noValidMapsErrorText'))
 67            raise RuntimeError('No valid maps found.')
 68
 69        self._config = config
 70        self._settings_defs = gametype.get_available_settings(sessiontype)
 71        self._completion_call = completion_call
 72
 73        # To start with, pick a random map out of the ones we own.
 74        unowned_maps = store.get_unowned_maps()
 75        valid_maps_owned = [m for m in valid_maps if m not in unowned_maps]
 76        if valid_maps_owned:
 77            self._map = valid_maps[random.randrange(len(valid_maps_owned))]
 78
 79        # Hmmm.. we own none of these maps.. just pick a random un-owned one
 80        # I guess.. should this ever happen?
 81        else:
 82            self._map = valid_maps[random.randrange(len(valid_maps))]
 83
 84        is_add = self._edit_info['editType'] == 'add'
 85
 86        # If there's a valid map name in the existing config, use that.
 87        try:
 88            if (
 89                config is not None
 90                and 'settings' in config
 91                and 'map' in config['settings']
 92            ):
 93                filtered_map_name = get_filtered_map_name(
 94                    config['settings']['map']
 95                )
 96                if filtered_map_name in valid_maps:
 97                    self._map = filtered_map_name
 98        except Exception:
 99            logging.exception('Error getting map for editor.')
100
101        if config is not None and 'settings' in config:
102            self._settings = config['settings']
103        else:
104            self._settings = {}
105
106        self._choice_selections: dict[str, int] = {}
107
108        uiscale = bui.app.ui_v1.uiscale
109        width = 820 if uiscale is bui.UIScale.SMALL else 620
110        x_inset = 100 if uiscale is bui.UIScale.SMALL else 0
111        height = (
112            400
113            if uiscale is bui.UIScale.SMALL
114            else 460 if uiscale is bui.UIScale.MEDIUM else 550
115        )
116        spacing = 52
117        y_extra = 15
118        y_extra2 = 21
119        yoffs = -30 if uiscale is bui.UIScale.SMALL else 0
120
121        map_tex_name = get_map_class(self._map).get_preview_texture_name()
122        if map_tex_name is None:
123            raise RuntimeError(f'No map preview tex found for {self._map}.')
124        map_tex = bui.gettexture(map_tex_name)
125
126        top_extra = 20 if uiscale is bui.UIScale.SMALL else 0
127        super().__init__(
128            root_widget=bui.containerwidget(
129                size=(width, height + top_extra),
130                scale=(
131                    1.95
132                    if uiscale is bui.UIScale.SMALL
133                    else 1.35 if uiscale is bui.UIScale.MEDIUM else 1.0
134                ),
135                stack_offset=(
136                    (0, 0) if uiscale is bui.UIScale.SMALL else (0, 0)
137                ),
138            ),
139            transition=transition,
140            origin_widget=origin_widget,
141        )
142
143        btn = bui.buttonwidget(
144            parent=self._root_widget,
145            position=(45 + x_inset, height - 82 + y_extra2 + yoffs),
146            size=(60, 48) if is_add else (180, 65),
147            label=(
148                bui.charstr(bui.SpecialChar.BACK)
149                if is_add
150                else bui.Lstr(resource='cancelText')
151            ),
152            button_type='backSmall' if is_add else None,
153            autoselect=True,
154            scale=1.0 if is_add else 0.75,
155            text_scale=1.3,
156            on_activate_call=bui.Call(self._cancel),
157        )
158        bui.containerwidget(edit=self._root_widget, cancel_button=btn)
159
160        add_button = bui.buttonwidget(
161            parent=self._root_widget,
162            position=(width - (193 + x_inset), height - 82 + y_extra2 + yoffs),
163            size=(200, 65),
164            scale=0.75,
165            text_scale=1.3,
166            label=(
167                bui.Lstr(resource=f'{self._r}.addGameText')
168                if is_add
169                else bui.Lstr(resource='applyText')
170            ),
171        )
172
173        pbtn = bui.get_special_widget('squad_button')
174        bui.widget(edit=add_button, right_widget=pbtn, up_widget=pbtn)
175
176        bui.textwidget(
177            parent=self._root_widget,
178            position=(-8, height - 70 + y_extra2 + yoffs),
179            size=(width, 25),
180            text=gametype.get_display_string(),
181            color=bui.app.ui_v1.title_color,
182            maxwidth=235,
183            scale=1.1,
184            h_align='center',
185            v_align='center',
186        )
187
188        map_height = 100
189
190        scroll_height = map_height + 10  # map select and margin
191
192        # Calc our total height we'll need
193        scroll_height += spacing * len(self._settings_defs)
194
195        scroll_width = width - (86 + 2 * x_inset)
196        self._scrollwidget = bui.scrollwidget(
197            parent=self._root_widget,
198            position=(
199                44 + x_inset,
200                (80 if uiscale is bui.UIScale.SMALL else 35) + y_extra + yoffs,
201            ),
202            size=(
203                scroll_width,
204                height - (166 if uiscale is bui.UIScale.SMALL else 116),
205            ),
206            highlight=False,
207            claims_left_right=True,
208            selection_loops_to_parent=True,
209            border_opacity=0.4,
210        )
211        self._subcontainer = bui.containerwidget(
212            parent=self._scrollwidget,
213            size=(scroll_width, scroll_height),
214            background=False,
215            claims_left_right=True,
216            selection_loops_to_parent=True,
217        )
218
219        v = scroll_height - 5
220        h = -40
221
222        # Keep track of all the selectable widgets we make so we can wire
223        # them up conveniently.
224        widget_column: list[list[bui.Widget]] = []
225
226        # Map select button.
227        bui.textwidget(
228            parent=self._subcontainer,
229            position=(h + 49, v - 63),
230            size=(100, 30),
231            maxwidth=110,
232            text=bui.Lstr(resource='mapText'),
233            h_align='left',
234            color=(0.8, 0.8, 0.8, 1.0),
235            v_align='center',
236        )
237
238        bui.imagewidget(
239            parent=self._subcontainer,
240            size=(256 * 0.7, 125 * 0.7),
241            position=(h + 261 - 128 + 128.0 * 0.56, v - 90),
242            texture=map_tex,
243            mesh_opaque=bui.getmesh('level_select_button_opaque'),
244            mesh_transparent=bui.getmesh('level_select_button_transparent'),
245            mask_texture=bui.gettexture('mapPreviewMask'),
246        )
247        map_button = btn = bui.buttonwidget(
248            parent=self._subcontainer,
249            size=(140, 60),
250            position=(h + 448, v - 72),
251            on_activate_call=bui.Call(self._select_map),
252            scale=0.7,
253            label=bui.Lstr(resource='mapSelectText'),
254        )
255        widget_column.append([btn])
256
257        bui.textwidget(
258            parent=self._subcontainer,
259            position=(h + 363 - 123, v - 114),
260            size=(100, 30),
261            flatness=1.0,
262            shadow=1.0,
263            scale=0.55,
264            maxwidth=256 * 0.7 * 0.8,
265            text=get_map_display_string(self._map),
266            h_align='center',
267            color=(0.6, 1.0, 0.6, 1.0),
268            v_align='center',
269        )
270        v -= map_height
271
272        for setting in self._settings_defs:
273            value = setting.default
274            value_type = type(value)
275
276            # Now, if there's an existing value for it in the config,
277            # override with that.
278            try:
279                if (
280                    config is not None
281                    and 'settings' in config
282                    and setting.name in config['settings']
283                ):
284                    value = value_type(config['settings'][setting.name])
285            except Exception:
286                logging.exception('Error getting game setting.')
287
288            # Shove the starting value in there to start.
289            self._settings[setting.name] = value
290
291            name_translated = self._get_localized_setting_name(setting.name)
292
293            mw1 = 280
294            mw2 = 70
295
296            # Handle types with choices specially:
297            if isinstance(setting, bs.ChoiceSetting):
298                for choice in setting.choices:
299                    if len(choice) != 2:
300                        raise ValueError(
301                            "Expected 2-member tuples for 'choices'; got: "
302                            + repr(choice)
303                        )
304                    if not isinstance(choice[0], str):
305                        raise TypeError(
306                            'First value for choice tuple must be a str; got: '
307                            + repr(choice)
308                        )
309                    if not isinstance(choice[1], value_type):
310                        raise TypeError(
311                            'Choice type does not match default value; choice:'
312                            + repr(choice)
313                            + '; setting:'
314                            + repr(setting)
315                        )
316                if value_type not in (int, float):
317                    raise TypeError(
318                        'Choice type setting must have int or float default; '
319                        'got: ' + repr(setting)
320                    )
321
322                # Start at the choice corresponding to the default if possible.
323                self._choice_selections[setting.name] = 0
324                for index, choice in enumerate(setting.choices):
325                    if choice[1] == value:
326                        self._choice_selections[setting.name] = index
327                        break
328
329                v -= spacing
330                bui.textwidget(
331                    parent=self._subcontainer,
332                    position=(h + 50, v),
333                    size=(100, 30),
334                    maxwidth=mw1,
335                    text=name_translated,
336                    h_align='left',
337                    color=(0.8, 0.8, 0.8, 1.0),
338                    v_align='center',
339                )
340                txt = bui.textwidget(
341                    parent=self._subcontainer,
342                    position=(h + 509 - 95, v),
343                    size=(0, 28),
344                    text=self._get_localized_setting_name(
345                        setting.choices[self._choice_selections[setting.name]][
346                            0
347                        ]
348                    ),
349                    editable=False,
350                    color=(0.6, 1.0, 0.6, 1.0),
351                    maxwidth=mw2,
352                    h_align='right',
353                    v_align='center',
354                    padding=2,
355                )
356                btn1 = bui.buttonwidget(
357                    parent=self._subcontainer,
358                    position=(h + 509 - 50 - 1, v),
359                    size=(28, 28),
360                    label='<',
361                    autoselect=True,
362                    on_activate_call=bui.Call(
363                        self._choice_inc, setting.name, txt, setting, -1
364                    ),
365                    repeat=True,
366                )
367                btn2 = bui.buttonwidget(
368                    parent=self._subcontainer,
369                    position=(h + 509 + 5, v),
370                    size=(28, 28),
371                    label='>',
372                    autoselect=True,
373                    on_activate_call=bui.Call(
374                        self._choice_inc, setting.name, txt, setting, 1
375                    ),
376                    repeat=True,
377                )
378                widget_column.append([btn1, btn2])
379
380            elif isinstance(setting, (bs.IntSetting, bs.FloatSetting)):
381                v -= spacing
382                min_value = setting.min_value
383                max_value = setting.max_value
384                increment = setting.increment
385                bui.textwidget(
386                    parent=self._subcontainer,
387                    position=(h + 50, v),
388                    size=(100, 30),
389                    text=name_translated,
390                    h_align='left',
391                    color=(0.8, 0.8, 0.8, 1.0),
392                    v_align='center',
393                    maxwidth=mw1,
394                )
395                txt = bui.textwidget(
396                    parent=self._subcontainer,
397                    position=(h + 509 - 95, v),
398                    size=(0, 28),
399                    text=str(value),
400                    editable=False,
401                    color=(0.6, 1.0, 0.6, 1.0),
402                    maxwidth=mw2,
403                    h_align='right',
404                    v_align='center',
405                    padding=2,
406                )
407                btn1 = bui.buttonwidget(
408                    parent=self._subcontainer,
409                    position=(h + 509 - 50 - 1, v),
410                    size=(28, 28),
411                    label='-',
412                    autoselect=True,
413                    on_activate_call=bui.Call(
414                        self._inc,
415                        txt,
416                        min_value,
417                        max_value,
418                        -increment,
419                        value_type,
420                        setting.name,
421                    ),
422                    repeat=True,
423                )
424                btn2 = bui.buttonwidget(
425                    parent=self._subcontainer,
426                    position=(h + 509 + 5, v),
427                    size=(28, 28),
428                    label='+',
429                    autoselect=True,
430                    on_activate_call=bui.Call(
431                        self._inc,
432                        txt,
433                        min_value,
434                        max_value,
435                        increment,
436                        value_type,
437                        setting.name,
438                    ),
439                    repeat=True,
440                )
441                widget_column.append([btn1, btn2])
442
443            elif value_type == bool:
444                v -= spacing
445                bui.textwidget(
446                    parent=self._subcontainer,
447                    position=(h + 50, v),
448                    size=(100, 30),
449                    text=name_translated,
450                    h_align='left',
451                    color=(0.8, 0.8, 0.8, 1.0),
452                    v_align='center',
453                    maxwidth=mw1,
454                )
455                txt = bui.textwidget(
456                    parent=self._subcontainer,
457                    position=(h + 509 - 95, v),
458                    size=(0, 28),
459                    text=(
460                        bui.Lstr(resource='onText')
461                        if value
462                        else bui.Lstr(resource='offText')
463                    ),
464                    editable=False,
465                    color=(0.6, 1.0, 0.6, 1.0),
466                    maxwidth=mw2,
467                    h_align='right',
468                    v_align='center',
469                    padding=2,
470                )
471                cbw = bui.checkboxwidget(
472                    parent=self._subcontainer,
473                    text='',
474                    position=(h + 505 - 50 - 5, v - 2),
475                    size=(200, 30),
476                    autoselect=True,
477                    textcolor=(0.8, 0.8, 0.8),
478                    value=value,
479                    on_value_change_call=bui.Call(
480                        self._check_value_change, setting.name, txt
481                    ),
482                )
483                widget_column.append([cbw])
484
485            else:
486                raise TypeError(f'Invalid value type: {value_type}.')
487
488        # Ok now wire up the column.
489        try:
490            prev_widgets: list[bui.Widget] | None = None
491            for cwdg in widget_column:
492                if prev_widgets is not None:
493                    # Wire our rightmost to their rightmost.
494                    bui.widget(edit=prev_widgets[-1], down_widget=cwdg[-1])
495                    bui.widget(edit=cwdg[-1], up_widget=prev_widgets[-1])
496
497                    # Wire our leftmost to their leftmost.
498                    bui.widget(edit=prev_widgets[0], down_widget=cwdg[0])
499                    bui.widget(edit=cwdg[0], up_widget=prev_widgets[0])
500                prev_widgets = cwdg
501        except Exception:
502            logging.exception(
503                'Error wiring up game-settings-select widget column.'
504            )
505
506        bui.buttonwidget(edit=add_button, on_activate_call=bui.Call(self._add))
507        bui.containerwidget(
508            edit=self._root_widget,
509            selected_child=add_button,
510            start_button=add_button,
511        )
512
513        if default_selection == 'map':
514            bui.containerwidget(
515                edit=self._root_widget, selected_child=self._scrollwidget
516            )
517            bui.containerwidget(
518                edit=self._subcontainer, selected_child=map_button
519            )
520
521    @override
522    def get_main_window_state(self) -> bui.MainWindowState:
523        # Support recreating our window for back/refresh purposes.
524        cls = type(self)
525
526        # Pull things out of self here so we don't refer to self in the
527        # lambda below which would keep us alive.
528        gametype = self._gametype
529        sessiontype = self._sessiontype
530        config = self._config
531        completion_call = self._completion_call
532
533        return bui.BasicMainWindowState(
534            create_call=lambda transition, origin_widget: cls(
535                transition=transition,
536                origin_widget=origin_widget,
537                gametype=gametype,
538                sessiontype=sessiontype,
539                config=config,
540                completion_call=completion_call,
541            )
542        )
543
544    def _get_localized_setting_name(self, name: str) -> bui.Lstr:
545        return bui.Lstr(translate=('settingNames', name))
546
547    def _select_map(self) -> None:
548        # pylint: disable=cyclic-import
549        from bauiv1lib.playlist.mapselect import PlaylistMapSelectWindow
550
551        # No-op if we're not in control.
552        if not self.main_window_has_control():
553            return
554
555        self._config = self._getconfig()
556
557        # Replace ourself with the map-select UI.
558        self.main_window_replace(
559            PlaylistMapSelectWindow(
560                self._gametype,
561                self._sessiontype,
562                self._config,
563                self._edit_info,
564                self._completion_call,
565            )
566        )
567
568    def _choice_inc(
569        self,
570        setting_name: str,
571        widget: bui.Widget,
572        setting: bs.ChoiceSetting,
573        increment: int,
574    ) -> None:
575        choices = setting.choices
576        if increment > 0:
577            self._choice_selections[setting_name] = min(
578                len(choices) - 1, self._choice_selections[setting_name] + 1
579            )
580        else:
581            self._choice_selections[setting_name] = max(
582                0, self._choice_selections[setting_name] - 1
583            )
584        bui.textwidget(
585            edit=widget,
586            text=self._get_localized_setting_name(
587                choices[self._choice_selections[setting_name]][0]
588            ),
589        )
590        self._settings[setting_name] = choices[
591            self._choice_selections[setting_name]
592        ][1]
593
594    def _cancel(self) -> None:
595        self._completion_call(None, self)
596
597    def _check_value_change(
598        self, setting_name: str, widget: bui.Widget, value: int
599    ) -> None:
600        bui.textwidget(
601            edit=widget,
602            text=(
603                bui.Lstr(resource='onText')
604                if value
605                else bui.Lstr(resource='offText')
606            ),
607        )
608        self._settings[setting_name] = value
609
610    def _getconfig(self) -> dict[str, Any]:
611        settings = copy.deepcopy(self._settings)
612        settings['map'] = self._map
613        return {'settings': settings}
614
615    def _add(self) -> None:
616        self._completion_call(self._getconfig(), self)
617
618    def _inc(
619        self,
620        ctrl: bui.Widget,
621        min_val: int | float,
622        max_val: int | float,
623        increment: int | float,
624        setting_type: type,
625        setting_name: str,
626    ) -> None:
627        # pylint: disable=too-many-positional-arguments
628        if setting_type == float:
629            val = float(cast(str, bui.textwidget(query=ctrl)))
630        else:
631            val = int(cast(str, bui.textwidget(query=ctrl)))
632        val += increment
633        val = max(min_val, min(val, max_val))
634        if setting_type == float:
635            bui.textwidget(edit=ctrl, text=str(round(val, 2)))
636        elif setting_type == int:
637            bui.textwidget(edit=ctrl, text=str(int(val)))
638        else:
639            raise TypeError('invalid vartype: ' + str(setting_type))
640        self._settings[setting_name] = val
class PlaylistEditGameWindow(bauiv1._uitypes.MainWindow):
 20class PlaylistEditGameWindow(bui.MainWindow):
 21    """Window for editing a game config."""
 22
 23    def __init__(
 24        self,
 25        gametype: type[bs.GameActivity],
 26        sessiontype: type[bs.Session],
 27        config: dict[str, Any] | None,
 28        completion_call: Callable[[dict[str, Any] | None, bui.MainWindow], Any],
 29        default_selection: str | None = None,
 30        transition: str | None = 'in_right',
 31        origin_widget: bui.Widget | None = None,
 32        edit_info: dict[str, Any] | None = None,
 33    ):
 34        # pylint: disable=too-many-branches
 35        # pylint: disable=too-many-positional-arguments
 36        # pylint: disable=too-many-statements
 37        # pylint: disable=too-many-locals
 38        from bascenev1 import (
 39            get_filtered_map_name,
 40            get_map_class,
 41            get_map_display_string,
 42        )
 43
 44        assert bui.app.classic is not None
 45        store = bui.app.classic.store
 46
 47        self._gametype = gametype
 48        self._sessiontype = sessiontype
 49
 50        # If we're within an editing session we get passed edit_info
 51        # (returning from map selection window, etc).
 52        if edit_info is not None:
 53            self._edit_info = edit_info
 54
 55        # ..otherwise determine whether we're adding or editing a game based
 56        # on whether an existing config was passed to us.
 57        else:
 58            if config is None:
 59                self._edit_info = {'editType': 'add'}
 60            else:
 61                self._edit_info = {'editType': 'edit'}
 62
 63        self._r = 'gameSettingsWindow'
 64
 65        valid_maps = gametype.get_supported_maps(sessiontype)
 66        if not valid_maps:
 67            bui.screenmessage(bui.Lstr(resource='noValidMapsErrorText'))
 68            raise RuntimeError('No valid maps found.')
 69
 70        self._config = config
 71        self._settings_defs = gametype.get_available_settings(sessiontype)
 72        self._completion_call = completion_call
 73
 74        # To start with, pick a random map out of the ones we own.
 75        unowned_maps = store.get_unowned_maps()
 76        valid_maps_owned = [m for m in valid_maps if m not in unowned_maps]
 77        if valid_maps_owned:
 78            self._map = valid_maps[random.randrange(len(valid_maps_owned))]
 79
 80        # Hmmm.. we own none of these maps.. just pick a random un-owned one
 81        # I guess.. should this ever happen?
 82        else:
 83            self._map = valid_maps[random.randrange(len(valid_maps))]
 84
 85        is_add = self._edit_info['editType'] == 'add'
 86
 87        # If there's a valid map name in the existing config, use that.
 88        try:
 89            if (
 90                config is not None
 91                and 'settings' in config
 92                and 'map' in config['settings']
 93            ):
 94                filtered_map_name = get_filtered_map_name(
 95                    config['settings']['map']
 96                )
 97                if filtered_map_name in valid_maps:
 98                    self._map = filtered_map_name
 99        except Exception:
100            logging.exception('Error getting map for editor.')
101
102        if config is not None and 'settings' in config:
103            self._settings = config['settings']
104        else:
105            self._settings = {}
106
107        self._choice_selections: dict[str, int] = {}
108
109        uiscale = bui.app.ui_v1.uiscale
110        width = 820 if uiscale is bui.UIScale.SMALL else 620
111        x_inset = 100 if uiscale is bui.UIScale.SMALL else 0
112        height = (
113            400
114            if uiscale is bui.UIScale.SMALL
115            else 460 if uiscale is bui.UIScale.MEDIUM else 550
116        )
117        spacing = 52
118        y_extra = 15
119        y_extra2 = 21
120        yoffs = -30 if uiscale is bui.UIScale.SMALL else 0
121
122        map_tex_name = get_map_class(self._map).get_preview_texture_name()
123        if map_tex_name is None:
124            raise RuntimeError(f'No map preview tex found for {self._map}.')
125        map_tex = bui.gettexture(map_tex_name)
126
127        top_extra = 20 if uiscale is bui.UIScale.SMALL else 0
128        super().__init__(
129            root_widget=bui.containerwidget(
130                size=(width, height + top_extra),
131                scale=(
132                    1.95
133                    if uiscale is bui.UIScale.SMALL
134                    else 1.35 if uiscale is bui.UIScale.MEDIUM else 1.0
135                ),
136                stack_offset=(
137                    (0, 0) if uiscale is bui.UIScale.SMALL else (0, 0)
138                ),
139            ),
140            transition=transition,
141            origin_widget=origin_widget,
142        )
143
144        btn = bui.buttonwidget(
145            parent=self._root_widget,
146            position=(45 + x_inset, height - 82 + y_extra2 + yoffs),
147            size=(60, 48) if is_add else (180, 65),
148            label=(
149                bui.charstr(bui.SpecialChar.BACK)
150                if is_add
151                else bui.Lstr(resource='cancelText')
152            ),
153            button_type='backSmall' if is_add else None,
154            autoselect=True,
155            scale=1.0 if is_add else 0.75,
156            text_scale=1.3,
157            on_activate_call=bui.Call(self._cancel),
158        )
159        bui.containerwidget(edit=self._root_widget, cancel_button=btn)
160
161        add_button = bui.buttonwidget(
162            parent=self._root_widget,
163            position=(width - (193 + x_inset), height - 82 + y_extra2 + yoffs),
164            size=(200, 65),
165            scale=0.75,
166            text_scale=1.3,
167            label=(
168                bui.Lstr(resource=f'{self._r}.addGameText')
169                if is_add
170                else bui.Lstr(resource='applyText')
171            ),
172        )
173
174        pbtn = bui.get_special_widget('squad_button')
175        bui.widget(edit=add_button, right_widget=pbtn, up_widget=pbtn)
176
177        bui.textwidget(
178            parent=self._root_widget,
179            position=(-8, height - 70 + y_extra2 + yoffs),
180            size=(width, 25),
181            text=gametype.get_display_string(),
182            color=bui.app.ui_v1.title_color,
183            maxwidth=235,
184            scale=1.1,
185            h_align='center',
186            v_align='center',
187        )
188
189        map_height = 100
190
191        scroll_height = map_height + 10  # map select and margin
192
193        # Calc our total height we'll need
194        scroll_height += spacing * len(self._settings_defs)
195
196        scroll_width = width - (86 + 2 * x_inset)
197        self._scrollwidget = bui.scrollwidget(
198            parent=self._root_widget,
199            position=(
200                44 + x_inset,
201                (80 if uiscale is bui.UIScale.SMALL else 35) + y_extra + yoffs,
202            ),
203            size=(
204                scroll_width,
205                height - (166 if uiscale is bui.UIScale.SMALL else 116),
206            ),
207            highlight=False,
208            claims_left_right=True,
209            selection_loops_to_parent=True,
210            border_opacity=0.4,
211        )
212        self._subcontainer = bui.containerwidget(
213            parent=self._scrollwidget,
214            size=(scroll_width, scroll_height),
215            background=False,
216            claims_left_right=True,
217            selection_loops_to_parent=True,
218        )
219
220        v = scroll_height - 5
221        h = -40
222
223        # Keep track of all the selectable widgets we make so we can wire
224        # them up conveniently.
225        widget_column: list[list[bui.Widget]] = []
226
227        # Map select button.
228        bui.textwidget(
229            parent=self._subcontainer,
230            position=(h + 49, v - 63),
231            size=(100, 30),
232            maxwidth=110,
233            text=bui.Lstr(resource='mapText'),
234            h_align='left',
235            color=(0.8, 0.8, 0.8, 1.0),
236            v_align='center',
237        )
238
239        bui.imagewidget(
240            parent=self._subcontainer,
241            size=(256 * 0.7, 125 * 0.7),
242            position=(h + 261 - 128 + 128.0 * 0.56, v - 90),
243            texture=map_tex,
244            mesh_opaque=bui.getmesh('level_select_button_opaque'),
245            mesh_transparent=bui.getmesh('level_select_button_transparent'),
246            mask_texture=bui.gettexture('mapPreviewMask'),
247        )
248        map_button = btn = bui.buttonwidget(
249            parent=self._subcontainer,
250            size=(140, 60),
251            position=(h + 448, v - 72),
252            on_activate_call=bui.Call(self._select_map),
253            scale=0.7,
254            label=bui.Lstr(resource='mapSelectText'),
255        )
256        widget_column.append([btn])
257
258        bui.textwidget(
259            parent=self._subcontainer,
260            position=(h + 363 - 123, v - 114),
261            size=(100, 30),
262            flatness=1.0,
263            shadow=1.0,
264            scale=0.55,
265            maxwidth=256 * 0.7 * 0.8,
266            text=get_map_display_string(self._map),
267            h_align='center',
268            color=(0.6, 1.0, 0.6, 1.0),
269            v_align='center',
270        )
271        v -= map_height
272
273        for setting in self._settings_defs:
274            value = setting.default
275            value_type = type(value)
276
277            # Now, if there's an existing value for it in the config,
278            # override with that.
279            try:
280                if (
281                    config is not None
282                    and 'settings' in config
283                    and setting.name in config['settings']
284                ):
285                    value = value_type(config['settings'][setting.name])
286            except Exception:
287                logging.exception('Error getting game setting.')
288
289            # Shove the starting value in there to start.
290            self._settings[setting.name] = value
291
292            name_translated = self._get_localized_setting_name(setting.name)
293
294            mw1 = 280
295            mw2 = 70
296
297            # Handle types with choices specially:
298            if isinstance(setting, bs.ChoiceSetting):
299                for choice in setting.choices:
300                    if len(choice) != 2:
301                        raise ValueError(
302                            "Expected 2-member tuples for 'choices'; got: "
303                            + repr(choice)
304                        )
305                    if not isinstance(choice[0], str):
306                        raise TypeError(
307                            'First value for choice tuple must be a str; got: '
308                            + repr(choice)
309                        )
310                    if not isinstance(choice[1], value_type):
311                        raise TypeError(
312                            'Choice type does not match default value; choice:'
313                            + repr(choice)
314                            + '; setting:'
315                            + repr(setting)
316                        )
317                if value_type not in (int, float):
318                    raise TypeError(
319                        'Choice type setting must have int or float default; '
320                        'got: ' + repr(setting)
321                    )
322
323                # Start at the choice corresponding to the default if possible.
324                self._choice_selections[setting.name] = 0
325                for index, choice in enumerate(setting.choices):
326                    if choice[1] == value:
327                        self._choice_selections[setting.name] = index
328                        break
329
330                v -= spacing
331                bui.textwidget(
332                    parent=self._subcontainer,
333                    position=(h + 50, v),
334                    size=(100, 30),
335                    maxwidth=mw1,
336                    text=name_translated,
337                    h_align='left',
338                    color=(0.8, 0.8, 0.8, 1.0),
339                    v_align='center',
340                )
341                txt = bui.textwidget(
342                    parent=self._subcontainer,
343                    position=(h + 509 - 95, v),
344                    size=(0, 28),
345                    text=self._get_localized_setting_name(
346                        setting.choices[self._choice_selections[setting.name]][
347                            0
348                        ]
349                    ),
350                    editable=False,
351                    color=(0.6, 1.0, 0.6, 1.0),
352                    maxwidth=mw2,
353                    h_align='right',
354                    v_align='center',
355                    padding=2,
356                )
357                btn1 = bui.buttonwidget(
358                    parent=self._subcontainer,
359                    position=(h + 509 - 50 - 1, v),
360                    size=(28, 28),
361                    label='<',
362                    autoselect=True,
363                    on_activate_call=bui.Call(
364                        self._choice_inc, setting.name, txt, setting, -1
365                    ),
366                    repeat=True,
367                )
368                btn2 = bui.buttonwidget(
369                    parent=self._subcontainer,
370                    position=(h + 509 + 5, v),
371                    size=(28, 28),
372                    label='>',
373                    autoselect=True,
374                    on_activate_call=bui.Call(
375                        self._choice_inc, setting.name, txt, setting, 1
376                    ),
377                    repeat=True,
378                )
379                widget_column.append([btn1, btn2])
380
381            elif isinstance(setting, (bs.IntSetting, bs.FloatSetting)):
382                v -= spacing
383                min_value = setting.min_value
384                max_value = setting.max_value
385                increment = setting.increment
386                bui.textwidget(
387                    parent=self._subcontainer,
388                    position=(h + 50, v),
389                    size=(100, 30),
390                    text=name_translated,
391                    h_align='left',
392                    color=(0.8, 0.8, 0.8, 1.0),
393                    v_align='center',
394                    maxwidth=mw1,
395                )
396                txt = bui.textwidget(
397                    parent=self._subcontainer,
398                    position=(h + 509 - 95, v),
399                    size=(0, 28),
400                    text=str(value),
401                    editable=False,
402                    color=(0.6, 1.0, 0.6, 1.0),
403                    maxwidth=mw2,
404                    h_align='right',
405                    v_align='center',
406                    padding=2,
407                )
408                btn1 = bui.buttonwidget(
409                    parent=self._subcontainer,
410                    position=(h + 509 - 50 - 1, v),
411                    size=(28, 28),
412                    label='-',
413                    autoselect=True,
414                    on_activate_call=bui.Call(
415                        self._inc,
416                        txt,
417                        min_value,
418                        max_value,
419                        -increment,
420                        value_type,
421                        setting.name,
422                    ),
423                    repeat=True,
424                )
425                btn2 = bui.buttonwidget(
426                    parent=self._subcontainer,
427                    position=(h + 509 + 5, v),
428                    size=(28, 28),
429                    label='+',
430                    autoselect=True,
431                    on_activate_call=bui.Call(
432                        self._inc,
433                        txt,
434                        min_value,
435                        max_value,
436                        increment,
437                        value_type,
438                        setting.name,
439                    ),
440                    repeat=True,
441                )
442                widget_column.append([btn1, btn2])
443
444            elif value_type == bool:
445                v -= spacing
446                bui.textwidget(
447                    parent=self._subcontainer,
448                    position=(h + 50, v),
449                    size=(100, 30),
450                    text=name_translated,
451                    h_align='left',
452                    color=(0.8, 0.8, 0.8, 1.0),
453                    v_align='center',
454                    maxwidth=mw1,
455                )
456                txt = bui.textwidget(
457                    parent=self._subcontainer,
458                    position=(h + 509 - 95, v),
459                    size=(0, 28),
460                    text=(
461                        bui.Lstr(resource='onText')
462                        if value
463                        else bui.Lstr(resource='offText')
464                    ),
465                    editable=False,
466                    color=(0.6, 1.0, 0.6, 1.0),
467                    maxwidth=mw2,
468                    h_align='right',
469                    v_align='center',
470                    padding=2,
471                )
472                cbw = bui.checkboxwidget(
473                    parent=self._subcontainer,
474                    text='',
475                    position=(h + 505 - 50 - 5, v - 2),
476                    size=(200, 30),
477                    autoselect=True,
478                    textcolor=(0.8, 0.8, 0.8),
479                    value=value,
480                    on_value_change_call=bui.Call(
481                        self._check_value_change, setting.name, txt
482                    ),
483                )
484                widget_column.append([cbw])
485
486            else:
487                raise TypeError(f'Invalid value type: {value_type}.')
488
489        # Ok now wire up the column.
490        try:
491            prev_widgets: list[bui.Widget] | None = None
492            for cwdg in widget_column:
493                if prev_widgets is not None:
494                    # Wire our rightmost to their rightmost.
495                    bui.widget(edit=prev_widgets[-1], down_widget=cwdg[-1])
496                    bui.widget(edit=cwdg[-1], up_widget=prev_widgets[-1])
497
498                    # Wire our leftmost to their leftmost.
499                    bui.widget(edit=prev_widgets[0], down_widget=cwdg[0])
500                    bui.widget(edit=cwdg[0], up_widget=prev_widgets[0])
501                prev_widgets = cwdg
502        except Exception:
503            logging.exception(
504                'Error wiring up game-settings-select widget column.'
505            )
506
507        bui.buttonwidget(edit=add_button, on_activate_call=bui.Call(self._add))
508        bui.containerwidget(
509            edit=self._root_widget,
510            selected_child=add_button,
511            start_button=add_button,
512        )
513
514        if default_selection == 'map':
515            bui.containerwidget(
516                edit=self._root_widget, selected_child=self._scrollwidget
517            )
518            bui.containerwidget(
519                edit=self._subcontainer, selected_child=map_button
520            )
521
522    @override
523    def get_main_window_state(self) -> bui.MainWindowState:
524        # Support recreating our window for back/refresh purposes.
525        cls = type(self)
526
527        # Pull things out of self here so we don't refer to self in the
528        # lambda below which would keep us alive.
529        gametype = self._gametype
530        sessiontype = self._sessiontype
531        config = self._config
532        completion_call = self._completion_call
533
534        return bui.BasicMainWindowState(
535            create_call=lambda transition, origin_widget: cls(
536                transition=transition,
537                origin_widget=origin_widget,
538                gametype=gametype,
539                sessiontype=sessiontype,
540                config=config,
541                completion_call=completion_call,
542            )
543        )
544
545    def _get_localized_setting_name(self, name: str) -> bui.Lstr:
546        return bui.Lstr(translate=('settingNames', name))
547
548    def _select_map(self) -> None:
549        # pylint: disable=cyclic-import
550        from bauiv1lib.playlist.mapselect import PlaylistMapSelectWindow
551
552        # No-op if we're not in control.
553        if not self.main_window_has_control():
554            return
555
556        self._config = self._getconfig()
557
558        # Replace ourself with the map-select UI.
559        self.main_window_replace(
560            PlaylistMapSelectWindow(
561                self._gametype,
562                self._sessiontype,
563                self._config,
564                self._edit_info,
565                self._completion_call,
566            )
567        )
568
569    def _choice_inc(
570        self,
571        setting_name: str,
572        widget: bui.Widget,
573        setting: bs.ChoiceSetting,
574        increment: int,
575    ) -> None:
576        choices = setting.choices
577        if increment > 0:
578            self._choice_selections[setting_name] = min(
579                len(choices) - 1, self._choice_selections[setting_name] + 1
580            )
581        else:
582            self._choice_selections[setting_name] = max(
583                0, self._choice_selections[setting_name] - 1
584            )
585        bui.textwidget(
586            edit=widget,
587            text=self._get_localized_setting_name(
588                choices[self._choice_selections[setting_name]][0]
589            ),
590        )
591        self._settings[setting_name] = choices[
592            self._choice_selections[setting_name]
593        ][1]
594
595    def _cancel(self) -> None:
596        self._completion_call(None, self)
597
598    def _check_value_change(
599        self, setting_name: str, widget: bui.Widget, value: int
600    ) -> None:
601        bui.textwidget(
602            edit=widget,
603            text=(
604                bui.Lstr(resource='onText')
605                if value
606                else bui.Lstr(resource='offText')
607            ),
608        )
609        self._settings[setting_name] = value
610
611    def _getconfig(self) -> dict[str, Any]:
612        settings = copy.deepcopy(self._settings)
613        settings['map'] = self._map
614        return {'settings': settings}
615
616    def _add(self) -> None:
617        self._completion_call(self._getconfig(), self)
618
619    def _inc(
620        self,
621        ctrl: bui.Widget,
622        min_val: int | float,
623        max_val: int | float,
624        increment: int | float,
625        setting_type: type,
626        setting_name: str,
627    ) -> None:
628        # pylint: disable=too-many-positional-arguments
629        if setting_type == float:
630            val = float(cast(str, bui.textwidget(query=ctrl)))
631        else:
632            val = int(cast(str, bui.textwidget(query=ctrl)))
633        val += increment
634        val = max(min_val, min(val, max_val))
635        if setting_type == float:
636            bui.textwidget(edit=ctrl, text=str(round(val, 2)))
637        elif setting_type == int:
638            bui.textwidget(edit=ctrl, text=str(int(val)))
639        else:
640            raise TypeError('invalid vartype: ' + str(setting_type))
641        self._settings[setting_name] = val

Window for editing a game config.

PlaylistEditGameWindow( gametype: type[bascenev1.GameActivity], sessiontype: type[bascenev1.Session], config: dict[str, typing.Any] | None, completion_call: Callable[[dict[str, Any] | None, bauiv1.MainWindow], Any], default_selection: str | None = None, transition: str | None = 'in_right', origin_widget: _bauiv1.Widget | None = None, edit_info: dict[str, typing.Any] | None = None)
 23    def __init__(
 24        self,
 25        gametype: type[bs.GameActivity],
 26        sessiontype: type[bs.Session],
 27        config: dict[str, Any] | None,
 28        completion_call: Callable[[dict[str, Any] | None, bui.MainWindow], Any],
 29        default_selection: str | None = None,
 30        transition: str | None = 'in_right',
 31        origin_widget: bui.Widget | None = None,
 32        edit_info: dict[str, Any] | None = None,
 33    ):
 34        # pylint: disable=too-many-branches
 35        # pylint: disable=too-many-positional-arguments
 36        # pylint: disable=too-many-statements
 37        # pylint: disable=too-many-locals
 38        from bascenev1 import (
 39            get_filtered_map_name,
 40            get_map_class,
 41            get_map_display_string,
 42        )
 43
 44        assert bui.app.classic is not None
 45        store = bui.app.classic.store
 46
 47        self._gametype = gametype
 48        self._sessiontype = sessiontype
 49
 50        # If we're within an editing session we get passed edit_info
 51        # (returning from map selection window, etc).
 52        if edit_info is not None:
 53            self._edit_info = edit_info
 54
 55        # ..otherwise determine whether we're adding or editing a game based
 56        # on whether an existing config was passed to us.
 57        else:
 58            if config is None:
 59                self._edit_info = {'editType': 'add'}
 60            else:
 61                self._edit_info = {'editType': 'edit'}
 62
 63        self._r = 'gameSettingsWindow'
 64
 65        valid_maps = gametype.get_supported_maps(sessiontype)
 66        if not valid_maps:
 67            bui.screenmessage(bui.Lstr(resource='noValidMapsErrorText'))
 68            raise RuntimeError('No valid maps found.')
 69
 70        self._config = config
 71        self._settings_defs = gametype.get_available_settings(sessiontype)
 72        self._completion_call = completion_call
 73
 74        # To start with, pick a random map out of the ones we own.
 75        unowned_maps = store.get_unowned_maps()
 76        valid_maps_owned = [m for m in valid_maps if m not in unowned_maps]
 77        if valid_maps_owned:
 78            self._map = valid_maps[random.randrange(len(valid_maps_owned))]
 79
 80        # Hmmm.. we own none of these maps.. just pick a random un-owned one
 81        # I guess.. should this ever happen?
 82        else:
 83            self._map = valid_maps[random.randrange(len(valid_maps))]
 84
 85        is_add = self._edit_info['editType'] == 'add'
 86
 87        # If there's a valid map name in the existing config, use that.
 88        try:
 89            if (
 90                config is not None
 91                and 'settings' in config
 92                and 'map' in config['settings']
 93            ):
 94                filtered_map_name = get_filtered_map_name(
 95                    config['settings']['map']
 96                )
 97                if filtered_map_name in valid_maps:
 98                    self._map = filtered_map_name
 99        except Exception:
100            logging.exception('Error getting map for editor.')
101
102        if config is not None and 'settings' in config:
103            self._settings = config['settings']
104        else:
105            self._settings = {}
106
107        self._choice_selections: dict[str, int] = {}
108
109        uiscale = bui.app.ui_v1.uiscale
110        width = 820 if uiscale is bui.UIScale.SMALL else 620
111        x_inset = 100 if uiscale is bui.UIScale.SMALL else 0
112        height = (
113            400
114            if uiscale is bui.UIScale.SMALL
115            else 460 if uiscale is bui.UIScale.MEDIUM else 550
116        )
117        spacing = 52
118        y_extra = 15
119        y_extra2 = 21
120        yoffs = -30 if uiscale is bui.UIScale.SMALL else 0
121
122        map_tex_name = get_map_class(self._map).get_preview_texture_name()
123        if map_tex_name is None:
124            raise RuntimeError(f'No map preview tex found for {self._map}.')
125        map_tex = bui.gettexture(map_tex_name)
126
127        top_extra = 20 if uiscale is bui.UIScale.SMALL else 0
128        super().__init__(
129            root_widget=bui.containerwidget(
130                size=(width, height + top_extra),
131                scale=(
132                    1.95
133                    if uiscale is bui.UIScale.SMALL
134                    else 1.35 if uiscale is bui.UIScale.MEDIUM else 1.0
135                ),
136                stack_offset=(
137                    (0, 0) if uiscale is bui.UIScale.SMALL else (0, 0)
138                ),
139            ),
140            transition=transition,
141            origin_widget=origin_widget,
142        )
143
144        btn = bui.buttonwidget(
145            parent=self._root_widget,
146            position=(45 + x_inset, height - 82 + y_extra2 + yoffs),
147            size=(60, 48) if is_add else (180, 65),
148            label=(
149                bui.charstr(bui.SpecialChar.BACK)
150                if is_add
151                else bui.Lstr(resource='cancelText')
152            ),
153            button_type='backSmall' if is_add else None,
154            autoselect=True,
155            scale=1.0 if is_add else 0.75,
156            text_scale=1.3,
157            on_activate_call=bui.Call(self._cancel),
158        )
159        bui.containerwidget(edit=self._root_widget, cancel_button=btn)
160
161        add_button = bui.buttonwidget(
162            parent=self._root_widget,
163            position=(width - (193 + x_inset), height - 82 + y_extra2 + yoffs),
164            size=(200, 65),
165            scale=0.75,
166            text_scale=1.3,
167            label=(
168                bui.Lstr(resource=f'{self._r}.addGameText')
169                if is_add
170                else bui.Lstr(resource='applyText')
171            ),
172        )
173
174        pbtn = bui.get_special_widget('squad_button')
175        bui.widget(edit=add_button, right_widget=pbtn, up_widget=pbtn)
176
177        bui.textwidget(
178            parent=self._root_widget,
179            position=(-8, height - 70 + y_extra2 + yoffs),
180            size=(width, 25),
181            text=gametype.get_display_string(),
182            color=bui.app.ui_v1.title_color,
183            maxwidth=235,
184            scale=1.1,
185            h_align='center',
186            v_align='center',
187        )
188
189        map_height = 100
190
191        scroll_height = map_height + 10  # map select and margin
192
193        # Calc our total height we'll need
194        scroll_height += spacing * len(self._settings_defs)
195
196        scroll_width = width - (86 + 2 * x_inset)
197        self._scrollwidget = bui.scrollwidget(
198            parent=self._root_widget,
199            position=(
200                44 + x_inset,
201                (80 if uiscale is bui.UIScale.SMALL else 35) + y_extra + yoffs,
202            ),
203            size=(
204                scroll_width,
205                height - (166 if uiscale is bui.UIScale.SMALL else 116),
206            ),
207            highlight=False,
208            claims_left_right=True,
209            selection_loops_to_parent=True,
210            border_opacity=0.4,
211        )
212        self._subcontainer = bui.containerwidget(
213            parent=self._scrollwidget,
214            size=(scroll_width, scroll_height),
215            background=False,
216            claims_left_right=True,
217            selection_loops_to_parent=True,
218        )
219
220        v = scroll_height - 5
221        h = -40
222
223        # Keep track of all the selectable widgets we make so we can wire
224        # them up conveniently.
225        widget_column: list[list[bui.Widget]] = []
226
227        # Map select button.
228        bui.textwidget(
229            parent=self._subcontainer,
230            position=(h + 49, v - 63),
231            size=(100, 30),
232            maxwidth=110,
233            text=bui.Lstr(resource='mapText'),
234            h_align='left',
235            color=(0.8, 0.8, 0.8, 1.0),
236            v_align='center',
237        )
238
239        bui.imagewidget(
240            parent=self._subcontainer,
241            size=(256 * 0.7, 125 * 0.7),
242            position=(h + 261 - 128 + 128.0 * 0.56, v - 90),
243            texture=map_tex,
244            mesh_opaque=bui.getmesh('level_select_button_opaque'),
245            mesh_transparent=bui.getmesh('level_select_button_transparent'),
246            mask_texture=bui.gettexture('mapPreviewMask'),
247        )
248        map_button = btn = bui.buttonwidget(
249            parent=self._subcontainer,
250            size=(140, 60),
251            position=(h + 448, v - 72),
252            on_activate_call=bui.Call(self._select_map),
253            scale=0.7,
254            label=bui.Lstr(resource='mapSelectText'),
255        )
256        widget_column.append([btn])
257
258        bui.textwidget(
259            parent=self._subcontainer,
260            position=(h + 363 - 123, v - 114),
261            size=(100, 30),
262            flatness=1.0,
263            shadow=1.0,
264            scale=0.55,
265            maxwidth=256 * 0.7 * 0.8,
266            text=get_map_display_string(self._map),
267            h_align='center',
268            color=(0.6, 1.0, 0.6, 1.0),
269            v_align='center',
270        )
271        v -= map_height
272
273        for setting in self._settings_defs:
274            value = setting.default
275            value_type = type(value)
276
277            # Now, if there's an existing value for it in the config,
278            # override with that.
279            try:
280                if (
281                    config is not None
282                    and 'settings' in config
283                    and setting.name in config['settings']
284                ):
285                    value = value_type(config['settings'][setting.name])
286            except Exception:
287                logging.exception('Error getting game setting.')
288
289            # Shove the starting value in there to start.
290            self._settings[setting.name] = value
291
292            name_translated = self._get_localized_setting_name(setting.name)
293
294            mw1 = 280
295            mw2 = 70
296
297            # Handle types with choices specially:
298            if isinstance(setting, bs.ChoiceSetting):
299                for choice in setting.choices:
300                    if len(choice) != 2:
301                        raise ValueError(
302                            "Expected 2-member tuples for 'choices'; got: "
303                            + repr(choice)
304                        )
305                    if not isinstance(choice[0], str):
306                        raise TypeError(
307                            'First value for choice tuple must be a str; got: '
308                            + repr(choice)
309                        )
310                    if not isinstance(choice[1], value_type):
311                        raise TypeError(
312                            'Choice type does not match default value; choice:'
313                            + repr(choice)
314                            + '; setting:'
315                            + repr(setting)
316                        )
317                if value_type not in (int, float):
318                    raise TypeError(
319                        'Choice type setting must have int or float default; '
320                        'got: ' + repr(setting)
321                    )
322
323                # Start at the choice corresponding to the default if possible.
324                self._choice_selections[setting.name] = 0
325                for index, choice in enumerate(setting.choices):
326                    if choice[1] == value:
327                        self._choice_selections[setting.name] = index
328                        break
329
330                v -= spacing
331                bui.textwidget(
332                    parent=self._subcontainer,
333                    position=(h + 50, v),
334                    size=(100, 30),
335                    maxwidth=mw1,
336                    text=name_translated,
337                    h_align='left',
338                    color=(0.8, 0.8, 0.8, 1.0),
339                    v_align='center',
340                )
341                txt = bui.textwidget(
342                    parent=self._subcontainer,
343                    position=(h + 509 - 95, v),
344                    size=(0, 28),
345                    text=self._get_localized_setting_name(
346                        setting.choices[self._choice_selections[setting.name]][
347                            0
348                        ]
349                    ),
350                    editable=False,
351                    color=(0.6, 1.0, 0.6, 1.0),
352                    maxwidth=mw2,
353                    h_align='right',
354                    v_align='center',
355                    padding=2,
356                )
357                btn1 = bui.buttonwidget(
358                    parent=self._subcontainer,
359                    position=(h + 509 - 50 - 1, v),
360                    size=(28, 28),
361                    label='<',
362                    autoselect=True,
363                    on_activate_call=bui.Call(
364                        self._choice_inc, setting.name, txt, setting, -1
365                    ),
366                    repeat=True,
367                )
368                btn2 = bui.buttonwidget(
369                    parent=self._subcontainer,
370                    position=(h + 509 + 5, v),
371                    size=(28, 28),
372                    label='>',
373                    autoselect=True,
374                    on_activate_call=bui.Call(
375                        self._choice_inc, setting.name, txt, setting, 1
376                    ),
377                    repeat=True,
378                )
379                widget_column.append([btn1, btn2])
380
381            elif isinstance(setting, (bs.IntSetting, bs.FloatSetting)):
382                v -= spacing
383                min_value = setting.min_value
384                max_value = setting.max_value
385                increment = setting.increment
386                bui.textwidget(
387                    parent=self._subcontainer,
388                    position=(h + 50, v),
389                    size=(100, 30),
390                    text=name_translated,
391                    h_align='left',
392                    color=(0.8, 0.8, 0.8, 1.0),
393                    v_align='center',
394                    maxwidth=mw1,
395                )
396                txt = bui.textwidget(
397                    parent=self._subcontainer,
398                    position=(h + 509 - 95, v),
399                    size=(0, 28),
400                    text=str(value),
401                    editable=False,
402                    color=(0.6, 1.0, 0.6, 1.0),
403                    maxwidth=mw2,
404                    h_align='right',
405                    v_align='center',
406                    padding=2,
407                )
408                btn1 = bui.buttonwidget(
409                    parent=self._subcontainer,
410                    position=(h + 509 - 50 - 1, v),
411                    size=(28, 28),
412                    label='-',
413                    autoselect=True,
414                    on_activate_call=bui.Call(
415                        self._inc,
416                        txt,
417                        min_value,
418                        max_value,
419                        -increment,
420                        value_type,
421                        setting.name,
422                    ),
423                    repeat=True,
424                )
425                btn2 = bui.buttonwidget(
426                    parent=self._subcontainer,
427                    position=(h + 509 + 5, v),
428                    size=(28, 28),
429                    label='+',
430                    autoselect=True,
431                    on_activate_call=bui.Call(
432                        self._inc,
433                        txt,
434                        min_value,
435                        max_value,
436                        increment,
437                        value_type,
438                        setting.name,
439                    ),
440                    repeat=True,
441                )
442                widget_column.append([btn1, btn2])
443
444            elif value_type == bool:
445                v -= spacing
446                bui.textwidget(
447                    parent=self._subcontainer,
448                    position=(h + 50, v),
449                    size=(100, 30),
450                    text=name_translated,
451                    h_align='left',
452                    color=(0.8, 0.8, 0.8, 1.0),
453                    v_align='center',
454                    maxwidth=mw1,
455                )
456                txt = bui.textwidget(
457                    parent=self._subcontainer,
458                    position=(h + 509 - 95, v),
459                    size=(0, 28),
460                    text=(
461                        bui.Lstr(resource='onText')
462                        if value
463                        else bui.Lstr(resource='offText')
464                    ),
465                    editable=False,
466                    color=(0.6, 1.0, 0.6, 1.0),
467                    maxwidth=mw2,
468                    h_align='right',
469                    v_align='center',
470                    padding=2,
471                )
472                cbw = bui.checkboxwidget(
473                    parent=self._subcontainer,
474                    text='',
475                    position=(h + 505 - 50 - 5, v - 2),
476                    size=(200, 30),
477                    autoselect=True,
478                    textcolor=(0.8, 0.8, 0.8),
479                    value=value,
480                    on_value_change_call=bui.Call(
481                        self._check_value_change, setting.name, txt
482                    ),
483                )
484                widget_column.append([cbw])
485
486            else:
487                raise TypeError(f'Invalid value type: {value_type}.')
488
489        # Ok now wire up the column.
490        try:
491            prev_widgets: list[bui.Widget] | None = None
492            for cwdg in widget_column:
493                if prev_widgets is not None:
494                    # Wire our rightmost to their rightmost.
495                    bui.widget(edit=prev_widgets[-1], down_widget=cwdg[-1])
496                    bui.widget(edit=cwdg[-1], up_widget=prev_widgets[-1])
497
498                    # Wire our leftmost to their leftmost.
499                    bui.widget(edit=prev_widgets[0], down_widget=cwdg[0])
500                    bui.widget(edit=cwdg[0], up_widget=prev_widgets[0])
501                prev_widgets = cwdg
502        except Exception:
503            logging.exception(
504                'Error wiring up game-settings-select widget column.'
505            )
506
507        bui.buttonwidget(edit=add_button, on_activate_call=bui.Call(self._add))
508        bui.containerwidget(
509            edit=self._root_widget,
510            selected_child=add_button,
511            start_button=add_button,
512        )
513
514        if default_selection == 'map':
515            bui.containerwidget(
516                edit=self._root_widget, selected_child=self._scrollwidget
517            )
518            bui.containerwidget(
519                edit=self._subcontainer, selected_child=map_button
520            )

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:
522    @override
523    def get_main_window_state(self) -> bui.MainWindowState:
524        # Support recreating our window for back/refresh purposes.
525        cls = type(self)
526
527        # Pull things out of self here so we don't refer to self in the
528        # lambda below which would keep us alive.
529        gametype = self._gametype
530        sessiontype = self._sessiontype
531        config = self._config
532        completion_call = self._completion_call
533
534        return bui.BasicMainWindowState(
535            create_call=lambda transition, origin_widget: cls(
536                transition=transition,
537                origin_widget=origin_widget,
538                gametype=gametype,
539                sessiontype=sessiontype,
540                config=config,
541                completion_call=completion_call,
542            )
543        )

Return a WindowState to recreate this window, if supported.