bastd.ui.gather

Provides UI for inviting/joining friends.

  1# Released under the MIT License. See LICENSE for details.
  2#
  3"""Provides UI for inviting/joining friends."""
  4
  5from __future__ import annotations
  6
  7import weakref
  8from enum import Enum
  9from typing import TYPE_CHECKING
 10
 11import ba
 12import ba.internal
 13from bastd.ui.tabs import TabRow
 14
 15if TYPE_CHECKING:
 16    pass
 17
 18
 19class GatherTab:
 20    """Defines a tab for use in the gather UI."""
 21
 22    def __init__(self, window: GatherWindow) -> None:
 23        self._window = weakref.ref(window)
 24
 25    @property
 26    def window(self) -> GatherWindow:
 27        """The GatherWindow that this tab belongs to."""
 28        window = self._window()
 29        if window is None:
 30            raise ba.NotFoundError("GatherTab's window no longer exists.")
 31        return window
 32
 33    def on_activate(
 34        self,
 35        parent_widget: ba.Widget,
 36        tab_button: ba.Widget,
 37        region_width: float,
 38        region_height: float,
 39        region_left: float,
 40        region_bottom: float,
 41    ) -> ba.Widget:
 42        """Called when the tab becomes the active one.
 43
 44        The tab should create and return a container widget covering the
 45        specified region.
 46        """
 47        raise RuntimeError('Should not get here.')
 48
 49    def on_deactivate(self) -> None:
 50        """Called when the tab will no longer be the active one."""
 51
 52    def save_state(self) -> None:
 53        """Called when the parent window is saving state."""
 54
 55    def restore_state(self) -> None:
 56        """Called when the parent window is restoring state."""
 57
 58
 59class GatherWindow(ba.Window):
 60    """Window for joining/inviting friends."""
 61
 62    class TabID(Enum):
 63        """Our available tab types."""
 64
 65        ABOUT = 'about'
 66        INTERNET = 'internet'
 67        PRIVATE = 'private'
 68        NEARBY = 'nearby'
 69        MANUAL = 'manual'
 70
 71    def __init__(
 72        self,
 73        transition: str | None = 'in_right',
 74        origin_widget: ba.Widget | None = None,
 75    ):
 76        # pylint: disable=too-many-statements
 77        # pylint: disable=too-many-locals
 78        # pylint: disable=cyclic-import
 79        from bastd.ui.gather.abouttab import AboutGatherTab
 80        from bastd.ui.gather.manualtab import ManualGatherTab
 81        from bastd.ui.gather.privatetab import PrivateGatherTab
 82        from bastd.ui.gather.publictab import PublicGatherTab
 83        from bastd.ui.gather.nearbytab import NearbyGatherTab
 84
 85        ba.set_analytics_screen('Gather Window')
 86        scale_origin: tuple[float, float] | None
 87        if origin_widget is not None:
 88            self._transition_out = 'out_scale'
 89            scale_origin = origin_widget.get_screen_space_center()
 90            transition = 'in_scale'
 91        else:
 92            self._transition_out = 'out_right'
 93            scale_origin = None
 94        ba.app.ui.set_main_menu_location('Gather')
 95        ba.internal.set_party_icon_always_visible(True)
 96        uiscale = ba.app.ui.uiscale
 97        self._width = 1240 if uiscale is ba.UIScale.SMALL else 1040
 98        x_offs = 100 if uiscale is ba.UIScale.SMALL else 0
 99        self._height = (
100            582
101            if uiscale is ba.UIScale.SMALL
102            else 680
103            if uiscale is ba.UIScale.MEDIUM
104            else 800
105        )
106        self._current_tab: GatherWindow.TabID | None = None
107        extra_top = 20 if uiscale is ba.UIScale.SMALL else 0
108        self._r = 'gatherWindow'
109
110        super().__init__(
111            root_widget=ba.containerwidget(
112                size=(self._width, self._height + extra_top),
113                transition=transition,
114                toolbar_visibility='menu_minimal',
115                scale_origin_stack_offset=scale_origin,
116                scale=(
117                    1.3
118                    if uiscale is ba.UIScale.SMALL
119                    else 0.97
120                    if uiscale is ba.UIScale.MEDIUM
121                    else 0.8
122                ),
123                stack_offset=(0, -11)
124                if uiscale is ba.UIScale.SMALL
125                else (0, 0)
126                if uiscale is ba.UIScale.MEDIUM
127                else (0, 0),
128            )
129        )
130
131        if uiscale is ba.UIScale.SMALL and ba.app.ui.use_toolbars:
132            ba.containerwidget(
133                edit=self._root_widget, on_cancel_call=self._back
134            )
135            self._back_button = None
136        else:
137            self._back_button = btn = ba.buttonwidget(
138                parent=self._root_widget,
139                position=(70 + x_offs, self._height - 74),
140                size=(140, 60),
141                scale=1.1,
142                autoselect=True,
143                label=ba.Lstr(resource='backText'),
144                button_type='back',
145                on_activate_call=self._back,
146            )
147            ba.containerwidget(edit=self._root_widget, cancel_button=btn)
148            ba.buttonwidget(
149                edit=btn,
150                button_type='backSmall',
151                position=(70 + x_offs, self._height - 78),
152                size=(60, 60),
153                label=ba.charstr(ba.SpecialChar.BACK),
154            )
155
156        condensed = uiscale is not ba.UIScale.LARGE
157        t_offs_y = (
158            0 if not condensed else 25 if uiscale is ba.UIScale.MEDIUM else 17
159        )
160        ba.textwidget(
161            parent=self._root_widget,
162            position=(self._width * 0.5, self._height - 42 + t_offs_y),
163            size=(0, 0),
164            color=ba.app.ui.title_color,
165            scale=(
166                1.5
167                if not condensed
168                else 1.0
169                if uiscale is ba.UIScale.MEDIUM
170                else 0.6
171            ),
172            h_align='center',
173            v_align='center',
174            text=ba.Lstr(resource=self._r + '.titleText'),
175            maxwidth=550,
176        )
177
178        scroll_buffer_h = 130 + 2 * x_offs
179        tab_buffer_h = (320 if condensed else 250) + 2 * x_offs
180
181        # Build up the set of tabs we want.
182        tabdefs: list[tuple[GatherWindow.TabID, ba.Lstr]] = [
183            (self.TabID.ABOUT, ba.Lstr(resource=self._r + '.aboutText'))
184        ]
185        if ba.internal.get_v1_account_misc_read_val(
186            'enablePublicParties', True
187        ):
188            tabdefs.append(
189                (self.TabID.INTERNET, ba.Lstr(resource=self._r + '.publicText'))
190            )
191        tabdefs.append(
192            (self.TabID.PRIVATE, ba.Lstr(resource=self._r + '.privateText'))
193        )
194        tabdefs.append(
195            (self.TabID.NEARBY, ba.Lstr(resource=self._r + '.nearbyText'))
196        )
197        tabdefs.append(
198            (self.TabID.MANUAL, ba.Lstr(resource=self._r + '.manualText'))
199        )
200
201        # On small UI, push our tabs up closer to the top of the screen to
202        # save a bit of space.
203        tabs_top_extra = 42 if condensed else 0
204        self._tab_row = TabRow(
205            self._root_widget,
206            tabdefs,
207            pos=(tab_buffer_h * 0.5, self._height - 130 + tabs_top_extra),
208            size=(self._width - tab_buffer_h, 50),
209            on_select_call=ba.WeakCall(self._set_tab),
210        )
211
212        # Now instantiate handlers for these tabs.
213        tabtypes: dict[GatherWindow.TabID, type[GatherTab]] = {
214            self.TabID.ABOUT: AboutGatherTab,
215            self.TabID.MANUAL: ManualGatherTab,
216            self.TabID.PRIVATE: PrivateGatherTab,
217            self.TabID.INTERNET: PublicGatherTab,
218            self.TabID.NEARBY: NearbyGatherTab,
219        }
220        self._tabs: dict[GatherWindow.TabID, GatherTab] = {}
221        for tab_id in self._tab_row.tabs:
222            tabtype = tabtypes.get(tab_id)
223            if tabtype is not None:
224                self._tabs[tab_id] = tabtype(self)
225
226        if ba.app.ui.use_toolbars:
227            ba.widget(
228                edit=self._tab_row.tabs[tabdefs[-1][0]].button,
229                right_widget=ba.internal.get_special_widget('party_button'),
230            )
231            if uiscale is ba.UIScale.SMALL:
232                ba.widget(
233                    edit=self._tab_row.tabs[tabdefs[0][0]].button,
234                    left_widget=ba.internal.get_special_widget('back_button'),
235                )
236
237        self._scroll_width = self._width - scroll_buffer_h
238        self._scroll_height = self._height - 180.0 + tabs_top_extra
239
240        self._scroll_left = (self._width - self._scroll_width) * 0.5
241        self._scroll_bottom = (
242            self._height - self._scroll_height - 79 - 48 + tabs_top_extra
243        )
244        buffer_h = 10
245        buffer_v = 4
246
247        # Not actually using a scroll widget anymore; just an image.
248        ba.imagewidget(
249            parent=self._root_widget,
250            position=(
251                self._scroll_left - buffer_h,
252                self._scroll_bottom - buffer_v,
253            ),
254            size=(
255                self._scroll_width + 2 * buffer_h,
256                self._scroll_height + 2 * buffer_v,
257            ),
258            texture=ba.gettexture('scrollWidget'),
259            model_transparent=ba.getmodel('softEdgeOutside'),
260        )
261        self._tab_container: ba.Widget | None = None
262
263        self._restore_state()
264
265    def __del__(self) -> None:
266        ba.internal.set_party_icon_always_visible(False)
267
268    def playlist_select(self, origin_widget: ba.Widget) -> None:
269        """Called by the private-hosting tab to select a playlist."""
270        from bastd.ui.play import PlayWindow
271
272        self._save_state()
273        ba.containerwidget(edit=self._root_widget, transition='out_left')
274        ba.app.ui.selecting_private_party_playlist = True
275        ba.app.ui.set_main_menu_window(
276            PlayWindow(origin_widget=origin_widget).get_root_widget()
277        )
278
279    def _set_tab(self, tab_id: TabID) -> None:
280        if self._current_tab is tab_id:
281            return
282        prev_tab_id = self._current_tab
283        self._current_tab = tab_id
284
285        # We wanna preserve our current tab between runs.
286        cfg = ba.app.config
287        cfg['Gather Tab'] = tab_id.value
288        cfg.commit()
289
290        # Update tab colors based on which is selected.
291        self._tab_row.update_appearance(tab_id)
292
293        if prev_tab_id is not None:
294            prev_tab = self._tabs.get(prev_tab_id)
295            if prev_tab is not None:
296                prev_tab.on_deactivate()
297
298        # Clear up prev container if it hasn't been done.
299        if self._tab_container:
300            self._tab_container.delete()
301
302        tab = self._tabs.get(tab_id)
303        if tab is not None:
304            self._tab_container = tab.on_activate(
305                self._root_widget,
306                self._tab_row.tabs[tab_id].button,
307                self._scroll_width,
308                self._scroll_height,
309                self._scroll_left,
310                self._scroll_bottom,
311            )
312            return
313
314    def _save_state(self) -> None:
315        try:
316            for tab in self._tabs.values():
317                tab.save_state()
318
319            sel = self._root_widget.get_selected_child()
320            selected_tab_ids = [
321                tab_id
322                for tab_id, tab in self._tab_row.tabs.items()
323                if sel == tab.button
324            ]
325            if sel == self._back_button:
326                sel_name = 'Back'
327            elif selected_tab_ids:
328                assert len(selected_tab_ids) == 1
329                sel_name = f'Tab:{selected_tab_ids[0].value}'
330            elif sel == self._tab_container:
331                sel_name = 'TabContainer'
332            else:
333                raise ValueError(f'unrecognized selection: \'{sel}\'')
334            ba.app.ui.window_states[type(self)] = {
335                'sel_name': sel_name,
336            }
337        except Exception:
338            ba.print_exception(f'Error saving state for {self}.')
339
340    def _restore_state(self) -> None:
341        from efro.util import enum_by_value
342
343        try:
344            for tab in self._tabs.values():
345                tab.restore_state()
346
347            sel: ba.Widget | None
348            winstate = ba.app.ui.window_states.get(type(self), {})
349            sel_name = winstate.get('sel_name', None)
350            assert isinstance(sel_name, (str, type(None)))
351            current_tab = self.TabID.ABOUT
352            gather_tab_val = ba.app.config.get('Gather Tab')
353            try:
354                stored_tab = enum_by_value(self.TabID, gather_tab_val)
355                if stored_tab in self._tab_row.tabs:
356                    current_tab = stored_tab
357            except ValueError:
358                pass
359            self._set_tab(current_tab)
360            if sel_name == 'Back':
361                sel = self._back_button
362            elif sel_name == 'TabContainer':
363                sel = self._tab_container
364            elif isinstance(sel_name, str) and sel_name.startswith('Tab:'):
365                try:
366                    sel_tab_id = enum_by_value(
367                        self.TabID, sel_name.split(':')[-1]
368                    )
369                except ValueError:
370                    sel_tab_id = self.TabID.ABOUT
371                sel = self._tab_row.tabs[sel_tab_id].button
372            else:
373                sel = self._tab_row.tabs[current_tab].button
374            ba.containerwidget(edit=self._root_widget, selected_child=sel)
375        except Exception:
376            ba.print_exception('Error restoring gather-win state.')
377
378    def _back(self) -> None:
379        from bastd.ui.mainmenu import MainMenuWindow
380
381        self._save_state()
382        ba.containerwidget(
383            edit=self._root_widget, transition=self._transition_out
384        )
385        ba.app.ui.set_main_menu_window(
386            MainMenuWindow(transition='in_left').get_root_widget()
387        )
class GatherTab:
20class GatherTab:
21    """Defines a tab for use in the gather UI."""
22
23    def __init__(self, window: GatherWindow) -> None:
24        self._window = weakref.ref(window)
25
26    @property
27    def window(self) -> GatherWindow:
28        """The GatherWindow that this tab belongs to."""
29        window = self._window()
30        if window is None:
31            raise ba.NotFoundError("GatherTab's window no longer exists.")
32        return window
33
34    def on_activate(
35        self,
36        parent_widget: ba.Widget,
37        tab_button: ba.Widget,
38        region_width: float,
39        region_height: float,
40        region_left: float,
41        region_bottom: float,
42    ) -> ba.Widget:
43        """Called when the tab becomes the active one.
44
45        The tab should create and return a container widget covering the
46        specified region.
47        """
48        raise RuntimeError('Should not get here.')
49
50    def on_deactivate(self) -> None:
51        """Called when the tab will no longer be the active one."""
52
53    def save_state(self) -> None:
54        """Called when the parent window is saving state."""
55
56    def restore_state(self) -> None:
57        """Called when the parent window is restoring state."""

