def show_dialog_add_playlist(self):
        """
        Show a dialog to ask the name of the new playlist.
        :return:
        """
        creation_time = datetime.now().strftime("%Y%m%d-%H%M%S")

        text = f"Concert_{creation_time}"

        content_obj = BoxLayout(orientation='vertical',
                                spacing="12dp",
                                size_hint_y=None)

        mdtf1 = MDTextField()
        mdtf1.text = text
        mdtf1.hint_text = f"Name of playlist"
        mdtf1.helper_text = f"{CU.tfs.dic['EXPLANATION_PLAYLIST_SONG_NAME'].value}"
        mdtf1.helper_text_mode = "on_focus"

        content_obj.add_widget(mdtf1)

        CU.show_input_dialog(
            title=f"Enter Name of New Playlist",
            content_obj=content_obj,
            size_hint=(.7, .4),
            text_button_ok="Add",
            text_button_cancel="Cancel",
            ok_callback_set=lambda *args, **kwargs:
            (self.add_playlist(mdtf1.text), self.refresh_list()))
Beispiel #2
0
    def show_dialog_add_lineup_entry(self):
        """
        Show a dialog to ask the name of the new lineup_entry.
        :return:
        """
        creation_time = datetime.now().strftime("%Y%m%d-%H%M%S")

        text = f"Concert_{creation_time}"

        content_obj = BoxLayout(orientation='vertical',
                                spacing="12dp",
                                size_hint_y=None)

        # mdlbl1 = MDLabel(text=str(CU.tfs.dic['EXPLANATION_WORKSPACE_PATH'].value))

        mdtf1 = MDTextField()

        mdtf1.text = text
        mdtf1.hint_text = f"Name of song"
        mdtf1.helper_text = f"{CU.tfs.dic['EXPLANATION_PLAYLIST_SONG_NAME'].value}"
        mdtf1.helper_text_mode = "on_focus"

        # content_obj.add_widget(mdlbl1)
        content_obj.add_widget(mdtf1)

        CU.show_input_dialog(
            title=f"Enter Name of New Songentry",
            content_obj=content_obj,
            size_hint=(.7, .4),
            text_button_ok="Add",
            ok_callback_set=lambda text_button, instance, *args, **kwargs: {
                self.add_lineup_entry(instance.text_field.text),
                self.refresh_list()
            })
    def show_dialog_rename_playlist(self, playlist_rowview):
        """
        Show a dialog to ask for the new name of the playlist.
        :param playlist_rowview:
        :return:
        """

        text = f"{str(playlist_rowview.playlist_obj.file_path.stem)}"

        content_obj = BoxLayout(orientation='vertical',
                                spacing="12dp",
                                size_hint_y=None)

        mdtf1 = MDTextField()
        mdtf1.text = text
        mdtf1.hint_text = f"Name of playlist"
        mdtf1.helper_text = f"{CU.tfs.dic['EXPLANATION_PLAYLIST_SONG_NAME'].value}"
        mdtf1.helper_text_mode = "on_focus"

        content_obj.add_widget(mdtf1)

        CU.show_input_dialog(title=f"Enter New Name for Playlist",
                             content_obj=content_obj,
                             size_hint=(.7, .4),
                             text_button_ok="Update",
                             text_button_cancel="Cancel",
                             ok_callback_set=lambda *args, **kwargs:
                             (self.rename_playlist(playlist_rowview, mdtf1.text
                                                   ), self.refresh_list()))
Beispiel #4
0
    def on_stop(self, *args, **kwargs):
        # Important: *args and **kwargs can both be passed to a method, *args takes care of all unexpected
        # (variable amount) of positional arguments and **kwargs takes care of all unexpected (variable amount)
        # of named arguments.
        CU.show_ok_cancel_dialog(
            title="Confirmation dialog",
            text=
            f"Are you sure you want to [color={get_hex_from_color(self.theme_cls.primary_color)}][b]quit[/b][/color] {CU.tfs.dic['APP_NAME'].value}?",
            size_hint=(0.5, 0.3),
            text_button_ok="Yes",
            text_button_cancel="No",
            ok_callback_set=lambda *args, **kwargs: self.stop(),
            cancel_callback_set=lambda *args, **kwargs: toast("Not quitting"))

        return True
