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

Window for editing a game config.

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