Defines a tab for use in the gather UI.

GatherTab(window: bastd.ui.gather.GatherWindow)
23    def __init__(self, window: GatherWindow) -> None:
24        self._window = weakref.ref(window)

The GatherWindow that this tab belongs to.

def on_activate( self, parent_widget: _ba.Widget, tab_button: _ba.Widget, region_width: float, region_height: float, region_left: float, region_bottom: float) -> _ba.Widget:
34    def on_activate(
35        self,
36        parent_widget: ba.Widget,
37        tab_button: ba.Widget,
38        region_width: float,
39        region_height: float,
40        region_left: float,
41        region_bottom: float,
42    ) -> ba.Widget:
43        """Called when the tab becomes the active one.
44
45        The tab should create and return a container widget covering the
46        specified region.
47        """
48        raise RuntimeError('Should not get here.')

Called when the tab becomes the active one.

The tab should create and return a container widget covering the specified region.

def on_deactivate(self) -> None:
50    def on_deactivate(self) -> None:
51        """Called when the tab will no longer be the active one."""

Called when the tab will no longer be the active one.

def save_state(self) -> None:
53    def save_state(self) -> None:
54        """Called when the parent window is saving state."""

Called when the parent window is saving state.

def restore_state(self) -> None:
56    def restore_state(self) -> None:
57        """Called when the parent window is restoring state."""