Beispiel #5
0
    def show_dialog_remove_song(self, song_rowview):
        """
        Show a dialog to ask for confirmation of the removal.
        :param song_rowview:
        :return:
        """
        text=f"Are you sure want to remove [color={get_hex_from_color(Songs.app.theme_cls.primary_color)}][b]{str(song_rowview.song_entry_obj.file_path.stem)}[/b][/color] from the list? This action cannot be undone."

        CU.show_ok_cancel_dialog(title=f"Are You Sure?",
                                 text=text,
                                 size_hint=(.7, .4),
                                 text_button_ok="Remove",
                                 text_button_cancel="Cancel",
                                 ok_callback_set=lambda *args, **kwargs: (self.remove_song(song_rowview, args), self.refresh_list()),
                                 cancel_callback_set=lambda *args, **kwargs: (toast(f"Canceled removal of {str(song_rowview.song_entry_obj.file_path.stem)}")))
Beispiel #6
0
    def set_file_path(self, file_path):
        file_path = CU.safe_cast(file_path, pl.Path, None)

        if self._file_path != file_path:
            self._file_path = file_path
            # Reset modal view when the file path changed, just to be sure it's recreated when needed next time:
            self._modal_view = None
Beispiel #7
0
    def on_selected_path_filemanager(self, *args, **kwargs):
        """It will be called when you click on the file name
        or the catalog selection button.
        :type path: str;
        :param path: path to the selected directory or file;
        """

        path = CU.safe_cast(args[0][0], pl.Path, None)
        if path.is_file():
            # To make sure that whole parent directory is scanned for additional song-files:
            path = path.parents[0]

        path_found = False
        i = 0

        # On each invocation of this method the default path is prepended because at some point the user might change the tf_workspace_path, and then this change is automatically refreshed.
        scanned_song_locations = [pl.Path(CU.tfs.dic['tf_workspace_path'].value / CU.tfs.dic['RAW_MIDI_DIR_NAME'].value)] + self._list_additional_song_locations

        # Searching until first occurrence of possible duplicate:
        while not path_found and i < len(scanned_song_locations):
            if path.samefile(scanned_song_locations[i]): # path == scanned_song_locations[i]:
                path_found = True
                toast(f"{os.sep}{path.name}-folder is already searched")
            i = i+1

        if not path_found:
            # If the path is not found yet, it can be added, this whole operation was to avoid duplicates:
            self._list_additional_song_locations.append(path)
            toast(f"{path} added to search scope")

        self.refresh_list()
Beispiel #8
0
 def set_songlevel_speedfactor(self, songlevel_speedfactor):
     """
     Set float _songlevel_speedfactor, to override the default overall_speed_factor inside the TF_Settigns at song level here.
     :param songlevel_speedfactor: bool
     :return:
     """
     songlevel_speedfactor = CU.safe_cast(songlevel_speedfactor, float, 1.0)
     self._songlevel_speedfactor = songlevel_speedfactor
Beispiel #9
0
 def set_mute_audio(self, mute_audio):
     """
     To mute the sound of a movie clip during playback.
     :param mute_audio:
     :return:
     """
     mute_audio = CU.safe_cast(mute_audio, bool, False)
     self._mute_audio = mute_audio
Beispiel #10
0
 def set_repeat(self, loop_repeat):
     """
     Loop repeat option comes in very handy when you e.g. turn a sponsor-slideshow into a movie-file which should repeat at some points in the concert.
     :param loop_repeat:
     :return:
     """
     loop_repeat = CU.safe_cast(loop_repeat, bool, False)
     self._loop_repeat = loop_repeat
 def set_file_path(self, file_path):
     """
     Setter _file_path
     :param file_path: _file_path
     :return:
     """
     file_path = CU.safe_cast(file_path, pl.Path, None)
     self._file_path = pl.Path(file_path)
 def condense_note_pitch(note_number):
     """
     Returns the note pitch where a given note_number boils down to, regardless of the octave
     :param note_number: 'uncondensed' note_number (i.e. note_number which may be > 11).
     :return: the corresponding note_number < 11
     """
     note_number = CU.safe_cast(note_number, int, 0)
     return note_number % MusicTheoryCoreUtils.AMOUNT_DISTINCT_NOTES
Beispiel #13
0
 def set_show_upside_down(self, show_upside_down):
     """
     Set boolean _show_upside_down, that will make the <LineupEntry> to be shown upside down. (This comes in handy when projecting vertically on the floor, so the image or clip to announce the next music-piece faces the audience)
     :param show_upside_down: bool
     :return:
     """
     show_upside_down = CU.safe_cast(show_upside_down, bool, False)
     self._show_upside_down = show_upside_down
