# Released under the MIT License. See LICENSE for details.
#
"""Provides a window for editing individual game playlists."""
from __future__ import annotations
import logging
from typing import TYPE_CHECKING, cast, override
import bascenev1 as bs
import bauiv1 as bui
if TYPE_CHECKING:
from bauiv1lib.playlist.editcontroller import PlaylistEditController
[docs]
class PlaylistEditWindow(bui.MainWindow):
"""Window for editing an individual game playlist."""
def __init__(
self,
editcontroller: PlaylistEditController,
transition: str | None = 'in_right',
origin_widget: bui.Widget | None = None,
):
# pylint: disable=too-many-statements
# pylint: disable=too-many-locals
prev_selection: str | None
self._editcontroller = editcontroller
self._r = 'editGameListWindow'
prev_selection = self._editcontroller.get_edit_ui_selection()
assert bui.app.classic is not None
uiscale = bui.app.ui_v1.uiscale
self._width = 870 if uiscale is bui.UIScale.SMALL else 670
x_inset = 100 if uiscale is bui.UIScale.SMALL else 0
self._height = (
500
if uiscale is bui.UIScale.SMALL
else 470 if uiscale is bui.UIScale.MEDIUM else 540
)
yoffs = -68 if uiscale is bui.UIScale.SMALL else 0
super().__init__(
root_widget=bui.containerwidget(
size=(self._width, self._height),
scale=(
2.0
if uiscale is bui.UIScale.SMALL
else 1.3 if uiscale is bui.UIScale.MEDIUM else 1.0
),
stack_offset=(
(0, 0) if uiscale is bui.UIScale.SMALL else (0, 0)
),
),
transition=transition,
origin_widget=origin_widget,
)
cancel_button = bui.buttonwidget(
parent=self._root_widget,
position=(35 + x_inset, self._height - 60 + yoffs),
scale=0.8,
size=(175, 60),
autoselect=True,
label=bui.Lstr(resource='cancelText'),
text_scale=1.2,
)
save_button = btn = bui.buttonwidget(
parent=self._root_widget,
position=(self._width - (195 + x_inset), self._height - 60 + yoffs),
scale=0.8,
size=(190, 60),
autoselect=True,
left_widget=cancel_button,
label=bui.Lstr(resource='saveText'),
text_scale=1.2,
)
bui.widget(
edit=btn,
right_widget=bui.get_special_widget('squad_button'),
)
bui.widget(
edit=cancel_button,
left_widget=cancel_button,
right_widget=save_button,
)
bui.textwidget(
parent=self._root_widget,
position=(-10, self._height - 50 + yoffs),
size=(self._width, 25),
text=bui.Lstr(resource=f'{self._r}.titleText'),
color=bui.app.ui_v1.title_color,
scale=1.05,
h_align='center',
v_align='center',
maxwidth=270,
)
v = self._height - 115.0 + yoffs
self._scroll_width = self._width - (205 + 2 * x_inset)
bui.textwidget(
parent=self._root_widget,
text=bui.Lstr(resource=f'{self._r}.listNameText'),
position=(196 + x_inset, v + 31),
maxwidth=150,
color=(0.8, 0.8, 0.8, 0.5),
size=(0, 0),
scale=0.75,
h_align='right',
v_align='center',
)
self._text_field = bui.textwidget(
parent=self._root_widget,
position=(210 + x_inset, v + 7),
size=(self._scroll_width - 53, 43),
text=self._editcontroller.getname(),
h_align='left',
v_align='center',
max_chars=40,
maxwidth=380,
autoselect=True,
color=(0.9, 0.9, 0.9, 1.0),
description=bui.Lstr(resource=f'{self._r}.listNameText'),
editable=True,
padding=4,
on_return_press_call=self._save_press_with_sound,
)
bui.widget(edit=cancel_button, down_widget=self._text_field)
self._list_widgets: list[bui.Widget] = []
h = 40 + x_inset
v = self._height - 172.0 + yoffs
b_color = (0.6, 0.53, 0.63)
b_textcolor = (0.75, 0.7, 0.8)
v -= 2.0
v += 63
scl = (
1.03
if uiscale is bui.UIScale.SMALL
else 1.36 if uiscale is bui.UIScale.MEDIUM else 1.74
)
v -= 63.0 * scl
add_game_button = bui.buttonwidget(
parent=self._root_widget,
position=(h, v),
size=(110, 61.0 * scl),
on_activate_call=self._add,
on_select_call=bui.Call(self._set_ui_selection, 'add_button'),
autoselect=True,
button_type='square',
color=b_color,
textcolor=b_textcolor,
text_scale=0.8,
label=bui.Lstr(resource=f'{self._r}.addGameText'),
)
bui.widget(edit=add_game_button, up_widget=self._text_field)
v -= 63.0 * scl
self._edit_button = edit_game_button = bui.buttonwidget(
parent=self._root_widget,
position=(h, v),
size=(110, 61.0 * scl),
on_activate_call=self._edit,
on_select_call=bui.Call(self._set_ui_selection, 'editButton'),
autoselect=True,
button_type='square',
color=b_color,
textcolor=b_textcolor,
text_scale=0.8,
label=bui.Lstr(resource=f'{self._r}.editGameText'),
)
v -= 63.0 * scl
remove_game_button = bui.buttonwidget(
parent=self._root_widget,
position=(h, v),
size=(110, 61.0 * scl),
text_scale=0.8,
on_activate_call=self._remove,
autoselect=True,
button_type='square',
color=b_color,
textcolor=b_textcolor,
label=bui.Lstr(resource=f'{self._r}.removeGameText'),
)
v -= 40
h += 9
bui.buttonwidget(
parent=self._root_widget,
position=(h, v),
size=(42, 35),
on_activate_call=self._move_up,
label=bui.charstr(bui.SpecialChar.UP_ARROW),
button_type='square',
color=b_color,
textcolor=b_textcolor,
autoselect=True,
repeat=True,
)
h += 52
bui.buttonwidget(
parent=self._root_widget,
position=(h, v),
size=(42, 35),
on_activate_call=self._move_down,
autoselect=True,
button_type='square',
color=b_color,
textcolor=b_textcolor,
label=bui.charstr(bui.SpecialChar.DOWN_ARROW),
repeat=True,
)
v = self._height - 100 + yoffs
scroll_height = self._height - (
250 if uiscale is bui.UIScale.SMALL else 155
)
scrollwidget = bui.scrollwidget(
parent=self._root_widget,
position=(160 + x_inset, v - scroll_height),
highlight=False,
on_select_call=bui.Call(self._set_ui_selection, 'gameList'),
size=(self._scroll_width, (scroll_height - 15)),
border_opacity=0.4,
)
bui.widget(
edit=scrollwidget,
left_widget=add_game_button,
right_widget=scrollwidget,
)
self._columnwidget = bui.columnwidget(
parent=scrollwidget, border=2, margin=0
)
bui.widget(edit=self._columnwidget, up_widget=self._text_field)
for button in [add_game_button, edit_game_button, remove_game_button]:
bui.widget(
edit=button, left_widget=button, right_widget=scrollwidget
)
self._refresh()
bui.buttonwidget(edit=cancel_button, on_activate_call=self._cancel)
bui.containerwidget(
edit=self._root_widget,
cancel_button=cancel_button,
selected_child=scrollwidget,
)
bui.buttonwidget(edit=save_button, on_activate_call=self._save_press)
bui.containerwidget(edit=self._root_widget, start_button=save_button)
if prev_selection == 'add_button':
bui.containerwidget(
edit=self._root_widget, selected_child=add_game_button
)
elif prev_selection == 'editButton':
bui.containerwidget(
edit=self._root_widget, selected_child=edit_game_button
)
elif prev_selection == 'gameList':
bui.containerwidget(
edit=self._root_widget, selected_child=scrollwidget
)
[docs]
@override
def get_main_window_state(self) -> bui.MainWindowState:
# Support recreating our window for back/refresh purposes.
cls = type(self)
editcontroller = self._editcontroller
return bui.BasicMainWindowState(
create_call=lambda transition, origin_widget: cls(
transition=transition,
origin_widget=origin_widget,
editcontroller=editcontroller,
)
)
def _set_ui_selection(self, selection: str) -> None:
self._editcontroller.set_edit_ui_selection(selection)
def _cancel(self) -> None:
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
bui.getsound('powerdown01').play()
self.main_window_back()
def _add(self) -> None:
# Store list name then tell the session to perform an add.
self._editcontroller.setname(
cast(str, bui.textwidget(query=self._text_field))
)
self._editcontroller.add_game_pressed(from_window=self)
def _edit(self) -> None:
# Store list name then tell the session to perform an add.
self._editcontroller.setname(
cast(str, bui.textwidget(query=self._text_field))
)
self._editcontroller.edit_game_pressed(from_window=self)
def _save_press(self) -> None:
# No-op if we're not in control.
if not self.main_window_has_control():
return
# no-op if our underlying widget is dead or on its way out.
if not self._root_widget or self._root_widget.transitioning_out:
return
plus = bui.app.plus
assert plus is not None
new_name = cast(str, bui.textwidget(query=self._text_field))
if (
new_name != self._editcontroller.get_existing_playlist_name()
and new_name
in bui.app.config[
self._editcontroller.get_config_name() + ' Playlists'
]
):
bui.screenmessage(
bui.Lstr(resource=f'{self._r}.cantSaveAlreadyExistsText')
)
bui.getsound('error').play()
return
if not new_name:
bui.getsound('error').play()
return
if not self._editcontroller.get_playlist():
bui.screenmessage(
bui.Lstr(resource=f'{self._r}.cantSaveEmptyListText')
)
bui.getsound('error').play()
return
# We couldn't actually replace the default list anyway, but disallow
# using its exact name to avoid confusion.
if new_name == self._editcontroller.get_default_list_name().evaluate():
bui.screenmessage(
bui.Lstr(resource=f'{self._r}.cantOverwriteDefaultText')
)
bui.getsound('error').play()
return
# If we had an old one, delete it.
if self._editcontroller.get_existing_playlist_name() is not None:
plus.add_v1_account_transaction(
{
'type': 'REMOVE_PLAYLIST',
'playlistType': self._editcontroller.get_config_name(),
'playlistName': (
self._editcontroller.get_existing_playlist_name()
),
}
)
plus.add_v1_account_transaction(
{
'type': 'ADD_PLAYLIST',
'playlistType': self._editcontroller.get_config_name(),
'playlistName': new_name,
'playlist': self._editcontroller.get_playlist(),
}
)
plus.run_v1_account_transactions()
bui.getsound('gunCocking').play()
self.main_window_back()
def _save_press_with_sound(self) -> None:
bui.getsound('swish').play()
self._save_press()
def _select(self, index: int) -> None:
self._editcontroller.set_selected_index(index)
def _refresh(self) -> None:
# Need to grab this here as rebuilding the list will
# change it otherwise.
old_selection_index = self._editcontroller.get_selected_index()
while self._list_widgets:
self._list_widgets.pop().delete()
for index, pentry in enumerate(self._editcontroller.get_playlist()):
try:
cls = bui.getclass(pentry['type'], subclassof=bs.GameActivity)
desc = cls.get_settings_display_string(pentry)
except Exception:
logging.exception('Error in playlist refresh.')
desc = "(invalid: '" + pentry['type'] + "')"
txtw = bui.textwidget(
parent=self._columnwidget,
size=(self._width - 80, 30),
on_select_call=bui.Call(self._select, index),
always_highlight=True,
color=(0.8, 0.8, 0.8, 1.0),
padding=0,
maxwidth=self._scroll_width * 0.93,
text=desc,
on_activate_call=self._edit_button.activate,
v_align='center',
selectable=True,
)
bui.widget(edit=txtw, show_buffer_top=50, show_buffer_bottom=50)
# Wanna be able to jump up to the text field from the top one.
if index == 0:
bui.widget(edit=txtw, up_widget=self._text_field)
self._list_widgets.append(txtw)
if old_selection_index == index:
bui.columnwidget(
edit=self._columnwidget,
selected_child=txtw,
visible_child=txtw,
)
def _move_down(self) -> None:
playlist = self._editcontroller.get_playlist()
index = self._editcontroller.get_selected_index()
if index >= len(playlist) - 1:
return
tmp = playlist[index]
playlist[index] = playlist[index + 1]
playlist[index + 1] = tmp
index += 1
self._editcontroller.set_playlist(playlist)
self._editcontroller.set_selected_index(index)
self._refresh()
def _move_up(self) -> None:
playlist = self._editcontroller.get_playlist()
index = self._editcontroller.get_selected_index()
if index < 1:
return
tmp = playlist[index]
playlist[index] = playlist[index - 1]
playlist[index - 1] = tmp
index -= 1
self._editcontroller.set_playlist(playlist)
self._editcontroller.set_selected_index(index)
self._refresh()
def _remove(self) -> None:
playlist = self._editcontroller.get_playlist()
index = self._editcontroller.get_selected_index()
if not playlist:
return
del playlist[index]
if index >= len(playlist):
index = len(playlist) - 1
self._editcontroller.set_playlist(playlist)
self._editcontroller.set_selected_index(index)
bui.getsound('shieldDown').play()
self._refresh()
# 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