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 ) 210 self._subcontainer = bui.containerwidget( 211 parent=self._scrollwidget, 212 size=(scroll_width, scroll_height), 213 background=False, 214 claims_left_right=True, 215 selection_loops_to_parent=True, 216 ) 217 218 v = scroll_height - 5 219 h = -40 220 221 # Keep track of all the selectable widgets we make so we can wire 222 # them up conveniently. 223 widget_column: list[list[bui.Widget]] = [] 224 225 # Map select button. 226 bui.textwidget( 227 parent=self._subcontainer, 228 position=(h + 49, v - 63), 229 size=(100, 30), 230 maxwidth=110, 231 text=bui.Lstr(resource='mapText'), 232 h_align='left', 233 color=(0.8, 0.8, 0.8, 1.0), 234 v_align='center', 235 ) 236 237 bui.imagewidget( 238 parent=self._subcontainer, 239 size=(256 * 0.7, 125 * 0.7), 240 position=(h + 261 - 128 + 128.0 * 0.56, v - 90), 241 texture=map_tex, 242 mesh_opaque=bui.getmesh('level_select_button_opaque'), 243 mesh_transparent=bui.getmesh('level_select_button_transparent'), 244 mask_texture=bui.gettexture('mapPreviewMask'), 245 ) 246 map_button = btn = bui.buttonwidget( 247 parent=self._subcontainer, 248 size=(140, 60), 249 position=(h + 448, v - 72), 250 on_activate_call=bui.Call(self._select_map), 251 scale=0.7, 252 label=bui.Lstr(resource='mapSelectText'), 253 ) 254 widget_column.append([btn]) 255 256 bui.textwidget( 257 parent=self._subcontainer, 258 position=(h + 363 - 123, v - 114), 259 size=(100, 30), 260 flatness=1.0, 261 shadow=1.0, 262 scale=0.55, 263 maxwidth=256 * 0.7 * 0.8, 264 text=get_map_display_string(self._map), 265 h_align='center', 266 color=(0.6, 1.0, 0.6, 1.0), 267 v_align='center', 268 ) 269 v -= map_height 270 271 for setting in self._settings_defs: 272 value = setting.default 273 value_type = type(value) 274 275 # Now, if there's an existing value for it in the config, 276 # override with that. 277 try: 278 if ( 279 config is not None 280 and 'settings' in config 281 and setting.name in config['settings'] 282 ): 283 value = value_type(config['settings'][setting.name]) 284 except Exception: 285 logging.exception('Error getting game setting.') 286 287 # Shove the starting value in there to start. 288 self._settings[setting.name] = value 289 290 name_translated = self._get_localized_setting_name(setting.name) 291 292 mw1 = 280 293 mw2 = 70 294 295 # Handle types with choices specially: 296 if isinstance(setting, bs.ChoiceSetting): 297 for choice in setting.choices: 298 if len(choice) != 2: 299 raise ValueError( 300 "Expected 2-member tuples for 'choices'; got: " 301 + repr(choice) 302 ) 303 if not isinstance(choice[0], str): 304 raise TypeError( 305 'First value for choice tuple must be a str; got: ' 306 + repr(choice) 307 ) 308 if not isinstance(choice[1], value_type): 309 raise TypeError( 310 'Choice type does not match default value; choice:' 311 + repr(choice) 312 + '; setting:' 313 + repr(setting) 314 ) 315 if value_type not in (int, float): 316 raise TypeError( 317 'Choice type setting must have int or float default; ' 318 'got: ' + repr(setting) 319 ) 320 321 # Start at the choice corresponding to the default if possible. 322 self._choice_selections[setting.name] = 0 323 for index, choice in enumerate(setting.choices): 324 if choice[1] == value: 325 self._choice_selections[setting.name] = index 326 break 327 328 v -= spacing 329 bui.textwidget( 330 parent=self._subcontainer, 331 position=(h + 50, v), 332 size=(100, 30), 333 maxwidth=mw1, 334 text=name_translated, 335 h_align='left', 336 color=(0.8, 0.8, 0.8, 1.0), 337 v_align='center', 338 ) 339 txt = bui.textwidget( 340 parent=self._subcontainer, 341 position=(h + 509 - 95, v), 342 size=(0, 28), 343 text=self._get_localized_setting_name( 344 setting.choices[self._choice_selections[setting.name]][ 345 0 346 ] 347 ), 348 editable=False, 349 color=(0.6, 1.0, 0.6, 1.0), 350 maxwidth=mw2, 351 h_align='right', 352 v_align='center', 353 padding=2, 354 ) 355 btn1 = bui.buttonwidget( 356 parent=self._subcontainer, 357 position=(h + 509 - 50 - 1, v), 358 size=(28, 28), 359 label='<', 360 autoselect=True, 361 on_activate_call=bui.Call( 362 self._choice_inc, setting.name, txt, setting, -1 363 ), 364 repeat=True, 365 ) 366 btn2 = bui.buttonwidget( 367 parent=self._subcontainer, 368 position=(h + 509 + 5, v), 369 size=(28, 28), 370 label='>', 371 autoselect=True, 372 on_activate_call=bui.Call( 373 self._choice_inc, setting.name, txt, setting, 1 374 ), 375 repeat=True, 376 ) 377 widget_column.append([btn1, btn2]) 378 379 elif isinstance(setting, (bs.IntSetting, bs.FloatSetting)): 380 v -= spacing 381 min_value = setting.min_value 382 max_value = setting.max_value 383 increment = setting.increment 384 bui.textwidget( 385 parent=self._subcontainer, 386 position=(h + 50, v), 387 size=(100, 30), 388 text=name_translated, 389 h_align='left', 390 color=(0.8, 0.8, 0.8, 1.0), 391 v_align='center', 392 maxwidth=mw1, 393 ) 394 txt = bui.textwidget( 395 parent=self._subcontainer, 396 position=(h + 509 - 95, v), 397 size=(0, 28), 398 text=str(value), 399 editable=False, 400 color=(0.6, 1.0, 0.6, 1.0), 401 maxwidth=mw2, 402 h_align='right', 403 v_align='center', 404 padding=2, 405 ) 406 btn1 = bui.buttonwidget( 407 parent=self._subcontainer, 408 position=(h + 509 - 50 - 1, v), 409 size=(28, 28), 410 label='-', 411 autoselect=True, 412 on_activate_call=bui.Call( 413 self._inc, 414 txt, 415 min_value, 416 max_value, 417 -increment, 418 value_type, 419 setting.name, 420 ), 421 repeat=True, 422 ) 423 btn2 = bui.buttonwidget( 424 parent=self._subcontainer, 425 position=(h + 509 + 5, v), 426 size=(28, 28), 427 label='+', 428 autoselect=True, 429 on_activate_call=bui.Call( 430 self._inc, 431 txt, 432 min_value, 433 max_value, 434 increment, 435 value_type, 436 setting.name, 437 ), 438 repeat=True, 439 ) 440 widget_column.append([btn1, btn2]) 441 442 elif value_type == bool: 443 v -= spacing 444 bui.textwidget( 445 parent=self._subcontainer, 446 position=(h + 50, v), 447 size=(100, 30), 448 text=name_translated, 449 h_align='left', 450 color=(0.8, 0.8, 0.8, 1.0), 451 v_align='center', 452 maxwidth=mw1, 453 ) 454 txt = bui.textwidget( 455 parent=self._subcontainer, 456 position=(h + 509 - 95, v), 457 size=(0, 28), 458 text=( 459 bui.Lstr(resource='onText') 460 if value 461 else bui.Lstr(resource='offText') 462 ), 463 editable=False, 464 color=(0.6, 1.0, 0.6, 1.0), 465 maxwidth=mw2, 466 h_align='right', 467 v_align='center', 468 padding=2, 469 ) 470 cbw = bui.checkboxwidget( 471 parent=self._subcontainer, 472 text='', 473 position=(h + 505 - 50 - 5, v - 2), 474 size=(200, 30), 475 autoselect=True, 476 textcolor=(0.8, 0.8, 0.8), 477 value=value, 478 on_value_change_call=bui.Call( 479 self._check_value_change, setting.name, txt 480 ), 481 ) 482 widget_column.append([cbw]) 483 484 else: 485 raise TypeError(f'Invalid value type: {value_type}.') 486 487 # Ok now wire up the column. 488 try: 489 prev_widgets: list[bui.Widget] | None = None 490 for cwdg in widget_column: 491 if prev_widgets is not None: 492 # Wire our rightmost to their rightmost. 493 bui.widget(edit=prev_widgets[-1], down_widget=cwdg[-1]) 494 bui.widget(edit=cwdg[-1], up_widget=prev_widgets[-1]) 495 496 # Wire our leftmost to their leftmost. 497 bui.widget(edit=prev_widgets[0], down_widget=cwdg[0]) 498 bui.widget(edit=cwdg[0], up_widget=prev_widgets[0]) 499 prev_widgets = cwdg 500 except Exception: 501 logging.exception( 502 'Error wiring up game-settings-select widget column.' 503 ) 504 505 bui.buttonwidget(edit=add_button, on_activate_call=bui.Call(self._add)) 506 bui.containerwidget( 507 edit=self._root_widget, 508 selected_child=add_button, 509 start_button=add_button, 510 ) 511 512 if default_selection == 'map': 513 bui.containerwidget( 514 edit=self._root_widget, selected_child=self._scrollwidget 515 ) 516 bui.containerwidget( 517 edit=self._subcontainer, selected_child=map_button 518 ) 519 520 @override 521 def get_main_window_state(self) -> bui.MainWindowState: 522 # Support recreating our window for back/refresh purposes. 523 cls = type(self) 524 525 # Pull things out of self here so we don't refer to self in the 526 # lambda below which would keep us alive. 527 gametype = self._gametype 528 sessiontype = self._sessiontype 529 config = self._config 530 completion_call = self._completion_call 531 532 return bui.BasicMainWindowState( 533 create_call=lambda transition, origin_widget: cls( 534 transition=transition, 535 origin_widget=origin_widget, 536 gametype=gametype, 537 sessiontype=sessiontype, 538 config=config, 539 completion_call=completion_call, 540 ) 541 ) 542 543 def _get_localized_setting_name(self, name: str) -> bui.Lstr: 544 return bui.Lstr(translate=('settingNames', name)) 545 546 def _select_map(self) -> None: 547 # pylint: disable=cyclic-import 548 from bauiv1lib.playlist.mapselect import PlaylistMapSelectWindow 549 550 # No-op if we're not in control. 551 if not self.main_window_has_control(): 552 return 553 554 self._config = self._getconfig() 555 556 # Replace ourself with the map-select UI. 557 self.main_window_replace( 558 PlaylistMapSelectWindow( 559 self._gametype, 560 self._sessiontype, 561 self._config, 562 self._edit_info, 563 self._completion_call, 564 ) 565 ) 566 567 def _choice_inc( 568 self, 569 setting_name: str, 570 widget: bui.Widget, 571 setting: bs.ChoiceSetting, 572 increment: int, 573 ) -> None: 574 choices = setting.choices 575 if increment > 0: 576 self._choice_selections[setting_name] = min( 577 len(choices) - 1, self._choice_selections[setting_name] + 1 578 ) 579 else: 580 self._choice_selections[setting_name] = max( 581 0, self._choice_selections[setting_name] - 1 582 ) 583 bui.textwidget( 584 edit=widget, 585 text=self._get_localized_setting_name( 586 choices[self._choice_selections[setting_name]][0] 587 ), 588 ) 589 self._settings[setting_name] = choices[ 590 self._choice_selections[setting_name] 591 ][1] 592 593 def _cancel(self) -> None: 594 self._completion_call(None, self) 595 596 def _check_value_change( 597 self, setting_name: str, widget: bui.Widget, value: int 598 ) -> None: 599 bui.textwidget( 600 edit=widget, 601 text=( 602 bui.Lstr(resource='onText') 603 if value 604 else bui.Lstr(resource='offText') 605 ), 606 ) 607 self._settings[setting_name] = value 608 609 def _getconfig(self) -> dict[str, Any]: 610 settings = copy.deepcopy(self._settings) 611 settings['map'] = self._map 612 return {'settings': settings} 613 614 def _add(self) -> None: 615 self._completion_call(self._getconfig(), self) 616 617 def _inc( 618 self, 619 ctrl: bui.Widget, 620 min_val: int | float, 621 max_val: int | float, 622 increment: int | float, 623 setting_type: type, 624 setting_name: str, 625 ) -> None: 626 # pylint: disable=too-many-positional-arguments 627 if setting_type == float: 628 val = float(cast(str, bui.textwidget(query=ctrl))) 629 else: 630 val = int(cast(str, bui.textwidget(query=ctrl))) 631 val += increment 632 val = max(min_val, min(val, max_val)) 633 if setting_type == float: 634 bui.textwidget(edit=ctrl, text=str(round(val, 2))) 635 elif setting_type == int: 636 bui.textwidget(edit=ctrl, text=str(int(val))) 637 else: 638 raise TypeError('invalid vartype: ' + str(setting_type)) 639 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 ) 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
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 ) 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 )
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.
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 )
Return a WindowState to recreate this window, if supported.