bauiv1lib.profile.browser
UI functionality related to browsing player profiles.
1# Released under the MIT License. See LICENSE for details. 2# 3"""UI functionality related to browsing player profiles.""" 4 5from __future__ import annotations 6 7import logging 8from typing import TYPE_CHECKING, override 9 10import bauiv1 as bui 11import bascenev1 as bs 12 13if TYPE_CHECKING: 14 from typing import Any 15 16 17class ProfileBrowserWindow(bui.MainWindow): 18 """Window for browsing player profiles.""" 19 20 def __init__( 21 self, 22 transition: str | None = 'in_right', 23 in_main_menu: bool = True, 24 selected_profile: str | None = None, 25 origin_widget: bui.Widget | None = None, 26 ): 27 # pylint: disable=too-many-statements 28 self._in_main_menu = in_main_menu 29 if self._in_main_menu: 30 back_label = bui.Lstr(resource='backText') 31 else: 32 back_label = bui.Lstr(resource='doneText') 33 assert bui.app.classic is not None 34 uiscale = bui.app.ui_v1.uiscale 35 self._width = 800.0 if uiscale is bui.UIScale.SMALL else 600.0 36 x_inset = 100.0 if uiscale is bui.UIScale.SMALL else 0.0 37 self._height = ( 38 360.0 39 if uiscale is bui.UIScale.SMALL 40 else 385.0 if uiscale is bui.UIScale.MEDIUM else 410.0 41 ) 42 43 # If we're being called up standalone, handle pause/resume ourself. 44 if not self._in_main_menu: 45 assert bui.app.classic is not None 46 bui.app.classic.pause() 47 48 # Need to handle out-transitions ourself for modal mode. 49 if origin_widget is not None: 50 self._transition_out = 'out_scale' 51 else: 52 self._transition_out = 'out_right' 53 54 self._r = 'playerProfilesWindow' 55 56 # Ensure we've got an account-profile in cases where we're signed in. 57 assert bui.app.classic is not None 58 bui.app.classic.accounts.ensure_have_account_player_profile() 59 60 top_extra = 20 if uiscale is bui.UIScale.SMALL else 0 61 62 super().__init__( 63 root_widget=bui.containerwidget( 64 size=(self._width, self._height + top_extra), 65 toolbar_visibility=( 66 'menu_minimal' 67 if uiscale is bui.UIScale.SMALL 68 else 'menu_full' 69 ), 70 scale=( 71 2.0 72 if uiscale is bui.UIScale.SMALL 73 else 1.5 if uiscale is bui.UIScale.MEDIUM else 1.0 74 ), 75 stack_offset=( 76 (0, -14) if uiscale is bui.UIScale.SMALL else (0, 0) 77 ), 78 ), 79 transition=transition, 80 origin_widget=origin_widget, 81 ) 82 83 if bui.app.ui_v1.uiscale is bui.UIScale.SMALL: 84 self._back_button = bui.get_special_widget('back_button') 85 bui.containerwidget( 86 edit=self._root_widget, on_cancel_call=self._back 87 ) 88 else: 89 self._back_button = btn = bui.buttonwidget( 90 parent=self._root_widget, 91 position=(40 + x_inset, self._height - 59), 92 size=(120, 60), 93 scale=0.8, 94 label=back_label, 95 button_type='back' if self._in_main_menu else None, 96 autoselect=True, 97 on_activate_call=self._back, 98 ) 99 bui.containerwidget(edit=self._root_widget, cancel_button=btn) 100 if self._in_main_menu: 101 bui.buttonwidget( 102 edit=btn, 103 button_type='backSmall', 104 size=(60, 60), 105 label=bui.charstr(bui.SpecialChar.BACK), 106 ) 107 108 bui.textwidget( 109 parent=self._root_widget, 110 position=(self._width * 0.5, self._height - 36), 111 size=(0, 0), 112 text=bui.Lstr(resource=f'{self._r}.titleText'), 113 maxwidth=300, 114 color=bui.app.ui_v1.title_color, 115 scale=0.9, 116 h_align='center', 117 v_align='center', 118 ) 119 120 scroll_height = self._height - 140.0 121 self._scroll_width = self._width - (188 + x_inset * 2) 122 v = self._height - 84.0 123 h = 50 + x_inset 124 b_color = (0.6, 0.53, 0.63) 125 126 scl = ( 127 1.055 128 if uiscale is bui.UIScale.SMALL 129 else 1.18 if uiscale is bui.UIScale.MEDIUM else 1.3 130 ) 131 v -= 70.0 * scl 132 self._new_button = bui.buttonwidget( 133 parent=self._root_widget, 134 position=(h, v), 135 size=(80, 66.0 * scl), 136 on_activate_call=self._new_profile, 137 color=b_color, 138 button_type='square', 139 autoselect=True, 140 textcolor=(0.75, 0.7, 0.8), 141 text_scale=0.7, 142 label=bui.Lstr(resource=f'{self._r}.newButtonText'), 143 ) 144 v -= 70.0 * scl 145 self._edit_button = bui.buttonwidget( 146 parent=self._root_widget, 147 position=(h, v), 148 size=(80, 66.0 * scl), 149 on_activate_call=self._edit_profile, 150 color=b_color, 151 button_type='square', 152 autoselect=True, 153 textcolor=(0.75, 0.7, 0.8), 154 text_scale=0.7, 155 label=bui.Lstr(resource=f'{self._r}.editButtonText'), 156 ) 157 v -= 70.0 * scl 158 self._delete_button = bui.buttonwidget( 159 parent=self._root_widget, 160 position=(h, v), 161 size=(80, 66.0 * scl), 162 on_activate_call=self._delete_profile, 163 color=b_color, 164 button_type='square', 165 autoselect=True, 166 textcolor=(0.75, 0.7, 0.8), 167 text_scale=0.7, 168 label=bui.Lstr(resource=f'{self._r}.deleteButtonText'), 169 ) 170 171 v = self._height - 87 172 173 bui.textwidget( 174 parent=self._root_widget, 175 position=(self._width * 0.5, self._height - 71), 176 size=(0, 0), 177 text=bui.Lstr(resource=f'{self._r}.explanationText'), 178 color=bui.app.ui_v1.infotextcolor, 179 maxwidth=self._width * 0.83, 180 scale=0.6, 181 h_align='center', 182 v_align='center', 183 ) 184 185 self._scrollwidget = bui.scrollwidget( 186 parent=self._root_widget, 187 highlight=False, 188 position=(140 + x_inset, v - scroll_height), 189 size=(self._scroll_width, scroll_height), 190 ) 191 bui.widget( 192 edit=self._scrollwidget, 193 autoselect=True, 194 left_widget=self._new_button, 195 ) 196 bui.containerwidget( 197 edit=self._root_widget, selected_child=self._scrollwidget 198 ) 199 self._subcontainer = bui.containerwidget( 200 parent=self._scrollwidget, 201 size=(self._scroll_width, 32), 202 background=False, 203 ) 204 v -= 255 205 self._profiles: dict[str, dict[str, Any]] | None = None 206 self._selected_profile = selected_profile 207 self._profile_widgets: list[bui.Widget] = [] 208 self._refresh() 209 self._restore_state() 210 211 @override 212 def get_main_window_state(self) -> bui.MainWindowState: 213 # Support recreating our window for back/refresh purposes. 214 cls = type(self) 215 return bui.BasicMainWindowState( 216 create_call=lambda transition, origin_widget: cls( 217 transition=transition, origin_widget=origin_widget 218 ) 219 ) 220 221 @override 222 def on_main_window_close(self) -> None: 223 self._save_state() 224 225 def _new_profile(self) -> None: 226 # pylint: disable=cyclic-import 227 from bauiv1lib.profile.edit import EditProfileWindow 228 from bauiv1lib.purchase import PurchaseWindow 229 230 # no-op if our underlying widget is dead or on its way out. 231 if not self._root_widget or self._root_widget.transitioning_out: 232 return 233 234 plus = bui.app.plus 235 assert plus is not None 236 237 # Limit to a handful profiles if they don't have pro-options. 238 max_non_pro_profiles = plus.get_v1_account_misc_read_val('mnpp', 5) 239 assert self._profiles is not None 240 assert bui.app.classic is not None 241 if ( 242 not bui.app.classic.accounts.have_pro_options() 243 and len(self._profiles) >= max_non_pro_profiles 244 ): 245 PurchaseWindow( 246 items=['pro'], 247 header_text=bui.Lstr( 248 resource='unlockThisProfilesText', 249 subs=[('${NUM}', str(max_non_pro_profiles))], 250 ), 251 ) 252 return 253 254 # Clamp at 100 profiles (otherwise the server will and that's less 255 # elegant looking). 256 if len(self._profiles) > 100: 257 bui.screenmessage( 258 bui.Lstr( 259 translate=( 260 'serverResponses', 261 'Max number of profiles reached.', 262 ) 263 ), 264 color=(1, 0, 0), 265 ) 266 bui.getsound('error').play() 267 return 268 269 self._save_state() 270 bui.containerwidget(edit=self._root_widget, transition='out_left') 271 bui.app.ui_v1.set_main_window( 272 EditProfileWindow( 273 existing_profile=None, in_main_menu=self._in_main_menu 274 ), 275 from_window=self if self._in_main_menu else False, 276 ) 277 278 def _delete_profile(self) -> None: 279 # pylint: disable=cyclic-import 280 from bauiv1lib import confirm 281 282 if self._selected_profile is None: 283 bui.getsound('error').play() 284 bui.screenmessage( 285 bui.Lstr(resource='nothingIsSelectedErrorText'), color=(1, 0, 0) 286 ) 287 return 288 if self._selected_profile == '__account__': 289 bui.getsound('error').play() 290 bui.screenmessage( 291 bui.Lstr(resource=f'{self._r}.cantDeleteAccountProfileText'), 292 color=(1, 0, 0), 293 ) 294 return 295 confirm.ConfirmWindow( 296 bui.Lstr( 297 resource=f'{self._r}.deleteConfirmText', 298 subs=[('${PROFILE}', self._selected_profile)], 299 ), 300 self._do_delete_profile, 301 350, 302 ) 303 304 def _do_delete_profile(self) -> None: 305 plus = bui.app.plus 306 assert plus is not None 307 308 plus.add_v1_account_transaction( 309 {'type': 'REMOVE_PLAYER_PROFILE', 'name': self._selected_profile} 310 ) 311 plus.run_v1_account_transactions() 312 bui.getsound('shieldDown').play() 313 self._refresh() 314 315 # Select profile list. 316 bui.containerwidget( 317 edit=self._root_widget, selected_child=self._scrollwidget 318 ) 319 320 def _edit_profile(self) -> None: 321 # pylint: disable=cyclic-import 322 from bauiv1lib.profile.edit import EditProfileWindow 323 324 # no-op if our underlying widget is dead or on its way out. 325 if not self._root_widget or self._root_widget.transitioning_out: 326 return 327 328 if self._selected_profile is None: 329 bui.getsound('error').play() 330 bui.screenmessage( 331 bui.Lstr(resource='nothingIsSelectedErrorText'), color=(1, 0, 0) 332 ) 333 return 334 self._save_state() 335 bui.containerwidget(edit=self._root_widget, transition='out_left') 336 assert bui.app.classic is not None 337 bui.app.ui_v1.set_main_window( 338 EditProfileWindow( 339 self._selected_profile, in_main_menu=self._in_main_menu 340 ), 341 from_window=self if self._in_main_menu else False, 342 ) 343 344 def _select(self, name: str, index: int) -> None: 345 del index # Unused. 346 self._selected_profile = name 347 348 def _back(self) -> None: 349 # pylint: disable=cyclic-import 350 351 if self._in_main_menu: 352 self.main_window_back() 353 return 354 355 # no-op if our underlying widget is dead or on its way out. 356 if not self._root_widget or self._root_widget.transitioning_out: 357 return 358 359 assert bui.app.classic is not None 360 361 self._save_state() 362 bui.containerwidget( 363 edit=self._root_widget, transition=self._transition_out 364 ) 365 # if self._in_main_menu: 366 # assert bui.app.classic is not None 367 # bui.app.ui_v1.set_main_window( 368 # AccountSettingsWindow(transition='in_left'), 369 # from_window=self, 370 # is_back=True, 371 # ) 372 373 # # If we're being called up standalone, handle pause/resume ourself. 374 # else: 375 bui.app.classic.resume() 376 377 def _refresh(self) -> None: 378 # pylint: disable=too-many-locals 379 # pylint: disable=too-many-statements 380 from efro.util import asserttype 381 from bascenev1 import PlayerProfilesChangedMessage 382 from bascenev1lib.actor import spazappearance 383 384 assert bui.app.classic is not None 385 386 plus = bui.app.plus 387 assert plus is not None 388 389 old_selection = self._selected_profile 390 391 # Delete old. 392 while self._profile_widgets: 393 self._profile_widgets.pop().delete() 394 self._profiles = bui.app.config.get('Player Profiles', {}) 395 assert self._profiles is not None 396 items = list(self._profiles.items()) 397 items.sort(key=lambda x: asserttype(x[0], str).lower()) 398 spazzes = spazappearance.get_appearances() 399 spazzes.sort() 400 icon_textures = [ 401 bui.gettexture(bui.app.classic.spaz_appearances[s].icon_texture) 402 for s in spazzes 403 ] 404 icon_tint_textures = [ 405 bui.gettexture( 406 bui.app.classic.spaz_appearances[s].icon_mask_texture 407 ) 408 for s in spazzes 409 ] 410 index = 0 411 y_val = 35 * (len(self._profiles) - 1) 412 account_name: str | None 413 if plus.get_v1_account_state() == 'signed_in': 414 account_name = plus.get_v1_account_display_string() 415 else: 416 account_name = None 417 widget_to_select = None 418 for p_name, p_info in items: 419 if p_name == '__account__' and account_name is None: 420 continue 421 color, _highlight = bui.app.classic.get_player_profile_colors( 422 p_name 423 ) 424 scl = 1.1 425 tval = ( 426 account_name 427 if p_name == '__account__' 428 else bui.app.classic.get_player_profile_icon(p_name) + p_name 429 ) 430 431 try: 432 char_index = spazzes.index(p_info['character']) 433 except Exception: 434 char_index = spazzes.index('Spaz') 435 436 assert isinstance(tval, str) 437 txtw = bui.textwidget( 438 parent=self._subcontainer, 439 position=(5, y_val), 440 size=((self._width - 210) / scl, 28), 441 text=bui.Lstr(value=f' {tval}'), 442 h_align='left', 443 v_align='center', 444 on_select_call=bui.WeakCall(self._select, p_name, index), 445 maxwidth=self._scroll_width * 0.86, 446 corner_scale=scl, 447 color=bui.safecolor(color, 0.4), 448 always_highlight=True, 449 on_activate_call=bui.Call(self._edit_button.activate), 450 selectable=True, 451 ) 452 character = bui.imagewidget( 453 parent=self._subcontainer, 454 position=(0, y_val), 455 size=(30, 30), 456 color=(1, 1, 1), 457 mask_texture=bui.gettexture('characterIconMask'), 458 tint_color=color, 459 tint2_color=_highlight, 460 texture=icon_textures[char_index], 461 tint_texture=icon_tint_textures[char_index], 462 ) 463 if index == 0: 464 bui.widget(edit=txtw, up_widget=self._back_button) 465 if self._selected_profile is None: 466 self._selected_profile = p_name 467 bui.widget(edit=txtw, show_buffer_top=40, show_buffer_bottom=40) 468 self._profile_widgets.append(txtw) 469 self._profile_widgets.append(character) 470 471 # Select/show this one if it was previously selected 472 # (but defer till after this loop since our height is 473 # still changing). 474 if p_name == old_selection: 475 widget_to_select = txtw 476 477 index += 1 478 y_val -= 35 479 480 bui.containerwidget( 481 edit=self._subcontainer, 482 size=(self._scroll_width, index * 35), 483 ) 484 if widget_to_select is not None: 485 bui.containerwidget( 486 edit=self._subcontainer, 487 selected_child=widget_to_select, 488 visible_child=widget_to_select, 489 ) 490 491 # If there's a team-chooser in existence, tell it the profile-list 492 # has probably changed. 493 session = bs.get_foreground_host_session() 494 if session is not None: 495 session.handlemessage(PlayerProfilesChangedMessage()) 496 497 def _save_state(self) -> None: 498 try: 499 sel = self._root_widget.get_selected_child() 500 if sel == self._new_button: 501 sel_name = 'New' 502 elif sel == self._edit_button: 503 sel_name = 'Edit' 504 elif sel == self._delete_button: 505 sel_name = 'Delete' 506 elif sel == self._scrollwidget: 507 sel_name = 'Scroll' 508 else: 509 sel_name = 'Back' 510 assert bui.app.classic is not None 511 bui.app.ui_v1.window_states[type(self)] = sel_name 512 except Exception: 513 logging.exception('Error saving state for %s.', self) 514 515 def _restore_state(self) -> None: 516 try: 517 assert bui.app.classic is not None 518 sel_name = bui.app.ui_v1.window_states.get(type(self)) 519 if sel_name == 'Scroll': 520 sel = self._scrollwidget 521 elif sel_name == 'New': 522 sel = self._new_button 523 elif sel_name == 'Delete': 524 sel = self._delete_button 525 elif sel_name == 'Edit': 526 sel = self._edit_button 527 elif sel_name == 'Back': 528 sel = self._back_button 529 else: 530 # By default we select our scroll widget if we have profiles; 531 # otherwise our new widget. 532 if not self._profile_widgets: 533 sel = self._new_button 534 else: 535 sel = self._scrollwidget 536 bui.containerwidget(edit=self._root_widget, selected_child=sel) 537 except Exception: 538 logging.exception('Error restoring state for %s.', self)
class
ProfileBrowserWindow(bauiv1._uitypes.MainWindow):
18class ProfileBrowserWindow(bui.MainWindow): 19 """Window for browsing player profiles.""" 20 21 def __init__( 22 self, 23 transition: str | None = 'in_right', 24 in_main_menu: bool = True, 25 selected_profile: str | None = None, 26 origin_widget: bui.Widget | None = None, 27 ): 28 # pylint: disable=too-many-statements 29 self._in_main_menu = in_main_menu 30 if self._in_main_menu: 31 back_label = bui.Lstr(resource='backText') 32 else: 33 back_label = bui.Lstr(resource='doneText') 34 assert bui.app.classic is not None 35 uiscale = bui.app.ui_v1.uiscale 36 self._width = 800.0 if uiscale is bui.UIScale.SMALL else 600.0 37 x_inset = 100.0 if uiscale is bui.UIScale.SMALL else 0.0 38 self._height = ( 39 360.0 40 if uiscale is bui.UIScale.SMALL 41 else 385.0 if uiscale is bui.UIScale.MEDIUM else 410.0 42 ) 43 44 # If we're being called up standalone, handle pause/resume ourself. 45 if not self._in_main_menu: 46 assert bui.app.classic is not None 47 bui.app.classic.pause() 48 49 # Need to handle out-transitions ourself for modal mode. 50 if origin_widget is not None: 51 self._transition_out = 'out_scale' 52 else: 53 self._transition_out = 'out_right' 54 55 self._r = 'playerProfilesWindow' 56 57 # Ensure we've got an account-profile in cases where we're signed in. 58 assert bui.app.classic is not None 59 bui.app.classic.accounts.ensure_have_account_player_profile() 60 61 top_extra = 20 if uiscale is bui.UIScale.SMALL else 0 62 63 super().__init__( 64 root_widget=bui.containerwidget( 65 size=(self._width, self._height + top_extra), 66 toolbar_visibility=( 67 'menu_minimal' 68 if uiscale is bui.UIScale.SMALL 69 else 'menu_full' 70 ), 71 scale=( 72 2.0 73 if uiscale is bui.UIScale.SMALL 74 else 1.5 if uiscale is bui.UIScale.MEDIUM else 1.0 75 ), 76 stack_offset=( 77 (0, -14) if uiscale is bui.UIScale.SMALL else (0, 0) 78 ), 79 ), 80 transition=transition, 81 origin_widget=origin_widget, 82 ) 83 84 if bui.app.ui_v1.uiscale is bui.UIScale.SMALL: 85 self._back_button = bui.get_special_widget('back_button') 86 bui.containerwidget( 87 edit=self._root_widget, on_cancel_call=self._back 88 ) 89 else: 90 self._back_button = btn = bui.buttonwidget( 91 parent=self._root_widget, 92 position=(40 + x_inset, self._height - 59), 93 size=(120, 60), 94 scale=0.8, 95 label=back_label, 96 button_type='back' if self._in_main_menu else None, 97 autoselect=True, 98 on_activate_call=self._back, 99 ) 100 bui.containerwidget(edit=self._root_widget, cancel_button=btn) 101 if self._in_main_menu: 102 bui.buttonwidget( 103 edit=btn, 104 button_type='backSmall', 105 size=(60, 60), 106 label=bui.charstr(bui.SpecialChar.BACK), 107 ) 108 109 bui.textwidget( 110 parent=self._root_widget, 111 position=(self._width * 0.5, self._height - 36), 112 size=(0, 0), 113 text=bui.Lstr(resource=f'{self._r}.titleText'), 114 maxwidth=300, 115 color=bui.app.ui_v1.title_color, 116 scale=0.9, 117 h_align='center', 118 v_align='center', 119 ) 120 121 scroll_height = self._height - 140.0 122 self._scroll_width = self._width - (188 + x_inset * 2) 123 v = self._height - 84.0 124 h = 50 + x_inset 125 b_color = (0.6, 0.53, 0.63) 126 127 scl = ( 128 1.055 129 if uiscale is bui.UIScale.SMALL 130 else 1.18 if uiscale is bui.UIScale.MEDIUM else 1.3 131 ) 132 v -= 70.0 * scl 133 self._new_button = bui.buttonwidget( 134 parent=self._root_widget, 135 position=(h, v), 136 size=(80, 66.0 * scl), 137 on_activate_call=self._new_profile, 138 color=b_color, 139 button_type='square', 140 autoselect=True, 141 textcolor=(0.75, 0.7, 0.8), 142 text_scale=0.7, 143 label=bui.Lstr(resource=f'{self._r}.newButtonText'), 144 ) 145 v -= 70.0 * scl 146 self._edit_button = bui.buttonwidget( 147 parent=self._root_widget, 148 position=(h, v), 149 size=(80, 66.0 * scl), 150 on_activate_call=self._edit_profile, 151 color=b_color, 152 button_type='square', 153 autoselect=True, 154 textcolor=(0.75, 0.7, 0.8), 155 text_scale=0.7, 156 label=bui.Lstr(resource=f'{self._r}.editButtonText'), 157 ) 158 v -= 70.0 * scl 159 self._delete_button = bui.buttonwidget( 160 parent=self._root_widget, 161 position=(h, v), 162 size=(80, 66.0 * scl), 163 on_activate_call=self._delete_profile, 164 color=b_color, 165 button_type='square', 166 autoselect=True, 167 textcolor=(0.75, 0.7, 0.8), 168 text_scale=0.7, 169 label=bui.Lstr(resource=f'{self._r}.deleteButtonText'), 170 ) 171 172 v = self._height - 87 173 174 bui.textwidget( 175 parent=self._root_widget, 176 position=(self._width * 0.5, self._height - 71), 177 size=(0, 0), 178 text=bui.Lstr(resource=f'{self._r}.explanationText'), 179 color=bui.app.ui_v1.infotextcolor, 180 maxwidth=self._width * 0.83, 181 scale=0.6, 182 h_align='center', 183 v_align='center', 184 ) 185 186 self._scrollwidget = bui.scrollwidget( 187 parent=self._root_widget, 188 highlight=False, 189 position=(140 + x_inset, v - scroll_height), 190 size=(self._scroll_width, scroll_height), 191 ) 192 bui.widget( 193 edit=self._scrollwidget, 194 autoselect=True, 195 left_widget=self._new_button, 196 ) 197 bui.containerwidget( 198 edit=self._root_widget, selected_child=self._scrollwidget 199 ) 200 self._subcontainer = bui.containerwidget( 201 parent=self._scrollwidget, 202 size=(self._scroll_width, 32), 203 background=False, 204 ) 205 v -= 255 206 self._profiles: dict[str, dict[str, Any]] | None = None 207 self._selected_profile = selected_profile 208 self._profile_widgets: list[bui.Widget] = [] 209 self._refresh() 210 self._restore_state() 211 212 @override 213 def get_main_window_state(self) -> bui.MainWindowState: 214 # Support recreating our window for back/refresh purposes. 215 cls = type(self) 216 return bui.BasicMainWindowState( 217 create_call=lambda transition, origin_widget: cls( 218 transition=transition, origin_widget=origin_widget 219 ) 220 ) 221 222 @override 223 def on_main_window_close(self) -> None: 224 self._save_state() 225 226 def _new_profile(self) -> None: 227 # pylint: disable=cyclic-import 228 from bauiv1lib.profile.edit import EditProfileWindow 229 from bauiv1lib.purchase import PurchaseWindow 230 231 # no-op if our underlying widget is dead or on its way out. 232 if not self._root_widget or self._root_widget.transitioning_out: 233 return 234 235 plus = bui.app.plus 236 assert plus is not None 237 238 # Limit to a handful profiles if they don't have pro-options. 239 max_non_pro_profiles = plus.get_v1_account_misc_read_val('mnpp', 5) 240 assert self._profiles is not None 241 assert bui.app.classic is not None 242 if ( 243 not bui.app.classic.accounts.have_pro_options() 244 and len(self._profiles) >= max_non_pro_profiles 245 ): 246 PurchaseWindow( 247 items=['pro'], 248 header_text=bui.Lstr( 249 resource='unlockThisProfilesText', 250 subs=[('${NUM}', str(max_non_pro_profiles))], 251 ), 252 ) 253 return 254 255 # Clamp at 100 profiles (otherwise the server will and that's less 256 # elegant looking). 257 if len(self._profiles) > 100: 258 bui.screenmessage( 259 bui.Lstr( 260 translate=( 261 'serverResponses', 262 'Max number of profiles reached.', 263 ) 264 ), 265 color=(1, 0, 0), 266 ) 267 bui.getsound('error').play() 268 return 269 270 self._save_state() 271 bui.containerwidget(edit=self._root_widget, transition='out_left') 272 bui.app.ui_v1.set_main_window( 273 EditProfileWindow( 274 existing_profile=None, in_main_menu=self._in_main_menu 275 ), 276 from_window=self if self._in_main_menu else False, 277 ) 278 279 def _delete_profile(self) -> None: 280 # pylint: disable=cyclic-import 281 from bauiv1lib import confirm 282 283 if self._selected_profile is None: 284 bui.getsound('error').play() 285 bui.screenmessage( 286 bui.Lstr(resource='nothingIsSelectedErrorText'), color=(1, 0, 0) 287 ) 288 return 289 if self._selected_profile == '__account__': 290 bui.getsound('error').play() 291 bui.screenmessage( 292 bui.Lstr(resource=f'{self._r}.cantDeleteAccountProfileText'), 293 color=(1, 0, 0), 294 ) 295 return 296 confirm.ConfirmWindow( 297 bui.Lstr( 298 resource=f'{self._r}.deleteConfirmText', 299 subs=[('${PROFILE}', self._selected_profile)], 300 ), 301 self._do_delete_profile, 302 350, 303 ) 304 305 def _do_delete_profile(self) -> None: 306 plus = bui.app.plus 307 assert plus is not None 308 309 plus.add_v1_account_transaction( 310 {'type': 'REMOVE_PLAYER_PROFILE', 'name': self._selected_profile} 311 ) 312 plus.run_v1_account_transactions() 313 bui.getsound('shieldDown').play() 314 self._refresh() 315 316 # Select profile list. 317 bui.containerwidget( 318 edit=self._root_widget, selected_child=self._scrollwidget 319 ) 320 321 def _edit_profile(self) -> None: 322 # pylint: disable=cyclic-import 323 from bauiv1lib.profile.edit import EditProfileWindow 324 325 # no-op if our underlying widget is dead or on its way out. 326 if not self._root_widget or self._root_widget.transitioning_out: 327 return 328 329 if self._selected_profile is None: 330 bui.getsound('error').play() 331 bui.screenmessage( 332 bui.Lstr(resource='nothingIsSelectedErrorText'), color=(1, 0, 0) 333 ) 334 return 335 self._save_state() 336 bui.containerwidget(edit=self._root_widget, transition='out_left') 337 assert bui.app.classic is not None 338 bui.app.ui_v1.set_main_window( 339 EditProfileWindow( 340 self._selected_profile, in_main_menu=self._in_main_menu 341 ), 342 from_window=self if self._in_main_menu else False, 343 ) 344 345 def _select(self, name: str, index: int) -> None: 346 del index # Unused. 347 self._selected_profile = name 348 349 def _back(self) -> None: 350 # pylint: disable=cyclic-import 351 352 if self._in_main_menu: 353 self.main_window_back() 354 return 355 356 # no-op if our underlying widget is dead or on its way out. 357 if not self._root_widget or self._root_widget.transitioning_out: 358 return 359 360 assert bui.app.classic is not None 361 362 self._save_state() 363 bui.containerwidget( 364 edit=self._root_widget, transition=self._transition_out 365 ) 366 # if self._in_main_menu: 367 # assert bui.app.classic is not None 368 # bui.app.ui_v1.set_main_window( 369 # AccountSettingsWindow(transition='in_left'), 370 # from_window=self, 371 # is_back=True, 372 # ) 373 374 # # If we're being called up standalone, handle pause/resume ourself. 375 # else: 376 bui.app.classic.resume() 377 378 def _refresh(self) -> None: 379 # pylint: disable=too-many-locals 380 # pylint: disable=too-many-statements 381 from efro.util import asserttype 382 from bascenev1 import PlayerProfilesChangedMessage 383 from bascenev1lib.actor import spazappearance 384 385 assert bui.app.classic is not None 386 387 plus = bui.app.plus 388 assert plus is not None 389 390 old_selection = self._selected_profile 391 392 # Delete old. 393 while self._profile_widgets: 394 self._profile_widgets.pop().delete() 395 self._profiles = bui.app.config.get('Player Profiles', {}) 396 assert self._profiles is not None 397 items = list(self._profiles.items()) 398 items.sort(key=lambda x: asserttype(x[0], str).lower()) 399 spazzes = spazappearance.get_appearances() 400 spazzes.sort() 401 icon_textures = [ 402 bui.gettexture(bui.app.classic.spaz_appearances[s].icon_texture) 403 for s in spazzes 404 ] 405 icon_tint_textures = [ 406 bui.gettexture( 407 bui.app.classic.spaz_appearances[s].icon_mask_texture 408 ) 409 for s in spazzes 410 ] 411 index = 0 412 y_val = 35 * (len(self._profiles) - 1) 413 account_name: str | None 414 if plus.get_v1_account_state() == 'signed_in': 415 account_name = plus.get_v1_account_display_string() 416 else: 417 account_name = None 418 widget_to_select = None 419 for p_name, p_info in items: 420 if p_name == '__account__' and account_name is None: 421 continue 422 color, _highlight = bui.app.classic.get_player_profile_colors( 423 p_name 424 ) 425 scl = 1.1 426 tval = ( 427 account_name 428 if p_name == '__account__' 429 else bui.app.classic.get_player_profile_icon(p_name) + p_name 430 ) 431 432 try: 433 char_index = spazzes.index(p_info['character']) 434 except Exception: 435 char_index = spazzes.index('Spaz') 436 437 assert isinstance(tval, str) 438 txtw = bui.textwidget( 439 parent=self._subcontainer, 440 position=(5, y_val), 441 size=((self._width - 210) / scl, 28), 442 text=bui.Lstr(value=f' {tval}'), 443 h_align='left', 444 v_align='center', 445 on_select_call=bui.WeakCall(self._select, p_name, index), 446 maxwidth=self._scroll_width * 0.86, 447 corner_scale=scl, 448 color=bui.safecolor(color, 0.4), 449 always_highlight=True, 450 on_activate_call=bui.Call(self._edit_button.activate), 451 selectable=True, 452 ) 453 character = bui.imagewidget( 454 parent=self._subcontainer, 455 position=(0, y_val), 456 size=(30, 30), 457 color=(1, 1, 1), 458 mask_texture=bui.gettexture('characterIconMask'), 459 tint_color=color, 460 tint2_color=_highlight, 461 texture=icon_textures[char_index], 462 tint_texture=icon_tint_textures[char_index], 463 ) 464 if index == 0: 465 bui.widget(edit=txtw, up_widget=self._back_button) 466 if self._selected_profile is None: 467 self._selected_profile = p_name 468 bui.widget(edit=txtw, show_buffer_top=40, show_buffer_bottom=40) 469 self._profile_widgets.append(txtw) 470 self._profile_widgets.append(character) 471 472 # Select/show this one if it was previously selected 473 # (but defer till after this loop since our height is 474 # still changing). 475 if p_name == old_selection: 476 widget_to_select = txtw 477 478 index += 1 479 y_val -= 35 480 481 bui.containerwidget( 482 edit=self._subcontainer, 483 size=(self._scroll_width, index * 35), 484 ) 485 if widget_to_select is not None: 486 bui.containerwidget( 487 edit=self._subcontainer, 488 selected_child=widget_to_select, 489 visible_child=widget_to_select, 490 ) 491 492 # If there's a team-chooser in existence, tell it the profile-list 493 # has probably changed. 494 session = bs.get_foreground_host_session() 495 if session is not None: 496 session.handlemessage(PlayerProfilesChangedMessage()) 497 498 def _save_state(self) -> None: 499 try: 500 sel = self._root_widget.get_selected_child() 501 if sel == self._new_button: 502 sel_name = 'New' 503 elif sel == self._edit_button: 504 sel_name = 'Edit' 505 elif sel == self._delete_button: 506 sel_name = 'Delete' 507 elif sel == self._scrollwidget: 508 sel_name = 'Scroll' 509 else: 510 sel_name = 'Back' 511 assert bui.app.classic is not None 512 bui.app.ui_v1.window_states[type(self)] = sel_name 513 except Exception: 514 logging.exception('Error saving state for %s.', self) 515 516 def _restore_state(self) -> None: 517 try: 518 assert bui.app.classic is not None 519 sel_name = bui.app.ui_v1.window_states.get(type(self)) 520 if sel_name == 'Scroll': 521 sel = self._scrollwidget 522 elif sel_name == 'New': 523 sel = self._new_button 524 elif sel_name == 'Delete': 525 sel = self._delete_button 526 elif sel_name == 'Edit': 527 sel = self._edit_button 528 elif sel_name == 'Back': 529 sel = self._back_button 530 else: 531 # By default we select our scroll widget if we have profiles; 532 # otherwise our new widget. 533 if not self._profile_widgets: 534 sel = self._new_button 535 else: 536 sel = self._scrollwidget 537 bui.containerwidget(edit=self._root_widget, selected_child=sel) 538 except Exception: 539 logging.exception('Error restoring state for %s.', self)
Window for browsing player profiles.
ProfileBrowserWindow( transition: str | None = 'in_right', in_main_menu: bool = True, selected_profile: str | None = None, origin_widget: _bauiv1.Widget | None = None)
21 def __init__( 22 self, 23 transition: str | None = 'in_right', 24 in_main_menu: bool = True, 25 selected_profile: str | None = None, 26 origin_widget: bui.Widget | None = None, 27 ): 28 # pylint: disable=too-many-statements 29 self._in_main_menu = in_main_menu 30 if self._in_main_menu: 31 back_label = bui.Lstr(resource='backText') 32 else: 33 back_label = bui.Lstr(resource='doneText') 34 assert bui.app.classic is not None 35 uiscale = bui.app.ui_v1.uiscale 36 self._width = 800.0 if uiscale is bui.UIScale.SMALL else 600.0 37 x_inset = 100.0 if uiscale is bui.UIScale.SMALL else 0.0 38 self._height = ( 39 360.0 40 if uiscale is bui.UIScale.SMALL 41 else 385.0 if uiscale is bui.UIScale.MEDIUM else 410.0 42 ) 43 44 # If we're being called up standalone, handle pause/resume ourself. 45 if not self._in_main_menu: 46 assert bui.app.classic is not None 47 bui.app.classic.pause() 48 49 # Need to handle out-transitions ourself for modal mode. 50 if origin_widget is not None: 51 self._transition_out = 'out_scale' 52 else: 53 self._transition_out = 'out_right' 54 55 self._r = 'playerProfilesWindow' 56 57 # Ensure we've got an account-profile in cases where we're signed in. 58 assert bui.app.classic is not None 59 bui.app.classic.accounts.ensure_have_account_player_profile() 60 61 top_extra = 20 if uiscale is bui.UIScale.SMALL else 0 62 63 super().__init__( 64 root_widget=bui.containerwidget( 65 size=(self._width, self._height + top_extra), 66 toolbar_visibility=( 67 'menu_minimal' 68 if uiscale is bui.UIScale.SMALL 69 else 'menu_full' 70 ), 71 scale=( 72 2.0 73 if uiscale is bui.UIScale.SMALL 74 else 1.5 if uiscale is bui.UIScale.MEDIUM else 1.0 75 ), 76 stack_offset=( 77 (0, -14) if uiscale is bui.UIScale.SMALL else (0, 0) 78 ), 79 ), 80 transition=transition, 81 origin_widget=origin_widget, 82 ) 83 84 if bui.app.ui_v1.uiscale is bui.UIScale.SMALL: 85 self._back_button = bui.get_special_widget('back_button') 86 bui.containerwidget( 87 edit=self._root_widget, on_cancel_call=self._back 88 ) 89 else: 90 self._back_button = btn = bui.buttonwidget( 91 parent=self._root_widget, 92 position=(40 + x_inset, self._height - 59), 93 size=(120, 60), 94 scale=0.8, 95 label=back_label, 96 button_type='back' if self._in_main_menu else None, 97 autoselect=True, 98 on_activate_call=self._back, 99 ) 100 bui.containerwidget(edit=self._root_widget, cancel_button=btn) 101 if self._in_main_menu: 102 bui.buttonwidget( 103 edit=btn, 104 button_type='backSmall', 105 size=(60, 60), 106 label=bui.charstr(bui.SpecialChar.BACK), 107 ) 108 109 bui.textwidget( 110 parent=self._root_widget, 111 position=(self._width * 0.5, self._height - 36), 112 size=(0, 0), 113 text=bui.Lstr(resource=f'{self._r}.titleText'), 114 maxwidth=300, 115 color=bui.app.ui_v1.title_color, 116 scale=0.9, 117 h_align='center', 118 v_align='center', 119 ) 120 121 scroll_height = self._height - 140.0 122 self._scroll_width = self._width - (188 + x_inset * 2) 123 v = self._height - 84.0 124 h = 50 + x_inset 125 b_color = (0.6, 0.53, 0.63) 126 127 scl = ( 128 1.055 129 if uiscale is bui.UIScale.SMALL 130 else 1.18 if uiscale is bui.UIScale.MEDIUM else 1.3 131 ) 132 v -= 70.0 * scl 133 self._new_button = bui.buttonwidget( 134 parent=self._root_widget, 135 position=(h, v), 136 size=(80, 66.0 * scl), 137 on_activate_call=self._new_profile, 138 color=b_color, 139 button_type='square', 140 autoselect=True, 141 textcolor=(0.75, 0.7, 0.8), 142 text_scale=0.7, 143 label=bui.Lstr(resource=f'{self._r}.newButtonText'), 144 ) 145 v -= 70.0 * scl 146 self._edit_button = bui.buttonwidget( 147 parent=self._root_widget, 148 position=(h, v), 149 size=(80, 66.0 * scl), 150 on_activate_call=self._edit_profile, 151 color=b_color, 152 button_type='square', 153 autoselect=True, 154 textcolor=(0.75, 0.7, 0.8), 155 text_scale=0.7, 156 label=bui.Lstr(resource=f'{self._r}.editButtonText'), 157 ) 158 v -= 70.0 * scl 159 self._delete_button = bui.buttonwidget( 160 parent=self._root_widget, 161 position=(h, v), 162 size=(80, 66.0 * scl), 163 on_activate_call=self._delete_profile, 164 color=b_color, 165 button_type='square', 166 autoselect=True, 167 textcolor=(0.75, 0.7, 0.8), 168 text_scale=0.7, 169 label=bui.Lstr(resource=f'{self._r}.deleteButtonText'), 170 ) 171 172 v = self._height - 87 173 174 bui.textwidget( 175 parent=self._root_widget, 176 position=(self._width * 0.5, self._height - 71), 177 size=(0, 0), 178 text=bui.Lstr(resource=f'{self._r}.explanationText'), 179 color=bui.app.ui_v1.infotextcolor, 180 maxwidth=self._width * 0.83, 181 scale=0.6, 182 h_align='center', 183 v_align='center', 184 ) 185 186 self._scrollwidget = bui.scrollwidget( 187 parent=self._root_widget, 188 highlight=False, 189 position=(140 + x_inset, v - scroll_height), 190 size=(self._scroll_width, scroll_height), 191 ) 192 bui.widget( 193 edit=self._scrollwidget, 194 autoselect=True, 195 left_widget=self._new_button, 196 ) 197 bui.containerwidget( 198 edit=self._root_widget, selected_child=self._scrollwidget 199 ) 200 self._subcontainer = bui.containerwidget( 201 parent=self._scrollwidget, 202 size=(self._scroll_width, 32), 203 background=False, 204 ) 205 v -= 255 206 self._profiles: dict[str, dict[str, Any]] | None = None 207 self._selected_profile = selected_profile 208 self._profile_widgets: list[bui.Widget] = [] 209 self._refresh() 210 self._restore_state()
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.
212 @override 213 def get_main_window_state(self) -> bui.MainWindowState: 214 # Support recreating our window for back/refresh purposes. 215 cls = type(self) 216 return bui.BasicMainWindowState( 217 create_call=lambda transition, origin_widget: cls( 218 transition=transition, origin_widget=origin_widget 219 ) 220 )
Return a WindowState to recreate this window, if supported.
@override
def
on_main_window_close(self) -> None:
Called before transitioning out a main window.
A good opportunity to save window state/etc.
Inherited Members
- bauiv1._uitypes.MainWindow
- main_window_back_state
- main_window_close
- can_change_main_window
- main_window_back
- main_window_replace
- bauiv1._uitypes.Window
- get_root_widget