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            claims_tab=True,
209            selection_loops_to_parent=True,
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            claims_tab=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
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            claims_tab=True,
210            selection_loops_to_parent=True,
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            claims_tab=True,
218            selection_loops_to_parent=True,
219        )
220
221        v = scroll_height - 5
222        h = -40
223
224        # Keep track of all the selectable widgets we make so we can wire
225        # them up conveniently.
226        widget_column: list[list[bui.Widget]] = []
227
228        # Map select button.
229        bui.textwidget(
230            parent=self._subcontainer,
231            position=(h + 49, v - 63),
232            size=(100, 30),
233            maxwidth=110,
234            text=bui.Lstr(resource='mapText'),
235            h_align='left',
236            color=(0.8, 0.8, 0.8, 1.0),
237            v_align='center',
238        )
239
240        bui.imagewidget(
241            parent=self._subcontainer,
242            size=(256 * 0.7, 125 * 0.7),
243            position=(h + 261 - 128 + 128.0 * 0.56, v - 90),
244            texture=map_tex,
245            mesh_opaque=bui.getmesh('level_select_button_opaque'),
246            mesh_transparent=bui.getmesh('level_select_button_transparent'),
247            mask_texture=bui.gettexture('mapPreviewMask'),
248        )
249        map_button = btn = bui.buttonwidget(
250            parent=self._subcontainer,
251            size=(140, 60),
252            position=(h + 448, v - 72),
253            on_activate_call=bui.Call(self._select_map),
254            scale=0.7,
255            label=bui.Lstr(resource='mapSelectText'),
256        )
257        widget_column.append([btn])
258
259        bui.textwidget(
260            parent=self._subcontainer,
261            position=(h + 363 - 123, v - 114),
262            size=(100, 30),
263            flatness=1.0,
264            shadow=1.0,
265            scale=0.55,
266            maxwidth=256 * 0.7 * 0.8,
267            text=get_map_display_string(self._map),
268            h_align='center',
269            color=(0.6, 1.0, 0.6, 1.0),
270            v_align='center',
271        )
272        v -= map_height
273
274        for setting in self._settings_defs:
275            value = setting.default
276            value_type = type(value)
277
278            # Now, if there's an existing value for it in the config,
279            # override with that.
280            try:
281                if (
282                    config is not None
283                    and 'settings' in config
284                    and setting.name in config['settings']
285                ):
286                    value = value_type(config['settings'][setting.name])
287            except Exception:
288                logging.exception('Error getting game setting.')
289
290            # Shove the starting value in there to start.
291            self._settings[setting.name] = value
292
293            name_translated = self._get_localized_setting_name(setting.name)
294
295            mw1 = 280
296            mw2 = 70
297
298            # Handle types with choices specially:
299            if isinstance(setting, bs.ChoiceSetting):
300                for choice in setting.choices:
301                    if len(choice) != 2:
302                        raise ValueError(
303                            "Expected 2-member tuples for 'choices'; got: "
304                            + repr(choice)
305                        )
306                    if not isinstance(choice[0], str):
307                        raise TypeError(
308                            'First value for choice tuple must be a str; got: '
309                            + repr(choice)
310                        )
311                    if not isinstance(choice[1], value_type):
312                        raise TypeError(
313                            'Choice type does not match default value; choice:'
314                            + repr(choice)
315                            + '; setting:'
316                            + repr(setting)
317                        )
318                if value_type not in (int, float):
319                    raise TypeError(
320                        'Choice type setting must have int or float default; '
321                        'got: ' + repr(setting)
322                    )
323
324                # Start at the choice corresponding to the default if possible.
325                self._choice_selections[setting.name] = 0
326                for index, choice in enumerate(setting.choices):
327                    if choice[1] == value:
328                        self._choice_selections[setting.name] = index
329                        break
330
331                v -= spacing
332                bui.textwidget(
333                    parent=self._subcontainer,
334                    position=(h + 50, v),
335                    size=(100, 30),
336                    maxwidth=mw1,
337                    text=name_translated,
338                    h_align='left',
339                    color=(0.8, 0.8, 0.8, 1.0),
340                    v_align='center',
341                )
342                txt = bui.textwidget(
343                    parent=self._subcontainer,
344                    position=(h + 509 - 95, v),
345                    size=(0, 28),
346                    text=self._get_localized_setting_name(
347                        setting.choices[self._choice_selections[setting.name]][
348                            0
349                        ]
350                    ),
351                    editable=False,
352                    color=(0.6, 1.0, 0.6, 1.0),
353                    maxwidth=mw2,
354                    h_align='right',
355                    v_align='center',
356                    padding=2,
357                )
358                btn1 = bui.buttonwidget(
359                    parent=self._subcontainer,
360                    position=(h + 509 - 50 - 1, v),
361                    size=(28, 28),
362                    label='<',
363                    autoselect=True,
364                    on_activate_call=bui.Call(
365                        self._choice_inc, setting.name, txt, setting, -1
366                    ),
367                    repeat=True,
368                )
369                btn2 = bui.buttonwidget(
370                    parent=self._subcontainer,
371                    position=(h + 509 + 5, v),
372                    size=(28, 28),
373                    label='>',
374                    autoselect=True,
375                    on_activate_call=bui.Call(
376                        self._choice_inc, setting.name, txt, setting, 1
377                    ),
378                    repeat=True,
379                )
380                widget_column.append([btn1, btn2])
381
382            elif isinstance(setting, (bs.IntSetting, bs.FloatSetting)):
383                v -= spacing
384                min_value = setting.min_value
385                max_value = setting.max_value
386                increment = setting.increment
387                bui.textwidget(
388                    parent=self._subcontainer,
389                    position=(h + 50, v),
390                    size=(100, 30),
391                    text=name_translated,
392                    h_align='left',
393                    color=(0.8, 0.8, 0.8, 1.0),
394                    v_align='center',
395                    maxwidth=mw1,
396                )
397                txt = bui.textwidget(
398                    parent=self._subcontainer,
399                    position=(h + 509 - 95, v),
400                    size=(0, 28),
401                    text=str(value),
402                    editable=False,
403                    color=(0.6, 1.0, 0.6, 1.0),
404                    maxwidth=mw2,
405                    h_align='right',
406                    v_align='center',
407                    padding=2,
408                )
409                btn1 = bui.buttonwidget(
410                    parent=self._subcontainer,
411                    position=(h + 509 - 50 - 1, v),
412                    size=(28, 28),
413                    label='-',
414                    autoselect=True,
415                    on_activate_call=bui.Call(
416                        self._inc,
417                        txt,
418                        min_value,
419                        max_value,
420                        -increment,
421                        value_type,
422                        setting.name,
423                    ),
424                    repeat=True,
425                )
426                btn2 = bui.buttonwidget(
427                    parent=self._subcontainer,
428                    position=(h + 509 + 5, v),
429                    size=(28, 28),
430                    label='+',
431                    autoselect=True,
432                    on_activate_call=bui.Call(
433                        self._inc,
434                        txt,
435                        min_value,
436                        max_value,
437                        increment,
438                        value_type,
439                        setting.name,
440                    ),
441                    repeat=True,
442                )
443                widget_column.append([btn1, btn2])
444
445            elif value_type == bool:
446                v -= spacing
447                bui.textwidget(
448                    parent=self._subcontainer,
449                    position=(h + 50, v),
450                    size=(100, 30),
451                    text=name_translated,
452                    h_align='left',
453                    color=(0.8, 0.8, 0.8, 1.0),
454                    v_align='center',
455                    maxwidth=mw1,
456                )
457                txt = bui.textwidget(
458                    parent=self._subcontainer,
459                    position=(h + 509 - 95, v),
460                    size=(0, 28),
461                    text=(
462                        bui.Lstr(resource='onText')
463                        if value
464                        else bui.Lstr(resource='offText')
465                    ),
466                    editable=False,
467                    color=(0.6, 1.0, 0.6, 1.0),
468                    maxwidth=mw2,
469                    h_align='right',
470                    v_align='center',
471                    padding=2,
472                )
473                cbw = bui.checkboxwidget(
474                    parent=self._subcontainer,
475                    text='',
476                    position=(h + 505 - 50 - 5, v - 2),
477                    size=(200, 30),
478                    autoselect=True,
479                    textcolor=(0.8, 0.8, 0.8),
480                    value=value,
481                    on_value_change_call=bui.Call(
482                        self._check_value_change, setting.name, txt
483                    ),
484                )
485                widget_column.append([cbw])
486
487            else:
488                raise TypeError(f'Invalid value type: {value_type}.')
489
490        # Ok now wire up the column.
491        try:
492            prev_widgets: list[bui.Widget] | None = None
493            for cwdg in widget_column:
494                if prev_widgets is not None:
495                    # Wire our rightmost to their rightmost.
496                    bui.widget(edit=prev_widgets[-1], down_widget=cwdg[-1])
497                    bui.widget(edit=cwdg[-1], up_widget=prev_widgets[-1])
498
499                    # Wire our leftmost to their leftmost.
500                    bui.widget(edit=prev_widgets[0], down_widget=cwdg[0])
501                    bui.widget(edit=cwdg[0], up_widget=prev_widgets[0])
502                prev_widgets = cwdg
503        except Exception:
504            logging.exception(
505                'Error wiring up game-settings-select widget column.'
506            )
507
508        bui.buttonwidget(edit=add_button, on_activate_call=bui.Call(self._add))
509        bui.containerwidget(
510            edit=self._root_widget,
511            selected_child=add_button,
512            start_button=add_button,
513        )
514
515        if default_selection == 'map':
516            bui.containerwidget(
517                edit=self._root_widget, selected_child=self._scrollwidget
518            )
519            bui.containerwidget(
520                edit=self._subcontainer, selected_child=map_button
521            )
522
523    @override
524    def get_main_window_state(self) -> bui.MainWindowState:
525        # Support recreating our window for back/refresh purposes.
526        cls = type(self)
527
528        # Pull things out of self here so we don't refer to self in the
529        # lambda below which would keep us alive.
530        gametype = self._gametype
531        sessiontype = self._sessiontype
532        config = self._config
533        completion_call = self._completion_call
534
535        return bui.BasicMainWindowState(
536            create_call=lambda transition, origin_widget: cls(
537                transition=transition,
538                origin_widget=origin_widget,
539                gametype=gametype,
540                sessiontype=sessiontype,
541                config=config,
542                completion_call=completion_call,
543            )
544        )
545
546    def _get_localized_setting_name(self, name: str) -> bui.Lstr:
547        return bui.Lstr(translate=('settingNames', name))
548
549    def _select_map(self) -> None:
550        # pylint: disable=cyclic-import
551        from bauiv1lib.playlist.mapselect import PlaylistMapSelectWindow
552
553        # No-op if we're not in control.
554        if not self.main_window_has_control():
555            return
556
557        self._config = self._getconfig()
558
559        # Replace ourself with the map-select UI.
560        self.main_window_replace(
561            PlaylistMapSelectWindow(
562                self._gametype,
563                self._sessiontype,
564                self._config,
565                self._edit_info,
566                self._completion_call,
567            )
568        )
569
570    def _choice_inc(
571        self,
572        setting_name: str,
573        widget: bui.Widget,
574        setting: bs.ChoiceSetting,
575        increment: int,
576    ) -> None:
577        choices = setting.choices
578        if increment > 0:
579            self._choice_selections[setting_name] = min(
580                len(choices) - 1, self._choice_selections[setting_name] + 1
581            )
582        else:
583            self._choice_selections[setting_name] = max(
584                0, self._choice_selections[setting_name] - 1
585            )
586        bui.textwidget(
587            edit=widget,
588            text=self._get_localized_setting_name(
589                choices[self._choice_selections[setting_name]][0]
590            ),
591        )
592        self._settings[setting_name] = choices[
593            self._choice_selections[setting_name]
594        ][1]
595
596    def _cancel(self) -> None:
597        self._completion_call(None, self)
598
599    def _check_value_change(
600        self, setting_name: str, widget: bui.Widget, value: int
601    ) -> None:
602        bui.textwidget(
603            edit=widget,
604            text=(
605                bui.Lstr(resource='onText')
606                if value
607                else bui.Lstr(resource='offText')
608            ),
609        )
610        self._settings[setting_name] = value
611
612    def _getconfig(self) -> dict[str, Any]:
613        settings = copy.deepcopy(self._settings)
614        settings['map'] = self._map
615        return {'settings': settings}
616
617    def _add(self) -> None:
618        self._completion_call(self._getconfig(), self)
619
620    def _inc(
621        self,
622        ctrl: bui.Widget,
623        min_val: int | float,
624        max_val: int | float,
625        increment: int | float,
626        setting_type: type,
627        setting_name: str,
628    ) -> None:
629        # pylint: disable=too-many-positional-arguments
630        if setting_type == float:
631            val = float(cast(str, bui.textwidget(query=ctrl)))
632        else:
633            val = int(cast(str, bui.textwidget(query=ctrl)))
634        val += increment
635        val = max(min_val, min(val, max_val))
636        if setting_type == float:
637            bui.textwidget(edit=ctrl, text=str(round(val, 2)))
638        elif setting_type == int:
639            bui.textwidget(edit=ctrl, text=str(int(val)))
640        else:
641            raise TypeError('invalid vartype: ' + str(setting_type))
642        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            claims_tab=True,
210            selection_loops_to_parent=True,
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            claims_tab=True,
218            selection_loops_to_parent=True,
219        )
220
221        v = scroll_height - 5
222        h = -40
223
224        # Keep track of all the selectable widgets we make so we can wire
225        # them up conveniently.
226        widget_column: list[list[bui.Widget]] = []
227
228        # Map select button.
229        bui.textwidget(
230            parent=self._subcontainer,
231            position=(h + 49, v - 63),
232            size=(100, 30),
233            maxwidth=110,
234            text=bui.Lstr(resource='mapText'),
235            h_align='left',
236            color=(0.8, 0.8, 0.8, 1.0),
237            v_align='center',
238        )
239
240        bui.imagewidget(
241            parent=self._subcontainer,
242            size=(256 * 0.7, 125 * 0.7),
243            position=(h + 261 - 128 + 128.0 * 0.56, v - 90),
244            texture=map_tex,
245            mesh_opaque=bui.getmesh('level_select_button_opaque'),
246            mesh_transparent=bui.getmesh('level_select_button_transparent'),
247            mask_texture=bui.gettexture('mapPreviewMask'),
248        )
249        map_button = btn = bui.buttonwidget(
250            parent=self._subcontainer,
251            size=(140, 60),
252            position=(h + 448, v - 72),
253            on_activate_call=bui.Call(self._select_map),
254            scale=0.7,
255            label=bui.Lstr(resource='mapSelectText'),
256        )
257        widget_column.append([btn])
258
259        bui.textwidget(
260            parent=self._subcontainer,
261            position=(h + 363 - 123, v - 114),
262            size=(100, 30),
263            flatness=1.0,
264            shadow=1.0,
265            scale=0.55,
266            maxwidth=256 * 0.7 * 0.8,
267            text=get_map_display_string(self._map),
268            h_align='center',
269            color=(0.6, 1.0, 0.6, 1.0),
270            v_align='center',
271        )
272        v -= map_height
273
274        for setting in self._settings_defs:
275            value = setting.default
276            value_type = type(value)
277
278            # Now, if there's an existing value for it in the config,
279            # override with that.
280            try:
281                if (
282                    config is not None
283                    and 'settings' in config
284                    and setting.name in config['settings']
285                ):
286                    value = value_type(config['settings'][setting.name])
287            except Exception:
288                logging.exception('Error getting game setting.')
289
290            # Shove the starting value in there to start.
291            self._settings[setting.name] = value
292
293            name_translated = self._get_localized_setting_name(setting.name)
294
295            mw1 = 280
296            mw2 = 70
297
298            # Handle types with choices specially:
299            if isinstance(setting, bs.ChoiceSetting):
300                for choice in setting.choices:
301                    if len(choice) != 2:
302                        raise ValueError(
303                            "Expected 2-member tuples for 'choices'; got: "
304                            + repr(choice)
305                        )
306                    if not isinstance(choice[0], str):
307                        raise TypeError(
308                            'First value for choice tuple must be a str; got: '
309                            + repr(choice)
310                        )
311                    if not isinstance(choice[1], value_type):
312                        raise TypeError(
313                            'Choice type does not match default value; choice:'
314                            + repr(choice)
315                            + '; setting:'
316                            + repr(setting)
317                        )
318                if value_type not in (int, float):
319                    raise TypeError(
320                        'Choice type setting must have int or float default; '
321                        'got: ' + repr(setting)
322                    )
323
324                # Start at the choice corresponding to the default if possible.
325                self._choice_selections[setting.name] = 0
326                for index, choice in enumerate(setting.choices):
327                    if choice[1] == value:
328                        self._choice_selections[setting.name] = index
329                        break
330
331                v -= spacing
332                bui.textwidget(
333                    parent=self._subcontainer,
334                    position=(h + 50, v),
335                    size=(100, 30),
336                    maxwidth=mw1,
337                    text=name_translated,
338                    h_align='left',
339                    color=(0.8, 0.8, 0.8, 1.0),
340                    v_align='center',
341                )
342                txt = bui.textwidget(
343                    parent=self._subcontainer,
344                    position=(h + 509 - 95, v),
345                    size=(0, 28),
346                    text=self._get_localized_setting_name(
347                        setting.choices[self._choice_selections[setting.name]][
348                            0
349                        ]
350                    ),
351                    editable=False,
352                    color=(0.6, 1.0, 0.6, 1.0),
353                    maxwidth=mw2,
354                    h_align='right',
355                    v_align='center',
356                    padding=2,
357                )
358                btn1 = bui.buttonwidget(
359                    parent=self._subcontainer,
360                    position=(h + 509 - 50 - 1, v),
361                    size=(28, 28),
362                    label='<',
363                    autoselect=True,
364                    on_activate_call=bui.Call(
365                        self._choice_inc, setting.name, txt, setting, -1
366                    ),
367                    repeat=True,
368                )
369                btn2 = bui.buttonwidget(
370                    parent=self._subcontainer,
371                    position=(h + 509 + 5, v),
372                    size=(28, 28),
373                    label='>',
374                    autoselect=True,
375                    on_activate_call=bui.Call(
376                        self._choice_inc, setting.name, txt, setting, 1
377                    ),
378                    repeat=True,
379                )
380                widget_column.append([btn1, btn2])
381
382            elif isinstance(setting, (bs.IntSetting, bs.FloatSetting)):
383                v -= spacing
384                min_value = setting.min_value
385                max_value = setting.max_value
386                increment = setting.increment
387                bui.textwidget(
388                    parent=self._subcontainer,
389                    position=(h + 50, v),
390                    size=(100, 30),
391                    text=name_translated,
392                    h_align='left',
393                    color=(0.8, 0.8, 0.8, 1.0),
394                    v_align='center',
395                    maxwidth=mw1,
396                )
397                txt = bui.textwidget(
398                    parent=self._subcontainer,
399                    position=(h + 509 - 95, v),
400                    size=(0, 28),
401                    text=str(value),
402                    editable=False,
403                    color=(0.6, 1.0, 0.6, 1.0),
404                    maxwidth=mw2,
405                    h_align='right',
406                    v_align='center',
407                    padding=2,
408                )
409                btn1 = bui.buttonwidget(
410                    parent=self._subcontainer,
411                    position=(h + 509 - 50 - 1, v),
412                    size=(28, 28),
413                    label='-',
414                    autoselect=True,
415                    on_activate_call=bui.Call(
416                        self._inc,
417                        txt,
418                        min_value,
419                        max_value,
420                        -increment,
421                        value_type,
422                        setting.name,
423                    ),
424                    repeat=True,
425                )
426                btn2 = bui.buttonwidget(
427                    parent=self._subcontainer,
428                    position=(h + 509 + 5, v),
429                    size=(28, 28),
430                    label='+',
431                    autoselect=True,
432                    on_activate_call=bui.Call(
433                        self._inc,
434                        txt,
435                        min_value,
436                        max_value,
437                        increment,
438                        value_type,
439                        setting.name,
440                    ),
441                    repeat=True,
442                )
443                widget_column.append([btn1, btn2])
444
445            elif value_type == bool:
446                v -= spacing
447                bui.textwidget(
448                    parent=self._subcontainer,
449                    position=(h + 50, v),
450                    size=(100, 30),
451                    text=name_translated,
452                    h_align='left',
453                    color=(0.8, 0.8, 0.8, 1.0),
454                    v_align='center',
455                    maxwidth=mw1,
456                )
457                txt = bui.textwidget(
458                    parent=self._subcontainer,
459                    position=(h + 509 - 95, v),
460                    size=(0, 28),
461                    text=(
462                        bui.Lstr(resource='onText')
463                        if value
464                        else bui.Lstr(resource='offText')
465                    ),
466                    editable=False,
467                    color=(0.6, 1.0, 0.6, 1.0),
468                    maxwidth=mw2,
469                    h_align='right',
470                    v_align='center',
471                    padding=2,
472                )
473                cbw = bui.checkboxwidget(
474                    parent=self._subcontainer,
475                    text='',
476                    position=(h + 505 - 50 - 5, v - 2),
477                    size=(200, 30),
478                    autoselect=True,
479                    textcolor=(0.8, 0.8, 0.8),
480                    value=value,
481                    on_value_change_call=bui.Call(
482                        self._check_value_change, setting.name, txt
483                    ),
484                )
485                widget_column.append([cbw])
486
487            else:
488                raise TypeError(f'Invalid value type: {value_type}.')
489
490        # Ok now wire up the column.
491        try:
492            prev_widgets: list[bui.Widget] | None = None
493            for cwdg in widget_column:
494                if prev_widgets is not None:
495                    # Wire our rightmost to their rightmost.
496                    bui.widget(edit=prev_widgets[-1], down_widget=cwdg[-1])
497                    bui.widget(edit=cwdg[-1], up_widget=prev_widgets[-1])
498
499                    # Wire our leftmost to their leftmost.
500                    bui.widget(edit=prev_widgets[0], down_widget=cwdg[0])
501                    bui.widget(edit=cwdg[0], up_widget=prev_widgets[0])
502                prev_widgets = cwdg
503        except Exception:
504            logging.exception(
505                'Error wiring up game-settings-select widget column.'
506            )
507
508        bui.buttonwidget(edit=add_button, on_activate_call=bui.Call(self._add))
509        bui.containerwidget(
510            edit=self._root_widget,
511            selected_child=add_button,
512            start_button=add_button,
513        )
514
515        if default_selection == 'map':
516            bui.containerwidget(
517                edit=self._root_widget, selected_child=self._scrollwidget
518            )
519            bui.containerwidget(
520                edit=self._subcontainer, selected_child=map_button
521            )

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

Return a WindowState to recreate this window, if supported.