Source code for bauiv1lib.soundtrack.edit

# 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, override

import bascenev1 as bs
import bauiv1 as bui

if TYPE_CHECKING:
    from typing import Any


[docs] class SoundtrackEditWindow(bui.MainWindow): """Window for editing a soundtrack.""" def __init__( self, existing_soundtrack: str | dict[str, Any] | None, transition: str | None = 'in_right', origin_widget: bui.Widget | None = None, ): # pylint: disable=too-many-statements appconfig = bui.app.config self._r = 'editSoundtrackWindow' self._folder_tex = bui.gettexture('folder') self._file_tex = bui.gettexture('file') assert bui.app.classic is not None uiscale = bui.app.ui_v1.uiscale self._width = 900 if uiscale is bui.UIScale.SMALL else 648 x_inset = 100 if uiscale is bui.UIScale.SMALL else 0 self._height = ( 450 if uiscale is bui.UIScale.SMALL else 450 if uiscale is bui.UIScale.MEDIUM else 560 ) yoffs = -48 if uiscale is bui.UIScale.SMALL else 0 super().__init__( root_widget=bui.containerwidget( size=(self._width, self._height), scale=( 1.8 if uiscale is bui.UIScale.SMALL else 1.5 if uiscale is bui.UIScale.MEDIUM else 1.0 ), stack_offset=( (0, 0) if uiscale is bui.UIScale.SMALL else (0, 15) if uiscale is bui.UIScale.MEDIUM else (0, 0) ), ), transition=transition, origin_widget=origin_widget, ) cancel_button = bui.buttonwidget( parent=self._root_widget, position=(38 + x_inset, self._height - 60 + yoffs), size=(160, 60), autoselect=True, label=bui.Lstr(resource='cancelText'), scale=0.8, ) save_button = bui.buttonwidget( parent=self._root_widget, position=(self._width - (168 + x_inset), self._height - 60 + yoffs), autoselect=True, size=(160, 60), label=bui.Lstr(resource='saveText'), scale=0.8, ) bui.widget(edit=save_button, left_widget=cancel_button) bui.widget(edit=cancel_button, right_widget=save_button) bui.textwidget( parent=self._root_widget, position=(0, self._height - 50 + yoffs), size=(self._width, 25), text=bui.Lstr( resource=self._r + ( '.editSoundtrackText' if existing_soundtrack is not None else '.newSoundtrackText' ) ), color=bui.app.ui_v1.title_color, h_align='center', v_align='center', maxwidth=280, ) v = self._height - 110 + yoffs if 'Soundtracks' not in appconfig: appconfig['Soundtracks'] = {} self._soundtrack_name: str | None self._existing_soundtrack = existing_soundtrack 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 bui.textwidget( parent=self._root_widget, text=bui.Lstr(resource=f'{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 = bui.Lstr( resource=f'{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 = bui.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=bui.Lstr(resource=f'{self._r}.nameText'), editable=True, padding=4, on_return_press_call=self._do_it_with_sound, ) scroll_height = self._height - ( 230 if uiscale is bui.UIScale.SMALL else 180 ) self._scrollwidget = scrollwidget = bui.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, ) bui.widget(edit=self._text_field, down_widget=self._scrollwidget) self._col = bui.columnwidget( parent=scrollwidget, claims_left_right=True, claims_tab=True, selection_loops_to_parent=True, ) self._song_type_buttons: dict[str, bui.Widget] = {} self._refresh() bui.buttonwidget(edit=cancel_button, on_activate_call=self._cancel) bui.containerwidget(edit=self._root_widget, cancel_button=cancel_button) bui.buttonwidget(edit=save_button, on_activate_call=self._do_it) bui.containerwidget(edit=self._root_widget, start_button=save_button) bui.widget(edit=self._text_field, up_widget=cancel_button) bui.widget(edit=cancel_button, down_widget=self._text_field)
[docs] @override def get_main_window_state(self) -> bui.MainWindowState: # Support recreating our window for back/refresh purposes. cls = type(self) # Pull this out of self here; if we do it in the lambda we'll # keep our window alive due to the 'self' reference. existing_soundtrack = { 'name': self._soundtrack_name, 'existing_name': self._existing_soundtrack_name, 'soundtrack': self._soundtrack, 'last_edited_song_type': self._last_edited_song_type, } return bui.BasicMainWindowState( create_call=lambda transition, origin_widget: cls( transition=transition, origin_widget=origin_widget, existing_soundtrack=existing_soundtrack, ) )
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 = bui.app.lang.get_resource('soundtrackTypeNames') prev_type_button: bui.Widget | None = None prev_test_button: bui.Widget | None = None for index, song_type in enumerate(types): row = bui.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) bui.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 = bui.buttonwidget( parent=row, size=(230, 32), label=self._get_entry_button_display_name(entry), text_scale=0.6, on_activate_call=bui.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: bui.widget(edit=btn, up_widget=self._text_field) bui.widget(edit=btn, down_widget=btn) if ( self._last_edited_song_type is not None and song_type == self._last_edited_song_type ): bui.containerwidget( edit=row, selected_child=btn, visible_child=btn ) bui.containerwidget( edit=self._col, selected_child=row, visible_child=row ) bui.containerwidget( edit=self._scrollwidget, selected_child=self._col, visible_child=self._col, ) bui.containerwidget( edit=self._root_widget, selected_child=self._scrollwidget, visible_child=self._scrollwidget, ) if prev_type_button is not None: bui.widget(edit=prev_type_button, down_widget=btn) prev_type_button = btn bui.textwidget(parent=row, size=(10, 32), text='') # spacing assert bui.app.classic is not None btn = bui.buttonwidget( parent=row, size=(50, 32), label=bui.Lstr(resource=f'{self._r}.testText'), text_scale=0.6, on_activate_call=bui.Call(self._test, bs.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: bui.widget(edit=prev_test_button, down_widget=btn) bui.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: assert bui.app.classic is not None music = bui.app.classic.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: bui.getsound('gunCocking').play() # 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 mainwindow = bui.app.ui_v1.get_main_window() assert mainwindow is not None mainwindow.main_window_back_state = state['back_state'] mainwindow.main_window_back() def _get_entry( self, song_type: str, entry: Any, selection_target_name: str ) -> None: assert bui.app.classic is not None music = bui.app.classic.music # no-op if we're not in control. if not self.main_window_has_control(): return 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, } new_win = music.get_music_player().select_entry( bui.Call(self._restore_editor, state, song_type), entry, selection_target_name, ) self.main_window_replace(new_win) # Once we've set the new window, grab the back-state; we'll use # that to jump back here after selection completes. assert new_win.main_window_back_state is not None state['back_state'] = new_win.main_window_back_state def _test(self, song_type: bs.MusicType) -> None: assert bui.app.classic is not None music = bui.app.classic.music # Warn if volume is zero. if bui.app.config.resolve('Music Volume') < 0.01: bui.getsound('error').play() bui.screenmessage( bui.Lstr(resource=f'{self._r}.musicVolumeZeroWarning'), color=(1, 0.5, 0), ) music.set_music_play_mode(bui.app.classic.MusicPlayMode.TEST) music.do_play_music( song_type, mode=bui.app.classic.MusicPlayMode.TEST, testsoundtrack=self._soundtrack, ) def _get_entry_button_display_name(self, entry: Any) -> str | bui.Lstr: assert bui.app.classic is not None music = bui.app.classic.music etype = music.get_soundtrack_entry_type(entry) ename: str | bui.Lstr if etype == 'default': ename = bui.Lstr(resource=f'{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: assert bui.app.classic is not None music = bui.app.classic.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 bauiv1lib.soundtrack.browser import SoundtrackBrowserWindow # 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 assert bui.app.classic is not None music = bui.app.classic.music # Resets music back to normal. music.set_music_play_mode(bui.app.classic.MusicPlayMode.REGULAR) self.main_window_back() def _do_it(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 assert bui.app.classic is not None music = bui.app.classic.music cfg = bui.app.config new_name = cast(str, bui.textwidget(query=self._text_field)) if new_name != self._soundtrack_name and new_name in cfg['Soundtracks']: bui.screenmessage( bui.Lstr(resource=f'{self._r}.cantSaveAlreadyExistsText') ) bui.getsound('error').play() return if not new_name: bui.getsound('error').play() return if ( new_name == bui.Lstr( resource=f'{self._r}.defaultSoundtrackNameText' ).evaluate() ): bui.screenmessage( bui.Lstr(resource=f'{self._r}.cantOverwriteDefaultText') ) bui.getsound('error').play() 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() bui.getsound('gunCocking').play() # Resets music back to normal. music.set_music_play_mode( bui.app.classic.MusicPlayMode.REGULAR, force_restart=True ) self.main_window_back() def _do_it_with_sound(self) -> None: bui.getsound('swish').play() self._do_it()