Called when the parent window is restoring state.

class GatherWindow(ba.ui.Window):
 60class GatherWindow(ba.Window):
 61    """Window for joining/inviting friends."""
 62
 63    class TabID(Enum):
 64        """Our available tab types."""
 65
 66        ABOUT = 'about'
 67        INTERNET = 'internet'
 68        PRIVATE = 'private'
 69        NEARBY = 'nearby'
 70        MANUAL = 'manual'
 71
 72    def __init__(
 73        self,
 74        transition: str | None = 'in_right',
 75        origin_widget: ba.Widget | None = None,
 76    ):
 77        # pylint: disable=too-many-statements
 78        # pylint: disable=too-many-locals
 79        # pylint: disable=cyclic-import
 80        from bastd.ui.gather.abouttab import AboutGatherTab
 81        from bastd.ui.gather.manualtab import ManualGatherTab
 82        from bastd.ui.gather.privatetab import PrivateGatherTab
 83        from bastd.ui.gather.publictab import PublicGatherTab
 84        from bastd.ui.gather.nearbytab import NearbyGatherTab
 85
 86        ba.set_analytics_screen('Gather Window')
 87        scale_origin: tuple[float, float] | None
 88        if origin_widget is not None:
 89            self._transition_out = 'out_scale'
 90            scale_origin = origin_widget.get_screen_space_center()
 91            transition = 'in_scale'
 92        else:
 93            self._transition_out = 'out_right'
 94            scale_origin = None
 95        ba.app.ui.set_main_menu_location('Gather')
 96        ba.internal.set_party_icon_always_visible(True)
 97        uiscale = ba.app.ui.uiscale
 98        self._width = 1240 if uiscale is ba.UIScale.SMALL else 1040
 99        x_offs = 100 if uiscale is ba.UIScale.SMALL else 0
