Added new files

This commit is contained in:
vortex 2024-02-19 18:29:19 +05:30
parent 5e004af549
commit 77ccb73089
1783 changed files with 566966 additions and 0 deletions

View file

@ -0,0 +1 @@
# Released under the MIT License. See LICENSE for details.

View file

@ -0,0 +1,570 @@
# Released under the MIT License. See LICENSE for details.
#
"""Provides UI for browsing soundtracks."""
from __future__ import annotations
import copy
from typing import TYPE_CHECKING
import ba
import ba.internal
if TYPE_CHECKING:
from typing import Any
class SoundtrackBrowserWindow(ba.Window):
"""Window for browsing soundtracks."""
def __init__(
self,
transition: str = 'in_right',
origin_widget: ba.Widget | None = None,
):
# pylint: disable=too-many-locals
# pylint: disable=too-many-statements
# If they provided an origin-widget, scale up from that.
scale_origin: tuple[float, float] | None
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._r = 'editSoundtrackWindow'
uiscale = ba.app.ui.uiscale
self._width = 800 if uiscale is ba.UIScale.SMALL else 600
x_inset = 100 if uiscale is ba.UIScale.SMALL else 0
self._height = (
340
if uiscale is ba.UIScale.SMALL
else 370
if uiscale is ba.UIScale.MEDIUM
else 440
)
spacing = 40.0
v = self._height - 40.0
v -= spacing * 1.0
super().__init__(
root_widget=ba.containerwidget(
size=(self._width, self._height),
transition=transition,
toolbar_visibility='menu_minimal',
scale_origin_stack_offset=scale_origin,
scale=(
2.3
if uiscale is ba.UIScale.SMALL
else 1.6
if uiscale is ba.UIScale.MEDIUM
else 1.0
),
stack_offset=(0, -18)
if uiscale is ba.UIScale.SMALL
else (0, 0),
)
)
if ba.app.ui.use_toolbars and uiscale is ba.UIScale.SMALL:
self._back_button = None
else:
self._back_button = ba.buttonwidget(
parent=self._root_widget,
position=(45 + x_inset, self._height - 60),
size=(120, 60),
scale=0.8,
label=ba.Lstr(resource='backText'),
button_type='back',
autoselect=True,
)
ba.buttonwidget(
edit=self._back_button,
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 - 35),
size=(0, 0),
maxwidth=300,
text=ba.Lstr(resource=self._r + '.titleText'),
color=ba.app.ui.title_color,
h_align='center',
v_align='center',
)
h = 43 + x_inset
v = self._height - 60
b_color = (0.6, 0.53, 0.63)
b_textcolor = (0.75, 0.7, 0.8)
lock_tex = ba.gettexture('lock')
self._lock_images: list[ba.Widget] = []
scl = (
1.0
if uiscale is ba.UIScale.SMALL
else 1.13
if uiscale is ba.UIScale.MEDIUM
else 1.4
)
v -= 60.0 * scl
self._new_button = btn = ba.buttonwidget(
parent=self._root_widget,
position=(h, v),
size=(100, 55.0 * scl),
on_activate_call=self._new_soundtrack,
color=b_color,
button_type='square',
autoselect=True,
textcolor=b_textcolor,
text_scale=0.7,
label=ba.Lstr(resource=self._r + '.newText'),
)
self._lock_images.append(
ba.imagewidget(
parent=self._root_widget,
size=(30, 30),
draw_controller=btn,
position=(h - 10, v + 55.0 * scl - 28),
texture=lock_tex,
)
)
if self._back_button is None:
ba.widget(
edit=btn,
left_widget=ba.internal.get_special_widget('back_button'),
)
v -= 60.0 * scl
self._edit_button = btn = ba.buttonwidget(
parent=self._root_widget,
position=(h, v),
size=(100, 55.0 * scl),
on_activate_call=self._edit_soundtrack,
color=b_color,
button_type='square',
autoselect=True,
textcolor=b_textcolor,
text_scale=0.7,
label=ba.Lstr(resource=self._r + '.editText'),
)
self._lock_images.append(
ba.imagewidget(
parent=self._root_widget,
size=(30, 30),
draw_controller=btn,
position=(h - 10, v + 55.0 * scl - 28),
texture=lock_tex,
)
)
if self._back_button is None:
ba.widget(
edit=btn,
left_widget=ba.internal.get_special_widget('back_button'),
)
v -= 60.0 * scl
self._duplicate_button = btn = ba.buttonwidget(
parent=self._root_widget,
position=(h, v),
size=(100, 55.0 * scl),
on_activate_call=self._duplicate_soundtrack,
button_type='square',
autoselect=True,
color=b_color,
textcolor=b_textcolor,
text_scale=0.7,
label=ba.Lstr(resource=self._r + '.duplicateText'),
)
self._lock_images.append(
ba.imagewidget(
parent=self._root_widget,
size=(30, 30),
draw_controller=btn,
position=(h - 10, v + 55.0 * scl - 28),
texture=lock_tex,
)
)
if self._back_button is None:
ba.widget(
edit=btn,
left_widget=ba.internal.get_special_widget('back_button'),
)
v -= 60.0 * scl
self._delete_button = btn = ba.buttonwidget(
parent=self._root_widget,
position=(h, v),
size=(100, 55.0 * scl),
on_activate_call=self._delete_soundtrack,
color=b_color,
button_type='square',
autoselect=True,
textcolor=b_textcolor,
text_scale=0.7,
label=ba.Lstr(resource=self._r + '.deleteText'),
)
self._lock_images.append(
ba.imagewidget(
parent=self._root_widget,
size=(30, 30),
draw_controller=btn,
position=(h - 10, v + 55.0 * scl - 28),
texture=lock_tex,
)
)
if self._back_button is None:
ba.widget(
edit=btn,
left_widget=ba.internal.get_special_widget('back_button'),
)
# Keep our lock images up to date/etc.
self._update_timer = ba.Timer(
1.0,
ba.WeakCall(self._update),
timetype=ba.TimeType.REAL,
repeat=True,
)
self._update()
v = self._height - 65
scroll_height = self._height - 105
v -= scroll_height
self._scrollwidget = scrollwidget = ba.scrollwidget(
parent=self._root_widget,
position=(152 + x_inset, v),
highlight=False,
size=(self._width - (205 + 2 * x_inset), scroll_height),
)
ba.widget(
edit=self._scrollwidget,
left_widget=self._new_button,
right_widget=ba.internal.get_special_widget('party_button')
if ba.app.ui.use_toolbars
else self._scrollwidget,
)
self._col = ba.columnwidget(parent=scrollwidget, border=2, margin=0)
self._soundtracks: dict[str, Any] | None = None
self._selected_soundtrack: str | None = None
self._selected_soundtrack_index: int | None = None
self._soundtrack_widgets: list[ba.Widget] = []
self._allow_changing_soundtracks = False
self._refresh()
if self._back_button is not None:
ba.buttonwidget(edit=self._back_button, on_activate_call=self._back)
ba.containerwidget(
edit=self._root_widget, cancel_button=self._back_button
)
else:
ba.containerwidget(
edit=self._root_widget, on_cancel_call=self._back
)
def _update(self) -> None:
have = ba.app.accounts_v1.have_pro_options()
for lock in self._lock_images:
ba.imagewidget(edit=lock, opacity=0.0 if have else 1.0)
def _do_delete_soundtrack(self) -> None:
cfg = ba.app.config
soundtracks = cfg.setdefault('Soundtracks', {})
if self._selected_soundtrack in soundtracks:
del soundtracks[self._selected_soundtrack]
cfg.commit()
ba.playsound(ba.getsound('shieldDown'))
assert self._selected_soundtrack_index is not None
assert self._soundtracks is not None
if self._selected_soundtrack_index >= len(self._soundtracks):
self._selected_soundtrack_index = len(self._soundtracks)
self._refresh()
def _delete_soundtrack(self) -> None:
# pylint: disable=cyclic-import
from bastd.ui.purchase import PurchaseWindow
from bastd.ui.confirm import ConfirmWindow
if not ba.app.accounts_v1.have_pro_options():
PurchaseWindow(items=['pro'])
return
if self._selected_soundtrack is None:
return
if self._selected_soundtrack == '__default__':
ba.playsound(ba.getsound('error'))
ba.screenmessage(
ba.Lstr(resource=self._r + '.cantDeleteDefaultText'),
color=(1, 0, 0),
)
else:
ConfirmWindow(
ba.Lstr(
resource=self._r + '.deleteConfirmText',
subs=[('${NAME}', self._selected_soundtrack)],
),
self._do_delete_soundtrack,
450,
150,
)
def _duplicate_soundtrack(self) -> None:
# pylint: disable=cyclic-import
from bastd.ui.purchase import PurchaseWindow
if not ba.app.accounts_v1.have_pro_options():
PurchaseWindow(items=['pro'])
return
cfg = ba.app.config
cfg.setdefault('Soundtracks', {})
if self._selected_soundtrack is None:
return
sdtk: dict[str, Any]
if self._selected_soundtrack == '__default__':
sdtk = {}
else:
sdtk = cfg['Soundtracks'][self._selected_soundtrack]
# Find a valid dup name that doesn't exist.
test_index = 1
copy_text = ba.Lstr(resource='copyOfText').evaluate()
# Get just 'Copy' or whatnot.
copy_word = copy_text.replace('${NAME}', '').strip()
base_name = self._get_soundtrack_display_name(
self._selected_soundtrack
).evaluate()
assert isinstance(base_name, str)
# If it looks like a copy, strip digits and spaces off the end.
if copy_word in base_name:
while base_name[-1].isdigit() or base_name[-1] == ' ':
base_name = base_name[:-1]
while True:
if copy_word in base_name:
test_name = base_name
else:
test_name = copy_text.replace('${NAME}', base_name)
if test_index > 1:
test_name += ' ' + str(test_index)
if test_name not in cfg['Soundtracks']:
break
test_index += 1
cfg['Soundtracks'][test_name] = copy.deepcopy(sdtk)
cfg.commit()
self._refresh(select_soundtrack=test_name)
def _select(self, name: str, index: int) -> None:
music = ba.app.music
self._selected_soundtrack_index = index
self._selected_soundtrack = name
cfg = ba.app.config
current_soundtrack = cfg.setdefault('Soundtrack', '__default__')
# If it varies from current, commit and play.
if current_soundtrack != name and self._allow_changing_soundtracks:
ba.playsound(ba.getsound('gunCocking'))
cfg['Soundtrack'] = self._selected_soundtrack
cfg.commit()
# Just play whats already playing.. this'll grab it from the
# new soundtrack.
music.do_play_music(music.music_types[ba.MusicPlayMode.REGULAR])
def _back(self) -> None:
# pylint: disable=cyclic-import
from bastd.ui.settings import audio
self._save_state()
ba.containerwidget(
edit=self._root_widget, transition=self._transition_out
)
ba.app.ui.set_main_menu_window(
audio.AudioSettingsWindow(transition='in_left').get_root_widget()
)
def _edit_soundtrack_with_sound(self) -> None:
# pylint: disable=cyclic-import
from bastd.ui.purchase import PurchaseWindow
if not ba.app.accounts_v1.have_pro_options():
PurchaseWindow(items=['pro'])
return
ba.playsound(ba.getsound('swish'))
self._edit_soundtrack()
def _edit_soundtrack(self) -> None:
# pylint: disable=cyclic-import
from bastd.ui.purchase import PurchaseWindow
from bastd.ui.soundtrack.edit import SoundtrackEditWindow
if not ba.app.accounts_v1.have_pro_options():
PurchaseWindow(items=['pro'])
return
if self._selected_soundtrack is None:
return
if self._selected_soundtrack == '__default__':
ba.playsound(ba.getsound('error'))
ba.screenmessage(
ba.Lstr(resource=self._r + '.cantEditDefaultText'),
color=(1, 0, 0),
)
return
self._save_state()
ba.containerwidget(edit=self._root_widget, transition='out_left')
ba.app.ui.set_main_menu_window(
SoundtrackEditWindow(
existing_soundtrack=self._selected_soundtrack
).get_root_widget()
)
def _get_soundtrack_display_name(self, soundtrack: str) -> ba.Lstr:
if soundtrack == '__default__':
return ba.Lstr(resource=self._r + '.defaultSoundtrackNameText')
return ba.Lstr(value=soundtrack)
def _refresh(self, select_soundtrack: str | None = None) -> None:
from efro.util import asserttype
self._allow_changing_soundtracks = False
old_selection = self._selected_soundtrack
# If there was no prev selection, look in prefs.
if old_selection is None:
old_selection = ba.app.config.get('Soundtrack')
old_selection_index = self._selected_soundtrack_index
# Delete old.
while self._soundtrack_widgets:
self._soundtrack_widgets.pop().delete()
self._soundtracks = ba.app.config.get('Soundtracks', {})
assert self._soundtracks is not None
items = list(self._soundtracks.items())
items.sort(key=lambda x: asserttype(x[0], str).lower())
items = [('__default__', None)] + items # default is always first
index = 0
for pname, _pval in items:
assert pname is not None
txtw = ba.textwidget(
parent=self._col,
size=(self._width - 40, 24),
text=self._get_soundtrack_display_name(pname),
h_align='left',
v_align='center',
maxwidth=self._width - 110,
always_highlight=True,
on_select_call=ba.WeakCall(self._select, pname, index),
on_activate_call=self._edit_soundtrack_with_sound,
selectable=True,
)
if index == 0:
ba.widget(edit=txtw, up_widget=self._back_button)
self._soundtrack_widgets.append(txtw)
# Select this one if the user requested it
if select_soundtrack is not None:
if pname == select_soundtrack:
ba.columnwidget(
edit=self._col, selected_child=txtw, visible_child=txtw
)
else:
# Select this one if it was previously selected.
# Go by index if there's one.
if old_selection_index is not None:
if index == old_selection_index:
ba.columnwidget(
edit=self._col,
selected_child=txtw,
visible_child=txtw,
)
else: # Otherwise look by name.
if pname == old_selection:
ba.columnwidget(
edit=self._col,
selected_child=txtw,
visible_child=txtw,
)
index += 1
# Explicitly run select callback on current one and re-enable
# callbacks.
# Eww need to run this in a timer so it happens after our select
# callbacks. With a small-enough time sometimes it happens before
# anyway. Ew. need a way to just schedule a callable i guess.
ba.timer(
0.1,
ba.WeakCall(self._set_allow_changing),
timetype=ba.TimeType.REAL,
)
def _set_allow_changing(self) -> None:
self._allow_changing_soundtracks = True
assert self._selected_soundtrack is not None
assert self._selected_soundtrack_index is not None
self._select(self._selected_soundtrack, self._selected_soundtrack_index)
def _new_soundtrack(self) -> None:
# pylint: disable=cyclic-import
from bastd.ui.purchase import PurchaseWindow
from bastd.ui.soundtrack.edit import SoundtrackEditWindow
if not ba.app.accounts_v1.have_pro_options():
PurchaseWindow(items=['pro'])
return
self._save_state()
ba.containerwidget(edit=self._root_widget, transition='out_left')
SoundtrackEditWindow(existing_soundtrack=None)
def _create_done(self, new_soundtrack: str) -> None:
if new_soundtrack is not None:
ba.playsound(ba.getsound('gunCocking'))
self._refresh(select_soundtrack=new_soundtrack)
def _save_state(self) -> None:
try:
sel = self._root_widget.get_selected_child()
if sel == self._scrollwidget:
sel_name = 'Scroll'
elif sel == self._new_button:
sel_name = 'New'
elif sel == self._edit_button:
sel_name = 'Edit'
elif sel == self._duplicate_button:
sel_name = 'Duplicate'
elif sel == self._delete_button:
sel_name = 'Delete'
elif sel == self._back_button:
sel_name = 'Back'
else:
raise ValueError(f'unrecognized selection \'{sel}\'')
ba.app.ui.window_states[type(self)] = sel_name
except Exception:
ba.print_exception(f'Error saving state for {self}.')
def _restore_state(self) -> None:
try:
sel_name = ba.app.ui.window_states.get(type(self))
if sel_name == 'Scroll':
sel = self._scrollwidget
elif sel_name == 'New':
sel = self._new_button
elif sel_name == 'Edit':
sel = self._edit_button
elif sel_name == 'Duplicate':
sel = self._duplicate_button
elif sel_name == 'Delete':
sel = self._delete_button
else:
sel = self._scrollwidget
ba.containerwidget(edit=self._root_widget, selected_child=sel)
except Exception:
ba.print_exception(f'Error restoring state for {self}.')