Beispiel #14
0
 def set_mute_play_along(self, mute_play_along):
     """
     Set boolean _mute_play_along, to toggle whether synthetic MIDI file sound will be played out loud in concertmode.
     :param mute_play_along: bool
     :return:
     """
     mute_play_along = CU.safe_cast(mute_play_along, bool, True)
     self._mute_play_along = mute_play_along
    def note_name_to_number(note_name):
        """
        Maps a human-readable note to its MIDI-encoded form. Supplied note_name should be formatted as {'C4', 'C#4/Db4', 'Db4/C#4', 'C#4', 'Db4', 'C#/Db4', 'Db/C#4'}.
        In other words, last character(s) must be devoted to denoting the octave. Midi now supports 10 octaves [0..9]
        :param note_name: string describing a note
        :return: MIDI-encoded note_number
        """
        note_name = CU.safe_cast(note_name, str, "")

        # Split on '/' to tell apart possible redundant namings:
        note_names = note_name.split("/")

        # Possible redundant namings are ignored by considering the last/default one:
        note_name = note_names[len(note_names) - 1]

        note_pitch_text, octave_text = CU.split_letters_from_digits(note_name)
        octave = CU.safe_cast(octave_text, int, 0)

        note_number = '-1'

        # Determine whether the note that has to be parsed is a black note or not.
        is_black_note = any((char in set('#b')) for char in note_pitch_text)

        # Save work in the next loop over the key-value pairs (parsing the note_pitch_text) by filtering the note_System:
        subdict_whites_or_blacks = {
            key: value
            for (key, value) in MusicTheoryCoreUtils.NOTE_SYSTEM.items()
            if ((key in MusicTheoryCoreUtils.WHITE_NOTES) == (
                not is_black_note))
        }

        # Map the retrieved note_pitch_text to a condensed_note_number:
        for key, value in subdict_whites_or_blacks.items():

            # As soon as the note is parsed exit the for loop:
            if note_pitch_text in value:
                note_number = key
                break

        if note_number != -1:
            # The reason why octave + 1 is multiplied with the amount of distinct notes rather than just octave is to meet the MIDI-standard. Apparently, an octave -1 is foreseen:
            note_number += MusicTheoryCoreUtils.AMOUNT_DISTINCT_NOTES * (
                octave + 1)

        return note_number
Beispiel #16
0
    def handle_exception(self, exception):
        # app = App.get_running_app()

        # If the current problem is cleared, "accept" the next one:
        if self._current_exception is None:
            # In order to make a new dialog, we get rid of a previous one if any:
            self._error_dialog = None
            # While showing the popup to the user with the error's stacktrace, all subsequent calls to handle_exception() should PASS. It turns out that while in error, that his method is constantly triggered!!
            self._PASS_or_RAISE = ExceptionManager.PASS

            if self._error_dialog is None:
                self._current_exception = exception

                bl1 = BoxLayout(orientation='vertical',
                                spacing="12dp",
                                size_hint_y=None)

                # Make sure the height is such that there is something to scroll.
                bl1.bind(minimum_height=bl1.setter('height'))

                mdlbl1 = MDLabel(
                    text=
                    f"[color={get_hex_from_color((1, 0, 0))}][i]{str(self._current_exception)}[/i][/color]{os.linesep}{os.linesep}"
                    f"[b]-> Our apologies for the inconvenience, please consult the stack trace below & mail screenshot to [email protected]:[/b]"
                    f"{os.linesep}{traceback.format_exc()}",
                    size_hint=(1, None),
                    markup=True)

                # TODO: Rather try the .kv alternative also provided at "https://stackoverflow.com/questions/43666381/wrapping-the-text-of-a-kivy-label" and "https://kivy.org/doc/stable/api-kivy.uix.scrollview.html" that will cleaner and more maintainable
                mdlbl1.bind(
                    width=lambda *args, **kwargs: mdlbl1.setter('text_size')
                    (mdlbl1, (mdlbl1.width, None)),
                    texture_size=lambda *args, **kwargs: mdlbl1.setter(
                        'height')(mdlbl1, mdlbl1.texture_size[1]))

                bl1.add_widget(mdlbl1)

                content_obj = ScrollView(do_scroll_x=False,
                                         do_scroll_y=True,
                                         size_hint=(1, None),
                                         scroll_type=['bars', 'content'])
                content_obj.add_widget(bl1)

                self._error_dialog = CU.show_input_dialog(
                    title=
                    f"{CU.tfs.dic['APP_NAME'].value} Encountered an Error & Needs to Shut Down",
                    content_obj=content_obj,
                    size_hint=(.8, .6),
                    text_button_ok="Quit",
                    text_button_cancel="Proceed @ Own Risk",
                    ok_callback_set=lambda *args, **kwargs:
                    (self.set_raise_or_pass(ExceptionManager.RAISE)),
                    cancel_callback_set=lambda *args, **kwargs:
                    (toast("Fingers crossed"),
                     self.set_raise_or_pass(ExceptionManager.PASS)))

        return self._PASS_or_RAISE
