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