View file

@ -0,0 +1,479 @@
# Released under the MIT License. See LICENSE for details.
#
"""Provides UI for editing a soundtrack."""
from __future__ import annotations
import copy
import os
from typing import TYPE_CHECKING, cast
import ba
if TYPE_CHECKING:
from typing import Any
class SoundtrackEditWindow(ba.Window):
"""Window for editing a soundtrack."""
def __init__(
self,
existing_soundtrack: str | dict[str, Any] | None,
transition: str = 'in_right',
):
# pylint: disable=too-many-statements
appconfig = ba.app.config
self._r = 'editSoundtrackWindow'
self._folder_tex = ba.gettexture('folder')
self._file_tex = ba.gettexture('file')
uiscale = ba.app.ui.uiscale
self._width = 848 if uiscale is ba.UIScale.SMALL else 648
x_inset = 100 if uiscale is ba.UIScale.SMALL else 0
self._height = (
395
if uiscale is ba.UIScale.SMALL
else 450
if uiscale is ba.UIScale.MEDIUM
else 560
)
super().__init__(
root_widget=ba.containerwidget(
size=(self._width, self._height),
transition=transition,
scale=(
2.08
if uiscale is ba.UIScale.SMALL
else 1.5
if uiscale is ba.UIScale.MEDIUM
else 1.0
),
stack_offset=(0, -48)
if uiscale is ba.UIScale.SMALL
else (0, 15)
if uiscale is ba.UIScale.MEDIUM
else (0, 0),
)
)
cancel_button = ba.buttonwidget(
parent=self._root_widget,
position=(38 + x_inset, self._height - 60),
size=(160, 60),
autoselect=True,
label=ba.Lstr(resource='cancelText'),
scale=0.8,
)
save_button = ba.buttonwidget(
parent=self._root_widget,
position=(self._width - (168 + x_inset), self._height - 60),
autoselect=True,
size=(160, 60),
label=ba.Lstr(resource='saveText'),
scale=0.8,
)
ba.widget(edit=save_button, left_widget=cancel_button)
ba.widget(edit=cancel_button, right_widget=save_button)
ba.textwidget(
parent=self._root_widget,
position=(0, self._height - 50),
size=(self._width, 25),
text=ba.Lstr(
resource=self._r
+ (
'.editSoundtrackText'
if existing_soundtrack is not None
else '.newSoundtrackText'
)
),
color=ba.app.ui.title_color,
h_align='center',
v_align='center',
maxwidth=280,
)
v = self._height - 110
if 'Soundtracks' not in appconfig:
appconfig['Soundtracks'] = {}
self._soundtrack_name: str | None
self._existing_soundtrack_name: str | None
if existing_soundtrack is not None:
# if they passed just a name, pull info from that soundtrack
if isinstance(existing_soundtrack, str):
self._soundtrack = copy.deepcopy(
appconfig['Soundtracks'][existing_soundtrack]
)
self._soundtrack_name = existing_soundtrack
self._existing_soundtrack_name = existing_soundtrack
self._last_edited_song_type = None
else:
# otherwise they can pass info on an in-progress edit
self._soundtrack = existing_soundtrack['soundtrack']
self._soundtrack_name = existing_soundtrack['name']
self._existing_soundtrack_name = existing_soundtrack[
'existing_name'
]
self._last_edited_song_type = existing_soundtrack[
'last_edited_song_type'
]
else:
self._soundtrack_name = None
self._existing_soundtrack_name = None
self._soundtrack = {}
self._last_edited_song_type = None
ba.textwidget(
parent=self._root_widget,
text=ba.Lstr(resource=self._r + '.nameText'),
maxwidth=80,
scale=0.8,
position=(105 + x_inset, v + 19),
color=(0.8, 0.8, 0.8, 0.5),
size=(0, 0),
h_align='right',
v_align='center',
)
# if there's no initial value, find a good initial unused name
if existing_soundtrack is None:
i = 1
st_name_text = ba.Lstr(
resource=self._r + '.newSoundtrackNameText'
).evaluate()
if '${COUNT}' not in st_name_text:
# make sure we insert number *somewhere*
st_name_text = st_name_text + ' ${COUNT}'
while True:
self._soundtrack_name = st_name_text.replace('${COUNT}', str(i))
if self._soundtrack_name not in appconfig['Soundtracks']:
break
i += 1
self._text_field = ba.textwidget(
parent=self._root_widget,
position=(120 + x_inset, v - 5),
size=(self._width - (160 + 2 * x_inset), 43),
text=self._soundtrack_name,
h_align='left',
v_align='center',
max_chars=32,
autoselect=True,
description=ba.Lstr(resource=self._r + '.nameText'),
editable=True,
padding=4,
on_return_press_call=self._do_it_with_sound,
)
scroll_height = self._height - 180
self._scrollwidget = scrollwidget = ba.scrollwidget(
parent=self._root_widget,
highlight=False,
position=(40 + x_inset, v - (scroll_height + 10)),
size=(self._width - (80 + 2 * x_inset), scroll_height),
simple_culling_v=10,
claims_left_right=True,
claims_tab=True,
selection_loops_to_parent=True,
)
ba.widget(edit=self._text_field, down_widget=self._scrollwidget)
self._col = ba.columnwidget(
parent=scrollwidget,
claims_left_right=True,
claims_tab=True,
selection_loops_to_parent=True,
)
self._song_type_buttons: dict[str, ba.Widget] = {}
self._refresh()
ba.buttonwidget(edit=cancel_button, on_activate_call=self._cancel)
ba.containerwidget(edit=self._root_widget, cancel_button=cancel_button)
ba.buttonwidget(edit=save_button, on_activate_call=self._do_it)
ba.containerwidget(edit=self._root_widget, start_button=save_button)
ba.widget(edit=self._text_field, up_widget=cancel_button)
ba.widget(edit=cancel_button, down_widget=self._text_field)
def _refresh(self) -> None:
for widget in self._col.get_children():
widget.delete()
types = [
'Menu',
'CharSelect',
'ToTheDeath',
'Onslaught',
'Keep Away',
'Race',
'Epic Race',
'ForwardMarch',
'FlagCatcher',
'Survival',
'Epic',
'Hockey',
'Football',
'Flying',
'Scary',
'Marching',
'GrandRomp',
'Chosen One',
'Scores',
'Victory',
]
# FIXME: We should probably convert this to use translations.
type_names_translated = ba.app.lang.get_resource('soundtrackTypeNames')
prev_type_button: ba.Widget | None = None
prev_test_button: ba.Widget | None = None
for index, song_type in enumerate(types):
row = ba.rowwidget(
parent=self._col,
size=(self._width - 40, 40),
claims_left_right=True,
claims_tab=True,
selection_loops_to_parent=True,
)
type_name = type_names_translated.get(song_type, song_type)
ba.textwidget(
parent=row,
size=(230, 25),
always_highlight=True,
text=type_name,
scale=0.7,
h_align='left',
v_align='center',
maxwidth=190,
)
if song_type in self._soundtrack:
entry = self._soundtrack[song_type]
else:
entry = None
if entry is not None:
# Make sure they don't muck with this after it gets to us.
entry = copy.deepcopy(entry)
icon_type = self._get_entry_button_display_icon_type(entry)
self._song_type_buttons[song_type] = btn = ba.buttonwidget(
parent=row,
size=(230, 32),
label=self._get_entry_button_display_name(entry),
text_scale=0.6,
on_activate_call=ba.Call(
self._get_entry, song_type, entry, type_name
),
icon=(
self._file_tex
if icon_type == 'file'
else self._folder_tex
if icon_type == 'folder'
else None
),
icon_color=(1.1, 0.8, 0.2)
if icon_type == 'folder'
else (1, 1, 1),
left_widget=self._text_field,
iconscale=0.7,
autoselect=True,
up_widget=prev_type_button,
)
if index == 0:
ba.widget(edit=btn, up_widget=self._text_field)
ba.widget(edit=btn, down_widget=btn)
if (
self._last_edited_song_type is not None
and song_type == self._last_edited_song_type
):
ba.containerwidget(
edit=row, selected_child=btn, visible_child=btn
)
ba.containerwidget(
edit=self._col, selected_child=row, visible_child=row
)
ba.containerwidget(
edit=self._scrollwidget,
selected_child=self._col,
visible_child=self._col,
)
ba.containerwidget(
edit=self._root_widget,
selected_child=self._scrollwidget,
visible_child=self._scrollwidget,
)
if prev_type_button is not None:
ba.widget(edit=prev_type_button, down_widget=btn)
prev_type_button = btn
ba.textwidget(parent=row, size=(10, 32), text='') # spacing
btn = ba.buttonwidget(
parent=row,
size=(50, 32),
label=ba.Lstr(resource=self._r + '.testText'),
text_scale=0.6,
on_activate_call=ba.Call(self._test, ba.MusicType(song_type)),
up_widget=prev_test_button
if prev_test_button is not None
else self._text_field,
)
if prev_test_button is not None:
ba.widget(edit=prev_test_button, down_widget=btn)
ba.widget(edit=btn, down_widget=btn, right_widget=btn)
prev_test_button = btn
@classmethod
def _restore_editor(
cls, state: dict[str, Any], musictype: str, entry: Any
) -> None:
music = ba.app.music
# Apply the change and recreate the window.
soundtrack = state['soundtrack']
existing_entry = (
None if musictype not in soundtrack else soundtrack[musictype]
)
if existing_entry != entry:
ba.playsound(ba.getsound('gunCocking'))
# Make sure this doesn't get mucked with after we get it.
if entry is not None:
entry = copy.deepcopy(entry)
entry_type = music.get_soundtrack_entry_type(entry)
if entry_type == 'default':
# For 'default' entries simply exclude them from the list.
if musictype in soundtrack:
del soundtrack[musictype]
else:
soundtrack[musictype] = entry
ba.app.ui.set_main_menu_window(
cls(state, transition='in_left').get_root_widget()
)
def _get_entry(
self, song_type: str, entry: Any, selection_target_name: str
) -> None:
music = ba.app.music
if selection_target_name != '':
selection_target_name = "'" + selection_target_name + "'"
state = {
'name': self._soundtrack_name,
'existing_name': self._existing_soundtrack_name,
'soundtrack': self._soundtrack,
'last_edited_song_type': song_type,
}
ba.containerwidget(edit=self._root_widget, transition='out_left')
ba.app.ui.set_main_menu_window(
music.get_music_player()
.select_entry(
ba.Call(self._restore_editor, state, song_type),
entry,
selection_target_name,
)
.get_root_widget()
)
def _test(self, song_type: ba.MusicType) -> None:
music = ba.app.music
# Warn if volume is zero.
if ba.app.config.resolve('Music Volume') < 0.01:
ba.playsound(ba.getsound('error'))
ba.screenmessage(
ba.Lstr(resource=self._r + '.musicVolumeZeroWarning'),
color=(1, 0.5, 0),
)
music.set_music_play_mode(ba.MusicPlayMode.TEST)
music.do_play_music(
song_type,
mode=ba.MusicPlayMode.TEST,
testsoundtrack=self._soundtrack,
)
def _get_entry_button_display_name(self, entry: Any) -> str | ba.Lstr:
music = ba.app.music
etype = music.get_soundtrack_entry_type(entry)
ename: str | ba.Lstr
if etype == 'default':
ename = ba.Lstr(resource=self._r + '.defaultGameMusicText')
elif etype in ('musicFile', 'musicFolder'):
ename = os.path.basename(music.get_soundtrack_entry_name(entry))
else:
ename = music.get_soundtrack_entry_name(entry)
return ename
def _get_entry_button_display_icon_type(self, entry: Any) -> str | None:
music = ba.app.music
etype = music.get_soundtrack_entry_type(entry)
if etype == 'musicFile':
return 'file'
if etype == 'musicFolder':
return 'folder'
return None
def _cancel(self) -> None:
from bastd.ui.soundtrack import browser as stb
music = ba.app.music
# Resets music back to normal.
music.set_music_play_mode(ba.MusicPlayMode.REGULAR)
ba.containerwidget(edit=self._root_widget, transition='out_right')
ba.app.ui.set_main_menu_window(
stb.SoundtrackBrowserWindow(transition='in_left').get_root_widget()
)
def _do_it(self) -> None:
from bastd.ui.soundtrack import browser as stb
music = ba.app.music
cfg = ba.app.config
new_name = cast(str, ba.textwidget(query=self._text_field))
if new_name != self._soundtrack_name and new_name in cfg['Soundtracks']:
ba.screenmessage(
ba.Lstr(resource=self._r + '.cantSaveAlreadyExistsText')
)
ba.playsound(ba.getsound('error'))
return
if not new_name:
ba.playsound(ba.getsound('error'))
return
if (
new_name
== ba.Lstr(
resource=self._r + '.defaultSoundtrackNameText'
).evaluate()
):
ba.screenmessage(
ba.Lstr(resource=self._r + '.cantOverwriteDefaultText')
)
ba.playsound(ba.getsound('error'))
return
# Make sure config exists.
if 'Soundtracks' not in cfg:
cfg['Soundtracks'] = {}
# If we had an old one, delete it.
if (
self._existing_soundtrack_name is not None
and self._existing_soundtrack_name in cfg['Soundtracks']
):
del cfg['Soundtracks'][self._existing_soundtrack_name]
cfg['Soundtracks'][new_name] = self._soundtrack
cfg['Soundtrack'] = new_name
cfg.commit()
ba.playsound(ba.getsound('gunCocking'))
ba.containerwidget(edit=self._root_widget, transition='out_right')
# Resets music back to normal.
music.set_music_play_mode(ba.MusicPlayMode.REGULAR, force_restart=True)
ba.app.ui.set_main_menu_window(
stb.SoundtrackBrowserWindow(transition='in_left').get_root_widget()
)
def _do_it_with_sound(self) -> None:
ba.playsound(ba.getsound('swish'))
self._do_it()

