class StoreBrowserWindow(ba.Window): """Window for browsing the store.""" class TabID(Enum): """Our available tab types.""" EXTRAS = 'extras' MAPS = 'maps' MINIGAMES = 'minigames' CHARACTERS = 'characters' ICONS = 'icons' def __init__(self, transition: str = 'in_right', modal: bool = False, show_tab: StoreBrowserWindow.TabID = None, on_close_call: Callable[[], Any] = None, back_location: str = None, origin_widget: ba.Widget = None): # pylint: disable=too-many-statements # pylint: disable=too-many-locals from bastd.ui.tabs import TabRow from ba import SpecialChar app = ba.app uiscale = app.ui.uiscale ba.set_analytics_screen('Store Window') scale_origin: Optional[Tuple[float, float]] # If they provided an origin-widget, scale up from that. if origin_widget is not None: self._transition_out = 'out_scale' scale_origin = origin_widget.get_screen_space_center() transition = 'in_scale' else: self._transition_out = 'out_right' scale_origin = None self.button_infos: Optional[Dict[str, Dict[str, Any]]] = None self.update_buttons_timer: Optional[ba.Timer] = None self._status_textwidget_update_timer = None self._back_location = back_location self._on_close_call = on_close_call self._show_tab = show_tab self._modal = modal self._width = 1240 if uiscale is ba.UIScale.SMALL else 1040 self._x_inset = x_inset = 100 if uiscale is ba.UIScale.SMALL else 0 self._height = (578 if uiscale is ba.UIScale.SMALL else 645 if uiscale is ba.UIScale.MEDIUM else 800) self._current_tab: Optional[StoreBrowserWindow.TabID] = None extra_top = 30 if uiscale is ba.UIScale.SMALL else 0 self._request: Any = None self._r = 'store' self._last_buy_time: Optional[float] = None super().__init__(root_widget=ba.containerwidget( size=(self._width, self._height + extra_top), transition=transition, toolbar_visibility='menu_full', scale=(1.3 if uiscale is ba.UIScale.SMALL else 0.9 if uiscale is ba.UIScale.MEDIUM else 0.8), scale_origin_stack_offset=scale_origin, stack_offset=((0, -5) if uiscale is ba.UIScale.SMALL else ( 0, 0) if uiscale is ba.UIScale.MEDIUM else (0, 0)))) self._back_button = btn = ba.buttonwidget( parent=self._root_widget, position=(70 + x_inset, self._height - 74), size=(140, 60), scale=1.1, autoselect=True, label=ba.Lstr(resource='doneText' if self._modal else 'backText'), button_type=None if self._modal else 'back', on_activate_call=self._back) ba.containerwidget(edit=self._root_widget, cancel_button=btn) self._ticket_count_text: Optional[ba.Widget] = None self._get_tickets_button: Optional[ba.Widget] = None if ba.app.allow_ticket_purchases: self._get_tickets_button = ba.buttonwidget( parent=self._root_widget, size=(210, 65), on_activate_call=self._on_get_more_tickets_press, autoselect=True, scale=0.9, text_scale=1.4, left_widget=self._back_button, color=(0.7, 0.5, 0.85), textcolor=(0.2, 1.0, 0.2), label=ba.Lstr(resource='getTicketsWindow.titleText')) else: self._ticket_count_text = ba.textwidget(parent=self._root_widget, size=(210, 64), color=(0.2, 1.0, 0.2), h_align='center', v_align='center') # Move this dynamically to keep it out of the way of the party icon. self._update_get_tickets_button_pos() self._get_ticket_pos_update_timer = ba.Timer( 1.0, ba.WeakCall(self._update_get_tickets_button_pos), repeat=True, timetype=ba.TimeType.REAL) if self._get_tickets_button: ba.widget(edit=self._back_button, right_widget=self._get_tickets_button) self._ticket_text_update_timer = ba.Timer( 1.0, ba.WeakCall(self._update_tickets_text), timetype=ba.TimeType.REAL, repeat=True) self._update_tickets_text() app = ba.app if app.platform in ['mac', 'ios'] and app.subplatform == 'appstore': ba.buttonwidget( parent=self._root_widget, position=(self._width * 0.5 - 70, 16), size=(230, 50), scale=0.65, on_activate_call=ba.WeakCall(self._restore_purchases), color=(0.35, 0.3, 0.4), selectable=False, textcolor=(0.55, 0.5, 0.6), label=ba.Lstr( resource='getTicketsWindow.restorePurchasesText')) ba.textwidget(parent=self._root_widget, position=(self._width * 0.5, self._height - 44), size=(0, 0), color=app.ui.title_color, scale=1.5, h_align='center', v_align='center', text=ba.Lstr(resource='storeText'), maxwidth=420) if not self._modal: ba.buttonwidget(edit=self._back_button, button_type='backSmall', size=(60, 60), label=ba.charstr(SpecialChar.BACK)) scroll_buffer_h = 130 + 2 * x_inset tab_buffer_h = 250 + 2 * x_inset tabs_def = [ (self.TabID.EXTRAS, ba.Lstr(resource=self._r + '.extrasText')), (self.TabID.MAPS, ba.Lstr(resource=self._r + '.mapsText')), (self.TabID.MINIGAMES, ba.Lstr(resource=self._r + '.miniGamesText')), (self.TabID.CHARACTERS, ba.Lstr(resource=self._r + '.charactersText')), (self.TabID.ICONS, ba.Lstr(resource=self._r + '.iconsText')), ] self._tab_row = TabRow(self._root_widget, tabs_def, pos=(tab_buffer_h * 0.5, self._height - 130), size=(self._width - tab_buffer_h, 50), on_select_call=self._set_tab) self._purchasable_count_widgets: Dict[StoreBrowserWindow.TabID, Dict[str, Any]] = {} # Create our purchasable-items tags and have them update over time. for tab_id, tab in self._tab_row.tabs.items(): pos = tab.position size = tab.size button = tab.button rad = 10 center = (pos[0] + 0.1 * size[0], pos[1] + 0.9 * size[1]) img = ba.imagewidget(parent=self._root_widget, position=(center[0] - rad * 1.04, center[1] - rad * 1.15), size=(rad * 2.2, rad * 2.2), texture=ba.gettexture('circleShadow'), color=(1, 0, 0)) txt = ba.textwidget(parent=self._root_widget, position=center, size=(0, 0), h_align='center', v_align='center', maxwidth=1.4 * rad, scale=0.6, shadow=1.0, flatness=1.0) rad = 20 sale_img = ba.imagewidget(parent=self._root_widget, position=(center[0] - rad, center[1] - rad), size=(rad * 2, rad * 2), draw_controller=button, texture=ba.gettexture('circleZigZag'), color=(0.5, 0, 1.0)) sale_title_text = ba.textwidget(parent=self._root_widget, position=(center[0], center[1] + 0.24 * rad), size=(0, 0), h_align='center', v_align='center', draw_controller=button, maxwidth=1.4 * rad, scale=0.6, shadow=0.0, flatness=1.0, color=(0, 1, 0)) sale_time_text = ba.textwidget(parent=self._root_widget, position=(center[0], center[1] - 0.29 * rad), size=(0, 0), h_align='center', v_align='center', draw_controller=button, maxwidth=1.4 * rad, scale=0.4, shadow=0.0, flatness=1.0, color=(0, 1, 0)) self._purchasable_count_widgets[tab_id] = { 'img': img, 'text': txt, 'sale_img': sale_img, 'sale_title_text': sale_title_text, 'sale_time_text': sale_time_text } self._tab_update_timer = ba.Timer(1.0, ba.WeakCall(self._update_tabs), timetype=ba.TimeType.REAL, repeat=True) self._update_tabs() if self._get_tickets_button: last_tab_button = self._tab_row.tabs[tabs_def[-1][0]].button ba.widget(edit=self._get_tickets_button, down_widget=last_tab_button) ba.widget(edit=last_tab_button, up_widget=self._get_tickets_button, right_widget=self._get_tickets_button) self._scroll_width = self._width - scroll_buffer_h self._scroll_height = self._height - 180 self._scrollwidget: Optional[ba.Widget] = None self._status_textwidget: Optional[ba.Widget] = None self._restore_state() def _update_get_tickets_button_pos(self) -> None: uiscale = ba.app.ui.uiscale pos = (self._width - 252 - (self._x_inset + (47 if uiscale is ba.UIScale.SMALL and _ba.is_party_icon_visible() else 0)), self._height - 70) if self._get_tickets_button: ba.buttonwidget(edit=self._get_tickets_button, position=pos) if self._ticket_count_text: ba.textwidget(edit=self._ticket_count_text, position=pos) def _restore_purchases(self) -> None: from bastd.ui import account if _ba.get_account_state() != 'signed_in': account.show_sign_in_prompt() else: _ba.restore_purchases() def _update_tabs(self) -> None: from ba.internal import (get_available_sale_time, get_available_purchase_count) if not self._root_widget: return for tab_id, tab_data in list(self._purchasable_count_widgets.items()): sale_time = get_available_sale_time(tab_id.value) if sale_time is not None: ba.textwidget(edit=tab_data['sale_title_text'], text=ba.Lstr(resource='store.saleText')) ba.textwidget(edit=tab_data['sale_time_text'], text=ba.timestring( sale_time, centi=False, timeformat=ba.TimeFormat.MILLISECONDS)) ba.imagewidget(edit=tab_data['sale_img'], opacity=1.0) count = 0 else: ba.textwidget(edit=tab_data['sale_title_text'], text='') ba.textwidget(edit=tab_data['sale_time_text'], text='') ba.imagewidget(edit=tab_data['sale_img'], opacity=0.0) count = get_available_purchase_count(tab_id.value) if count > 0: ba.textwidget(edit=tab_data['text'], text=str(count)) ba.imagewidget(edit=tab_data['img'], opacity=1.0) else: ba.textwidget(edit=tab_data['text'], text='') ba.imagewidget(edit=tab_data['img'], opacity=0.0) def _update_tickets_text(self) -> None: from ba import SpecialChar if not self._root_widget: return sval: Union[str, ba.Lstr] if _ba.get_account_state() == 'signed_in': sval = ba.charstr(SpecialChar.TICKET) + str( _ba.get_account_ticket_count()) else: sval = ba.Lstr(resource='getTicketsWindow.titleText') if self._get_tickets_button: ba.buttonwidget(edit=self._get_tickets_button, label=sval) if self._ticket_count_text: ba.textwidget(edit=self._ticket_count_text, text=sval) def _set_tab(self, tab_id: TabID) -> None: if self._current_tab is tab_id: return self._current_tab = tab_id # We wanna preserve our current tab between runs. cfg = ba.app.config cfg['Store Tab'] = tab_id.value cfg.commit() # Update tab colors based on which is selected. self._tab_row.update_appearance(tab_id) # (Re)create scroll widget. if self._scrollwidget: self._scrollwidget.delete() self._scrollwidget = ba.scrollwidget( parent=self._root_widget, highlight=False, position=((self._width - self._scroll_width) * 0.5, self._height - self._scroll_height - 79 - 48), size=(self._scroll_width, self._scroll_height), claims_left_right=True, claims_tab=True, selection_loops_to_parent=True) # NOTE: this stuff is modified by the _Store class. # Should maybe clean that up. self.button_infos = {} self.update_buttons_timer = None # Show status over top. if self._status_textwidget: self._status_textwidget.delete() self._status_textwidget = ba.textwidget( parent=self._root_widget, position=(self._width * 0.5, self._height * 0.5), size=(0, 0), color=(1, 0.7, 1, 0.5), h_align='center', v_align='center', text=ba.Lstr(resource=self._r + '.loadingText'), maxwidth=self._scroll_width * 0.9) class _Request: def __init__(self, window: StoreBrowserWindow): self._window = weakref.ref(window) data = {'tab': tab_id.value} ba.timer(0.1, ba.WeakCall(self._on_response, data), timetype=ba.TimeType.REAL) def _on_response(self, data: Optional[Dict[str, Any]]) -> None: # FIXME: clean this up. # pylint: disable=protected-access window = self._window() if window is not None and (window._request is self): window._request = None # noinspection PyProtectedMember window._on_response(data) # Kick off a server request. self._request = _Request(self) # Actually start the purchase locally. def _purchase_check_result(self, item: str, is_ticket_purchase: bool, result: Optional[Dict[str, Any]]) -> None: if result is None: ba.playsound(ba.getsound('error')) ba.screenmessage( ba.Lstr(resource='internal.unavailableNoConnectionText'), color=(1, 0, 0)) else: if is_ticket_purchase: if result['allow']: price = _ba.get_account_misc_read_val( 'price.' + item, None) if (price is None or not isinstance(price, int) or price <= 0): print('Error; got invalid local price of', price, 'for item', item) ba.playsound(ba.getsound('error')) else: ba.playsound(ba.getsound('click01')) _ba.in_game_purchase(item, price) else: if result['reason'] == 'versionTooOld': ba.playsound(ba.getsound('error')) ba.screenmessage(ba.Lstr( resource='getTicketsWindow.versionTooOldText'), color=(1, 0, 0)) else: ba.playsound(ba.getsound('error')) ba.screenmessage(ba.Lstr( resource='getTicketsWindow.unavailableText'), color=(1, 0, 0)) # Real in-app purchase. else: if result['allow']: _ba.purchase(item) else: if result['reason'] == 'versionTooOld': ba.playsound(ba.getsound('error')) ba.screenmessage(ba.Lstr( resource='getTicketsWindow.versionTooOldText'), color=(1, 0, 0)) else: ba.playsound(ba.getsound('error')) ba.screenmessage(ba.Lstr( resource='getTicketsWindow.unavailableText'), color=(1, 0, 0)) def _do_purchase_check(self, item: str, is_ticket_purchase: bool = False) -> None: from ba.internal import master_server_get # Here we ping the server to ask if it's valid for us to # purchase this. Better to fail now than after we've # paid locally. app = ba.app master_server_get( 'bsAccountPurchaseCheck', { 'item': item, 'platform': app.platform, 'subplatform': app.subplatform, 'version': app.version, 'buildNumber': app.build_number, 'purchaseType': 'ticket' if is_ticket_purchase else 'real' }, callback=ba.WeakCall(self._purchase_check_result, item, is_ticket_purchase), ) def buy(self, item: str) -> None: """Attempt to purchase the provided item.""" from ba.internal import (get_available_sale_time, get_store_item_name_translated) from bastd.ui import account from bastd.ui.confirm import ConfirmWindow from bastd.ui import getcurrency # Prevent pressing buy within a few seconds of the last press # (gives the buttons time to disable themselves and whatnot). curtime = ba.time(ba.TimeType.REAL) if self._last_buy_time is not None and (curtime - self._last_buy_time) < 2.0: ba.playsound(ba.getsound('error')) else: if _ba.get_account_state() != 'signed_in': account.show_sign_in_prompt() else: self._last_buy_time = curtime # Pro is an actual IAP; the rest are ticket purchases. if item == 'pro': ba.playsound(ba.getsound('click01')) # Purchase either pro or pro_sale depending on whether # there is a sale going on. self._do_purchase_check('pro' if get_available_sale_time( 'extras') is None else 'pro_sale') else: price = _ba.get_account_misc_read_val( 'price.' + item, None) our_tickets = _ba.get_account_ticket_count() if price is not None and our_tickets < price: ba.playsound(ba.getsound('error')) getcurrency.show_get_tickets_prompt() else: def do_it() -> None: self._do_purchase_check(item, is_ticket_purchase=True) ba.playsound(ba.getsound('swish')) ConfirmWindow( ba.Lstr(resource='store.purchaseConfirmText', subs=[ ('${ITEM}', get_store_item_name_translated(item)) ]), width=400, height=120, action=do_it, ok_text=ba.Lstr(resource='store.purchaseText', fallback_resource='okText')) def _print_already_own(self, charname: str) -> None: ba.screenmessage(ba.Lstr(resource=self._r + '.alreadyOwnText', subs=[('${NAME}', charname)]), color=(1, 0, 0)) ba.playsound(ba.getsound('error')) def update_buttons(self) -> None: """Update our buttons.""" # pylint: disable=too-many-statements # pylint: disable=too-many-branches # pylint: disable=too-many-locals from ba.internal import get_available_sale_time from ba import SpecialChar if not self._root_widget: return import datetime sales_raw = _ba.get_account_misc_read_val('sales', {}) sales = {} try: # Look at the current set of sales; filter any with time remaining. for sale_item, sale_info in list(sales_raw.items()): to_end = (datetime.datetime.utcfromtimestamp(sale_info['e']) - datetime.datetime.utcnow()).total_seconds() if to_end > 0: sales[sale_item] = { 'to_end': to_end, 'original_price': sale_info['op'] } except Exception: ba.print_exception('Error parsing sales.') assert self.button_infos is not None for b_type, b_info in self.button_infos.items(): if b_type in ['upgrades.pro', 'pro']: purchased = _ba.app.accounts.have_pro() else: purchased = _ba.get_purchased(b_type) sale_opacity = 0.0 sale_title_text: Union[str, ba.Lstr] = '' sale_time_text: Union[str, ba.Lstr] = '' if purchased: title_color = (0.8, 0.7, 0.9, 1.0) color = (0.63, 0.55, 0.78) extra_image_opacity = 0.5 call = ba.WeakCall(self._print_already_own, b_info['name']) price_text = '' price_text_left = '' price_text_right = '' show_purchase_check = True description_color: Sequence[float] = (0.4, 1.0, 0.4, 0.4) description_color2: Sequence[float] = (0.0, 0.0, 0.0, 0.0) price_color = (0.5, 1, 0.5, 0.3) else: title_color = (0.7, 0.9, 0.7, 1.0) color = (0.4, 0.8, 0.1) extra_image_opacity = 1.0 call = b_info['call'] if 'call' in b_info else None if b_type in ['upgrades.pro', 'pro']: sale_time = get_available_sale_time('extras') if sale_time is not None: priceraw = _ba.get_price('pro') price_text_left = (priceraw if priceraw is not None else '?') priceraw = _ba.get_price('pro_sale') price_text_right = (priceraw if priceraw is not None else '?') sale_opacity = 1.0 price_text = '' sale_title_text = ba.Lstr(resource='store.saleText') sale_time_text = ba.timestring( sale_time, centi=False, timeformat=ba.TimeFormat.MILLISECONDS) else: priceraw = _ba.get_price('pro') price_text = priceraw if priceraw is not None else '?' price_text_left = '' price_text_right = '' else: price = _ba.get_account_misc_read_val('price.' + b_type, 0) # Color the button differently if we cant afford this. if _ba.get_account_state() == 'signed_in': if _ba.get_account_ticket_count() < price: color = (0.6, 0.61, 0.6) price_text = ba.charstr(ba.SpecialChar.TICKET) + str( _ba.get_account_misc_read_val('price.' + b_type, '?')) price_text_left = '' price_text_right = '' # TESTING: if b_type in sales: sale_opacity = 1.0 price_text_left = ba.charstr(SpecialChar.TICKET) + str( sales[b_type]['original_price']) price_text_right = price_text price_text = '' sale_title_text = ba.Lstr(resource='store.saleText') sale_time_text = ba.timestring( int(sales[b_type]['to_end'] * 1000), centi=False, timeformat=ba.TimeFormat.MILLISECONDS) description_color = (0.5, 1.0, 0.5) description_color2 = (0.3, 1.0, 1.0) price_color = (0.2, 1, 0.2, 1.0) show_purchase_check = False if 'title_text' in b_info: ba.textwidget(edit=b_info['title_text'], color=title_color) if 'purchase_check' in b_info: ba.imagewidget(edit=b_info['purchase_check'], opacity=1.0 if show_purchase_check else 0.0) if 'price_widget' in b_info: ba.textwidget(edit=b_info['price_widget'], text=price_text, color=price_color) if 'price_widget_left' in b_info: ba.textwidget(edit=b_info['price_widget_left'], text=price_text_left) if 'price_widget_right' in b_info: ba.textwidget(edit=b_info['price_widget_right'], text=price_text_right) if 'price_slash_widget' in b_info: ba.imagewidget(edit=b_info['price_slash_widget'], opacity=sale_opacity) if 'sale_bg_widget' in b_info: ba.imagewidget(edit=b_info['sale_bg_widget'], opacity=sale_opacity) if 'sale_title_widget' in b_info: ba.textwidget(edit=b_info['sale_title_widget'], text=sale_title_text) if 'sale_time_widget' in b_info: ba.textwidget(edit=b_info['sale_time_widget'], text=sale_time_text) if 'button' in b_info: ba.buttonwidget(edit=b_info['button'], color=color, on_activate_call=call) if 'extra_backings' in b_info: for bck in b_info['extra_backings']: ba.imagewidget(edit=bck, color=color, opacity=extra_image_opacity) if 'extra_images' in b_info: for img in b_info['extra_images']: ba.imagewidget(edit=img, opacity=extra_image_opacity) if 'extra_texts' in b_info: for etxt in b_info['extra_texts']: ba.textwidget(edit=etxt, color=description_color) if 'extra_texts_2' in b_info: for etxt in b_info['extra_texts_2']: ba.textwidget(edit=etxt, color=description_color2) if 'descriptionText' in b_info: ba.textwidget(edit=b_info['descriptionText'], color=description_color) def _on_response(self, data: Optional[Dict[str, Any]]) -> None: # pylint: disable=too-many-statements # clear status text.. if self._status_textwidget: self._status_textwidget.delete() self._status_textwidget_update_timer = None if data is None: self._status_textwidget = ba.textwidget( parent=self._root_widget, position=(self._width * 0.5, self._height * 0.5), size=(0, 0), scale=1.3, transition_delay=0.1, color=(1, 0.3, 0.3, 1.0), h_align='center', v_align='center', text=ba.Lstr(resource=self._r + '.loadErrorText'), maxwidth=self._scroll_width * 0.9) else: class _Store: def __init__(self, store_window: StoreBrowserWindow, sdata: Dict[str, Any], width: float): from ba.internal import (get_store_item_display_size, get_store_layout) self._store_window = store_window self._width = width store_data = get_store_layout() self._tab = sdata['tab'] self._sections = copy.deepcopy(store_data[sdata['tab']]) self._height: Optional[float] = None uiscale = ba.app.ui.uiscale # Pre-calc a few things and add them to store-data. for section in self._sections: if self._tab == 'characters': dummy_name = 'characters.foo' elif self._tab == 'extras': dummy_name = 'pro' elif self._tab == 'maps': dummy_name = 'maps.foo' elif self._tab == 'icons': dummy_name = 'icons.foo' else: dummy_name = '' section['button_size'] = get_store_item_display_size( dummy_name) section['v_spacing'] = (-17 if self._tab == 'characters' else 0) if 'title' not in section: section['title'] = '' section['x_offs'] = (130 if self._tab == 'extras' else 270 if self._tab == 'maps' else 0) section['y_offs'] = ( 55 if (self._tab == 'extras' and uiscale is ba.UIScale.SMALL) else -20 if self._tab == 'icons' else 0) def instantiate(self, scrollwidget: ba.Widget, tab_button: ba.Widget) -> None: """Create the store.""" # pylint: disable=too-many-locals # pylint: disable=too-many-branches # pylint: disable=too-many-nested-blocks from bastd.ui.store import item as storeitemui title_spacing = 40 button_border = 20 button_spacing = 4 boffs_h = 40 self._height = 80.0 # Calc total height. for i, section in enumerate(self._sections): if section['title'] != '': assert self._height is not None self._height += title_spacing b_width, b_height = section['button_size'] b_column_count = int( math.floor((self._width - boffs_h - 20) / (b_width + button_spacing))) b_row_count = int( math.ceil( float(len(section['items'])) / b_column_count)) b_height_total = ( 2 * button_border + b_row_count * b_height + (b_row_count - 1) * section['v_spacing']) self._height += b_height_total assert self._height is not None cnt2 = ba.containerwidget(parent=scrollwidget, scale=1.0, size=(self._width, self._height), background=False, claims_left_right=True, claims_tab=True, selection_loops_to_parent=True) v = self._height - 20 if self._tab == 'characters': txt = ba.Lstr( resource='store.howToSwitchCharactersText', subs=[ ('${SETTINGS}', ba.Lstr( resource='accountSettingsWindow.titleText' )), ('${PLAYER_PROFILES}', ba.Lstr( resource='playerProfilesWindow.titleText') ) ]) ba.textwidget(parent=cnt2, text=txt, size=(0, 0), position=(self._width * 0.5, self._height - 28), h_align='center', v_align='center', color=(0.7, 1, 0.7, 0.4), scale=0.7, shadow=0, flatness=1.0, maxwidth=700, transition_delay=0.4) elif self._tab == 'icons': txt = ba.Lstr( resource='store.howToUseIconsText', subs=[ ('${SETTINGS}', ba.Lstr(resource='mainMenu.settingsText')), ('${PLAYER_PROFILES}', ba.Lstr( resource='playerProfilesWindow.titleText') ) ]) ba.textwidget(parent=cnt2, text=txt, size=(0, 0), position=(self._width * 0.5, self._height - 28), h_align='center', v_align='center', color=(0.7, 1, 0.7, 0.4), scale=0.7, shadow=0, flatness=1.0, maxwidth=700, transition_delay=0.4) elif self._tab == 'maps': assert self._width is not None assert self._height is not None txt = ba.Lstr(resource='store.howToUseMapsText') ba.textwidget(parent=cnt2, text=txt, size=(0, 0), position=(self._width * 0.5, self._height - 28), h_align='center', v_align='center', color=(0.7, 1, 0.7, 0.4), scale=0.7, shadow=0, flatness=1.0, maxwidth=700, transition_delay=0.4) prev_row_buttons: Optional[List] = None this_row_buttons = [] delay = 0.3 for section in self._sections: if section['title'] != '': ba.textwidget( parent=cnt2, position=(60, v - title_spacing * 0.8), size=(0, 0), scale=1.0, transition_delay=delay, color=(0.7, 0.9, 0.7, 1), h_align='left', v_align='center', text=ba.Lstr(resource=section['title']), maxwidth=self._width * 0.7) v -= title_spacing delay = max(0.100, delay - 0.100) v -= button_border b_width, b_height = section['button_size'] b_count = len(section['items']) b_column_count = int( math.floor((self._width - boffs_h - 20) / (b_width + button_spacing))) col = 0 item: Dict[str, Any] assert self._store_window.button_infos is not None for i, item_name in enumerate(section['items']): item = self._store_window.button_infos[ item_name] = {} item['call'] = ba.WeakCall(self._store_window.buy, item_name) if 'x_offs' in section: boffs_h2 = section['x_offs'] else: boffs_h2 = 0 if 'y_offs' in section: boffs_v2 = section['y_offs'] else: boffs_v2 = 0 b_pos = (boffs_h + boffs_h2 + (b_width + button_spacing) * col, v - b_height + boffs_v2) storeitemui.instantiate_store_item_display( item_name, item, parent_widget=cnt2, b_pos=b_pos, boffs_h=boffs_h, b_width=b_width, b_height=b_height, boffs_h2=boffs_h2, boffs_v2=boffs_v2, delay=delay) btn = item['button'] delay = max(0.1, delay - 0.1) this_row_buttons.append(btn) # Wire this button to the equivalent in the # previous row. if prev_row_buttons is not None: # pylint: disable=unsubscriptable-object if len(prev_row_buttons) > col: ba.widget(edit=btn, up_widget=prev_row_buttons[col]) ba.widget(edit=prev_row_buttons[col], down_widget=btn) # If we're the last button in our row, # wire any in the previous row past # our position to go to us if down is # pressed. if (col + 1 == b_column_count or i == b_count - 1): for b_prev in prev_row_buttons[col + 1:]: ba.widget(edit=b_prev, down_widget=btn) else: ba.widget(edit=btn, up_widget=prev_row_buttons[-1]) else: ba.widget(edit=btn, up_widget=tab_button) col += 1 if col == b_column_count or i == b_count - 1: prev_row_buttons = this_row_buttons this_row_buttons = [] col = 0 v -= b_height if i < b_count - 1: v -= section['v_spacing'] v -= button_border # Set a timer to update these buttons periodically as long # as we're alive (so if we buy one it will grey out, etc). self._store_window.update_buttons_timer = ba.Timer( 0.5, ba.WeakCall(self._store_window.update_buttons), repeat=True, timetype=ba.TimeType.REAL) # Also update them immediately. self._store_window.update_buttons() if self._current_tab in (self.TabID.EXTRAS, self.TabID.MINIGAMES, self.TabID.CHARACTERS, self.TabID.MAPS, self.TabID.ICONS): store = _Store(self, data, self._scroll_width) assert self._scrollwidget is not None store.instantiate( scrollwidget=self._scrollwidget, tab_button=self._tab_row.tabs[self._current_tab].button) else: cnt = ba.containerwidget(parent=self._scrollwidget, scale=1.0, size=(self._scroll_width, self._scroll_height * 0.95), background=False, claims_left_right=True, claims_tab=True, selection_loops_to_parent=True) self._status_textwidget = ba.textwidget( parent=cnt, position=(self._scroll_width * 0.5, self._scroll_height * 0.5), size=(0, 0), scale=1.3, transition_delay=0.1, color=(1, 1, 0.3, 1.0), h_align='center', v_align='center', text=ba.Lstr(resource=self._r + '.comingSoonText'), maxwidth=self._scroll_width * 0.9) def _save_state(self) -> None: try: 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._get_tickets_button: sel_name = 'GetTickets' elif sel == self._scrollwidget: sel_name = 'Scroll' elif 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}' else: raise ValueError(f'unrecognized selection \'{sel}\'') ba.app.ui.window_states[self.__class__.__name__] = { 'sel_name': sel_name, } except Exception: ba.print_exception(f'Error saving state for {self}.') def _restore_state(self) -> None: try: sel: Optional[ba.Widget] sel_name = ba.app.ui.window_states.get(self.__class__.__name__, {}).get('sel_name') assert isinstance(sel_name, (str, type(None))) try: current_tab = ba.enum_by_value(self.TabID, ba.app.config.get('Store Tab')) except ValueError: current_tab = self.TabID.CHARACTERS if self._show_tab is not None: current_tab = self._show_tab if sel_name == 'GetTickets' and self._get_tickets_button: sel = self._get_tickets_button elif sel_name == 'Back': sel = self._back_button elif sel_name == 'Scroll': sel = self._scrollwidget elif isinstance(sel_name, str) and sel_name.startswith('Tab:'): try: sel_tab_id = ba.enum_by_value(self.TabID, sel_name.split(':')[-1]) except ValueError: sel_tab_id = self.TabID.CHARACTERS sel = self._tab_row.tabs[sel_tab_id].button else: sel = self._tab_row.tabs[current_tab].button # If we were requested to show a tab, select it too.. if (self._show_tab is not None and self._show_tab in self._tab_row.tabs): sel = self._tab_row.tabs[self._show_tab].button self._set_tab(current_tab) if sel is not None: ba.containerwidget(edit=self._root_widget, selected_child=sel) except Exception: ba.print_exception(f'Error restoring state for {self}.') def _on_get_more_tickets_press(self) -> None: # pylint: disable=cyclic-import from bastd.ui.account import show_sign_in_prompt from bastd.ui.getcurrency import GetCurrencyWindow if _ba.get_account_state() != 'signed_in': show_sign_in_prompt() return self._save_state() ba.containerwidget(edit=self._root_widget, transition='out_left') window = GetCurrencyWindow( from_modal_store=self._modal, store_back_location=self._back_location).get_root_widget() if not self._modal: ba.app.ui.set_main_menu_window(window) def _back(self) -> None: # pylint: disable=cyclic-import from bastd.ui.coop.browser import CoopBrowserWindow from bastd.ui.mainmenu import MainMenuWindow self._save_state() ba.containerwidget(edit=self._root_widget, transition=self._transition_out) if not self._modal: if self._back_location == 'CoopBrowserWindow': ba.app.ui.set_main_menu_window( CoopBrowserWindow(transition='in_left').get_root_widget()) else: ba.app.ui.set_main_menu_window( MainMenuWindow(transition='in_left').get_root_widget()) if self._on_close_call is not None: self._on_close_call()
class WatchWindow(ba.Window): """Window for watching replays.""" class TabID(Enum): """Our available tab types.""" MY_REPLAYS = 'my_replays' TEST_TAB = 'test_tab' def __init__(self, transition: Optional[str] = 'in_right', origin_widget: ba.Widget = None): # pylint: disable=too-many-locals # pylint: disable=too-many-statements from bastd.ui.tabs import TabRow ba.set_analytics_screen('Watch Window') scale_origin: Optional[Tuple[float, float]] if origin_widget is not None: self._transition_out = 'out_scale' scale_origin = origin_widget.get_screen_space_center() transition = 'in_scale' else: self._transition_out = 'out_right' scale_origin = None ba.app.ui.set_main_menu_location('Watch') self._tab_data: Dict[str, Any] = {} self._my_replays_scroll_width: Optional[float] = None self._my_replays_watch_replay_button: Optional[ba.Widget] = None self._scrollwidget: Optional[ba.Widget] = None self._columnwidget: Optional[ba.Widget] = None self._my_replay_selected: Optional[str] = None self._my_replays_rename_window: Optional[ba.Widget] = None self._my_replay_rename_text: Optional[ba.Widget] = None self._r = 'watchWindow' uiscale = ba.app.ui.uiscale self._width = 1240 if uiscale is ba.UIScale.SMALL else 1040 x_inset = 100 if uiscale is ba.UIScale.SMALL else 0 self._height = (578 if uiscale is ba.UIScale.SMALL else 670 if uiscale is ba.UIScale.MEDIUM else 800) self._current_tab: Optional[WatchWindow.TabID] = None extra_top = 20 if uiscale is ba.UIScale.SMALL else 0 super().__init__(root_widget=ba.containerwidget( size=(self._width, self._height + extra_top), transition=transition, toolbar_visibility='menu_minimal', scale_origin_stack_offset=scale_origin, scale=(1.3 if uiscale is ba.UIScale.SMALL else 0.97 if uiscale is ba.UIScale.MEDIUM else 0.8), stack_offset=(0, -10) if uiscale is ba.UIScale.SMALL else ( 0, 15) if uiscale is ba.UIScale.MEDIUM else (0, 0))) if uiscale is ba.UIScale.SMALL and ba.app.ui.use_toolbars: ba.containerwidget(edit=self._root_widget, on_cancel_call=self._back) self._back_button = None else: self._back_button = btn = ba.buttonwidget( parent=self._root_widget, autoselect=True, position=(70 + x_inset, self._height - 74), size=(140, 60), scale=1.1, label=ba.Lstr(resource='backText'), button_type='back', on_activate_call=self._back) ba.containerwidget(edit=self._root_widget, cancel_button=btn) ba.buttonwidget(edit=btn, button_type='backSmall', size=(60, 60), label=ba.charstr(ba.SpecialChar.BACK)) ba.textwidget(parent=self._root_widget, position=(self._width * 0.5, self._height - 38), size=(0, 0), color=ba.app.ui.title_color, scale=1.5, h_align='center', v_align='center', text=ba.Lstr(resource=self._r + '.titleText'), maxwidth=400) tabdefs = [ (self.TabID.MY_REPLAYS, ba.Lstr(resource=self._r + '.myReplaysText')), # (self.TabID.TEST_TAB, ba.Lstr(value='Testing')), ] scroll_buffer_h = 130 + 2 * x_inset tab_buffer_h = 750 + 2 * x_inset self._tab_row = TabRow(self._root_widget, tabdefs, pos=(tab_buffer_h * 0.5, self._height - 130), size=(self._width - tab_buffer_h, 50), on_select_call=self._set_tab) if ba.app.ui.use_toolbars: first_tab = self._tab_row.tabs[tabdefs[0][0]] last_tab = self._tab_row.tabs[tabdefs[-1][0]] ba.widget(edit=last_tab.button, right_widget=_ba.get_special_widget('party_button')) if uiscale is ba.UIScale.SMALL: bbtn = _ba.get_special_widget('back_button') ba.widget(edit=first_tab.button, up_widget=bbtn, left_widget=bbtn) self._scroll_width = self._width - scroll_buffer_h self._scroll_height = self._height - 180 # Not actually using a scroll widget anymore; just an image. scroll_left = (self._width - self._scroll_width) * 0.5 scroll_bottom = self._height - self._scroll_height - 79 - 48 buffer_h = 10 buffer_v = 4 ba.imagewidget(parent=self._root_widget, position=(scroll_left - buffer_h, scroll_bottom - buffer_v), size=(self._scroll_width + 2 * buffer_h, self._scroll_height + 2 * buffer_v), texture=ba.gettexture('scrollWidget'), model_transparent=ba.getmodel('softEdgeOutside')) self._tab_container: Optional[ba.Widget] = None self._restore_state() def _set_tab(self, tab_id: TabID) -> None: # pylint: disable=too-many-locals if self._current_tab == tab_id: return self._current_tab = tab_id # Preserve our current tab between runs. cfg = ba.app.config cfg['Watch Tab'] = tab_id.value cfg.commit() # Update tab colors based on which is selected. # tabs.update_tab_button_colors(self._tab_buttons, tab) self._tab_row.update_appearance(tab_id) if self._tab_container: self._tab_container.delete() scroll_left = (self._width - self._scroll_width) * 0.5 scroll_bottom = self._height - self._scroll_height - 79 - 48 # A place where tabs can store data to get cleared when # switching to a different tab self._tab_data = {} uiscale = ba.app.ui.uiscale if tab_id is self.TabID.MY_REPLAYS: c_width = self._scroll_width c_height = self._scroll_height - 20 sub_scroll_height = c_height - 63 self._my_replays_scroll_width = sub_scroll_width = ( 680 if uiscale is ba.UIScale.SMALL else 640) self._tab_container = cnt = ba.containerwidget( parent=self._root_widget, position=(scroll_left, scroll_bottom + (self._scroll_height - c_height) * 0.5), size=(c_width, c_height), background=False, selection_loops_to_parent=True) v = c_height - 30 ba.textwidget(parent=cnt, position=(c_width * 0.5, v), color=(0.6, 1.0, 0.6), scale=0.7, size=(0, 0), maxwidth=c_width * 0.9, h_align='center', v_align='center', text=ba.Lstr( resource='replayRenameWarningText', subs=[('${REPLAY}', ba.Lstr(resource='replayNameDefaultText')) ])) b_width = 140 if uiscale is ba.UIScale.SMALL else 178 b_height = (107 if uiscale is ba.UIScale.SMALL else 142 if uiscale is ba.UIScale.MEDIUM else 190) b_space_extra = (0 if uiscale is ba.UIScale.SMALL else -2 if uiscale is ba.UIScale.MEDIUM else -5) b_color = (0.6, 0.53, 0.63) b_textcolor = (0.75, 0.7, 0.8) btnv = (c_height - (48 if uiscale is ba.UIScale.SMALL else 45 if uiscale is ba.UIScale.MEDIUM else 40) - b_height) btnh = 40 if uiscale is ba.UIScale.SMALL else 40 smlh = 190 if uiscale is ba.UIScale.SMALL else 225 tscl = 1.0 if uiscale is ba.UIScale.SMALL else 1.2 self._my_replays_watch_replay_button = btn1 = ba.buttonwidget( parent=cnt, size=(b_width, b_height), position=(btnh, btnv), button_type='square', color=b_color, textcolor=b_textcolor, on_activate_call=self._on_my_replay_play_press, text_scale=tscl, label=ba.Lstr(resource=self._r + '.watchReplayButtonText'), autoselect=True) ba.widget(edit=btn1, up_widget=self._tab_row.tabs[tab_id].button) if uiscale is ba.UIScale.SMALL and ba.app.ui.use_toolbars: ba.widget(edit=btn1, left_widget=_ba.get_special_widget('back_button')) btnv -= b_height + b_space_extra ba.buttonwidget(parent=cnt, size=(b_width, b_height), position=(btnh, btnv), button_type='square', color=b_color, textcolor=b_textcolor, on_activate_call=self._on_my_replay_rename_press, text_scale=tscl, label=ba.Lstr(resource=self._r + '.renameReplayButtonText'), autoselect=True) btnv -= b_height + b_space_extra ba.buttonwidget(parent=cnt, size=(b_width, b_height), position=(btnh, btnv), button_type='square', color=b_color, textcolor=b_textcolor, on_activate_call=self._on_my_replay_delete_press, text_scale=tscl, label=ba.Lstr(resource=self._r + '.deleteReplayButtonText'), autoselect=True) v -= sub_scroll_height + 23 self._scrollwidget = scrlw = ba.scrollwidget( parent=cnt, position=(smlh, v), size=(sub_scroll_width, sub_scroll_height)) ba.containerwidget(edit=cnt, selected_child=scrlw) self._columnwidget = ba.columnwidget(parent=scrlw, left_border=10, border=2, margin=0) ba.widget(edit=scrlw, autoselect=True, left_widget=btn1, up_widget=self._tab_row.tabs[tab_id].button) ba.widget(edit=self._tab_row.tabs[tab_id].button, down_widget=scrlw) self._my_replay_selected = None self._refresh_my_replays() def _no_replay_selected_error(self) -> None: ba.screenmessage(ba.Lstr(resource=self._r + '.noReplaySelectedErrorText'), color=(1, 0, 0)) ba.playsound(ba.getsound('error')) def _on_my_replay_play_press(self) -> None: if self._my_replay_selected is None: self._no_replay_selected_error() return _ba.increment_analytics_count('Replay watch') def do_it() -> None: try: # Reset to normal speed. _ba.set_replay_speed_exponent(0) _ba.fade_screen(True) assert self._my_replay_selected is not None _ba.new_replay_session(_ba.get_replays_dir() + '/' + self._my_replay_selected) except Exception: ba.print_exception('Error running replay session.') # Drop back into a fresh main menu session # in case we half-launched or something. from bastd import mainmenu _ba.new_host_session(mainmenu.MainMenuSession) _ba.fade_screen(False, endcall=ba.Call(ba.pushcall, do_it)) ba.containerwidget(edit=self._root_widget, transition='out_left') def _on_my_replay_rename_press(self) -> None: if self._my_replay_selected is None: self._no_replay_selected_error() return c_width = 600 c_height = 250 uiscale = ba.app.ui.uiscale self._my_replays_rename_window = cnt = ba.containerwidget( scale=(1.8 if uiscale is ba.UIScale.SMALL else 1.55 if uiscale is ba.UIScale.MEDIUM else 1.0), size=(c_width, c_height), transition='in_scale') dname = self._get_replay_display_name(self._my_replay_selected) ba.textwidget(parent=cnt, size=(0, 0), h_align='center', v_align='center', text=ba.Lstr(resource=self._r + '.renameReplayText', subs=[('${REPLAY}', dname)]), maxwidth=c_width * 0.8, position=(c_width * 0.5, c_height - 60)) self._my_replay_rename_text = txt = ba.textwidget( parent=cnt, size=(c_width * 0.8, 40), h_align='left', v_align='center', text=dname, editable=True, description=ba.Lstr(resource=self._r + '.replayNameText'), position=(c_width * 0.1, c_height - 140), autoselect=True, maxwidth=c_width * 0.7, max_chars=200) cbtn = ba.buttonwidget( parent=cnt, label=ba.Lstr(resource='cancelText'), on_activate_call=ba.Call( lambda c: ba.containerwidget(edit=c, transition='out_scale'), cnt), size=(180, 60), position=(30, 30), autoselect=True) okb = ba.buttonwidget(parent=cnt, label=ba.Lstr(resource=self._r + '.renameText'), size=(180, 60), position=(c_width - 230, 30), on_activate_call=ba.Call( self._rename_my_replay, self._my_replay_selected), autoselect=True) ba.widget(edit=cbtn, right_widget=okb) ba.widget(edit=okb, left_widget=cbtn) ba.textwidget(edit=txt, on_return_press_call=okb.activate) ba.containerwidget(edit=cnt, cancel_button=cbtn, start_button=okb) def _rename_my_replay(self, replay: str) -> None: new_name = None try: if not self._my_replay_rename_text: return new_name_raw = cast( str, ba.textwidget(query=self._my_replay_rename_text)) new_name = new_name_raw + '.brp' # Ignore attempts to change it to what it already is # (or what it looks like to the user). if (replay != new_name and self._get_replay_display_name(replay) != new_name_raw): old_name_full = (_ba.get_replays_dir() + '/' + replay).encode('utf-8') new_name_full = (_ba.get_replays_dir() + '/' + new_name).encode('utf-8') # False alarm; ba.textwidget can return non-None val. # pylint: disable=unsupported-membership-test if os.path.exists(new_name_full): ba.playsound(ba.getsound('error')) ba.screenmessage( ba.Lstr(resource=self._r + '.replayRenameErrorAlreadyExistsText'), color=(1, 0, 0)) elif any(char in new_name_raw for char in ['/', '\\', ':']): ba.playsound(ba.getsound('error')) ba.screenmessage(ba.Lstr(resource=self._r + '.replayRenameErrorInvalidName'), color=(1, 0, 0)) else: _ba.increment_analytics_count('Replay rename') os.rename(old_name_full, new_name_full) self._refresh_my_replays() ba.playsound(ba.getsound('gunCocking')) except Exception: ba.print_exception( f"Error renaming replay '{replay}' to '{new_name}'.") ba.playsound(ba.getsound('error')) ba.screenmessage( ba.Lstr(resource=self._r + '.replayRenameErrorText'), color=(1, 0, 0), ) ba.containerwidget(edit=self._my_replays_rename_window, transition='out_scale') def _on_my_replay_delete_press(self) -> None: from bastd.ui import confirm if self._my_replay_selected is None: self._no_replay_selected_error() return confirm.ConfirmWindow( ba.Lstr(resource=self._r + '.deleteConfirmText', subs=[('${REPLAY}', self._get_replay_display_name( self._my_replay_selected))]), ba.Call(self._delete_replay, self._my_replay_selected), 450, 150) def _get_replay_display_name(self, replay: str) -> str: if replay.endswith('.brp'): replay = replay[:-4] if replay == '__lastReplay': return ba.Lstr(resource='replayNameDefaultText').evaluate() return replay def _delete_replay(self, replay: str) -> None: try: _ba.increment_analytics_count('Replay delete') os.remove((_ba.get_replays_dir() + '/' + replay).encode('utf-8')) self._refresh_my_replays() ba.playsound(ba.getsound('shieldDown')) if replay == self._my_replay_selected: self._my_replay_selected = None except Exception: ba.print_exception(f"Error deleting replay '{replay}'.") ba.playsound(ba.getsound('error')) ba.screenmessage( ba.Lstr(resource=self._r + '.replayDeleteErrorText'), color=(1, 0, 0), ) def _on_my_replay_select(self, replay: str) -> None: self._my_replay_selected = replay def _refresh_my_replays(self) -> None: assert self._columnwidget is not None for child in self._columnwidget.get_children(): child.delete() t_scale = 1.6 try: names = os.listdir(_ba.get_replays_dir()) # Ignore random other files in there. names = [n for n in names if n.endswith('.brp')] names.sort(key=lambda x: x.lower()) except Exception: ba.print_exception('Error listing replays dir.') names = [] assert self._my_replays_scroll_width is not None assert self._my_replays_watch_replay_button is not None for i, name in enumerate(names): txt = ba.textwidget( parent=self._columnwidget, size=(self._my_replays_scroll_width / t_scale, 30), selectable=True, color=(1.0, 1, 0.4) if name == '__lastReplay.brp' else (1, 1, 1), always_highlight=True, on_select_call=ba.Call(self._on_my_replay_select, name), on_activate_call=self._my_replays_watch_replay_button.activate, text=self._get_replay_display_name(name), h_align='left', v_align='center', corner_scale=t_scale, maxwidth=(self._my_replays_scroll_width / t_scale) * 0.93) if i == 0: ba.widget( edit=txt, up_widget=self._tab_row.tabs[self.TabID.MY_REPLAYS].button) def _save_state(self) -> None: try: 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}') ba.app.ui.window_states[self.__class__.__name__] = { 'sel_name': sel_name } except Exception: ba.print_exception(f'Error saving state for {self}.') def _restore_state(self) -> None: from efro.util import enum_by_value try: sel: Optional[ba.Widget] sel_name = ba.app.ui.window_states.get(self.__class__.__name__, {}).get('sel_name') assert isinstance(sel_name, (str, type(None))) try: current_tab = enum_by_value(self.TabID, ba.app.config.get('Watch Tab')) except ValueError: current_tab = self.TabID.MY_REPLAYS 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 = enum_by_value(self.TabID, sel_name.split(':')[-1]) except ValueError: sel_tab_id = self.TabID.MY_REPLAYS sel = self._tab_row.tabs[sel_tab_id].button else: if self._tab_container is not None: sel = self._tab_container else: sel = self._tab_row.tabs[current_tab].button ba.containerwidget(edit=self._root_widget, selected_child=sel) except Exception: ba.print_exception(f'Error restoring state for {self}.') def _back(self) -> None: from bastd.ui.mainmenu import MainMenuWindow self._save_state() ba.containerwidget(edit=self._root_widget, transition=self._transition_out) ba.app.ui.set_main_menu_window( MainMenuWindow(transition='in_left').get_root_widget())
class GatherWindow(ba.Window): """Window for joining/inviting friends.""" class TabID(Enum): """Our available tab types.""" ABOUT = 'about' INTERNET = 'internet' PRIVATE = 'private' NEARBY = 'nearby' MANUAL = 'manual' def __init__(self, transition: Optional[str] = 'in_right', origin_widget: ba.Widget = None): # pylint: disable=too-many-statements # pylint: disable=too-many-locals # pylint: disable=cyclic-import from bastd.ui.gather.abouttab import AboutGatherTab from bastd.ui.gather.manualtab import ManualGatherTab from bastd.ui.gather.privatetab import PrivateGatherTab from bastd.ui.gather.publictab import PublicGatherTab from bastd.ui.gather.nearbytab import NearbyGatherTab ba.set_analytics_screen('Gather Window') scale_origin: Optional[tuple[float, float]] if origin_widget is not None: self._transition_out = 'out_scale' scale_origin = origin_widget.get_screen_space_center() transition = 'in_scale' else: self._transition_out = 'out_right' scale_origin = None ba.app.ui.set_main_menu_location('Gather') _ba.set_party_icon_always_visible(True) uiscale = ba.app.ui.uiscale self._width = 1240 if uiscale is ba.UIScale.SMALL else 1040 x_offs = 100 if uiscale is ba.UIScale.SMALL else 0 self._height = (582 if uiscale is ba.UIScale.SMALL else 680 if uiscale is ba.UIScale.MEDIUM else 800) self._current_tab: Optional[GatherWindow.TabID] = None extra_top = 20 if uiscale is ba.UIScale.SMALL else 0 self._r = 'gatherWindow' super().__init__(root_widget=ba.containerwidget( size=(self._width, self._height + extra_top), transition=transition, toolbar_visibility='menu_minimal', scale_origin_stack_offset=scale_origin, scale=(1.3 if uiscale is ba.UIScale.SMALL else 0.97 if uiscale is ba.UIScale.MEDIUM else 0.8), stack_offset=(0, -11) if uiscale is ba.UIScale.SMALL else ( 0, 0) if uiscale is ba.UIScale.MEDIUM else (0, 0))) if uiscale is ba.UIScale.SMALL and ba.app.ui.use_toolbars: ba.containerwidget(edit=self._root_widget, on_cancel_call=self._back) self._back_button = None else: self._back_button = btn = ba.buttonwidget( parent=self._root_widget, position=(70 + x_offs, self._height - 74), size=(140, 60), scale=1.1, autoselect=True, label=ba.Lstr(resource='backText'), button_type='back', on_activate_call=self._back) ba.containerwidget(edit=self._root_widget, cancel_button=btn) ba.buttonwidget(edit=btn, button_type='backSmall', position=(70 + x_offs, self._height - 78), size=(60, 60), label=ba.charstr(ba.SpecialChar.BACK)) condensed = uiscale is not ba.UIScale.LARGE t_offs_y = (0 if not condensed else 25 if uiscale is ba.UIScale.MEDIUM else 17) ba.textwidget(parent=self._root_widget, position=(self._width * 0.5, self._height - 42 + t_offs_y), size=(0, 0), color=ba.app.ui.title_color, scale=(1.5 if not condensed else 1.0 if uiscale is ba.UIScale.MEDIUM else 0.6), h_align='center', v_align='center', text=ba.Lstr(resource=self._r + '.titleText'), maxwidth=550) scroll_buffer_h = 130 + 2 * x_offs tab_buffer_h = ((320 if condensed else 250) + 2 * x_offs) # Build up the set of tabs we want. tabdefs: list[tuple[GatherWindow.TabID, ba.Lstr]] = [ (self.TabID.ABOUT, ba.Lstr(resource=self._r + '.aboutText')) ] if _ba.get_account_misc_read_val('enablePublicParties', True): tabdefs.append((self.TabID.INTERNET, ba.Lstr(resource=self._r + '.publicText'))) tabdefs.append( (self.TabID.PRIVATE, ba.Lstr(resource=self._r + '.privateText'))) tabdefs.append( (self.TabID.NEARBY, ba.Lstr(resource=self._r + '.nearbyText'))) tabdefs.append( (self.TabID.MANUAL, ba.Lstr(resource=self._r + '.manualText'))) # On small UI, push our tabs up closer to the top of the screen to # save a bit of space. tabs_top_extra = 42 if condensed else 0 self._tab_row = TabRow(self._root_widget, tabdefs, pos=(tab_buffer_h * 0.5, self._height - 130 + tabs_top_extra), size=(self._width - tab_buffer_h, 50), on_select_call=ba.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) if ba.app.ui.use_toolbars: ba.widget(edit=self._tab_row.tabs[tabdefs[-1][0]].button, right_widget=_ba.get_special_widget('party_button')) if uiscale is ba.UIScale.SMALL: ba.widget(edit=self._tab_row.tabs[tabdefs[0][0]].button, left_widget=_ba.get_special_widget('back_button')) self._scroll_width = self._width - scroll_buffer_h self._scroll_height = self._height - 180.0 + tabs_top_extra self._scroll_left = (self._width - self._scroll_width) * 0.5 self._scroll_bottom = (self._height - self._scroll_height - 79 - 48 + tabs_top_extra) buffer_h = 10 buffer_v = 4 # Not actually using a scroll widget anymore; just an image. ba.imagewidget(parent=self._root_widget, position=(self._scroll_left - buffer_h, self._scroll_bottom - buffer_v), size=(self._scroll_width + 2 * buffer_h, self._scroll_height + 2 * buffer_v), texture=ba.gettexture('scrollWidget'), model_transparent=ba.getmodel('softEdgeOutside')) self._tab_container: Optional[ba.Widget] = None self._restore_state() def __del__(self) -> None: _ba.set_party_icon_always_visible(False) def playlist_select(self, origin_widget: ba.Widget) -> None: """Called by the private-hosting tab to select a playlist.""" from bastd.ui.play import PlayWindow self._save_state() ba.containerwidget(edit=self._root_widget, transition='out_left') ba.app.ui.selecting_private_party_playlist = True ba.app.ui.set_main_menu_window( PlayWindow(origin_widget=origin_widget).get_root_widget()) 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 = ba.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}\'') ba.app.ui.window_states[type(self)] = { 'sel_name': sel_name, } except Exception: ba.print_exception(f'Error saving state for {self}.') def _restore_state(self) -> None: from efro.util import enum_by_value try: for tab in self._tabs.values(): tab.restore_state() sel: Optional[ba.Widget] winstate = ba.app.ui.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 = ba.app.config.get('Gather Tab') try: stored_tab = enum_by_value(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 = enum_by_value(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 ba.containerwidget(edit=self._root_widget, selected_child=sel) except Exception: ba.print_exception('Error restoring gather-win state.') def _back(self) -> None: from bastd.ui.mainmenu import MainMenuWindow self._save_state() ba.containerwidget(edit=self._root_widget, transition=self._transition_out) ba.app.ui.set_main_menu_window( MainMenuWindow(transition='in_left').get_root_widget())