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.5 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 plus = bui.app.plus 231 assert plus is not None 232 233 # Limit to a handful profiles if they don't have pro-options. 234 max_non_pro_profiles = plus.get_v1_account_misc_read_val('mnpp', 5) 235 assert self._profiles is not None 236 assert bui.app.classic is not None 237 if ( 238 bool(False) # Phasing out pro. 239 and not bui.app.classic.accounts.have_pro_options() 240 and len(self._profiles) >= max_non_pro_profiles 241 ): 242 PurchaseWindow( 243 items=['pro'], 244 header_text=bui.Lstr( 245 resource='unlockThisProfilesText', 246 subs=[('${NUM}', str(max_non_pro_profiles))], 247 ), 248 ) 249 return 250 251 # Clamp at 100 profiles (otherwise the server will and that's less 252 # elegant looking). 253 if len(self._profiles) > 100: 254 bui.screenmessage( 255 bui.Lstr( 256 translate=( 257 'serverResponses', 258 'Max number of profiles reached.', 259 ) 260 ), 261 color=(1, 0, 0), 262 ) 263 bui.getsound('error').play() 264 return 265 266 self.main_window_replace(EditProfileWindow(existing_profile=None)) 267 268 def _delete_profile(self) -> None: 269 # pylint: disable=cyclic-import 270 from bauiv1lib import confirm 271 272 if self._selected_profile is None: 273 bui.getsound('error').play() 274 bui.screenmessage( 275 bui.Lstr(resource='nothingIsSelectedErrorText'), color=(1, 0, 0) 276 ) 277 return 278 if self._selected_profile == '__account__': 279 bui.getsound('error').play() 280 bui.screenmessage( 281 bui.Lstr(resource=f'{self._r}.cantDeleteAccountProfileText'), 282 color=(1, 0, 0), 283 ) 284 return 285 confirm.ConfirmWindow( 286 bui.Lstr( 287 resource=f'{self._r}.deleteConfirmText', 288 subs=[('${PROFILE}', self._selected_profile)], 289 ), 290 self._do_delete_profile, 291 350, 292 ) 293 294 def _do_delete_profile(self) -> None: 295 plus = bui.app.plus 296 assert plus is not None 297 298 plus.add_v1_account_transaction( 299 {'type': 'REMOVE_PLAYER_PROFILE', 'name': self._selected_profile} 300 ) 301 plus.run_v1_account_transactions() 302 bui.getsound('shieldDown').play() 303 self._refresh() 304 305 # Select profile list. 306 bui.containerwidget( 307 edit=self._root_widget, selected_child=self._scrollwidget 308 ) 309 310 def _edit_profile(self) -> None: 311 # pylint: disable=cyclic-import 312 from bauiv1lib.profile.edit import EditProfileWindow 313 314 # No-op if we're not in control. 315 if not self.main_window_has_control(): 316 return 317 318 if self._selected_profile is None: 319 bui.getsound('error').play() 320 bui.screenmessage( 321 bui.Lstr(resource='nothingIsSelectedErrorText'), color=(1, 0, 0) 322 ) 323 return 324 325 self.main_window_replace(EditProfileWindow(self._selected_profile)) 326 327 def _select(self, name: str, index: int) -> None: 328 del index # Unused. 329 self._selected_profile = name 330 331 def _refresh(self) -> None: 332 # pylint: disable=too-many-locals 333 # pylint: disable=too-many-statements 334 from efro.util import asserttype 335 from bascenev1 import PlayerProfilesChangedMessage 336 from bascenev1lib.actor import spazappearance 337 338 assert bui.app.classic is not None 339 340 plus = bui.app.plus 341 assert plus is not None 342 343 old_selection = self._selected_profile 344 345 # Delete old. 346 while self._profile_widgets: 347 self._profile_widgets.pop().delete() 348 self._profiles = bui.app.config.get('Player Profiles', {}) 349 assert self._profiles is not None 350 items = list(self._profiles.items()) 351 items.sort(key=lambda x: asserttype(x[0], str).lower()) 352 spazzes = spazappearance.get_appearances() 353 spazzes.sort() 354 icon_textures = [ 355 bui.gettexture(bui.app.classic.spaz_appearances[s].icon_texture) 356 for s in spazzes 357 ] 358 icon_tint_textures = [ 359 bui.gettexture( 360 bui.app.classic.spaz_appearances[s].icon_mask_texture 361 ) 362 for s in spazzes 363 ] 364 index = 0 365 y_val = 35 * (len(self._profiles) - 1) 366 account_name: str | None 367 if plus.get_v1_account_state() == 'signed_in': 368 account_name = plus.get_v1_account_display_string() 369 else: 370 account_name = None 371 widget_to_select = None 372 for p_name, p_info in items: 373 if p_name == '__account__' and account_name is None: 374 continue 375 color, _highlight = bui.app.classic.get_player_profile_colors( 376 p_name 377 ) 378 scl = 1.1 379 tval = ( 380 account_name 381 if p_name == '__account__' 382 else bui.app.classic.get_player_profile_icon(p_name) + p_name 383 ) 384 385 try: 386 char_index = spazzes.index(p_info['character']) 387 except Exception: 388 char_index = spazzes.index('Spaz') 389 390 assert isinstance(tval, str) 391 txtw = bui.textwidget( 392 parent=self._subcontainer, 393 position=(5, y_val), 394 size=((self._width - 210) / scl, 28), 395 text=bui.Lstr(value=f' {tval}'), 396 h_align='left', 397 v_align='center', 398 on_select_call=bui.WeakCall(self._select, p_name, index), 399 maxwidth=self._scroll_width * 0.86, 400 corner_scale=scl, 401 color=bui.safecolor(color, 0.4), 402 always_highlight=True, 403 on_activate_call=bui.Call(self._edit_button.activate), 404 selectable=True, 405 ) 406 character = bui.imagewidget( 407 parent=self._subcontainer, 408 position=(0, y_val), 409 size=(30, 30), 410 color=(1, 1, 1), 411 mask_texture=bui.gettexture('characterIconMask'), 412 tint_color=color, 413 tint2_color=_highlight, 414 texture=icon_textures[char_index], 415 tint_texture=icon_tint_textures[char_index], 416 ) 417 if index == 0: 418 bui.widget(edit=txtw, up_widget=self._back_button) 419 if self._selected_profile is None: 420 self._selected_profile = p_name 421 bui.widget(edit=txtw, show_buffer_top=40, show_buffer_bottom=40) 422 self._profile_widgets.append(txtw) 423 self._profile_widgets.append(character) 424 425 # Select/show this one if it was previously selected 426 # (but defer till after this loop since our height is 427 # still changing). 428 if p_name == old_selection: 429 widget_to_select = txtw 430 431 index += 1 432 y_val -= 35 433 434 bui.containerwidget( 435 edit=self._subcontainer, 436 size=(self._scroll_width, index * 35), 437 ) 438 if widget_to_select is not None: 439 bui.containerwidget( 440 edit=self._subcontainer, 441 selected_child=widget_to_select, 442 visible_child=widget_to_select, 443 ) 444 445 # If there's a team-chooser in existence, tell it the profile-list 446 # has probably changed. 447 session = bs.get_foreground_host_session() 448 if session is not None: 449 session.handlemessage(PlayerProfilesChangedMessage()) 450 451 def _save_state(self) -> None: 452 try: 453 sel = self._root_widget.get_selected_child() 454 if sel == self._new_button: 455 sel_name = 'New' 456 elif sel == self._edit_button: 457 sel_name = 'Edit' 458 elif sel == self._delete_button: 459 sel_name = 'Delete' 460 elif sel == self._scrollwidget: 461 sel_name = 'Scroll' 462 else: 463 sel_name = 'Back' 464 assert bui.app.classic is not None 465 bui.app.ui_v1.window_states[type(self)] = sel_name 466 except Exception: 467 logging.exception('Error saving state for %s.', self) 468 469 def _restore_state(self) -> None: 470 try: 471 assert bui.app.classic is not None 472 sel_name = bui.app.ui_v1.window_states.get(type(self)) 473 if sel_name == 'Scroll': 474 sel = self._scrollwidget 475 elif sel_name == 'New': 476 sel = self._new_button 477 elif sel_name == 'Delete': 478 sel = self._delete_button 479 elif sel_name == 'Edit': 480 sel = self._edit_button 481 elif sel_name == 'Back': 482 sel = self._back_button 483 else: 484 # By default we select our scroll widget if we have profiles; 485 # otherwise our new widget. 486 if not self._profile_widgets: 487 sel = self._new_button 488 else: 489 sel = self._scrollwidget 490 bui.containerwidget(edit=self._root_widget, selected_child=sel) 491 except Exception: 492 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.5 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 plus = bui.app.plus 232 assert plus is not None 233 234 # Limit to a handful profiles if they don't have pro-options. 235 max_non_pro_profiles = plus.get_v1_account_misc_read_val('mnpp', 5) 236 assert self._profiles is not None 237 assert bui.app.classic is not None 238 if ( 239 bool(False) # Phasing out pro. 240 and not bui.app.classic.accounts.have_pro_options() 241 and len(self._profiles) >= max_non_pro_profiles 242 ): 243 PurchaseWindow( 244 items=['pro'], 245 header_text=bui.Lstr( 246 resource='unlockThisProfilesText', 247 subs=[('${NUM}', str(max_non_pro_profiles))], 248 ), 249 ) 250 return 251 252 # Clamp at 100 profiles (otherwise the server will and that's less 253 # elegant looking). 254 if len(self._profiles) > 100: 255 bui.screenmessage( 256 bui.Lstr( 257 translate=( 258 'serverResponses', 259 'Max number of profiles reached.', 260 ) 261 ), 262 color=(1, 0, 0), 263 ) 264 bui.getsound('error').play() 265 return 266 267 self.main_window_replace(EditProfileWindow(existing_profile=None)) 268 269 def _delete_profile(self) -> None: 270 # pylint: disable=cyclic-import 271 from bauiv1lib import confirm 272 273 if self._selected_profile is None: 274 bui.getsound('error').play() 275 bui.screenmessage( 276 bui.Lstr(resource='nothingIsSelectedErrorText'), color=(1, 0, 0) 277 ) 278 return 279 if self._selected_profile == '__account__': 280 bui.getsound('error').play() 281 bui.screenmessage( 282 bui.Lstr(resource=f'{self._r}.cantDeleteAccountProfileText'), 283 color=(1, 0, 0), 284 ) 285 return 286 confirm.ConfirmWindow( 287 bui.Lstr( 288 resource=f'{self._r}.deleteConfirmText', 289 subs=[('${PROFILE}', self._selected_profile)], 290 ), 291 self._do_delete_profile, 292 350, 293 ) 294 295 def _do_delete_profile(self) -> None: 296 plus = bui.app.plus 297 assert plus is not None 298 299 plus.add_v1_account_transaction( 300 {'type': 'REMOVE_PLAYER_PROFILE', 'name': self._selected_profile} 301 ) 302 plus.run_v1_account_transactions() 303 bui.getsound('shieldDown').play() 304 self._refresh() 305 306 # Select profile list. 307 bui.containerwidget( 308 edit=self._root_widget, selected_child=self._scrollwidget 309 ) 310 311 def _edit_profile(self) -> None: 312 # pylint: disable=cyclic-import 313 from bauiv1lib.profile.edit import EditProfileWindow 314 315 # No-op if we're not in control. 316 if not self.main_window_has_control(): 317 return 318 319 if self._selected_profile is None: 320 bui.getsound('error').play() 321 bui.screenmessage( 322 bui.Lstr(resource='nothingIsSelectedErrorText'), color=(1, 0, 0) 323 ) 324 return 325 326 self.main_window_replace(EditProfileWindow(self._selected_profile)) 327 328 def _select(self, name: str, index: int) -> None: 329 del index # Unused. 330 self._selected_profile = name 331 332 def _refresh(self) -> None: 333 # pylint: disable=too-many-locals 334 # pylint: disable=too-many-statements 335 from efro.util import asserttype 336 from bascenev1 import PlayerProfilesChangedMessage 337 from bascenev1lib.actor import spazappearance 338 339 assert bui.app.classic is not None 340 341 plus = bui.app.plus 342 assert plus is not None 343 344 old_selection = self._selected_profile 345 346 # Delete old. 347 while self._profile_widgets: 348 self._profile_widgets.pop().delete() 349 self._profiles = bui.app.config.get('Player Profiles', {}) 350 assert self._profiles is not None 351 items = list(self._profiles.items()) 352 items.sort(key=lambda x: asserttype(x[0], str).lower()) 353 spazzes = spazappearance.get_appearances() 354 spazzes.sort() 355 icon_textures = [ 356 bui.gettexture(bui.app.classic.spaz_appearances[s].icon_texture) 357 for s in spazzes 358 ] 359 icon_tint_textures = [ 360 bui.gettexture( 361 bui.app.classic.spaz_appearances[s].icon_mask_texture 362 ) 363 for s in spazzes 364 ] 365 index = 0 366 y_val = 35 * (len(self._profiles) - 1) 367 account_name: str | None 368 if plus.get_v1_account_state() == 'signed_in': 369 account_name = plus.get_v1_account_display_string() 370 else: 371 account_name = None 372 widget_to_select = None 373 for p_name, p_info in items: 374 if p_name == '__account__' and account_name is None: 375 continue 376 color, _highlight = bui.app.classic.get_player_profile_colors( 377 p_name 378 ) 379 scl = 1.1 380 tval = ( 381 account_name 382 if p_name == '__account__' 383 else bui.app.classic.get_player_profile_icon(p_name) + p_name 384 ) 385 386 try: 387 char_index = spazzes.index(p_info['character']) 388 except Exception: 389 char_index = spazzes.index('Spaz') 390 391 assert isinstance(tval, str) 392 txtw = bui.textwidget( 393 parent=self._subcontainer, 394 position=(5, y_val), 395 size=((self._width - 210) / scl, 28), 396 text=bui.Lstr(value=f' {tval}'), 397 h_align='left', 398 v_align='center', 399 on_select_call=bui.WeakCall(self._select, p_name, index), 400 maxwidth=self._scroll_width * 0.86, 401 corner_scale=scl, 402 color=bui.safecolor(color, 0.4), 403 always_highlight=True, 404 on_activate_call=bui.Call(self._edit_button.activate), 405 selectable=True, 406 ) 407 character = bui.imagewidget( 408 parent=self._subcontainer, 409 position=(0, y_val), 410 size=(30, 30), 411 color=(1, 1, 1), 412 mask_texture=bui.gettexture('characterIconMask'), 413 tint_color=color, 414 tint2_color=_highlight, 415 texture=icon_textures[char_index], 416 tint_texture=icon_tint_textures[char_index], 417 ) 418 if index == 0: 419 bui.widget(edit=txtw, up_widget=self._back_button) 420 if self._selected_profile is None: 421 self._selected_profile = p_name 422 bui.widget(edit=txtw, show_buffer_top=40, show_buffer_bottom=40) 423 self._profile_widgets.append(txtw) 424 self._profile_widgets.append(character) 425 426 # Select/show this one if it was previously selected 427 # (but defer till after this loop since our height is 428 # still changing). 429 if p_name == old_selection: 430 widget_to_select = txtw 431 432 index += 1 433 y_val -= 35 434 435 bui.containerwidget( 436 edit=self._subcontainer, 437 size=(self._scroll_width, index * 35), 438 ) 439 if widget_to_select is not None: 440 bui.containerwidget( 441 edit=self._subcontainer, 442 selected_child=widget_to_select, 443 visible_child=widget_to_select, 444 ) 445 446 # If there's a team-chooser in existence, tell it the profile-list 447 # has probably changed. 448 session = bs.get_foreground_host_session() 449 if session is not None: 450 session.handlemessage(PlayerProfilesChangedMessage()) 451 452 def _save_state(self) -> None: 453 try: 454 sel = self._root_widget.get_selected_child() 455 if sel == self._new_button: 456 sel_name = 'New' 457 elif sel == self._edit_button: 458 sel_name = 'Edit' 459 elif sel == self._delete_button: 460 sel_name = 'Delete' 461 elif sel == self._scrollwidget: 462 sel_name = 'Scroll' 463 else: 464 sel_name = 'Back' 465 assert bui.app.classic is not None 466 bui.app.ui_v1.window_states[type(self)] = sel_name 467 except Exception: 468 logging.exception('Error saving state for %s.', self) 469 470 def _restore_state(self) -> None: 471 try: 472 assert bui.app.classic is not None 473 sel_name = bui.app.ui_v1.window_states.get(type(self)) 474 if sel_name == 'Scroll': 475 sel = self._scrollwidget 476 elif sel_name == 'New': 477 sel = self._new_button 478 elif sel_name == 'Delete': 479 sel = self._delete_button 480 elif sel_name == 'Edit': 481 sel = self._edit_button 482 elif sel_name == 'Back': 483 sel = self._back_button 484 else: 485 # By default we select our scroll widget if we have profiles; 486 # otherwise our new widget. 487 if not self._profile_widgets: 488 sel = self._new_button 489 else: 490 sel = self._scrollwidget 491 bui.containerwidget(edit=self._root_widget, selected_child=sel) 492 except Exception: 493 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.5 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.