View file

@ -0,0 +1,237 @@
# Released under the MIT License. See LICENSE for details.
#
"""Provides UI for selecting soundtrack entry types."""
from __future__ import annotations
import copy
from typing import TYPE_CHECKING
import ba
import ba.internal
if TYPE_CHECKING:
from typing import Any, Callable
class SoundtrackEntryTypeSelectWindow(ba.Window):
"""Window for selecting a soundtrack entry type."""
def __init__(
self,
callback: Callable[[Any], Any],
current_entry: Any,
selection_target_name: str,
transition: str = 'in_right',
):
music = ba.app.music
self._r = 'editSoundtrackWindow'
self._callback = callback
self._current_entry = copy.deepcopy(current_entry)
self._width = 580
self._height = 220
spacing = 80
# FIXME: Generalize this so new custom soundtrack types can add
# themselves here.
do_default = True
do_mac_music_app_playlist = music.supports_soundtrack_entry_type(
'iTunesPlaylist'
)
do_music_file = music.supports_soundtrack_entry_type('musicFile')
do_music_folder = music.supports_soundtrack_entry_type('musicFolder')
if do_mac_music_app_playlist:
self._height += spacing
if do_music_file:
self._height += spacing
if do_music_folder:
self._height += spacing
uiscale = ba.app.ui.uiscale
# NOTE: When something is selected, we close our UI and kick off
# another window which then calls us back when its done, so the
# standard UI-cleanup-check complains that something is holding on
# to our instance after its ui is gone. Should restructure in a
# cleaner way, but just disabling that check for now.
super().__init__(
root_widget=ba.containerwidget(
size=(self._width, self._height),
transition=transition,
scale=(
1.7
if uiscale is ba.UIScale.SMALL
else 1.4
if uiscale is ba.UIScale.MEDIUM
else 1.0
),
),
cleanupcheck=False,
)
btn = ba.buttonwidget(
parent=self._root_widget,
position=(35, self._height - 65),
size=(160, 60),
scale=0.8,
text_scale=1.2,
label=ba.Lstr(resource='cancelText'),
on_activate_call=self._on_cancel_press,
)
ba.containerwidget(edit=self._root_widget, cancel_button=btn)
ba.textwidget(
parent=self._root_widget,
position=(self._width * 0.5, self._height - 32),
size=(0, 0),
text=ba.Lstr(resource=self._r + '.selectASourceText'),
color=ba.app.ui.title_color,
maxwidth=230,
h_align='center',
v_align='center',
)
ba.textwidget(
parent=self._root_widget,
position=(self._width * 0.5, self._height - 56),
size=(0, 0),
text=selection_target_name,
color=ba.app.ui.infotextcolor,
scale=0.7,
maxwidth=230,
h_align='center',
v_align='center',
)
v = self._height - 155
current_entry_type = music.get_soundtrack_entry_type(current_entry)
if do_default:
btn = ba.buttonwidget(
parent=self._root_widget,
size=(self._width - 100, 60),
position=(50, v),
label=ba.Lstr(resource=self._r + '.useDefaultGameMusicText'),
on_activate_call=self._on_default_press,
)
if current_entry_type == 'default':
ba.containerwidget(edit=self._root_widget, selected_child=btn)
v -= spacing
if do_mac_music_app_playlist:
btn = ba.buttonwidget(
parent=self._root_widget,
size=(self._width - 100, 60),
position=(50, v),
label=ba.Lstr(resource=self._r + '.useITunesPlaylistText'),
on_activate_call=self._on_mac_music_app_playlist_press,
icon=None,
)
if current_entry_type == 'iTunesPlaylist':
ba.containerwidget(edit=self._root_widget, selected_child=btn)
v -= spacing
if do_music_file:
btn = ba.buttonwidget(
parent=self._root_widget,
size=(self._width - 100, 60),
position=(50, v),
label=ba.Lstr(resource=self._r + '.useMusicFileText'),
on_activate_call=self._on_music_file_press,
icon=ba.gettexture('file'),
)
if current_entry_type == 'musicFile':
ba.containerwidget(edit=self._root_widget, selected_child=btn)
v -= spacing
if do_music_folder:
btn = ba.buttonwidget(
parent=self._root_widget,
size=(self._width - 100, 60),
position=(50, v),
label=ba.Lstr(resource=self._r + '.useMusicFolderText'),
on_activate_call=self._on_music_folder_press,
icon=ba.gettexture('folder'),
icon_color=(1.1, 0.8, 0.2),
)
if current_entry_type == 'musicFolder':
ba.containerwidget(edit=self._root_widget, selected_child=btn)
v -= spacing
def _on_mac_music_app_playlist_press(self) -> None:
music = ba.app.music
from bastd.ui.soundtrack.macmusicapp import (
MacMusicAppPlaylistSelectWindow,
)
ba.containerwidget(edit=self._root_widget, transition='out_left')
current_playlist_entry: str | None
if (
music.get_soundtrack_entry_type(self._current_entry)
== 'iTunesPlaylist'
):
current_playlist_entry = music.get_soundtrack_entry_name(
self._current_entry
)
else:
current_playlist_entry = None
ba.app.ui.set_main_menu_window(
MacMusicAppPlaylistSelectWindow(
self._callback, current_playlist_entry, self._current_entry
).get_root_widget()
)
def _on_music_file_press(self) -> None:
from ba.osmusic import OSMusicPlayer
from bastd.ui.fileselector import FileSelectorWindow
ba.containerwidget(edit=self._root_widget, transition='out_left')
base_path = ba.internal.android_get_external_files_dir()
ba.app.ui.set_main_menu_window(
FileSelectorWindow(
base_path,
callback=self._music_file_selector_cb,
show_base_path=False,
valid_file_extensions=(
OSMusicPlayer.get_valid_music_file_extensions()
),
allow_folders=False,
).get_root_widget()
)
def _on_music_folder_press(self) -> None:
from bastd.ui.fileselector import FileSelectorWindow
ba.containerwidget(edit=self._root_widget, transition='out_left')
base_path = ba.internal.android_get_external_files_dir()
ba.app.ui.set_main_menu_window(
FileSelectorWindow(
base_path,
callback=self._music_folder_selector_cb,
show_base_path=False,
valid_file_extensions=[],
allow_folders=True,
).get_root_widget()
)
def _music_file_selector_cb(self, result: str | None) -> None:
if result is None:
self._callback(self._current_entry)
else:
self._callback({'type': 'musicFile', 'name': result})
def _music_folder_selector_cb(self, result: str | None) -> None:
if result is None:
self._callback(self._current_entry)
else:
self._callback({'type': 'musicFolder', 'name': result})
def _on_default_press(self) -> None:
ba.containerwidget(edit=self._root_widget, transition='out_right')
self._callback(None)
def _on_cancel_press(self) -> None:
ba.containerwidget(edit=self._root_widget, transition='out_right')
self._callback(self._current_entry)