100        self._height = (
101            582
102            if uiscale is ba.UIScale.SMALL
103            else 680
104            if uiscale is ba.UIScale.MEDIUM
105            else 800
106        )
107        self._current_tab: GatherWindow.TabID | None = None
108        extra_top = 20 if uiscale is ba.UIScale.SMALL else 0
109        self._r = 'gatherWindow'
110
111        super().__init__(
112            root_widget=ba.containerwidget(
113                size=(self._width, self._height + extra_top),
114                transition=transition,
115                toolbar_visibility='menu_minimal',
116                scale_origin_stack_offset=scale_origin,
117                scale=(
118                    1.3
119                    if uiscale is ba.UIScale.SMALL
120                    else 0.97
121                    if uiscale is ba.UIScale.MEDIUM
122                    else 0.8
123                ),
124                stack_offset=(0, -11)
125                if uiscale is ba.UIScale.SMALL
126                else (0, 0)
127                if uiscale is ba.UIScale.MEDIUM
128                else (0, 0),
129            )
130        )
131
132        if uiscale is ba.UIScale.SMALL and ba.app.ui.use_toolbars:
133            ba.containerwidget(
134                edit=self._root_widget, on_cancel_call=self._back
135            )
136            self._back_button = None
137        else:
138            self._back_button = btn = ba.buttonwidget(
139                parent=self._root_widget,
140                position=(70 + x_offs, self._height - 74),
141                size=(140, 60),
142                scale=1.1,
143                autoselect=True,
144                label=ba.Lstr(resource='backText'),
145                button_type='back',
146                on_activate_call=self._back,
147            )
148            ba.containerwidget(edit=self._root_widget, cancel_button=btn)
149            ba.buttonwidget(
150                edit=btn,
151                button_type='backSmall',
152                position=(70 + x_offs, self._height - 78),
153                size=(60, 60),
154                label=ba.charstr(ba.SpecialChar.BACK),
155            )
156
157        condensed = uiscale is not ba.UIScale.LARGE
158        t_offs_y = (
159            0 if not condensed else 25 if uiscale is ba.UIScale.MEDIUM else 17
160        )
161        ba.textwidget(
162            parent=self._root_widget,
163            position=(self._width * 0.5, self._height - 42 + t_offs_y),
164            size=(0, 0),
165            color=ba.app.ui.title_color,
166            scale=(
167                1.5
168                if not condensed
169                else 1.0
170                if uiscale is ba.UIScale.MEDIUM
171                else 0.6
172            ),
173            h_align='center',
174            v_align='center',
175            text=ba.Lstr(resource=self._r + '.titleText'),
176            maxwidth=550,
177        )
178
179        scroll_buffer_h = 130 + 2 * x_offs
180        tab_buffer_h = (320 if condensed else 250) + 2 * x_offs
181
182        # Build up the set of tabs we want.
183        tabdefs: list[tuple[GatherWindow.TabID, ba.Lstr]] = [
184            (self.TabID.ABOUT, ba.Lstr(resource=self._r + '.aboutText'))
185        ]
186        if ba.internal.get_v1_account_misc_read_val(
187            'enablePublicParties', True
188        ):
189            tabdefs.append(
190                (self.TabID.INTERNET, ba.Lstr(resource=self._r + '.publicText'))
191            )
192        tabdefs.append(
193            (self.TabID.PRIVATE, ba.Lstr(resource=self._r + '.privateText'))
194        )
195        tabdefs.append(
196            (self.TabID.NEARBY, ba.Lstr(resource=self._r + '.nearbyText'))
197        )
198        tabdefs.append(
199            (self.TabID.MANUAL, ba.Lstr(resource=self._r + '.manualText'))
200        )
201
202        # On small UI, push our tabs up closer to the top of the screen to
203        # save a bit of space.
204        tabs_top_extra = 42 if condensed else 0
205        self._tab_row = TabRow(
206            self._root_widget,
207            tabdefs,
208            pos=(tab_buffer_h * 0.5, self._height - 130 + tabs_top_extra),
209            size=(self._width - tab_buffer_h, 50),
210            on_select_call=ba.WeakCall(self._set_tab),
211        )
212
213        # Now instantiate handlers for these tabs.
214        tabtypes: dict[GatherWindow.TabID, type[GatherTab]] = {
215            self.TabID.ABOUT: AboutGatherTab,
216            self.TabID.MANUAL: ManualGatherTab,
217            self.TabID.PRIVATE: PrivateGatherTab,
218            self.TabID.INTERNET: PublicGatherTab,
219            self.TabID.NEARBY: NearbyGatherTab,
220        }
221        self._tabs: dict[GatherWindow.TabID, GatherTab] = {}
222        for tab_id in self._tab_row.tabs:
223            tabtype = tabtypes.get(tab_id)
224            if tabtype is not None:
225                self._tabs[tab_id] = tabtype(self)
226
227        if ba.app.ui.use_toolbars:
228            ba.widget(
229                edit=self._tab_row.tabs[tabdefs[-1][0]].button,
230                right_widget=ba.internal.get_special_widget('party_button'),
231            )
232            if uiscale is ba.UIScale.SMALL:
233                ba.widget(
234                    edit=self._tab_row.tabs[tabdefs[0][0]].button,
235                    left_widget=ba.internal.get_special_widget('back_button'),
236                )
237
238        self._scroll_width = self._width - scroll_buffer_h
239        self._scroll_height = self._height - 180.0 + tabs_top_extra
240
241        self._scroll_left = (self._width - self._scroll_width) * 0.5
242        self._scroll_bottom = (
243            self._height - self._scroll_height - 79 - 48 + tabs_top_extra
244        )
245        buffer_h = 10
246        buffer_v = 4
247
248        # Not actually using a scroll widget anymore; just an image.
249        ba.imagewidget(
250            parent=self._root_widget,
251            position=(
252                self._scroll_left - buffer_h,
253                self._scroll_bottom - buffer_v,
254            ),
255            size=(
256                self._scroll_width + 2 * buffer_h,
257                self._scroll_height + 2 * buffer_v,
258            ),
259            texture=ba.gettexture('scrollWidget'),
260            model_transparent=ba.getmodel('softEdgeOutside'),
261        )
262        self._tab_container: ba.Widget | None = None
263
264        self._restore_state()
265
266    def __del__(self) -> None:
267        ba.internal.set_party_icon_always_visible(False)
268
269    def playlist_select(self, origin_widget: ba.Widget) -> None:
270        """Called by the private-hosting tab to select a playlist."""
271        from bastd.ui.play import PlayWindow
272
273        self._save_state()
274        ba.containerwidget(edit=self._root_widget, transition='out_left')
275        ba.app.ui.selecting_private_party_playlist = True
276        ba.app.ui.set_main_menu_window(
277            PlayWindow(origin_widget=origin_widget).get_root_widget()
278        )
279
280    def _set_tab(self, tab_id: TabID) -> None:
281        if self._current_tab is tab_id:
282            return
283        prev_tab_id = self._current_tab
284        self._current_tab = tab_id
285
286        # We wanna preserve our current tab between runs.
287        cfg = ba.app.config
288        cfg['Gather Tab'] = tab_id.value
289        cfg.commit()
290
291        # Update tab colors based on which is selected.
292        self._tab_row.update_appearance(tab_id)
293
294        if prev_tab_id is not None:
295            prev_tab = self._tabs.get(prev_tab_id)
296            if prev_tab is not None:
297                prev_tab.on_deactivate()
298
299        # Clear up prev container if it hasn't been done.
300        if self._tab_container:
301            self._tab_container.delete()
302
303        tab = self._tabs.get(tab_id)
304        if tab is not None:
305            self._tab_container = tab.on_activate(
306                self._root_widget,
307                self._tab_row.tabs[tab_id].button,
308                self._scroll_width,
309                self._scroll_height,
310                self._scroll_left,
311                self._scroll_bottom,
312            )
313            return
314
315    def _save_state(self) -> None:
316        try:
317            for tab in self._tabs.values():
318                tab.save_state()
319
320            sel = self._root_widget.get_selected_child()
321            selected_tab_ids = [
322                tab_id
323                for tab_id, tab in self._tab_row.tabs.items()
324                if sel == tab.button
325            ]
326            if sel == self._back_button:
327                sel_name = 'Back'
328            elif selected_tab_ids:
329                assert len(selected_tab_ids) == 1
330                sel_name = f'Tab:{selected_tab_ids[0].value}'
331            elif sel == self._tab_container:
332                sel_name = 'TabContainer'
333            else:
334                raise ValueError(f'unrecognized selection: \'{sel}\'')
335            ba.app.ui.window_states[type(self)] = {
336                'sel_name': sel_name,
337            }
338        except Exception:
339            ba.print_exception(f'Error saving state for {self}.')
340
341    def _restore_state(self) -> None:
342        from efro.util import enum_by_value
343
344        try:
345            for tab in self._tabs.values():
346                tab.restore_state()
347
348            sel: ba.Widget | None
349            winstate = ba.app.ui.window_states.get(type(self), {})
350            sel_name = winstate.get('sel_name', None)
351            assert isinstance(sel_name, (str, type(None)))
352            current_tab = self.TabID.ABOUT
353            gather_tab_val = ba.app.config.get('Gather Tab')
354            try:
355                stored_tab = enum_by_value(self.TabID, gather_tab_val)
356                if stored_tab in self._tab_row.tabs:
357                    current_tab = stored_tab
358            except ValueError:
359                pass
360            self._set_tab(current_tab)
361            if sel_name == 'Back':
362                sel = self._back_button
363            elif sel_name == 'TabContainer':
364                sel = self._tab_container
365            elif isinstance(sel_name, str) and sel_name.startswith('Tab:'):
366                try:
367                    sel_tab_id = enum_by_value(
368                        self.TabID, sel_name.split(':')[-1]
369                    )
370                except ValueError:
371                    sel_tab_id = self.TabID.ABOUT
372                sel = self._tab_row.tabs[sel_tab_id].button
373            else:
374                sel = self._tab_row.tabs[current_tab].button
375            ba.containerwidget(edit=self._root_widget, selected_child=sel)
376        except Exception:
377            ba.print_exception('Error restoring gather-win state.')
378
379    def _back(self) -> None:
380        from bastd.ui.mainmenu import MainMenuWindow
381
382        self._save_state()
383        ba.containerwidget(
384            edit=self._root_widget, transition=self._transition_out
385        )
386        ba.app.ui.set_main_menu_window(
387            MainMenuWindow(transition='in_left').get_root_widget()
388        )