Beispiel #17
0
    def set_value(self, value):
        # Check if the type of the new value matches with the type of the default value, try safe_casting:
        if (isinstance(self._default_value, pl.Path)):
            # When it's a pl.Path, then the exportable FILE_SEP_TEXT should be replaced:
            value = str(value).replace(f"{CU.tfs.dic['FILE_SEP_TEXT'].value}", f"{os.sep}")
        if (isinstance(value, str)):
            value = CU.with_consistent_linesep(value)

        # The callback can do some extra checking of the value while its for instance still string, and order of doing the safe cast and calling the callback used to be the other way around, but the pathlib library uses other windows-lineseparator when casting back to string, but then it's not possible to omit an explanation
        if (self._callback_on_set is not None):
            processed_value = self._callback_on_set(value)
        else:
            processed_value = value

        if processed_value is not None:
            self._value = CU.safe_cast(processed_value, type(self._default_value), "")
        else:
            # The previous _value is left unchanged.
            toast(f"Value for setting \"{self.name}\" is not valid{os.linesep}Original value was left unchanged.")
Beispiel #18
0
    def on_start(self):
        # As a proposal, the actual (default)value of the tf_workspace_path-param is copied to the clipboard:
        workspace_path_proposal = CU.tfs.dic['tf_workspace_path'].default_value
        pyperclip.copy(str(workspace_path_proposal))

        # In case the user did configure a customized path, that path will be filled in:
        text = "" if (CU.tfs.dic['tf_workspace_path'].default_value
                      == CU.tfs.dic['tf_workspace_path'].value
                      ) else f"{CU.tfs.dic['tf_workspace_path'].value}"

        content_obj = BoxLayout(orientation='vertical',
                                spacing="12dp",
                                size_hint_y=None)

        # mdlbl1 = MDLabel(text=str(CU.tfs.dic['EXPLANATION_WORKSPACE_PATH'].value))

        mdtf1 = MDTextField()

        mdtf1.text = text
        mdtf1.hint_text = f"{CU.tfs.dic['tf_workspace_path'].description}"
        mdtf1.helper_text = str(CU.tfs.dic['EXPLANATION_WORKSPACE_PATH'].value)
        mdtf1.helper_text_mode = "on_focus"

        # content_obj.add_widget(mdlbl1)
        content_obj.add_widget(mdtf1)

        CU.show_input_dialog(
            title=
            f"Enter Path to \"{CU.tfs.dic['WORKSPACE_NAME'].value}\"-Folder or to its Parent Folder",
            content_obj=content_obj,
            # hint_text=f"{CU.tfs.dic['tf_workspace_path'].description}",

            # size_hint=(.8, .4),
            text_button_ok="Load/Create",
            text_button_cancel="Cancel",
            ok_callback_set=lambda *args, **kwargs:
            (CU.tfs.dic['tf_workspace_path'].set_value(mdtf1.text),
             toast(str(CU.tfs.dic['tf_workspace_path'].value))),
            cancel_callback_set=lambda *args, **kwargs: toast(
                f"{CU.tfs.dic['WORKSPACE_NAME'].value} can still be changed anytime from the settings"
            ))
Beispiel #19
0
    async def async_add_lineup_entry(self, name_new_lineup_entry):
        """
        Actual process to add a lineup_entry.
        :param name_new_lineup_entry:
        :return:
        """
        # Omit the provided explanation-text in case it was not omitted:
        name_new_lineup_entry = str(
            CU.with_consistent_linesep(name_new_lineup_entry)
        ).replace(
            f"{CU.tfs.dic['EXPLANATION_PLAYLIST_SONG_NAME'].value}{os.linesep}",
            "")

        # Check the name_new_lineup_entry by means of a regular expression:
        # Only allow names entirely consisting of alphanumeric characters, dashes and underscores
        if re.match("^[\w\d_-]+$", str(name_new_lineup_entry)):
            filename_lineup_entry = f"{str(name_new_lineup_entry)}.json"
            if len(
                    list(
                        pl.Path(CU.tfs.dic['tf_workspace_path'].value /
                                CU.tfs.dic['PLAYLISTS_DIR_NAME'].value).glob(
                                    filename_lineup_entry))) > 0:
                toast(f"{name_new_lineup_entry} already exists")
            else:
                file_path = pl.Path(CU.tfs.dic['tf_workspace_path'].value /
                                    CU.tfs.dic['PLAYLISTS_DIR_NAME'].value /
                                    filename_lineup_entry)
                with open(str(file_path), "w") as json_file:
                    json_file.write("")

                # TODO: async option doesn't work in combination with asynckivy.start() error is TypeError: '_asyncio.Future' object is not callable
                # async with open(str(file_path), 'w') as json_file:
                #     await json_file.write("")

                toast(f"{name_new_lineup_entry} added")
        else:
            toast(
                f"Name cannot be empty nor contain{os.linesep}non-alphanumeric characters except for \"-_\")"
            )
        await asynckivy.sleep(0)
