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