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

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        )
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            )

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:
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        )

Return a WindowState to recreate this window, if supported.