Beispiel #20
0
    async def async_filter_list(self):
        """
        Filter the visual list on the provided search pattern.
        :return:
        """
        search_pattern = CU.safe_cast(self.ids.search_field.text, str, "")
        # print(f"search pattern is {search_pattern}")
        self.ids.rv.data = []

        for song in self._list:
            song_name = str(song.file_path.stem)
            if (len(search_pattern) == 0 or ((len(search_pattern) > 0) and (search_pattern.lower() in song_name.lower()))):

                self.ids.rv.data.append(
                    {
                        "viewclass": "SongRowView",
                        "list_obj": self,
                        "song_entry_obj": song,
                        "callback": None
                    }
                )
        await asynckivy.sleep(0)
Beispiel #21
0
    async def async_rename_help(self, help_rowview, new_name_help):
        """
        Actual process to rename a help.
        :param help_rowview:
        :param new_name_help:
        :return:
        """
        # Omit the provided explanation-text in case it was not omitted:
        new_name_help = str(CU.with_consistent_linesep(new_name_help)).replace(
            f"{CU.tfs.dic['EXPLANATION_PLAYLIST_SONG_NAME'].value}{os.linesep}",
            "")

        # Check the new_name_help by means of a regular expression:
        # Only allow names entirely consisting of alphanumeric characters, dashes and underscores
        help_to_rename = help_rowview.help_entry_obj

        if re.match("^[\w\d_-]+$", str(new_name_help)):
            filename_help = f"{str(new_name_help)}.{help_to_rename.file_path.suffix}"
            if len(
                    list(
                        pl.Path(CU.tfs.dic['tf_workspace_path'].value /
                                CU.tfs.dic['RAW_MIDI_DIR_NAME'].value).glob(
                                    filename_help))) > 0:
                toast(f"{new_name_help} already exists")

            elif help_to_rename.file_path.exists():
                old_name = str(help_to_rename.file_path.stem)
                file_path = pl.Path(help_to_rename.file_path.parents[0] /
                                    filename_help)
                pl.Path(help_to_rename.file_path).rename(file_path)
                toast(f"{old_name} renamed to {new_name_help}")
            else:
                toast(f"Help {help_to_rename.file_path.stem} not found")
        else:
            toast(
                f"Name cannot be empty nor contain{os.linesep}non-alphanumeric characters except for \"-_\")"
            )
        await asynckivy.sleep(0)
    async def async_editable_filter_list(self):
        """
        Filter the visual editable_list on the provided search pattern.
        :return:
        """
        search_pattern = CU.safe_cast(self.ids.search_field.text, str, "")
        # print(f"search pattern is {search_pattern}")
        self.ids.rv.data = []

        for tfsetting in self._editable_list:
            # Make sure that user can also search by entering a snippet of the value or the description.
            setting_name = f"{tfsetting.name}{str(tfsetting.value)}{str(tfsetting.description)}"
            if (len(search_pattern) == 0
                    or ((len(search_pattern) > 0) and
                        (search_pattern.lower() in setting_name.lower()))):

                self.ids.rv.data.append({
                    "viewclass": "TFSettingRowView",
                    "list_obj": self,
                    "tfsetting_obj": tfsetting,
                    "callback": None
                })
        await asynckivy.sleep(0)
 def set_editable_list(self, editable_list):
     editable_list = CU.safe_cast(editable_list,
                                  self._editable_list.__class__, "")
     self._editable_list = editable_list
Beispiel #24
0
 def set_name(self, name):
     name = CU.safe_cast(name, str, "")
     self._name = name
Beispiel #25
0
 def set_list(self, list):
     list = CU.safe_cast(list, self._list.__class__, "")
     self._list = list
Beispiel #26
0
 def set_description(self, description):
     description = CU.safe_cast(description, str, "")
     self._description = description
Beispiel #27
0
    def __init__(self, file_path, **kwargs):

        self._file_path = CU.safe_cast(file_path, pl.Path, None)

        # I chose to store the modal view locally when created, so it does not need to be created over and over again:
        self._modal_view = None