# Released under the MIT License. See LICENSE for details.
#
"""Provides UI for inviting/joining friends."""
from __future__ import annotations
import weakref
import logging
from enum import Enum
from typing import override, TYPE_CHECKING
from bauiv1lib.tabs import TabRow
import bauiv1 as bui
if TYPE_CHECKING:
from bauiv1lib.play import PlaylistSelectContext
[docs]
class GatherTab:
"""Defines a tab for use in the gather UI."""
def __init__(self, window: GatherWindow) -> None:
self._window = weakref.ref(window)
@property
def window(self) -> GatherWindow:
"""The GatherWindow that this tab belongs to."""
window = self._window()
if window is None:
raise bui.NotFoundError("GatherTab's window no longer exists.")
return window
[docs]
def on_activate(
self,
parent_widget: bui.Widget,
tab_button: bui.Widget,
region_width: float,
region_height: float,
region_left: float,
region_bottom: float,
) -> bui.Widget:
"""Called when the tab becomes the active one.
The tab should create and return a container widget covering the
specified region.
"""
# pylint: disable=too-many-positional-arguments
raise RuntimeError('Should not get here.')
[docs]
def on_deactivate(self) -> None:
"""Called when the tab will no longer be the active one."""
[docs]
def save_state(self) -> None:
"""Called when the parent window is saving state."""
[docs]
def restore_state(self) -> None:
"""Called when the parent window is restoring state."""
[docs]
class GatherWindow(bui.MainWindow):
"""Window for joining/inviting friends."""
[docs]
class TabID(Enum):
"""Our available tab types."""
ABOUT = 'about'
INTERNET = 'internet'
PRIVATE = 'private'
NEARBY = 'nearby'
MANUAL = 'manual'
def __init__(
self,
transition: str | None = 'in_right',
origin_widget: bui.Widget | None = None,
):
# pylint: disable=too-many-locals
# pylint: disable=cyclic-import
from bauiv1lib.gather.abouttab import AboutGatherTab
from bauiv1lib.gather.manualtab import ManualGatherTab
from bauiv1lib.gather.privatetab import PrivateGatherTab
from bauiv1lib.gather.publictab import PublicGatherTab
from bauiv1lib.gather.nearbytab import NearbyGatherTab
plus = bui.app.plus
assert plus is not None
bui.set_analytics_screen('Gather Window')
uiscale = bui.app.ui_v1.uiscale
self._width = (
1640
if uiscale is bui.UIScale.SMALL
else 1100 if uiscale is bui.UIScale.MEDIUM else 1200
)
self._height = (
1000
if uiscale is bui.UIScale.SMALL
else 730 if uiscale is bui.UIScale.MEDIUM else 900
)
self._current_tab: GatherWindow.TabID | None = None
self._r = 'gatherWindow'
# Do some fancy math to fill all available screen area up to the
# size of our backing container. This lets us fit to the exact
# screen shape at small ui scale.
screensize = bui.get_virtual_screen_size()
scale = (
1.4
if uiscale is bui.UIScale.SMALL
else 0.88 if uiscale is bui.UIScale.MEDIUM else 0.66
)
# Calc screen size in our local container space and clamp to a
# bit smaller than our container size.
target_width = min(self._width - 130, screensize[0] / scale)
target_height = min(self._height - 130, screensize[1] / scale)
# To get top/left coords, go to the center of our window and
# offset by half the width/height of our target area.
yoffs = 0.5 * self._height + 0.5 * target_height + 30.0
self._scroll_width = target_width
self._scroll_height = target_height - 65
self._scroll_bottom = yoffs - 93 - self._scroll_height
self._scroll_left = (self._width - self._scroll_width) * 0.5
super().__init__(
root_widget=bui.containerwidget(
size=(self._width, self._height),
toolbar_visibility=(
'menu_tokens'
if uiscale is bui.UIScale.SMALL
else 'menu_full'
),
scale=scale,
),
transition=transition,
origin_widget=origin_widget,
# We're affected by screen size only at small ui-scale.
refresh_on_screen_size_changes=uiscale is bui.UIScale.SMALL,
)
if uiscale is bui.UIScale.SMALL:
bui.containerwidget(
edit=self._root_widget, on_cancel_call=self.main_window_back
)
self._back_button = None
else:
self._back_button = btn = bui.buttonwidget(
parent=self._root_widget,
position=(70, yoffs - 43),
size=(60, 60),
scale=1.1,
autoselect=True,
label=bui.charstr(bui.SpecialChar.BACK),
button_type='backSmall',
on_activate_call=self.main_window_back,
)
bui.containerwidget(edit=self._root_widget, cancel_button=btn)
bui.textwidget(
parent=self._root_widget,
position=(
(
self._width * 0.5
+ (
(self._scroll_width * -0.5 + 170.0 - 70.0)
if uiscale is bui.UIScale.SMALL
else 0.0
)
),
yoffs - (64 if uiscale is bui.UIScale.SMALL else 4),
),
size=(0, 0),
color=bui.app.ui_v1.title_color,
scale=1.3 if uiscale is bui.UIScale.SMALL else 1.0,
h_align='left' if uiscale is bui.UIScale.SMALL else 'center',
v_align='center',
text=(bui.Lstr(resource=f'{self._r}.titleText')),
maxwidth=135 if uiscale is bui.UIScale.SMALL else 320,
)
# Build up the set of tabs we want.
tabdefs: list[tuple[GatherWindow.TabID, bui.Lstr]] = [
(self.TabID.ABOUT, bui.Lstr(resource=f'{self._r}.aboutText'))
]
if plus.get_v1_account_misc_read_val('enablePublicParties', True):
tabdefs.append(
(
self.TabID.INTERNET,
bui.Lstr(resource=f'{self._r}.publicText'),
)
)
tabdefs.append(
(self.TabID.PRIVATE, bui.Lstr(resource=f'{self._r}.privateText'))
)
tabdefs.append(
(self.TabID.NEARBY, bui.Lstr(resource=f'{self._r}.nearbyText'))
)
tabdefs.append(
(self.TabID.MANUAL, bui.Lstr(resource=f'{self._r}.manualText'))
)
tab_inset = 250.0 if uiscale is bui.UIScale.SMALL else 100.0
self._tab_row = TabRow(
self._root_widget,
tabdefs,
size=(self._scroll_width - 2.0 * tab_inset, 50),
pos=(
self._scroll_left + tab_inset,
self._scroll_bottom + self._scroll_height - 4.0,
),
on_select_call=bui.WeakCall(self._set_tab),
)
# Now instantiate handlers for these tabs.
tabtypes: dict[GatherWindow.TabID, type[GatherTab]] = {
self.TabID.ABOUT: AboutGatherTab,
self.TabID.MANUAL: ManualGatherTab,
self.TabID.PRIVATE: PrivateGatherTab,
self.TabID.INTERNET: PublicGatherTab,
self.TabID.NEARBY: NearbyGatherTab,
}
self._tabs: dict[GatherWindow.TabID, GatherTab] = {}
for tab_id in self._tab_row.tabs:
tabtype = tabtypes.get(tab_id)
if tabtype is not None:
self._tabs[tab_id] = tabtype(self)
# Eww; tokens meter may or may not be here; should be smarter
# about this.
bui.widget(
edit=self._tab_row.tabs[tabdefs[-1][0]].button,
right_widget=bui.get_special_widget('tokens_meter'),
)
if uiscale is bui.UIScale.SMALL:
bui.widget(
edit=self._tab_row.tabs[tabdefs[0][0]].button,
left_widget=bui.get_special_widget('back_button'),
up_widget=bui.get_special_widget('back_button'),
)
# Not actually using a scroll widget anymore; just an image.
bui.imagewidget(
parent=self._root_widget,
size=(self._scroll_width, self._scroll_height),
position=(
self._width * 0.5 - self._scroll_width * 0.5,
self._scroll_bottom,
),
texture=bui.gettexture('scrollWidget'),
mesh_transparent=bui.getmesh('softEdgeOutside'),
opacity=0.4,
)
self._tab_container: bui.Widget | None = None
self._restore_state()
[docs]
@override
def get_main_window_state(self) -> bui.MainWindowState:
# Support recreating our window for back/refresh purposes.
cls = type(self)
return bui.BasicMainWindowState(
create_call=lambda transition, origin_widget: cls(
transition=transition, origin_widget=origin_widget
)
)
[docs]
@override
def on_main_window_close(self) -> None:
self._save_state()
[docs]
def playlist_select(
self,
origin_widget: bui.Widget,
context: PlaylistSelectContext,
) -> None:
"""Called by the private-hosting tab to select a playlist."""
from bauiv1lib.play import PlayWindow
# Avoid redundant window spawns.
if not self.main_window_has_control():
return
playwindow = PlayWindow(
origin_widget=origin_widget, playlist_select_context=context
)
self.main_window_replace(playwindow)
# Grab the newly-set main-window's back-state; that will lead us
# back here once we're done going down our main-window
# rabbit-hole for playlist selection.
context.back_state = playwindow.main_window_back_state
def _set_tab(self, tab_id: TabID) -> None:
if self._current_tab is tab_id:
return
prev_tab_id = self._current_tab
self._current_tab = tab_id
# We wanna preserve our current tab between runs.
cfg = bui.app.config
cfg['Gather Tab'] = tab_id.value
cfg.commit()
# Update tab colors based on which is selected.
self._tab_row.update_appearance(tab_id)
if prev_tab_id is not None:
prev_tab = self._tabs.get(prev_tab_id)
if prev_tab is not None:
prev_tab.on_deactivate()
# Clear up prev container if it hasn't been done.
if self._tab_container:
self._tab_container.delete()
tab = self._tabs.get(tab_id)
if tab is not None:
self._tab_container = tab.on_activate(
self._root_widget,
self._tab_row.tabs[tab_id].button,
self._scroll_width,
self._scroll_height,
self._scroll_left,
self._scroll_bottom,
)
return
def _save_state(self) -> None:
try:
for tab in self._tabs.values():
tab.save_state()
sel = self._root_widget.get_selected_child()
selected_tab_ids = [
tab_id
for tab_id, tab in self._tab_row.tabs.items()
if sel == tab.button
]
if sel == self._back_button:
sel_name = 'Back'
elif selected_tab_ids:
assert len(selected_tab_ids) == 1
sel_name = f'Tab:{selected_tab_ids[0].value}'
elif sel == self._tab_container:
sel_name = 'TabContainer'
else:
raise ValueError(f'unrecognized selection: \'{sel}\'')
assert bui.app.classic is not None
bui.app.ui_v1.window_states[type(self)] = {
'sel_name': sel_name,
}
except Exception:
logging.exception('Error saving state for %s.', self)
def _restore_state(self) -> None:
try:
for tab in self._tabs.values():
tab.restore_state()
sel: bui.Widget | None
assert bui.app.classic is not None
winstate = bui.app.ui_v1.window_states.get(type(self), {})
sel_name = winstate.get('sel_name', None)
assert isinstance(sel_name, (str, type(None)))
current_tab = self.TabID.ABOUT
gather_tab_val = bui.app.config.get('Gather Tab')
try:
stored_tab = self.TabID(gather_tab_val)
if stored_tab in self._tab_row.tabs:
current_tab = stored_tab
except ValueError:
pass
self._set_tab(current_tab)
if sel_name == 'Back':
sel = self._back_button
elif sel_name == 'TabContainer':
sel = self._tab_container
elif isinstance(sel_name, str) and sel_name.startswith('Tab:'):
try:
sel_tab_id = self.TabID(sel_name.split(':')[-1])
except ValueError:
sel_tab_id = self.TabID.ABOUT
sel = self._tab_row.tabs[sel_tab_id].button
else:
sel = self._tab_row.tabs[current_tab].button
bui.containerwidget(edit=self._root_widget, selected_child=sel)
except Exception:
logging.exception('Error restoring state for %s.', self)
# Docs-generation hack; import some stuff that we likely only forward-declared
# in our actual source code so that docs tools can find it.
from typing import (Coroutine, Any, Literal, Callable,
Generator, Awaitable, Sequence, Self)
import asyncio
from concurrent.futures import Future
from pathlib import Path
from enum import Enum