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 if uiscale is bui.UIScale.MEDIUM else 550
112        )
113        spacing = 52
114        y_extra = 15
115        y_extra2 = 21
116
117        map_tex_name = get_map_class(self._map).get_preview_texture_name()
118        if map_tex_name is None:
119            raise RuntimeError(f'No map preview tex found for {self._map}.')
120        map_tex = bui.gettexture(map_tex_name)
121
122        top_extra = 20 if uiscale is bui.UIScale.SMALL else 0
123        super().__init__(
124            root_widget=bui.containerwidget(
125                size=(width, height + top_extra),
126                transition=transition,
127                scale=(
128                    2.19
129                    if uiscale is bui.UIScale.SMALL
130                    else 1.35 if uiscale is bui.UIScale.MEDIUM else 1.0
131                ),
132                stack_offset=(
133                    (0, -17) if uiscale is bui.UIScale.SMALL else (0, 0)
134                ),
135            )
136        )
137
138        btn = bui.buttonwidget(
139            parent=self._root_widget,
140            position=(45 + x_inset, height - 82 + y_extra2),
141            size=(180, 70) if is_add else (180, 65),
142            label=(
143                bui.Lstr(resource='backText')
144                if is_add
145                else bui.Lstr(resource='cancelText')
146            ),
147            button_type='back' if is_add else None,
148            autoselect=True,
149            scale=0.75,
150            text_scale=1.3,
151            on_activate_call=bui.Call(self._cancel),
152        )
153        bui.containerwidget(edit=self._root_widget, cancel_button=btn)
154
155        add_button = bui.buttonwidget(
156            parent=self._root_widget,
157            position=(width - (193 + x_inset), height - 82 + y_extra2),
158            size=(200, 65),
159            scale=0.75,
160            text_scale=1.3,
161            label=(
162                bui.Lstr(resource=self._r + '.addGameText')
163                if is_add
164                else bui.Lstr(resource='doneText')
165            ),
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=(
451                        bui.Lstr(resource='onText')
452                        if value
453                        else bui.Lstr(resource='offText')
454                    ),
455                    editable=False,
456                    color=(0.6, 1.0, 0.6, 1.0),
457                    maxwidth=mw2,
458                    h_align='right',
459                    v_align='center',
460                    padding=2,
461                )
462                cbw = bui.checkboxwidget(
463                    parent=self._subcontainer,
464                    text='',
465                    position=(h + 505 - 50 - 5, v - 2),
466                    size=(200, 30),
467                    autoselect=True,
468                    textcolor=(0.8, 0.8, 0.8),
469                    value=value,
470                    on_value_change_call=bui.Call(
471                        self._check_value_change, setting.name, txt
472                    ),
473                )
474                widget_column.append([cbw])
475
476            else:
477                raise TypeError(f'Invalid value type: {value_type}.')
478
479        # Ok now wire up the column.
480        try:
481            prev_widgets: list[bui.Widget] | None = None
482            for cwdg in widget_column:
483                if prev_widgets is not None:
484                    # Wire our rightmost to their rightmost.
485                    bui.widget(edit=prev_widgets[-1], down_widget=cwdg[-1])
486                    bui.widget(cwdg[-1], up_widget=prev_widgets[-1])
487
488                    # Wire our leftmost to their leftmost.
489                    bui.widget(edit=prev_widgets[0], down_widget=cwdg[0])
490                    bui.widget(cwdg[0], up_widget=prev_widgets[0])
491                prev_widgets = cwdg
492        except Exception:
493            logging.exception(
494                'Error wiring up game-settings-select widget column.'
495            )
496
497        bui.buttonwidget(edit=add_button, on_activate_call=bui.Call(self._add))
498        bui.containerwidget(
499            edit=self._root_widget,
500            selected_child=add_button,
501            start_button=add_button,
502        )
503
504        if default_selection == 'map':
505            bui.containerwidget(
506                edit=self._root_widget, selected_child=self._scrollwidget
507            )
508            bui.containerwidget(
509                edit=self._subcontainer, selected_child=map_button
510            )
511
512    def _get_localized_setting_name(self, name: str) -> bui.Lstr:
513        return bui.Lstr(translate=('settingNames', name))
514
515    def _select_map(self) -> None:
516        # pylint: disable=cyclic-import
517        from bauiv1lib.playlist.mapselect import PlaylistMapSelectWindow
518
519        # no-op if our underlying widget is dead or on its way out.
520        if not self._root_widget or self._root_widget.transitioning_out:
521            return
522
523        # Replace ourself with the map-select UI.
524        bui.containerwidget(edit=self._root_widget, transition='out_left')
525        assert bui.app.classic is not None
526        bui.app.ui_v1.set_main_menu_window(
527            PlaylistMapSelectWindow(
528                self._gametype,
529                self._sessiontype,
530                copy.deepcopy(self._getconfig()),
531                self._edit_info,
532                self._completion_call,
533            ).get_root_widget(),
534            from_window=self._root_widget,
535        )
536
537    def _choice_inc(
538        self,
539        setting_name: str,
540        widget: bui.Widget,
541        setting: bs.ChoiceSetting,
542        increment: int,
543    ) -> None:
544        choices = setting.choices
545        if increment > 0:
546            self._choice_selections[setting_name] = min(
547                len(choices) - 1, self._choice_selections[setting_name] + 1
548            )
549        else:
550            self._choice_selections[setting_name] = max(
551                0, self._choice_selections[setting_name] - 1
552            )
553        bui.textwidget(
554            edit=widget,
555            text=self._get_localized_setting_name(
556                choices[self._choice_selections[setting_name]][0]
557            ),
558        )
559        self._settings[setting_name] = choices[
560            self._choice_selections[setting_name]
561        ][1]
562
563    def _cancel(self) -> None:
564        self._completion_call(None)
565
566    def _check_value_change(
567        self, setting_name: str, widget: bui.Widget, value: int
568    ) -> None:
569        bui.textwidget(
570            edit=widget,
571            text=(
572                bui.Lstr(resource='onText')
573                if value
574                else bui.Lstr(resource='offText')
575            ),
576        )
577        self._settings[setting_name] = value
578
579    def _getconfig(self) -> dict[str, Any]:
580        settings = copy.deepcopy(self._settings)
581        settings['map'] = self._map
582        return {'settings': settings}
583
584    def _add(self) -> None:
585        self._completion_call(copy.deepcopy(self._getconfig()))
586
587    def _inc(
588        self,
589        ctrl: bui.Widget,
590        min_val: int | float,
591        max_val: int | float,
592        increment: int | float,
593        setting_type: type,
594        setting_name: str,
595    ) -> None:
596        if setting_type == float:
597            val = float(cast(str, bui.textwidget(query=ctrl)))
598        else:
599            val = int(cast(str, bui.textwidget(query=ctrl)))
600        val += increment
601        val = max(min_val, min(val, max_val))
602        if setting_type == float:
603            bui.textwidget(edit=ctrl, text=str(round(val, 2)))
604        elif setting_type == int:
605            bui.textwidget(edit=ctrl, text=str(int(val)))
606        else:
607            raise TypeError('invalid vartype: ' + str(setting_type))
608        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 if uiscale is bui.UIScale.MEDIUM else 550
113        )
114        spacing = 52
115        y_extra = 15
116        y_extra2 = 21
117
118        map_tex_name = get_map_class(self._map).get_preview_texture_name()
119        if map_tex_name is None:
120            raise RuntimeError(f'No map preview tex found for {self._map}.')
121        map_tex = bui.gettexture(map_tex_name)
122
123        top_extra = 20 if uiscale is bui.UIScale.SMALL else 0
124        super().__init__(
125            root_widget=bui.containerwidget(
126                size=(width, height + top_extra),
127                transition=transition,
128                scale=(
129                    2.19
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        )
138
139        btn = bui.buttonwidget(
140            parent=self._root_widget,
141            position=(45 + x_inset, height - 82 + y_extra2),
142            size=(180, 70) if is_add else (180, 65),
143            label=(
144                bui.Lstr(resource='backText')
145                if is_add
146                else bui.Lstr(resource='cancelText')
147            ),
148            button_type='back' if is_add else None,
149            autoselect=True,
150            scale=0.75,
151            text_scale=1.3,
152            on_activate_call=bui.Call(self._cancel),
153        )
154        bui.containerwidget(edit=self._root_widget, cancel_button=btn)
155
156        add_button = bui.buttonwidget(
157            parent=self._root_widget,
158            position=(width - (193 + x_inset), height - 82 + y_extra2),
159            size=(200, 65),
160            scale=0.75,
161            text_scale=1.3,
162            label=(
163                bui.Lstr(resource=self._r + '.addGameText')
164                if is_add
165                else bui.Lstr(resource='doneText')
166            ),
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=(
452                        bui.Lstr(resource='onText')
453                        if value
454                        else bui.Lstr(resource='offText')
455                    ),
456                    editable=False,
457                    color=(0.6, 1.0, 0.6, 1.0),
458                    maxwidth=mw2,
459                    h_align='right',
460                    v_align='center',
461                    padding=2,
462                )
463                cbw = bui.checkboxwidget(
464                    parent=self._subcontainer,
465                    text='',
466                    position=(h + 505 - 50 - 5, v - 2),
467                    size=(200, 30),
468                    autoselect=True,
469                    textcolor=(0.8, 0.8, 0.8),
470                    value=value,
471                    on_value_change_call=bui.Call(
472                        self._check_value_change, setting.name, txt
473                    ),
474                )
475                widget_column.append([cbw])
476
477            else:
478                raise TypeError(f'Invalid value type: {value_type}.')
479
480        # Ok now wire up the column.
481        try:
482            prev_widgets: list[bui.Widget] | None = None
483            for cwdg in widget_column:
484                if prev_widgets is not None:
485                    # Wire our rightmost to their rightmost.
486                    bui.widget(edit=prev_widgets[-1], down_widget=cwdg[-1])
487                    bui.widget(cwdg[-1], up_widget=prev_widgets[-1])
488
489                    # Wire our leftmost to their leftmost.
490                    bui.widget(edit=prev_widgets[0], down_widget=cwdg[0])
491                    bui.widget(cwdg[0], up_widget=prev_widgets[0])
492                prev_widgets = cwdg
493        except Exception:
494            logging.exception(
495                'Error wiring up game-settings-select widget column.'
496            )
497
498        bui.buttonwidget(edit=add_button, on_activate_call=bui.Call(self._add))
499        bui.containerwidget(
500            edit=self._root_widget,
501            selected_child=add_button,
502            start_button=add_button,
503        )
504
505        if default_selection == 'map':
506            bui.containerwidget(
507                edit=self._root_widget, selected_child=self._scrollwidget
508            )
509            bui.containerwidget(
510                edit=self._subcontainer, selected_child=map_button
511            )
512
513    def _get_localized_setting_name(self, name: str) -> bui.Lstr:
514        return bui.Lstr(translate=('settingNames', name))
515
516    def _select_map(self) -> None:
517        # pylint: disable=cyclic-import
518        from bauiv1lib.playlist.mapselect import PlaylistMapSelectWindow
519
520        # no-op if our underlying widget is dead or on its way out.
521        if not self._root_widget or self._root_widget.transitioning_out:
522            return
523
524        # Replace ourself with the map-select UI.
525        bui.containerwidget(edit=self._root_widget, transition='out_left')
526        assert bui.app.classic is not None
527        bui.app.ui_v1.set_main_menu_window(
528            PlaylistMapSelectWindow(
529                self._gametype,
530                self._sessiontype,
531                copy.deepcopy(self._getconfig()),
532                self._edit_info,
533                self._completion_call,
534            ).get_root_widget(),
535            from_window=self._root_widget,
536        )
537
538    def _choice_inc(
539        self,
540        setting_name: str,
541        widget: bui.Widget,
542        setting: bs.ChoiceSetting,
543        increment: int,
544    ) -> None:
545        choices = setting.choices
546        if increment > 0:
547            self._choice_selections[setting_name] = min(
548                len(choices) - 1, self._choice_selections[setting_name] + 1
549            )
550        else:
551            self._choice_selections[setting_name] = max(
552                0, self._choice_selections[setting_name] - 1
553            )
554        bui.textwidget(
555            edit=widget,
556            text=self._get_localized_setting_name(
557                choices[self._choice_selections[setting_name]][0]
558            ),
559        )
560        self._settings[setting_name] = choices[
561            self._choice_selections[setting_name]
562        ][1]
563
564    def _cancel(self) -> None:
565        self._completion_call(None)
566
567    def _check_value_change(
568        self, setting_name: str, widget: bui.Widget, value: int
569    ) -> None:
570        bui.textwidget(
571            edit=widget,
572            text=(
573                bui.Lstr(resource='onText')
574                if value
575                else bui.Lstr(resource='offText')
576            ),
577        )
578        self._settings[setting_name] = value
579
580    def _getconfig(self) -> dict[str, Any]:
581        settings = copy.deepcopy(self._settings)
582        settings['map'] = self._map
583        return {'settings': settings}
584
585    def _add(self) -> None:
586        self._completion_call(copy.deepcopy(self._getconfig()))
587
588    def _inc(
589        self,
590        ctrl: bui.Widget,
591        min_val: int | float,
592        max_val: int | float,
593        increment: int | float,
594        setting_type: type,
595        setting_name: str,
596    ) -> None:
597        if setting_type == float:
598            val = float(cast(str, bui.textwidget(query=ctrl)))
599        else:
600            val = int(cast(str, bui.textwidget(query=ctrl)))
601        val += increment
602        val = max(min_val, min(val, max_val))
603        if setting_type == float:
604            bui.textwidget(edit=ctrl, text=str(round(val, 2)))
605        elif setting_type == int:
606            bui.textwidget(edit=ctrl, text=str(int(val)))
607        else:
608            raise TypeError('invalid vartype: ' + str(setting_type))
609        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 if uiscale is bui.UIScale.MEDIUM else 550
113        )
114        spacing = 52
115        y_extra = 15
116        y_extra2 = 21
117
118        map_tex_name = get_map_class(self._map).get_preview_texture_name()
119        if map_tex_name is None:
120            raise RuntimeError(f'No map preview tex found for {self._map}.')
121        map_tex = bui.gettexture(map_tex_name)
122
123        top_extra = 20 if uiscale is bui.UIScale.SMALL else 0
124        super().__init__(
125            root_widget=bui.containerwidget(
126                size=(width, height + top_extra),
127                transition=transition,
128                scale=(
129                    2.19
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        )
138
139        btn = bui.buttonwidget(
140            parent=self._root_widget,
141            position=(45 + x_inset, height - 82 + y_extra2),
142            size=(180, 70) if is_add else (180, 65),
143            label=(
144                bui.Lstr(resource='backText')
145                if is_add
146                else bui.Lstr(resource='cancelText')
147            ),
148            button_type='back' if is_add else None,
149            autoselect=True,
150            scale=0.75,
151            text_scale=1.3,
152            on_activate_call=bui.Call(self._cancel),
153        )
154        bui.containerwidget(edit=self._root_widget, cancel_button=btn)
155
156        add_button = bui.buttonwidget(
157            parent=self._root_widget,
158            position=(width - (193 + x_inset), height - 82 + y_extra2),
159            size=(200, 65),
160            scale=0.75,
161            text_scale=1.3,
162            label=(
163                bui.Lstr(resource=self._r + '.addGameText')
164                if is_add
165                else bui.Lstr(resource='doneText')
166            ),
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=(
452                        bui.Lstr(resource='onText')
453                        if value
454                        else bui.Lstr(resource='offText')
455                    ),
456                    editable=False,
457                    color=(0.6, 1.0, 0.6, 1.0),
458                    maxwidth=mw2,
459                    h_align='right',
460                    v_align='center',
461                    padding=2,
462                )
463                cbw = bui.checkboxwidget(
464                    parent=self._subcontainer,
465                    text='',
466                    position=(h + 505 - 50 - 5, v - 2),
467                    size=(200, 30),
468                    autoselect=True,
469                    textcolor=(0.8, 0.8, 0.8),
470                    value=value,
471                    on_value_change_call=bui.Call(
472                        self._check_value_change, setting.name, txt
473                    ),
474                )
475                widget_column.append([cbw])
476
477            else:
478                raise TypeError(f'Invalid value type: {value_type}.')
479
480        # Ok now wire up the column.
481        try:
482            prev_widgets: list[bui.Widget] | None = None
483            for cwdg in widget_column:
484                if prev_widgets is not None:
485                    # Wire our rightmost to their rightmost.
486                    bui.widget(edit=prev_widgets[-1], down_widget=cwdg[-1])
487                    bui.widget(cwdg[-1], up_widget=prev_widgets[-1])
488
489                    # Wire our leftmost to their leftmost.
490                    bui.widget(edit=prev_widgets[0], down_widget=cwdg[0])
491                    bui.widget(cwdg[0], up_widget=prev_widgets[0])
492                prev_widgets = cwdg
493        except Exception:
494            logging.exception(
495                'Error wiring up game-settings-select widget column.'
496            )
497
498        bui.buttonwidget(edit=add_button, on_activate_call=bui.Call(self._add))
499        bui.containerwidget(
500            edit=self._root_widget,
501            selected_child=add_button,
502            start_button=add_button,
503        )
504
505        if default_selection == 'map':
506            bui.containerwidget(
507                edit=self._root_widget, selected_child=self._scrollwidget
508            )
509            bui.containerwidget(
510                edit=self._subcontainer, selected_child=map_button
511            )
Inherited Members
bauiv1._uitypes.Window
get_root_widget