diff --git a/openlp/core/common/settings.py b/openlp/core/common/settings.py index c92cefe60..c5984408a 100644 --- a/openlp/core/common/settings.py +++ b/openlp/core/common/settings.py @@ -342,6 +342,7 @@ class Settings(QtCore.QSettings): 'songs/songselect password': '', 'songs/songselect searches': '', 'songs/enable chords': True, + 'songs/warn about missing song key': True, 'songs/chord notation': 'english', # Can be english, german or neo-latin 'songs/disable chords import': False, 'songs/auto play audio': False, diff --git a/openlp/core/display/render.py b/openlp/core/display/render.py index 7c917c936..2e1cbbb06 100644 --- a/openlp/core/display/render.py +++ b/openlp/core/display/render.py @@ -44,13 +44,15 @@ from openlp.core.lib.formattingtags import FormattingTags log = logging.getLogger(__name__) -ENGLISH_NOTES = '[CDEFGAB]' -GERMAN_NOTES = '[CDEFGAH]' -NEOLATIN_NOTES = '(Do|Re|Mi|Fa|Sol|La|Si)' -CHORD_SUFFIXES = '(b|bb)?(#)?(m|maj7|maj|min7|min|sus)?(1|2|3|4|5|6|7|8|9)?' +ENGLISH_NOTES = '(C|D|E|F|G|A|B|N\\.C\\.)?' +GERMAN_NOTES = '(C|D|E|F|G|A|B|H|N\\.C\\.)?' +NEOLATIN_NOTES = '(Do|Re|Mi|Fa|Sol|La|Si|N\\.C\\.)?' +CHORD_PREFIXES = '(=|\\(|\\|)*?' +CHORD_SUFFIXES = '(b|#|x|\\+|-|M|m|Maj|maj|min|sus|dim|add|aug|dom|0|1|2|3|4|5|6|7|8|9|\\(|\\)|no|omit)*?' SLIM_CHARS = 'fiíIÍjlĺľrtť.,;/ ()|"\'!:\\' CHORD_TEMPLATE = '{chord}' -FIRST_CHORD_TEMPLATE = '{chord}' +FIRST_CHORD_TEMPLATE = '{chord}' +NO_CHORD_TEMPLATE = '{chord}' CHORD_LINE_TEMPLATE = '{chord}{tail}{whitespace}{remainder}' WHITESPACE_TEMPLATE = '{whitespaces}' VERSE = 'The Lord said to {r}Noah{/r}: \n' \ @@ -78,8 +80,8 @@ def _construct_chord_regex(notes): :param notes: The regular expression for a set of valid notes :return: An expanded regular expression for valid chords """ - chord = notes + CHORD_SUFFIXES - return '(' + chord + '(/' + chord + ')?)' + # chord = CHORD_PREFIXES + notes + CHORD_SUFFIXES + return '(' + CHORD_PREFIXES + notes + CHORD_SUFFIXES + '(/' + notes + CHORD_SUFFIXES + ')?)' def _construct_chord_match(notes): @@ -193,11 +195,11 @@ def render_chords_in_line(match): # The actual chord, would be "G" in match "[G]sweet the " chord = match.group(1) # The tailing word of the chord, would be "sweet" in match "[G]sweet the " - tail = match.group(11) + tail = match.group(8) # The remainder of the line, until line end or next chord. Would be " the " in match "[G]sweet the " - remainder = match.group(12) + remainder = match.group(9) # Line end if found, else None - end = match.group(13) + end = match.group(10) # Based on char width calculate width of chord for chord_char in chord: if chord_char not in SLIM_CHARS: @@ -275,7 +277,10 @@ def render_chords(text): rendered_lines.append(new_line) else: chords_on_prev_line = False - rendered_lines.append(html.escape(line)) + # rendered_lines.append(html.escape(line)) + chord_template = NO_CHORD_TEMPLATE + new_line = chord_template.format(chord=line) + rendered_lines.append(new_line) return '{br}'.join(rendered_lines) diff --git a/openlp/plugins/songs/forms/editsongform.py b/openlp/plugins/songs/forms/editsongform.py index 9024d1d41..ffee14013 100644 --- a/openlp/plugins/songs/forms/editsongform.py +++ b/openlp/plugins/songs/forms/editsongform.py @@ -28,15 +28,16 @@ from shutil import copyfile from PyQt5 import QtCore, QtWidgets, QtGui -from openlp.core.state import State from openlp.core.common.applocation import AppLocation from openlp.core.common.i18n import UiStrings, get_natural_key, translate from openlp.core.common.mixins import RegistryProperties from openlp.core.common.path import create_paths from openlp.core.common.registry import Registry from openlp.core.lib import MediaType, create_separated_list +from openlp.core.lib.formattingtags import FormattingTags from openlp.core.lib.plugin import PluginStatus from openlp.core.lib.ui import critical_error_message_box, find_and_set_in_combo_box, set_case_insensitive_completer +from openlp.core.state import State from openlp.core.widgets.dialogs import FileDialog from openlp.plugins.songs.forms.editsongdialog import Ui_EditSongDialog from openlp.plugins.songs.forms.editverseform import EditVerseForm @@ -44,8 +45,7 @@ from openlp.plugins.songs.forms.mediafilesform import MediaFilesForm from openlp.plugins.songs.lib import VerseType, clean_song from openlp.plugins.songs.lib.db import Author, AuthorType, Book, MediaFile, Song, SongBookEntry, Topic from openlp.plugins.songs.lib.openlyricsxml import SongXML -from openlp.plugins.songs.lib.ui import SongStrings -from openlp.core.lib.formattingtags import FormattingTags +from openlp.plugins.songs.lib.ui import SongStrings, show_key_warning log = logging.getLogger(__name__) @@ -245,14 +245,21 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties): # Validate tags (lp#1199639) misplaced_tags = [] verse_tags = [] + chords = [] for i in range(self.verse_list_widget.rowCount()): item = self.verse_list_widget.item(i, 0) tags = self.find_tags.findall(item.text()) + stripped_text = re.sub(r'\[---\]', "\n", re.sub(r'\[--}{--\]', "\n", item.text())) + r = re.compile(r'\[(.*?)\]') + for match in r.finditer(stripped_text): + chords += match[1] field = item.data(QtCore.Qt.UserRole) verse_tags.append(field) if not self._validate_tags(tags): misplaced_tags.append('{field1} {field2}'.format(field1=VerseType.translated_name(field[0]), field2=field[1:])) + if chords and not chords[0].startswith("="): + show_key_warning(self) if misplaced_tags: critical_error_message_box( message=translate('SongsPlugin.EditSongForm', diff --git a/openlp/plugins/songs/forms/editversedialog.py b/openlp/plugins/songs/forms/editversedialog.py index 11ef3122b..86cf54c5c 100644 --- a/openlp/plugins/songs/forms/editversedialog.py +++ b/openlp/plugins/songs/forms/editversedialog.py @@ -69,7 +69,8 @@ class Ui_EditVerseDialog(object): self.verse_type_layout.addStretch() self.dialog_layout.addLayout(self.verse_type_layout) if Registry().get('settings').value('songs/enable chords'): - self.transpose_layout = QtWidgets.QHBoxLayout() + self.transpose_widget = QtWidgets.QWidget() + self.transpose_layout = QtWidgets.QHBoxLayout(self.transpose_widget) self.transpose_layout.setObjectName('transpose_layout') self.transpose_label = QtWidgets.QLabel(edit_verse_dialog) self.transpose_label.setObjectName('transpose_label') @@ -82,7 +83,7 @@ class Ui_EditVerseDialog(object): self.transpose_down_button.setIcon(UiIcons().arrow_down) self.transpose_down_button.setObjectName('transpose_down') self.transpose_layout.addWidget(self.transpose_down_button) - self.dialog_layout.addLayout(self.transpose_layout) + self.dialog_layout.addWidget(self.transpose_widget) self.button_box = create_button_box(edit_verse_dialog, 'button_box', ['cancel', 'ok']) self.dialog_layout.addWidget(self.button_box) self.retranslate_ui(edit_verse_dialog) diff --git a/openlp/plugins/songs/forms/editverseform.py b/openlp/plugins/songs/forms/editverseform.py index 9f06bdafe..366505334 100644 --- a/openlp/plugins/songs/forms/editverseform.py +++ b/openlp/plugins/songs/forms/editverseform.py @@ -29,6 +29,7 @@ from openlp.core.common.registry import Registry from openlp.core.lib.ui import critical_error_message_box from openlp.plugins.songs.forms.editversedialog import Ui_EditVerseDialog from openlp.plugins.songs.lib import VerseType, transpose_lyrics +from openlp.plugins.songs.lib.ui import show_key_warning log = logging.getLogger(__name__) @@ -122,14 +123,19 @@ class EditVerseForm(QtWidgets.QDialog, Ui_EditVerseDialog): The transpose up button clicked """ try: + lyrics_stripped = re.sub(r'\[---\]', "\n", re.sub(r'---\[.*?\]---', "\n", re.sub(r'\[--}{--\]', "\n", + self.verse_text_edit.toPlainText()))) + chords = re.findall(r'\[(.*?)\]', lyrics_stripped) + if chords and not chords[0].startswith("="): + show_key_warning(self) transposed_lyrics = transpose_lyrics(self.verse_text_edit.toPlainText(), 1) self.verse_text_edit.setPlainText(transposed_lyrics) - except ValueError as ve: + except KeyError as ke: # Transposing failed critical_error_message_box(title=translate('SongsPlugin.EditVerseForm', 'Transposing failed'), message=translate('SongsPlugin.EditVerseForm', 'Transposing failed because of invalid chord:\n{err_msg}' - .format(err_msg=ve))) + .format(err_msg=ke))) return self.verse_text_edit.setFocus() self.verse_text_edit.moveCursor(QtGui.QTextCursor.End) @@ -139,16 +145,20 @@ class EditVerseForm(QtWidgets.QDialog, Ui_EditVerseDialog): The transpose down button clicked """ try: + lyrics_stripped = re.sub(r'\[---\]', "\n", re.sub(r'---\[.*?\]---', "\n", re.sub(r'\[--}{--\]', "\n", + self.verse_text_edit.toPlainText()))) + chords = re.findall(r'\[(.*?)\]', lyrics_stripped) + if chords and not chords[0].startswith("="): + show_key_warning(self) transposed_lyrics = transpose_lyrics(self.verse_text_edit.toPlainText(), -1) self.verse_text_edit.setPlainText(transposed_lyrics) - except ValueError as ve: + except KeyError as ke: # Transposing failed critical_error_message_box(title=translate('SongsPlugin.EditVerseForm', 'Transposing failed'), message=translate('SongsPlugin.EditVerseForm', 'Transposing failed because of invalid chord:\n{err_msg}' - .format(err_msg=ve))) + .format(err_msg=ke))) return - self.verse_text_edit.setPlainText(transposed_lyrics) self.verse_text_edit.setFocus() self.verse_text_edit.moveCursor(QtGui.QTextCursor.End) @@ -197,12 +207,14 @@ class EditVerseForm(QtWidgets.QDialog, Ui_EditVerseDialog): self.verse_type_combo_box.setCurrentIndex(verse_type_index) self.verse_number_box.setValue(int(verse_number)) self.insert_button.setVisible(False) + self.transpose_widget.setVisible(False) else: if not text: text = '---[{tag}:1]---\n'.format(tag=VerseType.translated_names[VerseType.Verse]) self.verse_type_combo_box.setCurrentIndex(0) self.verse_number_box.setValue(1) self.insert_button.setVisible(True) + self.transpose_widget.setVisible(True) self.verse_text_edit.setPlainText(text) self.verse_text_edit.setFocus() self.verse_text_edit.moveCursor(QtGui.QTextCursor.End) @@ -233,13 +245,13 @@ class EditVerseForm(QtWidgets.QDialog, Ui_EditVerseDialog): """ if Registry().get('settings').value('songs/enable chords'): try: - transpose_lyrics(self.verse_text_edit.toPlainText(), 1) + transpose_lyrics(self.verse_text_edit.toPlainText(), 0) super(EditVerseForm, self).accept() - except ValueError as ve: + except KeyError as ke: # Transposing failed critical_error_message_box(title=translate('SongsPlugin.EditVerseForm', 'Invalid Chord'), message=translate('SongsPlugin.EditVerseForm', 'An invalid chord was detected:\n{err_msg}' - .format(err_msg=ve))) + .format(err_msg=ke))) else: super(EditVerseForm, self).accept() diff --git a/openlp/plugins/songs/lib/__init__.py b/openlp/plugins/songs/lib/__init__.py index 1bcd3a714..66138a84e 100644 --- a/openlp/plugins/songs/lib/__init__.py +++ b/openlp/plugins/songs/lib/__init__.py @@ -559,15 +559,17 @@ def transpose_lyrics(lyrics, transpose_value): verse_list = re.split(r'(---\[.+?:.+?\]---|\[---\])', lyrics) transposed_lyrics = '' notation = Registry().get('settings').value('songs/chord notation') + key = None for verse in verse_list: if verse.startswith('---[') or verse == '[---]': transposed_lyrics += verse else: - transposed_lyrics += transpose_verse(verse, transpose_value, notation) + transposed_lyric, key = transpose_verse(verse, transpose_value, notation, key) + transposed_lyrics += transposed_lyric return transposed_lyrics -def transpose_verse(verse_text, transpose_value, notation): +def transpose_verse(verse_text, transpose_value, notation, key): """ Transpose Verse @@ -577,10 +579,13 @@ def transpose_verse(verse_text, transpose_value, notation): :return: The transposed lyrics """ if '[' not in verse_text: - return verse_text - # Split the lyrics based on chord tags + return verse_text, key + # Split the lyrics based on chord tags, based on this, chords and bass will be treated equally and separately, + # 6/9 chords should be noted 6-9 or 69 or 6add9 lyric_list = re.split(r'(\[|\]|/)', verse_text) transposed_lyrics = '' + is_bass = False + last_chord = None in_tag = False for word in lyric_list: if not in_tag: @@ -591,19 +596,25 @@ def transpose_verse(verse_text, transpose_value, notation): if word == ']': in_tag = False transposed_lyrics += word - elif word == '/' or word == '--}{--': + elif word == '/': + is_bass = True + transposed_lyrics += word + elif word == '--}{--': transposed_lyrics += word else: # This MUST be a chord - transposed_lyrics += transpose_chord(word, transpose_value, notation) + transposed_chord, key, last_chord = transpose_chord(word, transpose_value, notation, key, last_chord, + is_bass) + is_bass = False + transposed_lyrics += transposed_chord # If still inside a chord tag something is wrong! if in_tag: - return verse_text + return verse_text, key else: - return transposed_lyrics + return transposed_lyrics, key -def transpose_chord(chord, transpose_value, notation): +def transpose_chord(chord, transpose_value, notation, key, last_chord, is_bass): """ Transpose chord according to the notation used. NOTE: This function has a javascript equivalent in chords.js - make sure to update both! @@ -624,47 +635,237 @@ def transpose_chord(chord, transpose_value, notation): 'english': ['C', 'Db', 'D', 'Eb', 'Fb', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B'], 'neo-latin': ['Do', 'Reb', 'Re', 'Mib', 'Fab', 'Fa', 'Solb', 'Sol', 'Lab', 'La', 'Sib', 'Si'] } - chord_split = chord.replace('♭', 'b').split('/') + scales = { + 'german': { + 'C': ['C', 'Db', 'D', 'Eb', 'E', 'F', 'F#', 'G', 'Ab', 'A', 'B', 'H'], + 'Am': ['C', 'Db', 'D', 'Eb', 'E', 'F', 'F#', 'G', 'Ab', 'A', 'B', 'H'], + 'C#': ['H#', 'C#', 'D', 'D#', 'E', 'E#', 'F#', 'G', 'G#', 'A', 'A#', 'H'], + 'A#m': ['H#', 'C#', 'D', 'D#', 'E', 'E#', 'F#', 'G', 'G#', 'A', 'A#', 'H'], + 'Db': ['C', 'Db', 'Ebb', 'Eb', 'Fb', 'F', 'Gb', 'Abb', 'Ab', 'Bb', 'B', 'Cb'], + 'Bm': ['C', 'Db', 'Ebb', 'Eb', 'Fb', 'F', 'Gb', 'Abb', 'Ab', 'Bb', 'B', 'Cb'], + 'D': ['C', 'C#', 'D', 'Eb', 'E', 'F', 'F#', 'G', 'Ab', 'A', 'B', 'H'], + 'Hm': ['C', 'C#', 'D', 'Eb', 'E', 'F', 'F#', 'G', 'Ab', 'A', 'B', 'H'], + 'D#': ['H#', 'C#', 'Cx', 'D#', 'E', 'E#', 'F#', 'Fx', 'G#', 'A', 'A#', 'H'], + 'H#m': ['H#', 'C#', 'Cx', 'D#', 'E', 'E#', 'F#', 'Fx', 'G#', 'A', 'A#', 'H'], + 'Eb': ['C', 'Db', 'D', 'Eb', 'Fb', 'F', 'Gb', 'G', 'Ab', 'Bb', 'B', 'Cb'], + 'Cm': ['C', 'Db', 'D', 'Eb', 'Fb', 'F', 'Gb', 'G', 'Ab', 'Bb', 'B', 'Cb'], + 'E': ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'B', 'H'], + 'C#m': ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'B', 'H'], + 'F': ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'B', 'Cb'], + 'Dm': ['C', 'C#', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'B', 'H'], + 'F#': ['C', 'C#', 'D', 'D#', 'E', 'E#', 'F#', 'G', 'G#', 'A', 'A#', 'H'], + 'D#m': ['C', 'C#', 'D', 'D#', 'E', 'E#', 'F#', 'G', 'G#', 'A', 'A#', 'H'], + 'Gb': ['Dbb', 'Db', 'Ebb', 'Eb', 'Fb', 'F', 'Gb', 'Abb', 'Ab', 'Bb', 'B', 'Cb'], + 'Ebm': ['Dbb', 'Db', 'Ebb', 'Eb', 'Fb', 'F', 'Gb', 'Abb', 'Ab', 'Bb', 'B', 'Cb'], + 'G': ['C', 'Db', 'D', 'Eb', 'E', 'F', 'F#', 'G', 'Ab', 'A', 'B', 'H'], + 'Em': ['C', 'Db', 'D', 'Eb', 'E', 'F', 'F#', 'G', 'Ab', 'A', 'B', 'H'], + 'G#': ['H#', 'C#', 'D', 'D#', 'E', 'E#', 'F#', 'Fx', 'G#', 'A', 'A#', 'H'], + 'E#m': ['H#', 'C#', 'D', 'D#', 'E', 'E#', 'F#', 'Fx', 'G#', 'A', 'A#', 'H'], + 'Ab': ['C', 'Db', 'Ebb', 'Eb', 'Fb', 'F', 'Gb', 'G', 'Ab', 'Bb', 'B', 'Cb'], + 'Fm': ['C', 'Db', 'Ebb', 'Eb', 'Fb', 'F', 'Gb', 'G', 'Ab', 'Bb', 'B', 'Cb'], + 'A': ['C', 'C#', 'D', 'Eb', 'E', 'F', 'F#', 'G', 'G#', 'A', 'B', 'H'], + 'F#m': ['C', 'C#', 'D', 'Eb', 'E', 'F', 'F#', 'G', 'G#', 'A', 'B', 'H'], + 'A#': ['H#', 'C#', 'Cx', 'D#', 'E', 'E#', 'F#', 'Fx', 'G#', 'Gx', 'A#', 'H'], + 'F##m': ['H#', 'C#', 'Cx', 'D#', 'E', 'E#', 'F#', 'Fx', 'G#', 'Gx', 'A#', 'H'], + 'Fxm': ['H#', 'C#', 'Cx', 'D#', 'E', 'E#', 'F#', 'Fx', 'G#', 'Gx', 'A#', 'H'], + 'B': ['C', 'Db', 'D', 'Eb', 'Fb', 'F', 'Gb', 'G', 'Ab', 'A', 'B', 'Cb'], + 'Gm': ['C', 'Db', 'D', 'Eb', 'Fb', 'F', 'Gb', 'G', 'Ab', 'A', 'B', 'Cb'], + 'H': ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'H'], + 'G#m': ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'H'], + 'Cb': ['Dbb', 'Db', 'Ebb', 'Eb', 'Fb', 'Gbb', 'Gb', 'Abb', 'Ab', 'Bb', 'B', 'Cb'], + 'Abm': ['Dbb', 'Db', 'Ebb', 'Eb', 'Fb', 'Gbb', 'Gb', 'Abb', 'Ab', 'Bb', 'B', 'Cb'] + }, + 'english': { + 'C': ['C', 'Db', 'D', 'Eb', 'E', 'F', 'F#', 'G', 'Ab', 'A', 'Bb', 'B'], + 'Am': ['C', 'Db', 'D', 'Eb', 'E', 'F', 'F#', 'G', 'Ab', 'A', 'Bb', 'B'], + 'C#': ['B#', 'C#', 'D', 'D#', 'E', 'E#', 'F#', 'G', 'G#', 'A', 'A#', 'B'], + 'A#m': ['B#', 'C#', 'D', 'D#', 'E', 'E#', 'F#', 'G', 'G#', 'A', 'A#', 'B'], + 'Db': ['C', 'Db', 'Ebb', 'Eb', 'Fb', 'F', 'Gb', 'Abb', 'Ab', 'Bbb', 'Bb', 'Cb'], + 'Bbm': ['C', 'Db', 'Ebb', 'Eb', 'Fb', 'F', 'Gb', 'Abb', 'Ab', 'Bbb', 'Bb', 'Cb'], + 'D': ['C', 'C#', 'D', 'Eb', 'E', 'F', 'F#', 'G', 'Ab', 'A', 'Bb', 'B'], + 'Bm': ['C', 'C#', 'D', 'Eb', 'E', 'F', 'F#', 'G', 'Ab', 'A', 'Bb', 'B'], + 'D#': ['B#', 'C#', 'Cx', 'D#', 'E', 'E#', 'F#', 'Fx', 'G#', 'A', 'A#', 'B'], + 'B#m': ['B#', 'C#', 'Cx', 'D#', 'E', 'E#', 'F#', 'Fx', 'G#', 'A', 'A#', 'B'], + 'Eb': ['C', 'Db', 'D', 'Eb', 'Fb', 'F', 'Gb', 'G', 'Ab', 'Bbb', 'Bb', 'Cb'], + 'Cm': ['C', 'Db', 'D', 'Eb', 'Fb', 'F', 'Gb', 'G', 'Ab', 'Bbb', 'Bb', 'Cb'], + 'E': ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'Bb', 'B'], + 'C#m': ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'Bb', 'B'], + 'F': ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'Cb'], + 'Dm': ['C', 'C#', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B'], + 'F#': ['C', 'C#', 'D', 'D#', 'E', 'E#', 'F#', 'G', 'G#', 'A', 'A#', 'B'], + 'D#m': ['C', 'C#', 'D', 'D#', 'E', 'E#', 'F#', 'G', 'G#', 'A', 'A#', 'B'], + 'Gb': ['Dbb', 'Db', 'Ebb', 'Eb', 'Fb', 'F', 'Gb', 'Abb', 'Ab', 'Bbb', 'Bb', 'Cb'], + 'Ebm': ['Dbb', 'Db', 'Ebb', 'Eb', 'Fb', 'F', 'Gb', 'Abb', 'Ab', 'Bbb', 'Bb', 'Cb'], + 'G': ['C', 'Db', 'D', 'Eb', 'E', 'F', 'F#', 'G', 'Ab', 'A', 'Bb', 'B'], + 'Em': ['C', 'Db', 'D', 'Eb', 'E', 'F', 'F#', 'G', 'Ab', 'A', 'Bb', 'B'], + 'G#': ['B#', 'C#', 'D', 'D#', 'E', 'E#', 'F#', 'Fx', 'G#', 'A', 'A#', 'B'], + 'E#m': ['B#', 'C#', 'D', 'D#', 'E', 'E#', 'F#', 'Fx', 'G#', 'A', 'A#', 'B'], + 'Ab': ['C', 'Db', 'Ebb', 'Eb', 'Fb', 'F', 'Gb', 'G', 'Ab', 'Bbb', 'Bb', 'Cb'], + 'Fm': ['C', 'Db', 'Ebb', 'Eb', 'Fb', 'F', 'Gb', 'G', 'Ab', 'Bbb', 'Bb', 'Cb'], + 'A': ['C', 'C#', 'D', 'Eb', 'E', 'F', 'F#', 'G', 'G#', 'A', 'Bb', 'B'], + 'F#m': ['C', 'C#', 'D', 'Eb', 'E', 'F', 'F#', 'G', 'G#', 'A', 'Bb', 'B'], + 'A#': ['B#', 'C#', 'Cx', 'D#', 'E', 'E#', 'F#', 'Fx', 'G#', 'Gx', 'A#', 'B'], + 'F##m': ['B#', 'C#', 'Cx', 'D#', 'E', 'E#', 'F#', 'Fx', 'G#', 'Gx', 'A#', 'B'], + 'Fxm': ['B#', 'C#', 'Cx', 'D#', 'E', 'E#', 'F#', 'Fx', 'G#', 'Gx', 'A#', 'B'], + 'Bb': ['C', 'Db', 'D', 'Eb', 'Fb', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'Cb'], + 'Gm': ['C', 'Db', 'D', 'Eb', 'Fb', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'Cb'], + 'B': ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'], + 'G#m': ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'], + 'Cb': ['Dbb', 'Db', 'Ebb', 'Eb', 'Fb', 'Gbb', 'Gb', 'Abb', 'Ab', 'Bbb', 'Bb', 'Cb'], + 'Abm': ['Dbb', 'Db', 'Ebb', 'Eb', 'Fb', 'Gbb', 'Gb', 'Abb', 'Ab', 'Bbb', 'Bb', 'Cb'] + }, + 'neo-latin': { + 'Do': ['Do', 'Reb', 'Re', 'Mib', 'Mi', 'Fa', 'Fa#', 'Sol', 'Lab', 'La', 'Sib', 'Si'], + 'Lam': ['Do', 'Reb', 'Re', 'Mib', 'Mi', 'Fa', 'Fa#', 'Sol', 'Lab', 'La', 'Sib', 'Si'], + 'Do#': ['Si#', 'Do#', 'Re', 'Re#', 'Mi', 'Mi#', 'Fa#', 'Sol', 'Sol#', 'La', 'La#', 'Si'], + 'La#m': ['Si#', 'Do#', 'Re', 'Re#', 'Mi', 'Mi#', 'Fa#', 'Sol', 'Sol#', 'La', 'La#', 'Si'], + 'Reb': ['Do', 'Reb', 'Mibb', 'Mib', 'Fab', 'Fa', 'Solb', 'Labb', 'Lab', 'Sibb', 'Sib', 'Dob'], + 'Sibm': ['Do', 'Reb', 'Mibb', 'Mib', 'Fab', 'Fa', 'Solb', 'Labb', 'Lab', 'Sibb', 'Sib', 'Dob'], + 'Re': ['Do', 'Do#', 'Re', 'Mib', 'Mi', 'Fa', 'Fa#', 'Sol', 'Lab', 'La', 'Sib', 'Si'], + 'Sim': ['Do', 'Do#', 'Re', 'Mib', 'Mi', 'Fa', 'Fa#', 'Sol', 'Lab', 'La', 'Sib', 'Si'], + 'Re#': ['Si#', 'Do#', 'Dox', 'Re#', 'Mi', 'Mi#', 'Fa#', 'Fax', 'Sol#', 'La', 'La#', 'Si'], + 'Si#m': ['Si#', 'Do#', 'Dox', 'Re#', 'Mi', 'Mi#', 'Fa#', 'Fax', 'Sol#', 'La', 'La#', 'Si'], + 'Mib': ['Do', 'Reb', 'Re', 'Mib', 'Fab', 'Fa', 'Solb', 'Sol', 'Lab', 'Sibb', 'Sib', 'Dob'], + 'Dom': ['Do', 'Reb', 'Re', 'Mib', 'Fab', 'Fa', 'Solb', 'Sol', 'Lab', 'Sibb', 'Sib', 'Dob'], + 'Mi': ['Do', 'Do#', 'Re', 'Re#', 'Mi', 'Fa', 'Fa#', 'Sol', 'Sol#', 'La', 'Sib', 'Si'], + 'Do#m': ['Do', 'Do#', 'Re', 'Re#', 'Mi', 'Fa', 'Fa#', 'Sol', 'Sol#', 'La', 'Sib', 'Si'], + 'Fa': ['Do', 'Reb', 'Re', 'Mib', 'Mi', 'Fa', 'Solb', 'Sol', 'Lab', 'La', 'Sib', 'Dob'], + 'Rem': ['Do', 'Do#', 'Re', 'Mib', 'Mi', 'Fa', 'Solb', 'Sol', 'Lab', 'La', 'Sib', 'Si'], + 'Fa#': ['Do', 'Do#', 'Re', 'Re#', 'Mi', 'Mi#', 'Fa#', 'Sol', 'Sol#', 'La', 'La#', 'Si'], + 'Re#m': ['Do', 'Do#', 'Re', 'Re#', 'Mi', 'Mi#', 'Fa#', 'Sol', 'Sol#', 'La', 'La#', 'Si'], + 'Solb': ['Rebb', 'Reb', 'Mibb', 'Mib', 'Fab', 'Fa', 'Solb', 'Labb', 'Lab', 'Sibb', 'Sib', 'Dob'], + 'Mibm': ['Rebb', 'Reb', 'Mibb', 'Mib', 'Fab', 'Fa', 'Solb', 'Labb', 'Lab', 'Sibb', 'Sib', 'Dob'], + 'Sol': ['Do', 'Reb', 'Re', 'Mib', 'Mi', 'Fa', 'Fa#', 'Sol', 'Lab', 'La', 'Sib', 'Si'], + 'Mim': ['Do', 'Reb', 'Re', 'Mib', 'Mi', 'Fa', 'Fa#', 'Sol', 'Lab', 'La', 'Sib', 'Si'], + 'Sol#': ['Si#', 'Do#', 'Re', 'Re#', 'Mi', 'Mi#', 'Fa#', 'Fax', 'Sol#', 'La', 'La#', 'Si'], + 'Mi#m': ['Si#', 'Do#', 'Re', 'Re#', 'Mi', 'Mi#', 'Fa#', 'Fax', 'Sol#', 'La', 'La#', 'Si'], + 'Lab': ['Do', 'Reb', 'Mibb', 'Mib', 'Fab', 'Fa', 'Solb', 'Sol', 'Lab', 'Sibb', 'Sib', 'Dob'], + 'Fam': ['Do', 'Reb', 'Mibb', 'Mib', 'Fab', 'Fa', 'Solb', 'Sol', 'Lab', 'Sibb', 'Sib', 'Dob'], + 'La': ['Do', 'Do#', 'Re', 'Mib', 'Mi', 'Fa', 'Fa#', 'Sol', 'Sol#', 'La', 'Sib', 'Si'], + 'Fa#m': ['Do', 'Do#', 'Re', 'Mib', 'Mi', 'Fa', 'Fa#', 'Sol', 'Sol#', 'La', 'Sib', 'Si'], + 'La#': ['Si#', 'Do#', 'Dox', 'Re#', 'Mi', 'Mi#', 'Fa#', 'Fax', 'Sol#', 'Solx', 'La#', 'Si'], + 'Fa##m': ['Si#', 'Do#', 'Dox', 'Re#', 'Mi', 'Mi#', 'Fa#', 'Fax', 'Sol#', 'Solx', 'La#', 'Si'], + 'Faxm': ['Si#', 'Do#', 'Dox', 'Re#', 'Mi', 'Mi#', 'Fa#', 'Fax', 'Sol#', 'Solx', 'La#', 'Si'], + 'Sib': ['Do', 'Reb', 'Re', 'Mib', 'Fab', 'Fa', 'Solb', 'Sol', 'Lab', 'La', 'Sib', 'Dob'], + 'Solm': ['Do', 'Reb', 'Re', 'Mib', 'Fab', 'Fa', 'Solb', 'Sol', 'Lab', 'La', 'Sib', 'Dob'], + 'Si': ['Do', 'Do#', 'Re', 'Re#', 'Mi', 'Fa', 'Fa#', 'Sol', 'Sol#', 'La', 'La#', 'Si'], + 'Sol#m': ['Do', 'Do#', 'Re', 'Re#', 'Mi', 'Fa', 'Fa#', 'Sol', 'Sol#', 'La', 'La#', 'Si'], + 'Dob': ['Rebb', 'Reb', 'Mibb', 'Mib', 'Fab', 'Solbb', 'Solb', 'Labb', 'Lab', 'Sibb', 'Sib', 'Dob'], + 'Labm': ['Rebb', 'Reb', 'Mibb', 'Mib', 'Fab', 'Solbb', 'Solb', 'Labb', 'Lab', 'Sibb', 'Sib', 'Dob'] + } + } + note_numbers = { + 'german': { + 'C': 0, 'H#': 0, 'B##': 0, 'Bx': 0, 'Dbb': 0, + 'C#': 1, 'Db': 1, + 'D': 2, 'C##': 2, 'Cx': 2, 'Ebb': 2, + 'D#': 3, 'Eb': 3, + 'E': 4, 'D##': 4, 'Dx': 4, 'Fb': 4, + 'F': 5, 'E#': 5, 'Gbb': 5, + 'F#': 6, 'Gb': 6, + 'G': 7, 'F##': 7, 'Fx': 7, 'Abb': 7, + 'G#': 8, 'Ab': 8, + 'A': 9, 'G##': 9, 'Gx': 9, 'Bb': 9, 'Hbb': 9, + 'B': 10, 'A#': 10, 'Hb': 10, + 'H': 11, 'B#': 11, 'A##': 11, 'Ax': 11, 'Cb': 11 + }, + 'english': { + 'C': 0, 'B#': 0, 'Dbb': 0, + 'C#': 1, 'Db': 1, 'B##': 1, 'Bx': 1, + 'D': 2, 'C##': 2, 'Cx': 2, 'Ebb': 2, + 'D#': 3, 'Eb': 3, 'Fbb': 3, + 'E': 4, 'D##': 4, 'Dx': 4, 'Fb': 4, + 'F': 5, 'E#': 5, 'Gbb': 5, + 'F#': 6, 'Gb': 6, 'E##': 6, 'Ex': 6, + 'G': 7, 'F##': 7, 'Fx': 7, 'Abb': 7, + 'G#': 8, 'Ab': 8, + 'A': 9, 'G##': 9, 'Gx': 9, 'Bbb': 9, + 'Bb': 10, 'A#': 10, 'Cbb': 10, + 'B': 11, 'A##': 11, 'Ax': 11, 'Cb': 11 + }, + 'neo-latin': { + 'Do': 0, 'Si#': 0, 'Rebb': 0, + 'Do#': 1, 'Reb': 1, 'Si##': 1, 'Six': 1, + 'Re': 2, 'Do##': 2, 'Dox': 2, 'Mibb': 2, + 'Re#': 3, 'Mib': 3, 'Fabb': 3, + 'Mi': 4, 'Re##': 4, 'Rex': 4, 'Fab': 4, + 'Fa': 5, 'Mi#': 5, 'Solbb': 5, + 'Fa#': 6, 'Solb': 6, 'Mi##': 6, 'Mix': 6, + 'Sol': 7, 'Fa##': 7, 'Fax': 7, 'Labb': 7, + 'Sol#': 8, 'Lab': 8, + 'La': 9, 'Sol##': 9, 'Solx': 9, 'Sibb': 9, + 'Sib': 10, 'La#': 10, 'Dobb': 10, + 'Si': 11, 'La##': 11, 'Lax': 11, 'Dob': 11 + } + } + chord = chord.replace('♭', 'b').replace('♯', '#') transposed_chord = '' - last_chord = '' + minor = '' + is_key_change_chord = False notes_sharp = notes_sharp_notation[notation] notes_flat = notes_flat_notation[notation] - notes_preferred = ['b', '#', '#', '#', '#', '#', '#', '#', '#', '#', '#', '#'] - for i in range(0, len(chord_split)): - if i > 0: - transposed_chord += '/' - current_chord = chord_split[i] - if current_chord and current_chord[0] == '(': - transposed_chord += '(' - if len(current_chord) > 1: - current_chord = current_chord[1:] + notes_preferred = ['b', '#', '#', 'b', '#', 'b', '#', '#', 'b', '#', 'b', '#'] + if chord and chord[0] == '(': + transposed_chord += '(' + if len(chord) > 1: + chord = chord[1:] + else: + chord = '' + if chord and chord[0] == '=': + transposed_chord += '=' + if len(chord) > 1: + chord = chord[1:] + is_key_change_chord = True + else: + chord = '' + if chord and chord[0] == '|': + transposed_chord += '|' + if len(chord) > 1: + chord = chord[1:] + else: + chord = '' + if len(chord) > 0: + if notation == 'neo-latin': + if len(chord) > 2 and chord[0:3].lower() == 'sol': + note = chord[0:3] + chord = chord[3:] if len(chord) > 3 else '' + elif len(chord) > 1: + note = chord[0:2] + chord = chord[2:] if len(chord) > 2 else '' + else: + note = chord[0] + chord = chord[1:] if len(chord) > 1 else '' + while len(chord) > 0 and '#bx'.find(chord[0]) > -1: + note += chord[0] + chord = chord[1:] if len(chord) > 0 else '' + if len(chord) > 0: + if 'm-'.find(chord[0]) > -1 or (len(chord) > 1 and chord[0:2].lower() == 'mi'): + minor = chord[0] + chord = chord[1:] if len(chord) > 1 else '' else: - current_chord = '' - if len(current_chord) > 0: - if len(current_chord) > 1: - if '#b'.find(current_chord[1]) == -1: - note = current_chord[0:1] - rest = current_chord[1:] - else: - note = current_chord[0:2] - rest = current_chord[2:] + minor = '' + note_number = note_numbers[notation][note] + note_number += transpose_value + while note_number > 11: + note_number -= 12 + while note_number < 0: + note_number += 12 + if is_bass: + if last_chord: + note = scales[notation][last_chord][note_number] + elif key: + note = scales[notation][key][note_number] else: - note = current_chord - rest = '' - note_number = notes_flat.index(note) if note not in notes_sharp else notes_sharp.index(note) - note_number += transpose_value - while note_number > 11: - note_number -= 12 - while note_number < 0: - note_number += 12 - if i == 0: - current_chord = notes_sharp[note_number] if notes_preferred[note_number] == '#' else notes_flat[ - note_number] - last_chord = current_chord + note = notes_sharp[note_number] if notes_preferred[note_number] == '#' else notes_flat[note_number] + else: + if not key or is_key_change_chord: + note = notes_sharp[note_number] if notes_preferred[note_number] == '#' else notes_flat[note_number] else: - current_chord = notes_flat[note_number] if last_chord not in notes_sharp else notes_sharp[note_number] - if not (note not in notes_flat and note not in notes_sharp): - transposed_chord += current_chord + rest - else: - transposed_chord += note + rest - return transposed_chord + note = scales[notation][key][note_number] + transposed_chord += note + minor + chord + if is_key_change_chord: + key = note + minor + else: + if not is_bass: + last_chord = note + minor + return transposed_chord, key, last_chord diff --git a/openlp/plugins/songs/lib/songstab.py b/openlp/plugins/songs/lib/songstab.py index f70190d4e..86727279c 100644 --- a/openlp/plugins/songs/lib/songstab.py +++ b/openlp/plugins/songs/lib/songstab.py @@ -67,7 +67,10 @@ class SongsTab(SettingsTab): self.chords_layout.addWidget(self.chords_info_label) self.disable_chords_import_check_box = QtWidgets.QCheckBox(self.mode_group_box) self.disable_chords_import_check_box.setObjectName('disable_chords_import_check_box') + self.song_key_warning_check_box = QtWidgets.QCheckBox(self.mode_group_box) + self.song_key_warning_check_box.setObjectName('song_key_warning_checkbox') self.chords_layout.addWidget(self.disable_chords_import_check_box) + self.chords_layout.addWidget(self.song_key_warning_check_box) # Chords notation group box self.chord_notation_label = QtWidgets.QLabel(self.chords_group_box) @@ -128,6 +131,7 @@ class SongsTab(SettingsTab): self.songbook_slide_check_box.stateChanged.connect(self.on_songbook_slide_check_box_changed) self.auto_play_check_box.stateChanged.connect(self.on_auto_play_check_box_changed) self.disable_chords_import_check_box.stateChanged.connect(self.on_disable_chords_import_check_box_changed) + self.song_key_warning_check_box.stateChanged.connect(self.on_song_key_warning_check_box_changed) self.english_notation_radio_button.clicked.connect(self.on_english_notation_button_clicked) self.german_notation_radio_button.clicked.connect(self.on_german_notation_button_clicked) self.neolatin_notation_radio_button.clicked.connect(self.on_neolatin_notation_button_clicked) @@ -156,6 +160,7 @@ class SongsTab(SettingsTab): self.german_notation_radio_button.setText(translate('SongsPlugin.SongsTab', 'German') + ' (C-D-E-F-G-A-H)') self.neolatin_notation_radio_button.setText( translate('SongsPlugin.SongsTab', 'Neo-Latin') + ' (Do-Re-Mi-Fa-Sol-La-Si)') + self.song_key_warning_check_box.setText(translate('SongsPlugin.SongsTab', 'Warn about missing song key')) self.footer_group_box.setTitle(translate('SongsPlugin.SongsTab', 'Footer')) # Keep this in sync with the list in mediaitem.py const = '"{}"' @@ -224,6 +229,9 @@ class SongsTab(SettingsTab): def on_disable_chords_import_check_box_changed(self, check_state): self.disable_chords_import = (check_state == QtCore.Qt.Checked) + def on_song_key_warning_check_box_changed(self, check_state): + self.song_key_warning = (check_state == QtCore.Qt.Checked) + def on_english_notation_button_clicked(self): self.chord_notation = 'english' @@ -245,11 +253,13 @@ class SongsTab(SettingsTab): self.enable_chords = self.settings.value('songs/enable chords') self.chord_notation = self.settings.value('songs/chord notation') self.disable_chords_import = self.settings.value('songs/disable chords import') + self.song_key_warning = self.settings.value('songs/warn about missing song key') self.tool_bar_active_check_box.setChecked(self.tool_bar) self.update_on_edit_check_box.setChecked(self.update_edit) self.add_from_service_check_box.setChecked(self.update_load) self.chords_group_box.setChecked(self.enable_chords) self.disable_chords_import_check_box.setChecked(self.disable_chords_import) + self.song_key_warning_check_box.setChecked(self.song_key_warning) if self.chord_notation == 'german': self.german_notation_radio_button.setChecked(True) elif self.chord_notation == 'neo-latin': @@ -267,6 +277,7 @@ class SongsTab(SettingsTab): self.settings.setValue('songs/auto play audio', self.auto_play) self.settings.setValue('songs/enable chords', self.chords_group_box.isChecked()) self.settings.setValue('songs/disable chords import', self.disable_chords_import) + self.settings.setValue('songs/warn about missing song key', self.song_key_warning) self.settings.setValue('songs/chord notation', self.chord_notation) self.settings.setValue('songs/songselect username', self.ccli_username.text()) # Only save password if it's blank or the user acknowleges the warning diff --git a/openlp/plugins/songs/lib/ui.py b/openlp/plugins/songs/lib/ui.py index e6f76229e..47b7a1b3e 100644 --- a/openlp/plugins/songs/lib/ui.py +++ b/openlp/plugins/songs/lib/ui.py @@ -22,7 +22,10 @@ The :mod:`openlp.plugins.songs.lib.ui` module provides standard UI components for the songs plugin. """ +from PyQt5 import QtWidgets + from openlp.core.common.i18n import translate +from openlp.core.common.registry import Registry class SongStrings(object): @@ -41,3 +44,18 @@ class SongStrings(object): Topic = translate('OpenLP.Ui', 'Topic', 'Singular') Topics = translate('OpenLP.Ui', 'Topics', 'Plural') XMLSyntaxError = translate('OpenLP.Ui', 'XML syntax error') + + +def show_key_warning(parent): + """ + Check the settings to see if we need to show the warning message, and then show a warning about the key of the song + """ + if Registry().get('settings').value('songs/enable chords') and \ + Registry().get('settings').value('songs/warn about missing song key'): + QtWidgets.QMessageBox.warning( + parent, + translate('SongsPlugin.UI', 'Song key warning'), + translate('SongsPlugin.UI', 'No musical key has been detected for this song, it should be placed before ' + 'the first chord.\nFor an optimal chord experience, please include a song key at the beginning ' + 'of the song. For example: [=G]\n\nYou can disable this warning message in songs settings.') + ) diff --git a/tests/openlp_core/display/test_render.py b/tests/openlp_core/display/test_render.py index b0e281f6a..0b231ed2e 100644 --- a/tests/openlp_core/display/test_render.py +++ b/tests/openlp_core/display/test_render.py @@ -116,7 +116,7 @@ def test_render_chords(settings): text_with_rendered_chords = render_chords(text_with_chords) # THEN: We should get html that looks like below - expected_html = 'HC' \ + expected_html = 'HC' \ 'alleluya.F' \ '   G/B' assert text_with_rendered_chords == expected_html, 'The rendered chords should look as expected' @@ -134,7 +134,7 @@ def test_render_chords_with_special_chars(settings): text_with_rendered_chords = render_tags(text_with_chords, can_render_chords=True) # THEN: We should get html that looks like below - expected_html = 'ID' \ + expected_html = 'ID' \ ''M NOT MOVED BY WHAT I SEE HALLEF' \ 'LUJACH' assert text_with_rendered_chords == expected_html, 'The rendered chords should look as expected' diff --git a/tests/openlp_core/lib/test_serviceitem.py b/tests/openlp_core/lib/test_serviceitem.py index db769a8a9..d7d19c5e5 100644 --- a/tests/openlp_core/lib/test_serviceitem.py +++ b/tests/openlp_core/lib/test_serviceitem.py @@ -725,10 +725,11 @@ def test_to_dict_text_item(state_media, settings, service_item_env): 'notes': '', 'slides': [ { - 'chords': 'Amazing Grace! how sweet the sound\n' + 'chords': '' + 'Amazing Grace! how sweet the sound\n' 'That saved a wretch like me;\n' 'I once was lost, but now am found,\n' - 'Was blind, but now I see.', + 'Was blind, but now I see.', 'html': 'Amazing Grace! how sweet the sound\n' 'That saved a wretch like me;\n' 'I once was lost, but now am found,\n' @@ -743,10 +744,11 @@ def test_to_dict_text_item(state_media, settings, service_item_env): 'footer': 'Amazing Grace
Written by: John Newton' }, { - 'chords': '’Twas grace that taught my heart to fear,\n' + 'chords': '' + '’Twas grace that taught my heart to fear,\n' 'And grace my fears relieved;\n' 'How precious did that grace appear,\n' - 'The hour I first believed!', + 'The hour I first believed!', 'html': '’Twas grace that taught my heart to fear,\n' 'And grace my fears relieved;\n' 'How precious did that grace appear,\n' @@ -761,10 +763,11 @@ def test_to_dict_text_item(state_media, settings, service_item_env): 'footer': 'Amazing Grace
Written by: John Newton' }, { - 'chords': 'Through many dangers, toils and snares\n' + 'chords': '' + 'Through many dangers, toils and snares\n' 'I have already come;\n' '’Tis grace that brought me safe thus far,\n' - 'And grace will lead me home.', + 'And grace will lead me home.', 'html': 'Through many dangers, toils and snares\n' 'I have already come;\n' '’Tis grace that brought me safe thus far,\n' @@ -779,10 +782,11 @@ def test_to_dict_text_item(state_media, settings, service_item_env): 'footer': 'Amazing Grace
Written by: John Newton' }, { - 'chords': 'The Lord has promised good to me,\n' + 'chords': '' + 'The Lord has promised good to me,\n' 'His word my hope secures;\n' 'He will my shield and portion be\n' - 'As long as life endures.', + 'As long as life endures.', 'html': 'The Lord has promised good to me,\n' 'His word my hope secures;\n' 'He will my shield and portion be\n' @@ -797,10 +801,11 @@ def test_to_dict_text_item(state_media, settings, service_item_env): 'footer': 'Amazing Grace
Written by: John Newton' }, { - 'chords': 'Yes, when this heart and flesh shall fail,\n' + 'chords': '' + 'Yes, when this heart and flesh shall fail,\n' 'And mortal life shall cease,\n' 'I shall possess within the veil\n' - 'A life of joy and peace.', + 'A life of joy and peace.', 'html': 'Yes, when this heart and flesh shall fail,\n' 'And mortal life shall cease,\n' 'I shall possess within the veil\n' @@ -815,10 +820,11 @@ def test_to_dict_text_item(state_media, settings, service_item_env): 'footer': 'Amazing Grace
Written by: John Newton' }, { - 'chords': 'When we’ve been there a thousand years,\n' + 'chords': '' + 'When we’ve been there a thousand years,\n' 'Bright shining as the sun,\n' 'We’ve no less days to sing God’s praise\n' - 'Than when we first begun.', + 'Than when we first begun.', 'html': 'When we’ve been there a thousand years,\n' 'Bright shining as the sun,\n' 'We’ve no less days to sing God’s praise\n' diff --git a/tests/openlp_plugins/songs/test_editverseform.py b/tests/openlp_plugins/songs/test_editverseform.py index 8cb8aae6b..24a623145 100644 --- a/tests/openlp_plugins/songs/test_editverseform.py +++ b/tests/openlp_plugins/songs/test_editverseform.py @@ -22,7 +22,7 @@ This module contains tests for the editverseform of the Songs plugin. """ import pytest -from unittest.mock import MagicMock +from unittest.mock import MagicMock, patch from openlp.plugins.songs.forms.editverseform import EditVerseForm @@ -94,3 +94,29 @@ def test_on_split_button_clicked(edit_verse_form): # THEN the verse number must not be changed assert '[---]\nText\n' == edit_verse_form.verse_text_edit.toPlainText(), \ 'The verse number should be [---]\nText\n' + + +@patch('openlp.plugins.songs.forms.editverseform.show_key_warning') +def test_on_transpose_up_button_clicked(mocked_show_key_warning, edit_verse_form): + """ + Test that transpose button will transpose the chords and warn about missing song key + """ + # GIVEN some input values + edit_verse_form.verse_text_edit.setPlainText('Am[G]azing gr[G/B]ace, how sw[C]eet the s[G]ound') + # WHEN the method is called + edit_verse_form.on_transpose_up_button_clicked() + mocked_show_key_warning.assert_called_once_with(edit_verse_form) + # THEN chords should be transposed up + assert 'Am[Ab]azing gr[Ab/C]ace, how sw[C#]eet the s[Ab]ound' == edit_verse_form.verse_text_edit.toPlainText() + + +def test_on_transpose_down_button_clicked(edit_verse_form): + """ + Test that transpose button will transpose the chords and warn about missing song key + """ + # GIVEN some input values + edit_verse_form.verse_text_edit.setPlainText('[=G]Am[G]azing gr[G/B]ace, how sw[C]eet the s[G]ound') + # WHEN the method is called + edit_verse_form.on_transpose_down_button_clicked() + # THEN chords should be transposed up + assert '[=F#]Am[F#]azing gr[F#/A#]ace, how sw[B]eet the s[F#]ound' == edit_verse_form.verse_text_edit.toPlainText() diff --git a/tests/openlp_plugins/songs/test_lib.py b/tests/openlp_plugins/songs/test_lib.py index ff5516a72..c4563a911 100644 --- a/tests/openlp_plugins/songs/test_lib.py +++ b/tests/openlp_plugins/songs/test_lib.py @@ -275,12 +275,17 @@ def test_transpose_chord_up(): """ # GIVEN: A Chord chord = 'C' + key = None + last_chord = None + is_bass = False # WHEN: Transposing it 1 up - new_chord = transpose_chord(chord, 1, 'english') + new_chord, key, last_chord = transpose_chord(chord, 1, 'english', key, last_chord, is_bass) # THEN: The chord should be transposed up one note assert new_chord == 'C#', 'The chord should be transposed up.' + assert key is None, 'The key should not be set' + assert last_chord == 'C#', 'If not is_bass, then last_chord should be returned' def test_transpose_chord_up_adv(): @@ -288,13 +293,23 @@ def test_transpose_chord_up_adv(): Test that the transpose_chord() method works when transposing up an advanced chord """ # GIVEN: An advanced Chord - chord = '(C/D#)' - + chord = '(D/F#)' + key = None + last_chord = None + is_bass = False + chord_split = chord.split("/") # WHEN: Transposing it 1 up - new_chord = transpose_chord(chord, 1, 'english') + new_chord, key, last_chord = transpose_chord(chord_split[0], 1, 'english', key, last_chord, is_bass) + + # AFTER "/" isbass is true, lastchord is set + is_bass = True + new_bass, key, last_chord = transpose_chord(chord_split[1], 1, 'english', key, last_chord, is_bass) # THEN: The chord should be transposed up one note - assert new_chord == '(C#/E)', 'The chord should be transposed up.' + assert new_chord == '(Eb', 'The chord should be transposed up.' + assert new_bass == 'G)', 'Bass should be transposed up.' + assert key is None, 'no key should be defined' + assert last_chord == 'Eb', 'last_chord is generated' def test_transpose_chord_down(): @@ -303,12 +318,17 @@ def test_transpose_chord_down(): """ # GIVEN: A Chord chord = 'C' + key = None + last_chord = None + is_bass = False # WHEN: Transposing it 1 down - new_chord = transpose_chord(chord, -1, 'english') + new_chord, key, last_chord = transpose_chord(chord, -1, 'english', key, last_chord, is_bass) # THEN: The chord should be transposed down one note assert new_chord == 'B', 'The chord should be transposed down.' + assert key is None, 'The key should not be set' + assert last_chord == 'B', 'If not is_bass, then last_chord should be returned' def test_transpose_chord_error(): @@ -320,10 +340,10 @@ def test_transpose_chord_error(): # WHEN: Transposing it 1 down # THEN: An exception should be raised - with pytest.raises(ValueError) as err: - transpose_chord(chord, -1, 'english') - assert err.value != ValueError('\'T\' is not in list'), \ - 'ValueError exception should have been thrown for invalid chord' + with pytest.raises(KeyError) as err: + transpose_chord(chord, -1, 'english', None, None, False) + assert err.value != KeyError('\'T\' is not in list'), \ + 'KeyError exception should have been thrown for invalid chord' @patch('openlp.plugins.songs.lib.transpose_verse') @@ -339,15 +359,15 @@ def test_transpose_lyrics(mocked_transpose_verse, mock_settings): '---[Verse:2]---\n'\ 'I once was lost but now I\'m found.' mock_settings.value.return_value = 'english' - + mocked_transpose_verse.return_value = ['', None] # WHEN: Transposing the lyrics transpose_lyrics(lyrics, 1) # THEN: transpose_verse should have been called - mocked_transpose_verse.assert_any_call('', 1, 'english') - mocked_transpose_verse.assert_any_call('\nAmazing grace how sweet the sound\n', 1, 'english') - mocked_transpose_verse.assert_any_call('\nThat saved a wretch like me.\n', 1, 'english') - mocked_transpose_verse.assert_any_call('\nI once was lost but now I\'m found.', 1, 'english') + mocked_transpose_verse.assert_any_call('', 1, 'english', None) + mocked_transpose_verse.assert_any_call('\nAmazing grace how sweet the sound\n', 1, 'english', None) + mocked_transpose_verse.assert_any_call('\nThat saved a wretch like me.\n', 1, 'english', None) + mocked_transpose_verse.assert_any_call('\nI once was lost but now I\'m found.', 1, 'english', None) def test_translated_tag(): diff --git a/tests/openlp_plugins/songs/test_songstab.py b/tests/openlp_plugins/songs/test_songstab.py index 105f7013f..966a578b5 100644 --- a/tests/openlp_plugins/songs/test_songstab.py +++ b/tests/openlp_plugins/songs/test_songstab.py @@ -150,7 +150,7 @@ def test_password_change(mocked_settings_set_val, mocked_question, form): form.save() # THEN: footer should not have been saved (one less call than the change test below) mocked_question.assert_called_once() - assert mocked_settings_set_val.call_count == 10 + assert mocked_settings_set_val.call_count == 11 @patch('openlp.plugins.songs.lib.songstab.QtWidgets.QMessageBox.question') @@ -166,7 +166,7 @@ def test_password_change_cancelled(mocked_settings_set_val, mocked_question, for form.save() # THEN: footer should not have been saved (one less call than the change test below) mocked_question.assert_called_once() - assert mocked_settings_set_val.call_count == 9 + assert mocked_settings_set_val.call_count == 10 @patch('openlp.core.common.settings.Settings.setValue') @@ -178,7 +178,7 @@ def test_footer_nochange(mocked_settings_set_val, form): # WHEN: save is invoked form.save() # THEN: footer should not have been saved (one less call than the change test below) - assert mocked_settings_set_val.call_count == 10 + assert mocked_settings_set_val.call_count == 11 @patch('openlp.core.common.settings.Settings.setValue') @@ -191,7 +191,7 @@ def test_footer_change(mocked_settings_set_val, form): # WHEN: save is invoked form.save() # THEN: footer should have been saved (one more call to setValue than the nochange test) - assert mocked_settings_set_val.call_count == 11 + assert mocked_settings_set_val.call_count == 12 assert form.footer_edit_box.toPlainText() == 'A new footer'