bauiv1lib.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
  8import logging
  9from enum import Enum
 10from typing import override, TYPE_CHECKING
 11
 12from bauiv1lib.tabs import TabRow
 13import bauiv1 as bui
 14
 15if TYPE_CHECKING:
 16    from bauiv1lib.play import PlaylistSelectContext
 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 bui.NotFoundError("GatherTab's window no longer exists.")
 31        return window
 32
 33    def on_activate(
 34        self,
 35        parent_widget: bui.Widget,
 36        tab_button: bui.Widget,
 37        region_width: float,
 38        region_height: float,
 39        region_left: float,
 40        region_bottom: float,
 41    ) -> bui.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        # pylint: disable=too-many-positional-arguments
 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."""
 58
 59
 60class GatherWindow(bui.MainWindow):
 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: bui.Widget | None = None,
 76    ):
 77        # pylint: disable=too-many-statements
 78        # pylint: disable=too-many-locals
 79        # pylint: disable=cyclic-import
 80        from bauiv1lib.gather.abouttab import AboutGatherTab
 81        from bauiv1lib.gather.manualtab import ManualGatherTab
 82        from bauiv1lib.gather.privatetab import PrivateGatherTab
 83        from bauiv1lib.gather.publictab import PublicGatherTab
 84        from bauiv1lib.gather.nearbytab import NearbyGatherTab
 85
 86        plus = bui.app.plus
 87        assert plus is not None
 88
 89        bui.set_analytics_screen('Gather Window')
 90        uiscale = bui.app.ui_v1.uiscale
 91        self._width = 1640 if uiscale is bui.UIScale.SMALL else 1040
 92        x_offs = 200 if uiscale is bui.UIScale.SMALL else 0
 93        y_offs = -65 if uiscale is bui.UIScale.SMALL else 0
 94        self._height = (
 95            650
 96            if uiscale is bui.UIScale.SMALL
 97            else 680 if uiscale is bui.UIScale.MEDIUM else 800
 98        )
 99        self._current_tab: GatherWindow.TabID | None = None
100        extra_top = 20 if uiscale is bui.UIScale.SMALL else 0
101        self._r = 'gatherWindow'
102
103        super().__init__(
104            root_widget=bui.containerwidget(
105                size=(self._width, self._height + extra_top),
106                toolbar_visibility=(
107                    'menu_tokens'
108                    if uiscale is bui.UIScale.SMALL
109                    else 'menu_full'
110                ),
111                scale=(
112                    1.15
113                    if uiscale is bui.UIScale.SMALL
114                    else 0.95 if uiscale is bui.UIScale.MEDIUM else 0.7
115                ),
116                stack_offset=(
117                    (0, 0)
118                    if uiscale is bui.UIScale.SMALL
119                    else (0, 0) if uiscale is bui.UIScale.MEDIUM else (0, 0)
120                ),
121            ),
122            transition=transition,
123            origin_widget=origin_widget,
124        )
125
126        if uiscale is bui.UIScale.SMALL:
127            bui.containerwidget(
128                edit=self._root_widget, on_cancel_call=self.main_window_back
129            )
130            self._back_button = None
131        else:
132            self._back_button = btn = bui.buttonwidget(
133                parent=self._root_widget,
134                position=(70 + x_offs, self._height - 74 + y_offs),
135                size=(140, 60),
136                scale=1.1,
137                autoselect=True,
138                label=bui.Lstr(resource='backText'),
139                button_type='back',
140                on_activate_call=self.main_window_back,
141            )
142            bui.containerwidget(edit=self._root_widget, cancel_button=btn)
143            bui.buttonwidget(
144                edit=btn,
145                button_type='backSmall',
146                position=(70 + x_offs, self._height - 78),
147                size=(60, 60),
148                label=bui.charstr(bui.SpecialChar.BACK),
149            )
150
151        condensed = uiscale is not bui.UIScale.LARGE
152        t_offs_y = (
153            0 if not condensed else 25 if uiscale is bui.UIScale.MEDIUM else 33
154        )
155        bui.textwidget(
156            parent=self._root_widget,
157            position=(self._width * 0.5, self._height - 42 + t_offs_y + y_offs),
158            size=(0, 0),
159            color=bui.app.ui_v1.title_color,
160            scale=(
161                1.5
162                if not condensed
163                else 1.0 if uiscale is bui.UIScale.MEDIUM else 1.0
164            ),
165            h_align='center',
166            v_align='center',
167            text=bui.Lstr(resource=f'{self._r}.titleText'),
168            maxwidth=320,
169        )
170
171        scroll_buffer_h = 130 + 2 * x_offs
172        tab_buffer_h = (320 if condensed else 250) + 2 * x_offs
173
174        # Build up the set of tabs we want.
175        tabdefs: list[tuple[GatherWindow.TabID, bui.Lstr]] = [
176            (self.TabID.ABOUT, bui.Lstr(resource=f'{self._r}.aboutText'))
177        ]
178        if plus.get_v1_account_misc_read_val('enablePublicParties', True):
179            tabdefs.append(
180                (
181                    self.TabID.INTERNET,
182                    bui.Lstr(resource=f'{self._r}.publicText'),
183                )
184            )
185        tabdefs.append(
186            (self.TabID.PRIVATE, bui.Lstr(resource=f'{self._r}.privateText'))
187        )
188        tabdefs.append(
189            (self.TabID.NEARBY, bui.Lstr(resource=f'{self._r}.nearbyText'))
190        )
191        tabdefs.append(
192            (self.TabID.MANUAL, bui.Lstr(resource=f'{self._r}.manualText'))
193        )
194
195        # On small UI, push our tabs up closer to the top of the screen to
196        # save a bit of space.
197        tabs_top_extra = 42 if condensed else 0
198        self._tab_row = TabRow(
199            self._root_widget,
200            tabdefs,
201            pos=(
202                tab_buffer_h * 0.5,
203                self._height - 130 + tabs_top_extra + y_offs,
204            ),
205            size=(self._width - tab_buffer_h, 50),
206            on_select_call=bui.WeakCall(self._set_tab),
207        )
208
209        # Now instantiate handlers for these tabs.
210        tabtypes: dict[GatherWindow.TabID, type[GatherTab]] = {
211            self.TabID.ABOUT: AboutGatherTab,
212            self.TabID.MANUAL: ManualGatherTab,
213            self.TabID.PRIVATE: PrivateGatherTab,
214            self.TabID.INTERNET: PublicGatherTab,
215            self.TabID.NEARBY: NearbyGatherTab,
216        }
217        self._tabs: dict[GatherWindow.TabID, GatherTab] = {}
218        for tab_id in self._tab_row.tabs:
219            tabtype = tabtypes.get(tab_id)
220            if tabtype is not None:
221                self._tabs[tab_id] = tabtype(self)
222
223        bui.widget(
224            edit=self._tab_row.tabs[tabdefs[-1][0]].button,
225            right_widget=bui.get_special_widget('squad_button'),
226        )
227        if uiscale is bui.UIScale.SMALL:
228            bui.widget(
229                edit=self._tab_row.tabs[tabdefs[0][0]].button,
230                left_widget=bui.get_special_widget('back_button'),
231            )
232
233        self._scroll_width = self._width - scroll_buffer_h
234        self._scroll_height = (
235            self._height
236            - (270.0 if uiscale is bui.UIScale.SMALL else 180.0)
237            + tabs_top_extra
238        )
239
240        self._scroll_left = (self._width - self._scroll_width) * 0.5
241        self._scroll_bottom = (
242            self._height
243            - self._scroll_height
244            - 79
245            - 48
246            + tabs_top_extra
247            + y_offs
248        )
249        buffer_h = 10
250        buffer_v = 4
251
252        # Not actually using a scroll widget anymore; just an image.
253        bui.imagewidget(
254            parent=self._root_widget,
255            position=(
256                self._scroll_left - buffer_h,
257                self._scroll_bottom - buffer_v,
258            ),
259            size=(
260                self._scroll_width + 2 * buffer_h,
261                self._scroll_height + 2 * buffer_v,
262            ),
263            texture=bui.gettexture('scrollWidget'),
264            mesh_transparent=bui.getmesh('softEdgeOutside'),
265        )
266        self._tab_container: bui.Widget | None = None
267
268        self._restore_state()
269
270    @override
271    def get_main_window_state(self) -> bui.MainWindowState:
272        # Support recreating our window for back/refresh purposes.
273        cls = type(self)
274        return bui.BasicMainWindowState(
275            create_call=lambda transition, origin_widget: cls(
276                transition=transition, origin_widget=origin_widget
277            )
278        )
279
280    @override
281    def on_main_window_close(self) -> None:
282        self._save_state()
283
284    def playlist_select(
285        self,
286        origin_widget: bui.Widget,
287        context: PlaylistSelectContext,
288    ) -> None:
289        """Called by the private-hosting tab to select a playlist."""
290        from bauiv1lib.play import PlayWindow
291
292        # Avoid redundant window spawns.
293        if not self.main_window_has_control():
294            return
295
296        playwindow = PlayWindow(
297            origin_widget=origin_widget, playlist_select_context=context
298        )
299        self.main_window_replace(playwindow)
300
301        # Grab the newly-set main-window's back-state; that will lead us
302        # back here once we're done going down our main-window
303        # rabbit-hole for playlist selection.
304        context.back_state = playwindow.main_window_back_state
305
306    def _set_tab(self, tab_id: TabID) -> None:
307        if self._current_tab is tab_id:
308            return
309        prev_tab_id = self._current_tab
310        self._current_tab = tab_id
311
312        # We wanna preserve our current tab between runs.
313        cfg = bui.app.config
314        cfg['Gather Tab'] = tab_id.value
315        cfg.commit()
316
317        # Update tab colors based on which is selected.
318        self._tab_row.update_appearance(tab_id)
319
320        if prev_tab_id is not None:
321            prev_tab = self._tabs.get(prev_tab_id)
322            if prev_tab is not None:
323                prev_tab.on_deactivate()
324
325        # Clear up prev container if it hasn't been done.
326        if self._tab_container:
327            self._tab_container.delete()
328
329        tab = self._tabs.get(tab_id)
330        if tab is not None:
331            self._tab_container = tab.on_activate(
332                self._root_widget,
333                self._tab_row.tabs[tab_id].button,
334                self._scroll_width,
335                self._scroll_height,
336                self._scroll_left,
337                self._scroll_bottom,
338            )
339            return
340
341    def _save_state(self) -> None:
342        try:
343            for tab in self._tabs.values():
344                tab.save_state()
345
346            sel = self._root_widget.get_selected_child()
347            selected_tab_ids = [
348                tab_id
349                for tab_id, tab in self._tab_row.tabs.items()
350                if sel == tab.button
351            ]
352            if sel == self._back_button:
353                sel_name = 'Back'
354            elif selected_tab_ids:
355                assert len(selected_tab_ids) == 1
356                sel_name = f'Tab:{selected_tab_ids[0].value}'
357            elif sel == self._tab_container:
358                sel_name = 'TabContainer'
359            else:
360                raise ValueError(f'unrecognized selection: \'{sel}\'')
361            assert bui.app.classic is not None
362            bui.app.ui_v1.window_states[type(self)] = {
363                'sel_name': sel_name,
364            }
365        except Exception:
366            logging.exception('Error saving state for %s.', self)
367
368    def _restore_state(self) -> None:
369        try:
370            for tab in self._tabs.values():
371                tab.restore_state()
372
373            sel: bui.Widget | None
374            assert bui.app.classic is not None
375            winstate = bui.app.ui_v1.window_states.get(type(self), {})
376            sel_name = winstate.get('sel_name', None)
377            assert isinstance(sel_name, (str, type(None)))
378            current_tab = self.TabID.ABOUT
379            gather_tab_val = bui.app.config.get('Gather Tab')
380            try:
381                stored_tab = self.TabID(gather_tab_val)
382                if stored_tab in self._tab_row.tabs:
383                    current_tab = stored_tab
384            except ValueError:
385                pass
386            self._set_tab(current_tab)
387            if sel_name == 'Back':
388                sel = self._back_button
389            elif sel_name == 'TabContainer':
390                sel = self._tab_container
391            elif isinstance(sel_name, str) and sel_name.startswith('Tab:'):
392                try:
393                    sel_tab_id = self.TabID(sel_name.split(':')[-1])
394                except ValueError:
395                    sel_tab_id = self.TabID.ABOUT
396                sel = self._tab_row.tabs[sel_tab_id].button
397            else:
398                sel = self._tab_row.tabs[current_tab].button
399            bui.containerwidget(edit=self._root_widget, selected_child=sel)
400
401        except Exception:
402            logging.exception('Error restoring state for %s.', self)
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 bui.NotFoundError("GatherTab's window no longer exists.")
32        return window
33
34    def on_activate(
35        self,
36        parent_widget: bui.Widget,
37        tab_button: bui.Widget,
38        region_width: float,
39        region_height: float,
40        region_left: float,
41        region_bottom: float,
42    ) -> bui.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        # pylint: disable=too-many-positional-arguments
49        raise RuntimeError('Should not get here.')
50
51    def on_deactivate(self) -> None:
52        """Called when the tab will no longer be the active one."""
53
54    def save_state(self) -> None:
55        """Called when the parent window is saving state."""
56
57    def restore_state(self) -> None:
58        """Called when the parent window is restoring state."""

Defines a tab for use in the gather UI.

GatherTab(window: GatherWindow)
23    def __init__(self, window: GatherWindow) -> None:
24        self._window = weakref.ref(window)
window: GatherWindow
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 bui.NotFoundError("GatherTab's window no longer exists.")
32        return window

The GatherWindow that this tab belongs to.

def on_activate( self, parent_widget: _bauiv1.Widget, tab_button: _bauiv1.Widget, region_width: float, region_height: float, region_left: float, region_bottom: float) -> _bauiv1.Widget:
34    def on_activate(
35        self,
36        parent_widget: bui.Widget,
37        tab_button: bui.Widget,
38        region_width: float,
39        region_height: float,
40        region_left: float,
41        region_bottom: float,
42    ) -> bui.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        # pylint: disable=too-many-positional-arguments
49        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:
51    def on_deactivate(self) -> None:
52        """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:
54    def save_state(self) -> None:
55        """Called when the parent window is saving state."""

