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

Return a WindowState to recreate this window, if supported.

@override
def on_main_window_close(self) -> None:
270    @override
271    def on_main_window_close(self) -> None:
272        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:
274    def playlist_select(
275        self,
276        origin_widget: bui.Widget,
277        context: PlaylistSelectContext,
278    ) -> None:
279        """Called by the private-hosting tab to select a playlist."""
280        from bauiv1lib.play import PlayWindow
281
282        # Avoid redundant window spawns.
283        if not self.main_window_has_control():
284            return
285
286        playwindow = PlayWindow(
287            origin_widget=origin_widget, playlist_select_context=context
288        )
289        self.main_window_replace(playwindow)
290
291        # Grab the newly-set main-window's back-state; that will lead us
292        # back here once we're done going down our main-window
293        # rabbit-hole for playlist selection.
294        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'>