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