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

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

Return a WindowState to recreate this window, if supported.

Inherited Members
bauiv1._uitypes.MainWindow
main_window_back_state
main_window_close
can_change_main_window
main_window_back
main_window_replace
on_main_window_close
bauiv1._uitypes.Window
get_root_widget