bastd.ui.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 7from typing import TYPE_CHECKING 8 9import ba 10import ba.internal 11 12if TYPE_CHECKING: 13 from typing import Any 14 15 16class ProfileBrowserWindow(ba.Window): 17 """Window for browsing player profiles.""" 18 19 def __init__( 20 self, 21 transition: str = 'in_right', 22 in_main_menu: bool = True, 23 selected_profile: str | None = None, 24 origin_widget: ba.Widget | None = None, 25 ): 26 # pylint: disable=too-many-statements 27 # pylint: disable=too-many-locals 28 self._in_main_menu = in_main_menu 29 if self._in_main_menu: 30 back_label = ba.Lstr(resource='backText') 31 else: 32 back_label = ba.Lstr(resource='doneText') 33 uiscale = ba.app.ui.uiscale 34 self._width = 700.0 if uiscale is ba.UIScale.SMALL else 600.0 35 x_inset = 50.0 if uiscale is ba.UIScale.SMALL else 0.0 36 self._height = ( 37 360.0 38 if uiscale is ba.UIScale.SMALL 39 else 385.0 40 if uiscale is ba.UIScale.MEDIUM 41 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 ba.app.pause() 47 48 # If they provided an origin-widget, scale up from that. 49 scale_origin: tuple[float, float] | None 50 if origin_widget is not None: 51 self._transition_out = 'out_scale' 52 scale_origin = origin_widget.get_screen_space_center() 53 transition = 'in_scale' 54 else: 55 self._transition_out = 'out_right' 56 scale_origin = None 57 58 self._r = 'playerProfilesWindow' 59 60 # Ensure we've got an account-profile in cases where we're signed in. 61 ba.app.accounts_v1.ensure_have_account_player_profile() 62 63 top_extra = 20 if uiscale is ba.UIScale.SMALL else 0 64 65 super().__init__( 66 root_widget=ba.containerwidget( 67 size=(self._width, self._height + top_extra), 68 transition=transition, 69 scale_origin_stack_offset=scale_origin, 70 scale=( 71 2.2 72 if uiscale is ba.UIScale.SMALL 73 else 1.6 74 if uiscale is ba.UIScale.MEDIUM 75 else 1.0 76 ), 77 stack_offset=(0, -14) 78 if uiscale is ba.UIScale.SMALL 79 else (0, 0), 80 ) 81 ) 82 83 self._back_button = btn = ba.buttonwidget( 84 parent=self._root_widget, 85 position=(40 + x_inset, self._height - 59), 86 size=(120, 60), 87 scale=0.8, 88 label=back_label, 89 button_type='back' if self._in_main_menu else None, 90 autoselect=True, 91 on_activate_call=self._back, 92 ) 93 ba.containerwidget(edit=self._root_widget, cancel_button=btn) 94 95 ba.textwidget( 96 parent=self._root_widget, 97 position=(self._width * 0.5, self._height - 36), 98 size=(0, 0), 99 text=ba.Lstr(resource=self._r + '.titleText'), 100 maxwidth=300, 101 color=ba.app.ui.title_color, 102 scale=0.9, 103 h_align='center', 104 v_align='center', 105 ) 106 107 if self._in_main_menu: 108 ba.buttonwidget( 109 edit=btn, 110 button_type='backSmall', 111 size=(60, 60), 112 label=ba.charstr(ba.SpecialChar.BACK), 113 ) 114 115 scroll_height = self._height - 140.0 116 self._scroll_width = self._width - (188 + x_inset * 2) 117 v = self._height - 84.0 118 h = 50 + x_inset 119 b_color = (0.6, 0.53, 0.63) 120 121 scl = ( 122 1.055 123 if uiscale is ba.UIScale.SMALL 124 else 1.18 125 if uiscale is ba.UIScale.MEDIUM 126 else 1.3 127 ) 128 v -= 70.0 * scl 129 self._new_button = ba.buttonwidget( 130 parent=self._root_widget, 131 position=(h, v), 132 size=(80, 66.0 * scl), 133 on_activate_call=self._new_profile, 134 color=b_color, 135 button_type='square', 136 autoselect=True, 137 textcolor=(0.75, 0.7, 0.8), 138 text_scale=0.7, 139 label=ba.Lstr(resource=self._r + '.newButtonText'), 140 ) 141 v -= 70.0 * scl 142 self._edit_button = ba.buttonwidget( 143 parent=self._root_widget, 144 position=(h, v), 145 size=(80, 66.0 * scl), 146 on_activate_call=self._edit_profile, 147 color=b_color, 148 button_type='square', 149 autoselect=True, 150 textcolor=(0.75, 0.7, 0.8), 151 text_scale=0.7, 152 label=ba.Lstr(resource=self._r + '.editButtonText'), 153 ) 154 v -= 70.0 * scl 155 self._delete_button = ba.buttonwidget( 156 parent=self._root_widget, 157 position=(h, v), 158 size=(80, 66.0 * scl), 159 on_activate_call=self._delete_profile, 160 color=b_color, 161 button_type='square', 162 autoselect=True, 163 textcolor=(0.75, 0.7, 0.8), 164 text_scale=0.7, 165 label=ba.Lstr(resource=self._r + '.deleteButtonText'), 166 ) 167 168 v = self._height - 87 169 170 ba.textwidget( 171 parent=self._root_widget, 172 position=(self._width * 0.5, self._height - 71), 173 size=(0, 0), 174 text=ba.Lstr(resource=self._r + '.explanationText'), 175 color=ba.app.ui.infotextcolor, 176 maxwidth=self._width * 0.83, 177 scale=0.6, 178 h_align='center', 179 v_align='center', 180 ) 181 182 self._scrollwidget = ba.scrollwidget( 183 parent=self._root_widget, 184 highlight=False, 185 position=(140 + x_inset, v - scroll_height), 186 size=(self._scroll_width, scroll_height), 187 ) 188 ba.widget( 189 edit=self._scrollwidget, 190 autoselect=True, 191 left_widget=self._new_button, 192 ) 193 ba.containerwidget( 194 edit=self._root_widget, selected_child=self._scrollwidget 195 ) 196 self._columnwidget = ba.columnwidget( 197 parent=self._scrollwidget, border=2, margin=0 198 ) 199 v -= 255 200 self._profiles: dict[str, dict[str, Any]] | None = None 201 self._selected_profile = selected_profile 202 self._profile_widgets: list[ba.Widget] = [] 203 self._refresh() 204 self._restore_state() 205 206 def _new_profile(self) -> None: 207 # pylint: disable=cyclic-import 208 from bastd.ui.profile.edit import EditProfileWindow 209 from bastd.ui.purchase import PurchaseWindow 210 211 # Limit to a handful profiles if they don't have pro-options. 212 max_non_pro_profiles = ba.internal.get_v1_account_misc_read_val( 213 'mnpp', 5 214 ) 215 assert self._profiles is not None 216 if ( 217 not ba.app.accounts_v1.have_pro_options() 218 and len(self._profiles) >= max_non_pro_profiles 219 ): 220 PurchaseWindow( 221 items=['pro'], 222 header_text=ba.Lstr( 223 resource='unlockThisProfilesText', 224 subs=[('${NUM}', str(max_non_pro_profiles))], 225 ), 226 ) 227 return 228 229 # Clamp at 100 profiles (otherwise the server will and that's less 230 # elegant looking). 231 if len(self._profiles) > 100: 232 ba.screenmessage( 233 ba.Lstr( 234 translate=( 235 'serverResponses', 236 'Max number of profiles reached.', 237 ) 238 ), 239 color=(1, 0, 0), 240 ) 241 ba.playsound(ba.getsound('error')) 242 return 243 244 self._save_state() 245 ba.containerwidget(edit=self._root_widget, transition='out_left') 246 ba.app.ui.set_main_menu_window( 247 EditProfileWindow( 248 existing_profile=None, in_main_menu=self._in_main_menu 249 ).get_root_widget() 250 ) 251 252 def _delete_profile(self) -> None: 253 # pylint: disable=cyclic-import 254 from bastd.ui import confirm 255 256 if self._selected_profile is None: 257 ba.playsound(ba.getsound('error')) 258 ba.screenmessage( 259 ba.Lstr(resource='nothingIsSelectedErrorText'), color=(1, 0, 0) 260 ) 261 return 262 if self._selected_profile == '__account__': 263 ba.playsound(ba.getsound('error')) 264 ba.screenmessage( 265 ba.Lstr(resource=self._r + '.cantDeleteAccountProfileText'), 266 color=(1, 0, 0), 267 ) 268 return 269 confirm.ConfirmWindow( 270 ba.Lstr( 271 resource=self._r + '.deleteConfirmText', 272 subs=[('${PROFILE}', self._selected_profile)], 273 ), 274 self._do_delete_profile, 275 350, 276 ) 277 278 def _do_delete_profile(self) -> None: 279 ba.internal.add_transaction( 280 {'type': 'REMOVE_PLAYER_PROFILE', 'name': self._selected_profile} 281 ) 282 ba.internal.run_transactions() 283 ba.playsound(ba.getsound('shieldDown')) 284 self._refresh() 285 286 # Select profile list. 287 ba.containerwidget( 288 edit=self._root_widget, selected_child=self._scrollwidget 289 ) 290 291 def _edit_profile(self) -> None: 292 # pylint: disable=cyclic-import 293 from bastd.ui.profile.edit import EditProfileWindow 294 295 if self._selected_profile is None: 296 ba.playsound(ba.getsound('error')) 297 ba.screenmessage( 298 ba.Lstr(resource='nothingIsSelectedErrorText'), color=(1, 0, 0) 299 ) 300 return 301 self._save_state() 302 ba.containerwidget(edit=self._root_widget, transition='out_left') 303 ba.app.ui.set_main_menu_window( 304 EditProfileWindow( 305 self._selected_profile, in_main_menu=self._in_main_menu 306 ).get_root_widget() 307 ) 308 309 def _select(self, name: str, index: int) -> None: 310 del index # Unused. 311 self._selected_profile = name 312 313 def _back(self) -> None: 314 # pylint: disable=cyclic-import 315 from bastd.ui.account.settings import AccountSettingsWindow 316 317 self._save_state() 318 ba.containerwidget( 319 edit=self._root_widget, transition=self._transition_out 320 ) 321 if self._in_main_menu: 322 ba.app.ui.set_main_menu_window( 323 AccountSettingsWindow(transition='in_left').get_root_widget() 324 ) 325 326 # If we're being called up standalone, handle pause/resume ourself. 327 else: 328 ba.app.resume() 329 330 def _refresh(self) -> None: 331 # pylint: disable=too-many-locals 332 from efro.util import asserttype 333 from ba.internal import ( 334 PlayerProfilesChangedMessage, 335 get_player_profile_colors, 336 get_player_profile_icon, 337 ) 338 339 old_selection = self._selected_profile 340 341 # Delete old. 342 while self._profile_widgets: 343 self._profile_widgets.pop().delete() 344 self._profiles = ba.app.config.get('Player Profiles', {}) 345 assert self._profiles is not None 346 items = list(self._profiles.items()) 347 items.sort(key=lambda x: asserttype(x[0], str).lower()) 348 index = 0 349 account_name: str | None 350 if ba.internal.get_v1_account_state() == 'signed_in': 351 account_name = ba.internal.get_v1_account_display_string() 352 else: 353 account_name = None 354 widget_to_select = None 355 for p_name, _ in items: 356 if p_name == '__account__' and account_name is None: 357 continue 358 color, _highlight = get_player_profile_colors(p_name) 359 scl = 1.1 360 tval = ( 361 account_name 362 if p_name == '__account__' 363 else get_player_profile_icon(p_name) + p_name 364 ) 365 assert isinstance(tval, str) 366 txtw = ba.textwidget( 367 parent=self._columnwidget, 368 position=(0, 32), 369 size=((self._width - 40) / scl, 28), 370 text=ba.Lstr(value=tval), 371 h_align='left', 372 v_align='center', 373 on_select_call=ba.WeakCall(self._select, p_name, index), 374 maxwidth=self._scroll_width * 0.92, 375 corner_scale=scl, 376 color=ba.safecolor(color, 0.4), 377 always_highlight=True, 378 on_activate_call=ba.Call(self._edit_button.activate), 379 selectable=True, 380 ) 381 if index == 0: 382 ba.widget(edit=txtw, up_widget=self._back_button) 383 ba.widget(edit=txtw, show_buffer_top=40, show_buffer_bottom=40) 384 self._profile_widgets.append(txtw) 385 386 # Select/show this one if it was previously selected 387 # (but defer till after this loop since our height is 388 # still changing). 389 if p_name == old_selection: 390 widget_to_select = txtw 391 392 index += 1 393 394 if widget_to_select is not None: 395 ba.columnwidget( 396 edit=self._columnwidget, 397 selected_child=widget_to_select, 398 visible_child=widget_to_select, 399 ) 400 401 # If there's a team-chooser in existence, tell it the profile-list 402 # has probably changed. 403 session = ba.internal.get_foreground_host_session() 404 if session is not None: 405 session.handlemessage(PlayerProfilesChangedMessage()) 406 407 def _save_state(self) -> None: 408 try: 409 sel = self._root_widget.get_selected_child() 410 if sel == self._new_button: 411 sel_name = 'New' 412 elif sel == self._edit_button: 413 sel_name = 'Edit' 414 elif sel == self._delete_button: 415 sel_name = 'Delete' 416 elif sel == self._scrollwidget: 417 sel_name = 'Scroll' 418 else: 419 sel_name = 'Back' 420 ba.app.ui.window_states[type(self)] = sel_name 421 except Exception: 422 ba.print_exception(f'Error saving state for {self}.') 423 424 def _restore_state(self) -> None: 425 try: 426 sel_name = ba.app.ui.window_states.get(type(self)) 427 if sel_name == 'Scroll': 428 sel = self._scrollwidget 429 elif sel_name == 'New': 430 sel = self._new_button 431 elif sel_name == 'Delete': 432 sel = self._delete_button 433 elif sel_name == 'Edit': 434 sel = self._edit_button 435 elif sel_name == 'Back': 436 sel = self._back_button 437 else: 438 # By default we select our scroll widget if we have profiles; 439 # otherwise our new widget. 440 if not self._profile_widgets: 441 sel = self._new_button 442 else: 443 sel = self._scrollwidget 444 ba.containerwidget(edit=self._root_widget, selected_child=sel) 445 except Exception: 446 ba.print_exception(f'Error restoring state for {self}.')
class
ProfileBrowserWindow(ba.ui.Window):
17class ProfileBrowserWindow(ba.Window): 18 """Window for browsing player profiles.""" 19 20 def __init__( 21 self, 22 transition: str = 'in_right', 23 in_main_menu: bool = True, 24 selected_profile: str | None = None, 25 origin_widget: ba.Widget | None = None, 26 ): 27 # pylint: disable=too-many-statements 28 # pylint: disable=too-many-locals 29 self._in_main_menu = in_main_menu 30 if self._in_main_menu: 31 back_label = ba.Lstr(resource='backText') 32 else: 33 back_label = ba.Lstr(resource='doneText') 34 uiscale = ba.app.ui.uiscale 35 self._width = 700.0 if uiscale is ba.UIScale.SMALL else 600.0 36 x_inset = 50.0 if uiscale is ba.UIScale.SMALL else 0.0 37 self._height = ( 38 360.0 39 if uiscale is ba.UIScale.SMALL 40 else 385.0 41 if uiscale is ba.UIScale.MEDIUM 42 else 410.0 43 ) 44 45 # If we're being called up standalone, handle pause/resume ourself. 46 if not self._in_main_menu: 47 ba.app.pause() 48 49 # If they provided an origin-widget, scale up from that. 50 scale_origin: tuple[float, float] | None 51 if origin_widget is not None: 52 self._transition_out = 'out_scale' 53 scale_origin = origin_widget.get_screen_space_center() 54 transition = 'in_scale' 55 else: 56 self._transition_out = 'out_right' 57 scale_origin = None 58 59 self._r = 'playerProfilesWindow' 60 61 # Ensure we've got an account-profile in cases where we're signed in. 62 ba.app.accounts_v1.ensure_have_account_player_profile() 63 64 top_extra = 20 if uiscale is ba.UIScale.SMALL else 0 65 66 super().__init__( 67 root_widget=ba.containerwidget( 68 size=(self._width, self._height + top_extra), 69 transition=transition, 70 scale_origin_stack_offset=scale_origin, 71 scale=( 72 2.2 73 if uiscale is ba.UIScale.SMALL 74 else 1.6 75 if uiscale is ba.UIScale.MEDIUM 76 else 1.0 77 ), 78 stack_offset=(0, -14) 79 if uiscale is ba.UIScale.SMALL 80 else (0, 0), 81 ) 82 ) 83 84 self._back_button = btn = ba.buttonwidget( 85 parent=self._root_widget, 86 position=(40 + x_inset, self._height - 59), 87 size=(120, 60), 88 scale=0.8, 89 label=back_label, 90 button_type='back' if self._in_main_menu else None, 91 autoselect=True, 92 on_activate_call=self._back, 93 ) 94 ba.containerwidget(edit=self._root_widget, cancel_button=btn) 95 96 ba.textwidget( 97 parent=self._root_widget, 98 position=(self._width * 0.5, self._height - 36), 99 size=(0, 0), 100 text=ba.Lstr(resource=self._r + '.titleText'), 101 maxwidth=300, 102 color=ba.app.ui.title_color, 103 scale=0.9, 104 h_align='center', 105 v_align='center', 106 ) 107 108 if self._in_main_menu: 109 ba.buttonwidget( 110 edit=btn, 111 button_type='backSmall', 112 size=(60, 60), 113 label=ba.charstr(ba.SpecialChar.BACK), 114 ) 115 116 scroll_height = self._height - 140.0 117 self._scroll_width = self._width - (188 + x_inset * 2) 118 v = self._height - 84.0 119 h = 50 + x_inset 120 b_color = (0.6, 0.53, 0.63) 121 122 scl = ( 123 1.055 124 if uiscale is ba.UIScale.SMALL 125 else 1.18 126 if uiscale is ba.UIScale.MEDIUM 127 else 1.3 128 ) 129 v -= 70.0 * scl 130 self._new_button = ba.buttonwidget( 131 parent=self._root_widget, 132 position=(h, v), 133 size=(80, 66.0 * scl), 134 on_activate_call=self._new_profile, 135 color=b_color, 136 button_type='square', 137 autoselect=True, 138 textcolor=(0.75, 0.7, 0.8), 139 text_scale=0.7, 140 label=ba.Lstr(resource=self._r + '.newButtonText'), 141 ) 142 v -= 70.0 * scl 143 self._edit_button = ba.buttonwidget( 144 parent=self._root_widget, 145 position=(h, v), 146 size=(80, 66.0 * scl), 147 on_activate_call=self._edit_profile, 148 color=b_color, 149 button_type='square', 150 autoselect=True, 151 textcolor=(0.75, 0.7, 0.8), 152 text_scale=0.7, 153 label=ba.Lstr(resource=self._r + '.editButtonText'), 154 ) 155 v -= 70.0 * scl 156 self._delete_button = ba.buttonwidget( 157 parent=self._root_widget, 158 position=(h, v), 159 size=(80, 66.0 * scl), 160 on_activate_call=self._delete_profile, 161 color=b_color, 162 button_type='square', 163 autoselect=True, 164 textcolor=(0.75, 0.7, 0.8), 165 text_scale=0.7, 166 label=ba.Lstr(resource=self._r + '.deleteButtonText'), 167 ) 168 169 v = self._height - 87 170 171 ba.textwidget( 172 parent=self._root_widget, 173 position=(self._width * 0.5, self._height - 71), 174 size=(0, 0), 175 text=ba.Lstr(resource=self._r + '.explanationText'), 176 color=ba.app.ui.infotextcolor, 177 maxwidth=self._width * 0.83, 178 scale=0.6, 179 h_align='center', 180 v_align='center', 181 ) 182 183 self._scrollwidget = ba.scrollwidget( 184 parent=self._root_widget, 185 highlight=False, 186 position=(140 + x_inset, v - scroll_height), 187 size=(self._scroll_width, scroll_height), 188 ) 189 ba.widget( 190 edit=self._scrollwidget, 191 autoselect=True, 192 left_widget=self._new_button, 193 ) 194 ba.containerwidget( 195 edit=self._root_widget, selected_child=self._scrollwidget 196 ) 197 self._columnwidget = ba.columnwidget( 198 parent=self._scrollwidget, border=2, margin=0 199 ) 200 v -= 255 201 self._profiles: dict[str, dict[str, Any]] | None = None 202 self._selected_profile = selected_profile 203 self._profile_widgets: list[ba.Widget] = [] 204 self._refresh() 205 self._restore_state() 206 207 def _new_profile(self) -> None: 208 # pylint: disable=cyclic-import 209 from bastd.ui.profile.edit import EditProfileWindow 210 from bastd.ui.purchase import PurchaseWindow 211 212 # Limit to a handful profiles if they don't have pro-options. 213 max_non_pro_profiles = ba.internal.get_v1_account_misc_read_val( 214 'mnpp', 5 215 ) 216 assert self._profiles is not None 217 if ( 218 not ba.app.accounts_v1.have_pro_options() 219 and len(self._profiles) >= max_non_pro_profiles 220 ): 221 PurchaseWindow( 222 items=['pro'], 223 header_text=ba.Lstr( 224 resource='unlockThisProfilesText', 225 subs=[('${NUM}', str(max_non_pro_profiles))], 226 ), 227 ) 228 return 229 230 # Clamp at 100 profiles (otherwise the server will and that's less 231 # elegant looking). 232 if len(self._profiles) > 100: 233 ba.screenmessage( 234 ba.Lstr( 235 translate=( 236 'serverResponses', 237 'Max number of profiles reached.', 238 ) 239 ), 240 color=(1, 0, 0), 241 ) 242 ba.playsound(ba.getsound('error')) 243 return 244 245 self._save_state() 246 ba.containerwidget(edit=self._root_widget, transition='out_left') 247 ba.app.ui.set_main_menu_window( 248 EditProfileWindow( 249 existing_profile=None, in_main_menu=self._in_main_menu 250 ).get_root_widget() 251 ) 252 253 def _delete_profile(self) -> None: 254 # pylint: disable=cyclic-import 255 from bastd.ui import confirm 256 257 if self._selected_profile is None: 258 ba.playsound(ba.getsound('error')) 259 ba.screenmessage( 260 ba.Lstr(resource='nothingIsSelectedErrorText'), color=(1, 0, 0) 261 ) 262 return 263 if self._selected_profile == '__account__': 264 ba.playsound(ba.getsound('error')) 265 ba.screenmessage( 266 ba.Lstr(resource=self._r + '.cantDeleteAccountProfileText'), 267 color=(1, 0, 0), 268 ) 269 return 270 confirm.ConfirmWindow( 271 ba.Lstr( 272 resource=self._r + '.deleteConfirmText', 273 subs=[('${PROFILE}', self._selected_profile)], 274 ), 275 self._do_delete_profile, 276 350, 277 ) 278 279 def _do_delete_profile(self) -> None: 280 ba.internal.add_transaction( 281 {'type': 'REMOVE_PLAYER_PROFILE', 'name': self._selected_profile} 282 ) 283 ba.internal.run_transactions() 284 ba.playsound(ba.getsound('shieldDown')) 285 self._refresh() 286 287 # Select profile list. 288 ba.containerwidget( 289 edit=self._root_widget, selected_child=self._scrollwidget 290 ) 291 292 def _edit_profile(self) -> None: 293 # pylint: disable=cyclic-import 294 from bastd.ui.profile.edit import EditProfileWindow 295 296 if self._selected_profile is None: 297 ba.playsound(ba.getsound('error')) 298 ba.screenmessage( 299 ba.Lstr(resource='nothingIsSelectedErrorText'), color=(1, 0, 0) 300 ) 301 return 302 self._save_state() 303 ba.containerwidget(edit=self._root_widget, transition='out_left') 304 ba.app.ui.set_main_menu_window( 305 EditProfileWindow( 306 self._selected_profile, in_main_menu=self._in_main_menu 307 ).get_root_widget() 308 ) 309 310 def _select(self, name: str, index: int) -> None: 311 del index # Unused. 312 self._selected_profile = name 313 314 def _back(self) -> None: 315 # pylint: disable=cyclic-import 316 from bastd.ui.account.settings import AccountSettingsWindow 317 318 self._save_state() 319 ba.containerwidget( 320 edit=self._root_widget, transition=self._transition_out 321 ) 322 if self._in_main_menu: 323 ba.app.ui.set_main_menu_window( 324 AccountSettingsWindow(transition='in_left').get_root_widget() 325 ) 326 327 # If we're being called up standalone, handle pause/resume ourself. 328 else: 329 ba.app.resume() 330 331 def _refresh(self) -> None: 332 # pylint: disable=too-many-locals 333 from efro.util import asserttype 334 from ba.internal import ( 335 PlayerProfilesChangedMessage, 336 get_player_profile_colors, 337 get_player_profile_icon, 338 ) 339 340 old_selection = self._selected_profile 341 342 # Delete old. 343 while self._profile_widgets: 344 self._profile_widgets.pop().delete() 345 self._profiles = ba.app.config.get('Player Profiles', {}) 346 assert self._profiles is not None 347 items = list(self._profiles.items()) 348 items.sort(key=lambda x: asserttype(x[0], str).lower()) 349 index = 0 350 account_name: str | None 351 if ba.internal.get_v1_account_state() == 'signed_in': 352 account_name = ba.internal.get_v1_account_display_string() 353 else: 354 account_name = None 355 widget_to_select = None 356 for p_name, _ in items: 357 if p_name == '__account__' and account_name is None: 358 continue 359 color, _highlight = get_player_profile_colors(p_name) 360 scl = 1.1 361 tval = ( 362 account_name 363 if p_name == '__account__' 364 else get_player_profile_icon(p_name) + p_name 365 ) 366 assert isinstance(tval, str) 367 txtw = ba.textwidget( 368 parent=self._columnwidget, 369 position=(0, 32), 370 size=((self._width - 40) / scl, 28), 371 text=ba.Lstr(value=tval), 372 h_align='left', 373 v_align='center', 374 on_select_call=ba.WeakCall(self._select, p_name, index), 375 maxwidth=self._scroll_width * 0.92, 376 corner_scale=scl, 377 color=ba.safecolor(color, 0.4), 378 always_highlight=True, 379 on_activate_call=ba.Call(self._edit_button.activate), 380 selectable=True, 381 ) 382 if index == 0: 383 ba.widget(edit=txtw, up_widget=self._back_button) 384 ba.widget(edit=txtw, show_buffer_top=40, show_buffer_bottom=40) 385 self._profile_widgets.append(txtw) 386 387 # Select/show this one if it was previously selected 388 # (but defer till after this loop since our height is 389 # still changing). 390 if p_name == old_selection: 391 widget_to_select = txtw 392 393 index += 1 394 395 if widget_to_select is not None: 396 ba.columnwidget( 397 edit=self._columnwidget, 398 selected_child=widget_to_select, 399 visible_child=widget_to_select, 400 ) 401 402 # If there's a team-chooser in existence, tell it the profile-list 403 # has probably changed. 404 session = ba.internal.get_foreground_host_session() 405 if session is not None: 406 session.handlemessage(PlayerProfilesChangedMessage()) 407 408 def _save_state(self) -> None: 409 try: 410 sel = self._root_widget.get_selected_child() 411 if sel == self._new_button: 412 sel_name = 'New' 413 elif sel == self._edit_button: 414 sel_name = 'Edit' 415 elif sel == self._delete_button: 416 sel_name = 'Delete' 417 elif sel == self._scrollwidget: 418 sel_name = 'Scroll' 419 else: 420 sel_name = 'Back' 421 ba.app.ui.window_states[type(self)] = sel_name 422 except Exception: 423 ba.print_exception(f'Error saving state for {self}.') 424 425 def _restore_state(self) -> None: 426 try: 427 sel_name = ba.app.ui.window_states.get(type(self)) 428 if sel_name == 'Scroll': 429 sel = self._scrollwidget 430 elif sel_name == 'New': 431 sel = self._new_button 432 elif sel_name == 'Delete': 433 sel = self._delete_button 434 elif sel_name == 'Edit': 435 sel = self._edit_button 436 elif sel_name == 'Back': 437 sel = self._back_button 438 else: 439 # By default we select our scroll widget if we have profiles; 440 # otherwise our new widget. 441 if not self._profile_widgets: 442 sel = self._new_button 443 else: 444 sel = self._scrollwidget 445 ba.containerwidget(edit=self._root_widget, selected_child=sel) 446 except Exception: 447 ba.print_exception(f'Error restoring state for {self}.')
Window for browsing player profiles.
ProfileBrowserWindow( transition: str = 'in_right', in_main_menu: bool = True, selected_profile: str | None = None, origin_widget: _ba.Widget | None = None)
20 def __init__( 21 self, 22 transition: str = 'in_right', 23 in_main_menu: bool = True, 24 selected_profile: str | None = None, 25 origin_widget: ba.Widget | None = None, 26 ): 27 # pylint: disable=too-many-statements 28 # pylint: disable=too-many-locals 29 self._in_main_menu = in_main_menu 30 if self._in_main_menu: 31 back_label = ba.Lstr(resource='backText') 32 else: 33 back_label = ba.Lstr(resource='doneText') 34 uiscale = ba.app.ui.uiscale 35 self._width = 700.0 if uiscale is ba.UIScale.SMALL else 600.0 36 x_inset = 50.0 if uiscale is ba.UIScale.SMALL else 0.0 37 self._height = ( 38 360.0 39 if uiscale is ba.UIScale.SMALL 40 else 385.0 41 if uiscale is ba.UIScale.MEDIUM 42 else 410.0 43 ) 44 45 # If we're being called up standalone, handle pause/resume ourself. 46 if not self._in_main_menu: 47 ba.app.pause() 48 49 # If they provided an origin-widget, scale up from that. 50 scale_origin: tuple[float, float] | None 51 if origin_widget is not None: 52 self._transition_out = 'out_scale' 53 scale_origin = origin_widget.get_screen_space_center() 54 transition = 'in_scale' 55 else: 56 self._transition_out = 'out_right' 57 scale_origin = None 58 59 self._r = 'playerProfilesWindow' 60 61 # Ensure we've got an account-profile in cases where we're signed in. 62 ba.app.accounts_v1.ensure_have_account_player_profile() 63 64 top_extra = 20 if uiscale is ba.UIScale.SMALL else 0 65 66 super().__init__( 67 root_widget=ba.containerwidget( 68 size=(self._width, self._height + top_extra), 69 transition=transition, 70 scale_origin_stack_offset=scale_origin, 71 scale=( 72 2.2 73 if uiscale is ba.UIScale.SMALL 74 else 1.6 75 if uiscale is ba.UIScale.MEDIUM 76 else 1.0 77 ), 78 stack_offset=(0, -14) 79 if uiscale is ba.UIScale.SMALL 80 else (0, 0), 81 ) 82 ) 83 84 self._back_button = btn = ba.buttonwidget( 85 parent=self._root_widget, 86 position=(40 + x_inset, self._height - 59), 87 size=(120, 60), 88 scale=0.8, 89 label=back_label, 90 button_type='back' if self._in_main_menu else None, 91 autoselect=True, 92 on_activate_call=self._back, 93 ) 94 ba.containerwidget(edit=self._root_widget, cancel_button=btn) 95 96 ba.textwidget( 97 parent=self._root_widget, 98 position=(self._width * 0.5, self._height - 36), 99 size=(0, 0), 100 text=ba.Lstr(resource=self._r + '.titleText'), 101 maxwidth=300, 102 color=ba.app.ui.title_color, 103 scale=0.9, 104 h_align='center', 105 v_align='center', 106 ) 107 108 if self._in_main_menu: 109 ba.buttonwidget( 110 edit=btn, 111 button_type='backSmall', 112 size=(60, 60), 113 label=ba.charstr(ba.SpecialChar.BACK), 114 ) 115 116 scroll_height = self._height - 140.0 117 self._scroll_width = self._width - (188 + x_inset * 2) 118 v = self._height - 84.0 119 h = 50 + x_inset 120 b_color = (0.6, 0.53, 0.63) 121 122 scl = ( 123 1.055 124 if uiscale is ba.UIScale.SMALL 125 else 1.18 126 if uiscale is ba.UIScale.MEDIUM 127 else 1.3 128 ) 129 v -= 70.0 * scl 130 self._new_button = ba.buttonwidget( 131 parent=self._root_widget, 132 position=(h, v), 133 size=(80, 66.0 * scl), 134 on_activate_call=self._new_profile, 135 color=b_color, 136 button_type='square', 137 autoselect=True, 138 textcolor=(0.75, 0.7, 0.8), 139 text_scale=0.7, 140 label=ba.Lstr(resource=self._r + '.newButtonText'), 141 ) 142 v -= 70.0 * scl 143 self._edit_button = ba.buttonwidget( 144 parent=self._root_widget, 145 position=(h, v), 146 size=(80, 66.0 * scl), 147 on_activate_call=self._edit_profile, 148 color=b_color, 149 button_type='square', 150 autoselect=True, 151 textcolor=(0.75, 0.7, 0.8), 152 text_scale=0.7, 153 label=ba.Lstr(resource=self._r + '.editButtonText'), 154 ) 155 v -= 70.0 * scl 156 self._delete_button = ba.buttonwidget( 157 parent=self._root_widget, 158 position=(h, v), 159 size=(80, 66.0 * scl), 160 on_activate_call=self._delete_profile, 161 color=b_color, 162 button_type='square', 163 autoselect=True, 164 textcolor=(0.75, 0.7, 0.8), 165 text_scale=0.7, 166 label=ba.Lstr(resource=self._r + '.deleteButtonText'), 167 ) 168 169 v = self._height - 87 170 171 ba.textwidget( 172 parent=self._root_widget, 173 position=(self._width * 0.5, self._height - 71), 174 size=(0, 0), 175 text=ba.Lstr(resource=self._r + '.explanationText'), 176 color=ba.app.ui.infotextcolor, 177 maxwidth=self._width * 0.83, 178 scale=0.6, 179 h_align='center', 180 v_align='center', 181 ) 182 183 self._scrollwidget = ba.scrollwidget( 184 parent=self._root_widget, 185 highlight=False, 186 position=(140 + x_inset, v - scroll_height), 187 size=(self._scroll_width, scroll_height), 188 ) 189 ba.widget( 190 edit=self._scrollwidget, 191 autoselect=True, 192 left_widget=self._new_button, 193 ) 194 ba.containerwidget( 195 edit=self._root_widget, selected_child=self._scrollwidget 196 ) 197 self._columnwidget = ba.columnwidget( 198 parent=self._scrollwidget, border=2, margin=0 199 ) 200 v -= 255 201 self._profiles: dict[str, dict[str, Any]] | None = None 202 self._selected_profile = selected_profile 203 self._profile_widgets: list[ba.Widget] = [] 204 self._refresh() 205 self._restore_state()
Inherited Members
- ba.ui.Window
- get_root_widget