Called when the parent window is saving state.

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

Called when the parent window is restoring state.

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

Window for joining/inviting friends.

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

Create a MainWindow given a root widget and transition info.

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

@override
def get_main_window_state(self) -> bauiv1.MainWindowState:
271    @override
272    def get_main_window_state(self) -> bui.MainWindowState:
273        # Support recreating our window for back/refresh purposes.
274        cls = type(self)
275        return bui.BasicMainWindowState(
276            create_call=lambda transition, origin_widget: cls(
277                transition=transition, origin_widget=origin_widget
278            )
279        )

Return a WindowState to recreate this window, if supported.

@override
def on_main_window_close(self) -> None:
281    @override
282    def on_main_window_close(self) -> None:
283        self._save_state()

Called before transitioning out a main window.

A good opportunity to save window state/etc.

def playlist_select( self, origin_widget: _bauiv1.Widget, context: bauiv1lib.play.PlaylistSelectContext) -> None:
285    def playlist_select(
286        self,
287        origin_widget: bui.Widget,
288        context: PlaylistSelectContext,
289    ) -> None:
290        """Called by the private-hosting tab to select a playlist."""
291        from bauiv1lib.play import PlayWindow
292
293        # Avoid redundant window spawns.
294        if not self.main_window_has_control():
295            return
296
297        playwindow = PlayWindow(
298            origin_widget=origin_widget, playlist_select_context=context
299        )
300        self.main_window_replace(playwindow)
301
302        # Grab the newly-set main-window's back-state; that will lead us
303        # back here once we're done going down our main-window
304        # rabbit-hole for playlist selection.
305        context.back_state = playwindow.main_window_back_state

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

class GatherWindow.TabID(enum.Enum):
64    class TabID(Enum):
65        """Our available tab types."""
66
67        ABOUT = 'about'
68        INTERNET = 'internet'
69        PRIVATE = 'private'
70        NEARBY = 'nearby'
71        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'>