Window for joining/inviting friends.

GatherWindow( transition: str | None = 'in_right', origin_widget: _ba.Widget | None = None)
 72    def __init__(
 73        self,
 74        transition: str | None = 'in_right',
 75        origin_widget: ba.Widget | None = None,
 76    ):
 77        # pylint: disable=too-many-statements
 78        # pylint: disable=too-many-locals
 79        # pylint: disable=cyclic-import
 80        from bastd.ui.gather.abouttab import AboutGatherTab
 81        from bastd.ui.gather.manualtab import ManualGatherTab
 82        from bastd.ui.gather.privatetab import PrivateGatherTab
 83        from bastd.ui.gather.publictab import PublicGatherTab
 84        from bastd.ui.gather.nearbytab import NearbyGatherTab
 85
 86        ba.set_analytics_screen('Gather Window')
 87        scale_origin: tuple[float, float] | None
 88        if origin_widget is not None:
 89            self._transition_out = 'out_scale'
 90            scale_origin = origin_widget.get_screen_space_center()
 91            transition = 'in_scale'
 92        else:
 93            self._transition_out = 'out_right'
 94            scale_origin = None
 95        ba.app.ui.set_main_menu_location('Gather')
 96        ba.internal.set_party_icon_always_visible(True)
 97        uiscale = ba.app.ui.uiscale
 98        self._width = 1240 if uiscale is ba.UIScale.SMALL else 1040
 99        x_offs = 100 if uiscale is ba.UIScale.SMALL else 0
