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