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

Window for editing a game config.

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