100        self._height = (
101            582
102            if uiscale is ba.UIScale.SMALL
103            else 680
104            if uiscale is ba.UIScale.MEDIUM
105            else 800
106        )
107        self._current_tab: GatherWindow.TabID | None = None
108        extra_top = 20 if uiscale is ba.UIScale.SMALL else 0
109        self._r = 'gatherWindow'
110
111        super().__init__(
112            root_widget=ba.containerwidget(
113                size=(self._width, self._height + extra_top),
114                transition=transition,
115                toolbar_visibility='menu_minimal',
116                scale_origin_stack_offset=scale_origin,
117                scale=(
118                    1.3
119                    if uiscale is ba.UIScale.SMALL
120                    else 0.97
121                    if uiscale is ba.UIScale.MEDIUM
122                    else 0.8
123                ),
124                stack_offset=(0, -11)
125                if uiscale is ba.UIScale.SMALL
126                else (0, 0)
127                if uiscale is ba.UIScale.MEDIUM
128                else (0, 0),
129            )
130        )
131
132        if uiscale is ba.UIScale.SMALL and ba.app.ui.use_toolbars:
133            ba.containerwidget(
134                edit=self._root_widget, on_cancel_call=self._back
135            )
136            self._back_button = None
137        else:
138            self._back_button = btn = ba.buttonwidget(
139                parent=self._root_widget,
140                position=(70 + x_offs, self._height - 74),
141                size=(140, 60),
142                scale=1.1,
143                autoselect=True,
144                label=ba.Lstr(resource='backText'),
145                button_type='back',
146                on_activate_call=self._back,
147            )
148            ba.containerwidget(edit=self._root_widget, cancel_button=btn)
149            ba.buttonwidget(
150                edit=btn,
151                button_type='backSmall',
152                position=(70 + x_offs, self._height - 78),
153                size=(60, 60),
154                label=ba.charstr(ba.SpecialChar.BACK),
155            )
156
157        condensed = uiscale is not ba.UIScale.LARGE
158        t_offs_y = (
159            0 if not condensed else 25 if uiscale is ba.UIScale.MEDIUM else 17
160        )
161        ba.textwidget(
162            parent=self._root_widget,
163            position=(self._width * 0.5, self._height - 42 + t_offs_y),
164            size=(0, 0),
165            color=ba.app.ui.title_color,
166            scale=(
167                1.5
168                if not condensed
169                else 1.0
170                if uiscale is ba.UIScale.MEDIUM
171                else 0.6
172            ),
173            h_align='center',
174            v_align='center',
175            text=ba.Lstr(resource=self._r + '.titleText'),
176            maxwidth=550,
177        )
178
179        scroll_buffer_h = 130 + 2 * x_offs
180        tab_buffer_h = (320 if condensed else 250) + 2 * x_offs
181
182        # Build up the set of tabs we want.
183        tabdefs: list[tuple[GatherWindow.TabID, ba.Lstr]] = [
184            (self.TabID.ABOUT, ba.Lstr(resource=self._r + '.aboutText'))
185        ]
186        if ba.internal.get_v1_account_misc_read_val(
187            'enablePublicParties', True
188        ):
189            tabdefs.append(
190                (self.TabID.INTERNET, ba.Lstr(resource=self._r + '.publicText'))
191            )
192        tabdefs.append(
193            (self.TabID.PRIVATE, ba.Lstr(resource=self._r + '.privateText'))
194        )
195        tabdefs.append(
196            (self.TabID.NEARBY, ba.Lstr(resource=self._r + '.nearbyText'))
197        )
198        tabdefs.append(
199            (self.TabID.MANUAL, ba.Lstr(resource=self._r + '.manualText'))
200        )
201
202        # On small UI, push our tabs up closer to the top of the screen to
203        # save a bit of space.
204        tabs_top_extra = 42 if condensed else 0
205        self._tab_row = TabRow(
206            self._root_widget,
207            tabdefs,
208            pos=(tab_buffer_h * 0.5, self._height - 130 + tabs_top_extra),
209            size=(self._width - tab_buffer_h, 50),
210            on_select_call=ba.WeakCall(self._set_tab),
211        )
212
213        # Now instantiate handlers for these tabs.
214        tabtypes: dict[GatherWindow.TabID, type[GatherTab]] = {
215            self.TabID.ABOUT: AboutGatherTab,
216            self.TabID.MANUAL: ManualGatherTab,
217            self.TabID.PRIVATE: PrivateGatherTab,
218            self.TabID.INTERNET: PublicGatherTab,
219            self.TabID.NEARBY: NearbyGatherTab,
220        }
221        self._tabs: dict[GatherWindow.TabID, GatherTab] = {}
222        for tab_id in self._tab_row.tabs:
223            tabtype = tabtypes.get(tab_id)
224            if tabtype is not None:
225                self._tabs[tab_id] = tabtype(self)
226
227        if ba.app.ui.use_toolbars:
228            ba.widget(
229                edit=self._tab_row.tabs[tabdefs[-1][0]].button,
230                right_widget=ba.internal.get_special_widget('party_button'),
231            )
232            if uiscale is ba.UIScale.SMALL:
233                ba.widget(
234                    edit=self._tab_row.tabs[tabdefs[0][0]].button,
235                    left_widget=ba.internal.get_special_widget('back_button'),
236                )
237
238        self._scroll_width = self._width - scroll_buffer_h
239        self._scroll_height = self._height - 180.0 + tabs_top_extra
240
241        self._scroll_left = (self._width - self._scroll_width) * 0.5
242        self._scroll_bottom = (
243            self._height - self._scroll_height - 79 - 48 + tabs_top_extra
244        )
245        buffer_h = 10
246        buffer_v = 4
247
248        # Not actually using a scroll widget anymore; just an image.
249        ba.imagewidget(
250            parent=self._root_widget,
251            position=(
252                self._scroll_left - buffer_h,
253                self._scroll_bottom - buffer_v,
254            ),
255            size=(
256                self._scroll_width + 2 * buffer_h,
257                self._scroll_height + 2 * buffer_v,
258            ),
259            texture=ba.gettexture('scrollWidget'),
260            model_transparent=ba.getmodel('softEdgeOutside'),
261        )
262        self._tab_container: ba.Widget | None = None
263
264        self._restore_state()
def playlist_select(self, origin_widget: _ba.Widget) -> None:
269    def playlist_select(self, origin_widget: ba.Widget) -> None:
270        """Called by the private-hosting tab to select a playlist."""
271        from bastd.ui.play import PlayWindow
272
273        self._save_state()
274        ba.containerwidget(edit=self._root_widget, transition='out_left')
275        ba.app.ui.selecting_private_party_playlist = True
276        ba.app.ui.set_main_menu_window(
277            PlayWindow(origin_widget=origin_widget).get_root_widget()
278        )

Called by the private-hosting tab to select a playlist.

Inherited Members
ba.ui.Window
get_root_widget
class GatherWindow.TabID(enum.Enum):
63    class TabID(Enum):
64        """Our available tab types."""
65
66        ABOUT = 'about'
67        INTERNET = 'internet'
68        PRIVATE = 'private'
69        NEARBY = 'nearby'
70        MANUAL = 'manual'

Our available tab types.

ABOUT = <TabID.ABOUT: 'about'>
INTERNET = <TabID.INTERNET: 'internet'>
PRIVATE = <TabID.PRIVATE: 'private'>
NEARBY = <TabID.NEARBY: 'nearby'>
MANUAL = <TabID.MANUAL: 'manual'>
Inherited Members
enum.Enum
name
value