View file

@ -0,0 +1,118 @@
# Released under the MIT License. See LICENSE for details.
#
"""UI functionality related to using the macOS Music app for soundtracks."""
from __future__ import annotations
import copy
from typing import TYPE_CHECKING
import ba
if TYPE_CHECKING:
from typing import Any, Callable
class MacMusicAppPlaylistSelectWindow(ba.Window):
"""Window for selecting an iTunes playlist."""
def __init__(
self,
callback: Callable[[Any], Any],
existing_playlist: str | None,
existing_entry: Any,
):
from ba.macmusicapp import MacMusicAppMusicPlayer
self._r = 'editSoundtrackWindow'
self._callback = callback
self._existing_playlist = existing_playlist
self._existing_entry = copy.deepcopy(existing_entry)
self._width = 520.0
self._height = 520.0
self._spacing = 45.0
v = self._height - 90.0
v -= self._spacing * 1.0
super().__init__(
root_widget=ba.containerwidget(
size=(self._width, self._height), transition='in_right'
)
)
btn = ba.buttonwidget(
parent=self._root_widget,
position=(35, self._height - 65),
size=(130, 50),
label=ba.Lstr(resource='cancelText'),
on_activate_call=self._back,
autoselect=True,
)
ba.containerwidget(edit=self._root_widget, cancel_button=btn)
ba.textwidget(
parent=self._root_widget,
position=(20, self._height - 54),
size=(self._width, 25),
text=ba.Lstr(resource=self._r + '.selectAPlaylistText'),
color=ba.app.ui.title_color,
h_align='center',
v_align='center',
maxwidth=200,
)
self._scrollwidget = ba.scrollwidget(
parent=self._root_widget,
position=(40, v - 340),
size=(self._width - 80, 400),
claims_tab=True,
selection_loops_to_parent=True,
)
ba.widget(edit=self._scrollwidget, right_widget=self._scrollwidget)
self._column = ba.columnwidget(
parent=self._scrollwidget,
claims_tab=True,
selection_loops_to_parent=True,
)
ba.textwidget(
parent=self._column,
size=(self._width - 80, 22),
text=ba.Lstr(resource=self._r + '.fetchingITunesText'),
color=(0.6, 0.9, 0.6, 1.0),
scale=0.8,
)
musicplayer = ba.app.music.get_music_player()
assert isinstance(musicplayer, MacMusicAppMusicPlayer)
musicplayer.get_playlists(self._playlists_cb)
ba.containerwidget(
edit=self._root_widget, selected_child=self._scrollwidget
)
def _playlists_cb(self, playlists: list[str]) -> None:
if self._column:
for widget in self._column.get_children():
widget.delete()
for i, playlist in enumerate(playlists):
txt = ba.textwidget(
parent=self._column,
size=(self._width - 80, 30),
text=playlist,
v_align='center',
maxwidth=self._width - 110,
selectable=True,
on_activate_call=ba.Call(self._sel, playlist),
click_activate=True,
)
ba.widget(edit=txt, show_buffer_top=40, show_buffer_bottom=40)
if playlist == self._existing_playlist:
ba.columnwidget(
edit=self._column, selected_child=txt, visible_child=txt
)
if i == len(playlists) - 1:
ba.widget(edit=txt, down_widget=txt)
def _sel(self, selection: str) -> None:
if self._root_widget:
ba.containerwidget(edit=self._root_widget, transition='out_right')
self._callback({'type': 'iTunesPlaylist', 'name': selection})
def _back(self) -> None:
ba.containerwidget(edit=self._root_widget, transition='out_right')
self._callback(self._existing_entry)