bauiv1lib.profile.browser

UI functionality related to browsing player profiles.

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

Window for browsing player profiles.

ProfileBrowserWindow( transition: str | None = 'in_right', in_main_menu: bool = True, selected_profile: str | None = None, origin_widget: _bauiv1.Widget | None = None)
 21    def __init__(
 22        self,
 23        transition: str | None = 'in_right',
 24        in_main_menu: bool = True,
 25        selected_profile: str | None = None,
 26        origin_widget: bui.Widget | None = None,
 27    ):
 28        # pylint: disable=too-many-statements
 29        self._in_main_menu = in_main_menu
 30        if self._in_main_menu:
 31            back_label = bui.Lstr(resource='backText')
 32        else:
 33            back_label = bui.Lstr(resource='doneText')
 34        assert bui.app.classic is not None
 35        uiscale = bui.app.ui_v1.uiscale
 36        self._width = 800.0 if uiscale is bui.UIScale.SMALL else 600.0
 37        x_inset = 100.0 if uiscale is bui.UIScale.SMALL else 0.0
 38        self._height = (
 39            360.0
 40            if uiscale is bui.UIScale.SMALL
 41            else 385.0 if uiscale is bui.UIScale.MEDIUM else 410.0
 42        )
 43
 44        # If we're being called up standalone, handle pause/resume ourself.
 45        if not self._in_main_menu:
 46            assert bui.app.classic is not None
 47            bui.app.classic.pause()
 48
 49        # Need to handle out-transitions ourself for modal mode.
 50        if origin_widget is not None:
 51            self._transition_out = 'out_scale'
 52        else:
 53            self._transition_out = 'out_right'
 54
 55        self._r = 'playerProfilesWindow'
 56
 57        # Ensure we've got an account-profile in cases where we're signed in.
 58        assert bui.app.classic is not None
 59        bui.app.classic.accounts.ensure_have_account_player_profile()
 60
 61        top_extra = 20 if uiscale is bui.UIScale.SMALL else 0
 62
 63        super().__init__(
 64            root_widget=bui.containerwidget(
 65                size=(self._width, self._height + top_extra),
 66                toolbar_visibility=(
 67                    'menu_minimal'
 68                    if uiscale is bui.UIScale.SMALL
 69                    else 'menu_full'
 70                ),
 71                scale=(
 72                    2.0
 73                    if uiscale is bui.UIScale.SMALL
 74                    else 1.5 if uiscale is bui.UIScale.MEDIUM else 1.0
 75                ),
 76                stack_offset=(
 77                    (0, -14) if uiscale is bui.UIScale.SMALL else (0, 0)
 78                ),
 79            ),
 80            transition=transition,
 81            origin_widget=origin_widget,
 82        )
 83
 84        if bui.app.ui_v1.uiscale is bui.UIScale.SMALL:
 85            self._back_button = bui.get_special_widget('back_button')
 86            bui.containerwidget(
 87                edit=self._root_widget, on_cancel_call=self._back
 88            )
 89        else:
 90            self._back_button = btn = bui.buttonwidget(
 91                parent=self._root_widget,
 92                position=(40 + x_inset, self._height - 59),
 93                size=(120, 60),
 94                scale=0.8,
 95                label=back_label,
 96                button_type='back' if self._in_main_menu else None,
 97                autoselect=True,
 98                on_activate_call=self._back,
 99            )
