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