# Released under the MIT License. See LICENSE for details.
#
"""Provides a popup window to view achievements."""
from __future__ import annotations # Docs-generation hack.
from typing import override
from bauiv1lib.utils import (
get_screen_margins,
scroll_fade_bottom,
scroll_fade_top,
)
import bauiv1 as bui
from bauiv1 import _commonassets, _classicassets
[docs]
class AchievementsWindow(bui.MainWindow):
"""Popup window to view achievements."""
def __init__(
self,
transition: str | None = 'in_right',
origin_widget: bui.Widget | None = None,
auxiliary_style: bool = True,
):
# pylint: disable=too-many-locals
# pylint: disable=cyclic-import
from baclassic import (
CHEST_APPEARANCE_DISPLAY_INFOS,
CHEST_APPEARANCE_DISPLAY_INFO_DEFAULT,
)
assert bui.app.classic is not None
uiscale = bui.app.ui_v1.uiscale
self._uiopenstate = bui.UIOpenState('classicachievements')
self._width = 800 if uiscale is bui.UIScale.SMALL else 550
self._height = (
450
if uiscale is bui.UIScale.SMALL
else 370 if uiscale is bui.UIScale.MEDIUM else 450
)
# 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 = (
2.4
if uiscale is bui.UIScale.SMALL
else 1.5 if uiscale is bui.UIScale.MEDIUM else 1.2
)
# Calc screen size in our local container space and clamp to a
# bit smaller than our container size.
target_width = min(self._width - 60, screensize[0] / scale)
target_height = min(self._height - 70, screensize[1] / scale)
# In small ui we extend our scrollable area out into the screen
# margins (space between the virtual bounds and the actual
# screen edges) while keeping content laid out within the
# virtual bounds.
margin_left, margin_right, margin_bottom, margin_top = (
get_screen_margins(scale)
if uiscale is bui.UIScale.SMALL
else (0.0, 0.0, 0.0, 0.0)
)
# 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
scroll_width = target_width
scroll_height = target_height - 25
scroll_bottom = yoffs - 54 - scroll_height
# Go with full-screen scrollable area in small ui.
if uiscale is bui.UIScale.SMALL:
scroll_height += 30
scroll_bottom -= 3
# Extend the scroll area out into the screen margins (content
# stays put; see below).
scroll_x = self._width * 0.5 - scroll_width * 0.5 - margin_left
scroll_width, scroll_height, scroll_bottom = (
scroll_width + margin_left + margin_right,
scroll_height + margin_bottom + margin_top,
scroll_bottom - margin_bottom,
)
super().__init__(
root_widget=bui.containerwidget(
size=(self._width, self._height),
toolbar_visibility='menu_full',
toolbar_cancel_button_style=(
'close' if auxiliary_style else 'back'
),
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 = bui.buttonwidget(
parent=self._root_widget,
id=f'{self.main_window_id_prefix}|back',
autoselect=True,
position=(50, yoffs - 48),
size=(60, 60),
scale=0.6,
label=bui.charstr(
bui.SpecialChar.CLOSE
if auxiliary_style
else bui.SpecialChar.BACK
),
button_type=None if auxiliary_style else 'backSmall',
on_activate_call=self.main_window_back,
)
bui.containerwidget(
edit=self._root_widget, cancel_button=self._back_button
)
achievements = bui.app.classic.ach.achievements
num_complete = len([a for a in achievements if a.complete])
self._scrollwidget = bui.scrollwidget(
parent=self._root_widget,
id=f'{self.main_window_id_prefix}|scroll',
size=(scroll_width, scroll_height),
position=(scroll_x, scroll_bottom),
capture_arrows=True,
simple_culling_v=10,
border_opacity=0.4,
)
bui.containerwidget(
edit=self._root_widget, selected_child=self._scrollwidget
)
bui.widget(edit=self._scrollwidget, autoselect=True)
if uiscale is bui.UIScale.SMALL:
bui.widget(
edit=self._scrollwidget,
left_widget=bui.get_special_widget('back_button'),
)
# With full-screen scrolling, fade content as it approaches
# toolbars.
if uiscale is bui.UIScale.SMALL:
# Note that we intentionally use the original
# un-margin-extended scroll geometry here; the fades were
# placed to coincide with toolbar elements, which don't
# move when we extend out into screen margins.
scroll_fade_top(
self._root_widget,
scroll_x + margin_left,
scroll_bottom + margin_bottom,
scroll_width - margin_left - margin_right,
scroll_height - margin_bottom - margin_top,
)
scroll_fade_bottom(
self._root_widget,
scroll_x + margin_left,
scroll_bottom + margin_bottom,
scroll_width - margin_left - margin_right,
scroll_height - margin_bottom - margin_top,
)
# In small UI mode when the screen is narrow enough we need to
# go with a smaller title to avoid it overlapping with toolbar
# bits.
if uiscale is bui.UIScale.SMALL and target_width < 630:
self._title_text = bui.textwidget(
parent=self._root_widget,
position=(self._width * 0.5, yoffs - 42),
size=(0, 0),
h_align='center',
v_align='center',
scale=0.6,
text=_commonassets.strings.compose.spaced_pair(
first=_commonassets.strings.compose.heading_suffix(
main=_classicassets.strings.ui.achievements
),
second=f'{num_complete}/{len(achievements)}',
),
maxwidth=86,
color=bui.app.ui_v1.title_color,
)
else:
self._title_text = bui.textwidget(
parent=self._root_widget,
position=(
self._width * 0.5,
yoffs - (42 if uiscale is bui.UIScale.SMALL else 30),
),
size=(0, 0),
h_align='center',
v_align='center',
scale=0.6,
text=_classicassets.strings.account.achievement_progress(
complete=str(num_complete),
total=str(len(achievements)),
),
maxwidth=180,
color=bui.app.ui_v1.title_color,
)
bui.containerwidget(
edit=self._root_widget, cancel_button=self._back_button
)
incr = 36
sub_width = scroll_width - 25
sub_height = 85 + len(achievements) * incr + margin_bottom + margin_top
# For fullscreen scrollable, account for toolbar.
if uiscale is bui.UIScale.SMALL:
sub_height += 30
# Our scroll area is widened out into the screen margins, but
# content should stay laid out within the virtual bounds; do
# our layout math against this un-widened width offset by the
# left margin.
content_width = sub_width - margin_left - margin_right
self._subcontainer = bui.containerwidget(
parent=self._scrollwidget,
id=f'{self.main_window_id_prefix}|subcontainer',
size=(sub_width, sub_height),
background=False,
)
basey = sub_height - margin_top
# For fullscreen scrollable, account for toolbar.
if uiscale is bui.UIScale.SMALL:
basey -= 30
total_pts = 0
for i, ach in enumerate(achievements):
complete = ach.complete
bui.textwidget(
parent=self._subcontainer,
position=(
margin_left + content_width * 0.08 - 5,
basey - 20 - incr * i,
),
maxwidth=20,
scale=0.5,
color=(0.6, 0.6, 0.7) if complete else (0.6, 0.6, 0.7, 0.2),
flatness=1.0,
shadow=0.0,
text=str(i + 1),
size=(0, 0),
h_align='right',
v_align='center',
)
bui.imagewidget(
parent=self._subcontainer,
position=(
(
margin_left + content_width * 0.10 + 1,
basey - 20 - incr * i - 9,
)
if complete
else (
margin_left + content_width * 0.10 - 4,
basey - 20 - incr * i - 14,
)
),
size=(18, 18) if complete else (27, 27),
opacity=1.0 if complete else 0.3,
color=ach.get_icon_color(complete)[:3],
texture=ach.get_icon_ui_texture(complete),
)
if complete:
bui.imagewidget(
parent=self._subcontainer,
position=(
margin_left + content_width * 0.10 - 4,
basey - 25 - incr * i - 9,
),
size=(28, 28),
color=(2, 1.4, 0),
texture=_classicassets.textures.achievement_outline.get(),
)
bui.textwidget(
parent=self._subcontainer,
position=(
margin_left + content_width * 0.19,
basey - 19 - incr * i + 3,
),
maxwidth=content_width * 0.62,
scale=0.6,
flatness=1.0,
shadow=0.0,
color=(1, 1, 1) if complete else (1, 1, 1, 0.2),
text=ach.display_name_langstr,
size=(0, 0),
h_align='left',
v_align='center',
)
bui.textwidget(
parent=self._subcontainer,
position=(
margin_left + content_width * 0.19,
basey - 19 - incr * i - 10,
),
maxwidth=content_width * 0.62,
scale=0.4,
flatness=1.0,
shadow=0.0,
color=(0.83, 0.8, 0.85) if complete else (0.8, 0.8, 0.8, 0.2),
text=(
ach.description_full_complete_langstr
if complete
else ach.description_full_langstr
),
size=(0, 0),
h_align='left',
v_align='center',
)
chest_type = ach.get_award_chest_type()
chestdisplayinfo = CHEST_APPEARANCE_DISPLAY_INFOS.get(
chest_type, CHEST_APPEARANCE_DISPLAY_INFO_DEFAULT
)
chestsize = 24.0
bui.imagewidget(
parent=self._subcontainer,
opacity=0.0 if complete else 1.0,
position=(
margin_left + content_width * 0.92 - 40.0 - chestsize * 0.5,
basey - 20 - incr * i - chestsize * 0.5,
),
size=(chestsize, chestsize),
color=chestdisplayinfo.color,
texture=chestdisplayinfo.texclosed.ui().get(),
tint_texture=chestdisplayinfo.texclosedtint.ui().get(),
tint_color=chestdisplayinfo.tint,
tint2_color=chestdisplayinfo.tint2,
)
pts = ach.power_ranking_value
bui.textwidget(
parent=self._subcontainer,
position=(
margin_left + content_width * 0.92,
basey - 20 - incr * i,
),
maxwidth=content_width * 0.15,
color=(0.7, 0.8, 1.0) if complete else (0.9, 0.9, 1.0, 0.3),
flatness=1.0,
shadow=0.0,
scale=0.6,
text=_classicassets.strings.coop.power_ranking_points(
number=str(pts)
),
size=(0, 0),
h_align='center',
v_align='center',
)
if complete:
total_pts += pts
bui.textwidget(
parent=self._subcontainer,
position=(
margin_left + content_width * 1.0,
basey - 20 - incr * len(achievements),
),
maxwidth=content_width * 0.5,
scale=0.7,
color=(0.7, 0.8, 1.0),
flatness=1.0,
shadow=0.0,
text=_commonassets.strings.compose.spaced_pair(
first=_commonassets.strings.values.total,
second=(
_classicassets.strings.league
).power_ranking_points_equals(number=str(total_pts)),
),
size=(0, 0),
h_align='right',
v_align='center',
)
[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 main_window_should_preserve_selection(self) -> bool:
return True
# 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