100            bui.containerwidget(edit=self._root_widget, cancel_button=btn)
101            if self._in_main_menu:
102                bui.buttonwidget(
103                    edit=btn,
104                    button_type='backSmall',
105                    size=(60, 60),
106                    label=bui.charstr(bui.SpecialChar.BACK),
107                )
108
109        bui.textwidget(
110            parent=self._root_widget,
111            position=(self._width * 0.5, self._height - 36),
112            size=(0, 0),
113            text=bui.Lstr(resource=f'{self._r}.titleText'),
114            maxwidth=300,
115            color=bui.app.ui_v1.title_color,
116            scale=0.9,
117            h_align='center',
118            v_align='center',
119        )
120
121        scroll_height = self._height - 140.0
122        self._scroll_width = self._width - (188 + x_inset * 2)
123        v = self._height - 84.0
124        h = 50 + x_inset
125        b_color = (0.6, 0.53, 0.63)
126
127        scl = (
128            1.055
129            if uiscale is bui.UIScale.SMALL
130            else 1.18 if uiscale is bui.UIScale.MEDIUM else 1.3
131        )
132        v -= 70.0 * scl
133        self._new_button = bui.buttonwidget(
134            parent=self._root_widget,
135            position=(h, v),
136            size=(80, 66.0 * scl),
137            on_activate_call=self._new_profile,
138            color=b_color,
139            button_type='square',
140            autoselect=True,
141            textcolor=(0.75, 0.7, 0.8),
142            text_scale=0.7,
143            label=bui.Lstr(resource=f'{self._r}.newButtonText'),
144        )
145        v -= 70.0 * scl
146        self._edit_button = bui.buttonwidget(
147            parent=self._root_widget,
148            position=(h, v),
149            size=(80, 66.0 * scl),
150            on_activate_call=self._edit_profile,
151            color=b_color,
152            button_type='square',
153            autoselect=True,
154            textcolor=(0.75, 0.7, 0.8),
155            text_scale=0.7,
156            label=bui.Lstr(resource=f'{self._r}.editButtonText'),
157        )
158        v -= 70.0 * scl
159        self._delete_button = bui.buttonwidget(
160            parent=self._root_widget,
161            position=(h, v),
162            size=(80, 66.0 * scl),
163            on_activate_call=self._delete_profile,
164            color=b_color,
165            button_type='square',
166            autoselect=True,
167            textcolor=(0.75, 0.7, 0.8),
168            text_scale=0.7,
169            label=bui.Lstr(resource=f'{self._r}.deleteButtonText'),
170        )
171
172        v = self._height - 87
173
174        bui.textwidget(
175            parent=self._root_widget,
176            position=(self._width * 0.5, self._height - 71),
177            size=(0, 0),
178            text=bui.Lstr(resource=f'{self._r}.explanationText'),
179            color=bui.app.ui_v1.infotextcolor,
180            maxwidth=self._width * 0.83,
181            scale=0.6,
182            h_align='center',
183            v_align='center',
184        )
185
186        self._scrollwidget = bui.scrollwidget(
187            parent=self._root_widget,
188            highlight=False,
189            position=(140 + x_inset, v - scroll_height),
190            size=(self._scroll_width, scroll_height),
191        )
192        bui.widget(
193            edit=self._scrollwidget,
194            autoselect=True,
195            left_widget=self._new_button,
196        )
197        bui.containerwidget(
198            edit=self._root_widget, selected_child=self._scrollwidget
199        )
200        self._subcontainer = bui.containerwidget(
201            parent=self._scrollwidget,
202            size=(self._scroll_width, 32),
203            background=False,
204        )
205        v -= 255
206        self._profiles: dict[str, dict[str, Any]] | None = None
207        self._selected_profile = selected_profile
208        self._profile_widgets: list[bui.Widget] = []
209        self._refresh()
210        self._restore_state()

Create a MainWindow given a root widget and transition info.

Automatically handles in and out transitions on the provided widget, so there is no need to set transitions when creating it.

@override
def get_main_window_state(self) -> bauiv1.MainWindowState:
212    @override
213    def get_main_window_state(self) -> bui.MainWindowState:
214        # Support recreating our window for back/refresh purposes.
215        cls = type(self)
216        return bui.BasicMainWindowState(
217            create_call=lambda transition, origin_widget: cls(
218                transition=transition, origin_widget=origin_widget
219            )
220        )

Return a WindowState to recreate this window, if supported.

@override
def on_main_window_close(self) -> None:
222    @override
223    def on_main_window_close(self) -> None:
224        self._save_state()

Called before transitioning out a main window.

A good opportunity to save window state/etc.

Inherited Members
bauiv1._uitypes.MainWindow
main_window_back_state
main_window_close
can_change_main_window
main_window_back
main_window_replace
bauiv1._uitypes.Window
get_root_widget