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