Merge branch 'advanced-chord-transpose-support' into 'master'

Advanced chord transpose support

See merge request openlp/openlp!341
This commit is contained in:
Tim Bentley 2022-02-17 07:55:28 +00:00
commit 330db2209c
13 changed files with 413 additions and 105 deletions

View File

@ -342,6 +342,7 @@ class Settings(QtCore.QSettings):
'songs/songselect password': '', 'songs/songselect password': '',
'songs/songselect searches': '', 'songs/songselect searches': '',
'songs/enable chords': True, 'songs/enable chords': True,
'songs/warn about missing song key': True,
'songs/chord notation': 'english', # Can be english, german or neo-latin 'songs/chord notation': 'english', # Can be english, german or neo-latin
'songs/disable chords import': False, 'songs/disable chords import': False,
'songs/auto play audio': False, 'songs/auto play audio': False,

View File

@ -44,13 +44,15 @@ from openlp.core.lib.formattingtags import FormattingTags
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
ENGLISH_NOTES = '[CDEFGAB]' ENGLISH_NOTES = '(C|D|E|F|G|A|B|N\\.C\\.)?'
GERMAN_NOTES = '[CDEFGAH]' GERMAN_NOTES = '(C|D|E|F|G|A|B|H|N\\.C\\.)?'
NEOLATIN_NOTES = '(Do|Re|Mi|Fa|Sol|La|Si)' NEOLATIN_NOTES = '(Do|Re|Mi|Fa|Sol|La|Si|N\\.C\\.)?'
CHORD_SUFFIXES = '(b|bb)?(#)?(m|maj7|maj|min7|min|sus)?(1|2|3|4|5|6|7|8|9)?' 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ť.,;/ ()|"\'!:\\' SLIM_CHARS = 'fiíIÍjlĺľrtť.,;/ ()|"\'!:\\'
CHORD_TEMPLATE = '<span class="chordline">{chord}</span>' CHORD_TEMPLATE = '<span class="chordline">{chord}</span>'
FIRST_CHORD_TEMPLATE = '<span class="chordline firstchordline">{chord}</span>' FIRST_CHORD_TEMPLATE = '<span class="chordline">{chord}</span>'
NO_CHORD_TEMPLATE = '<span class="nochordline">{chord}</span>'
CHORD_LINE_TEMPLATE = '<span class="chord"><span><strong>{chord}</strong></span></span>{tail}{whitespace}{remainder}' CHORD_LINE_TEMPLATE = '<span class="chord"><span><strong>{chord}</strong></span></span>{tail}{whitespace}{remainder}'
WHITESPACE_TEMPLATE = '<span class="ws">{whitespaces}</span>' WHITESPACE_TEMPLATE = '<span class="ws">{whitespaces}</span>'
VERSE = 'The Lord said to {r}Noah{/r}: \n' \ 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 :param notes: The regular expression for a set of valid notes
:return: An expanded regular expression for valid chords :return: An expanded regular expression for valid chords
""" """
chord = notes + CHORD_SUFFIXES # chord = CHORD_PREFIXES + notes + CHORD_SUFFIXES
return '(' + chord + '(/' + chord + ')?)' return '(' + CHORD_PREFIXES + notes + CHORD_SUFFIXES + '(/' + notes + CHORD_SUFFIXES + ')?)'
def _construct_chord_match(notes): 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 " # The actual chord, would be "G" in match "[G]sweet the "
chord = match.group(1) chord = match.group(1)
# The tailing word of the chord, would be "sweet" in match "[G]sweet the " # 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 " # 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 # Line end if found, else None
end = match.group(13) end = match.group(10)
# Based on char width calculate width of chord # Based on char width calculate width of chord
for chord_char in chord: for chord_char in chord:
if chord_char not in SLIM_CHARS: if chord_char not in SLIM_CHARS:
@ -275,7 +277,10 @@ def render_chords(text):
rendered_lines.append(new_line) rendered_lines.append(new_line)
else: else:
chords_on_prev_line = False 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) return '{br}'.join(rendered_lines)

View File

@ -28,15 +28,16 @@ from shutil import copyfile
from PyQt5 import QtCore, QtWidgets, QtGui from PyQt5 import QtCore, QtWidgets, QtGui
from openlp.core.state import State
from openlp.core.common.applocation import AppLocation from openlp.core.common.applocation import AppLocation
from openlp.core.common.i18n import UiStrings, get_natural_key, translate from openlp.core.common.i18n import UiStrings, get_natural_key, translate
from openlp.core.common.mixins import RegistryProperties from openlp.core.common.mixins import RegistryProperties
from openlp.core.common.path import create_paths from openlp.core.common.path import create_paths
from openlp.core.common.registry import Registry from openlp.core.common.registry import Registry
from openlp.core.lib import MediaType, create_separated_list 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.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.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.core.widgets.dialogs import FileDialog
from openlp.plugins.songs.forms.editsongdialog import Ui_EditSongDialog from openlp.plugins.songs.forms.editsongdialog import Ui_EditSongDialog
from openlp.plugins.songs.forms.editverseform import EditVerseForm 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 import VerseType, clean_song
from openlp.plugins.songs.lib.db import Author, AuthorType, Book, MediaFile, Song, SongBookEntry, Topic 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.openlyricsxml import SongXML
from openlp.plugins.songs.lib.ui import SongStrings from openlp.plugins.songs.lib.ui import SongStrings, show_key_warning
from openlp.core.lib.formattingtags import FormattingTags
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -245,14 +245,21 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties):
# Validate tags (lp#1199639) # Validate tags (lp#1199639)
misplaced_tags = [] misplaced_tags = []
verse_tags = [] verse_tags = []
chords = []
for i in range(self.verse_list_widget.rowCount()): for i in range(self.verse_list_widget.rowCount()):
item = self.verse_list_widget.item(i, 0) item = self.verse_list_widget.item(i, 0)
tags = self.find_tags.findall(item.text()) 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) field = item.data(QtCore.Qt.UserRole)
verse_tags.append(field) verse_tags.append(field)
if not self._validate_tags(tags): if not self._validate_tags(tags):
misplaced_tags.append('{field1} {field2}'.format(field1=VerseType.translated_name(field[0]), misplaced_tags.append('{field1} {field2}'.format(field1=VerseType.translated_name(field[0]),
field2=field[1:])) field2=field[1:]))
if chords and not chords[0].startswith("="):
show_key_warning(self)
if misplaced_tags: if misplaced_tags:
critical_error_message_box( critical_error_message_box(
message=translate('SongsPlugin.EditSongForm', message=translate('SongsPlugin.EditSongForm',

View File

@ -69,7 +69,8 @@ class Ui_EditVerseDialog(object):
self.verse_type_layout.addStretch() self.verse_type_layout.addStretch()
self.dialog_layout.addLayout(self.verse_type_layout) self.dialog_layout.addLayout(self.verse_type_layout)
if Registry().get('settings').value('songs/enable chords'): 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_layout.setObjectName('transpose_layout')
self.transpose_label = QtWidgets.QLabel(edit_verse_dialog) self.transpose_label = QtWidgets.QLabel(edit_verse_dialog)
self.transpose_label.setObjectName('transpose_label') 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.setIcon(UiIcons().arrow_down)
self.transpose_down_button.setObjectName('transpose_down') self.transpose_down_button.setObjectName('transpose_down')
self.transpose_layout.addWidget(self.transpose_down_button) 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.button_box = create_button_box(edit_verse_dialog, 'button_box', ['cancel', 'ok'])
self.dialog_layout.addWidget(self.button_box) self.dialog_layout.addWidget(self.button_box)
self.retranslate_ui(edit_verse_dialog) self.retranslate_ui(edit_verse_dialog)

View File

@ -29,6 +29,7 @@ from openlp.core.common.registry import Registry
from openlp.core.lib.ui import critical_error_message_box from openlp.core.lib.ui import critical_error_message_box
from openlp.plugins.songs.forms.editversedialog import Ui_EditVerseDialog from openlp.plugins.songs.forms.editversedialog import Ui_EditVerseDialog
from openlp.plugins.songs.lib import VerseType, transpose_lyrics from openlp.plugins.songs.lib import VerseType, transpose_lyrics
from openlp.plugins.songs.lib.ui import show_key_warning
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -122,14 +123,19 @@ class EditVerseForm(QtWidgets.QDialog, Ui_EditVerseDialog):
The transpose up button clicked The transpose up button clicked
""" """
try: 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) transposed_lyrics = transpose_lyrics(self.verse_text_edit.toPlainText(), 1)
self.verse_text_edit.setPlainText(transposed_lyrics) self.verse_text_edit.setPlainText(transposed_lyrics)
except ValueError as ve: except KeyError as ke:
# Transposing failed # Transposing failed
critical_error_message_box(title=translate('SongsPlugin.EditVerseForm', 'Transposing failed'), critical_error_message_box(title=translate('SongsPlugin.EditVerseForm', 'Transposing failed'),
message=translate('SongsPlugin.EditVerseForm', message=translate('SongsPlugin.EditVerseForm',
'Transposing failed because of invalid chord:\n{err_msg}' 'Transposing failed because of invalid chord:\n{err_msg}'
.format(err_msg=ve))) .format(err_msg=ke)))
return return
self.verse_text_edit.setFocus() self.verse_text_edit.setFocus()
self.verse_text_edit.moveCursor(QtGui.QTextCursor.End) self.verse_text_edit.moveCursor(QtGui.QTextCursor.End)
@ -139,16 +145,20 @@ class EditVerseForm(QtWidgets.QDialog, Ui_EditVerseDialog):
The transpose down button clicked The transpose down button clicked
""" """
try: 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) transposed_lyrics = transpose_lyrics(self.verse_text_edit.toPlainText(), -1)
self.verse_text_edit.setPlainText(transposed_lyrics) self.verse_text_edit.setPlainText(transposed_lyrics)
except ValueError as ve: except KeyError as ke:
# Transposing failed # Transposing failed
critical_error_message_box(title=translate('SongsPlugin.EditVerseForm', 'Transposing failed'), critical_error_message_box(title=translate('SongsPlugin.EditVerseForm', 'Transposing failed'),
message=translate('SongsPlugin.EditVerseForm', message=translate('SongsPlugin.EditVerseForm',
'Transposing failed because of invalid chord:\n{err_msg}' 'Transposing failed because of invalid chord:\n{err_msg}'
.format(err_msg=ve))) .format(err_msg=ke)))
return return
self.verse_text_edit.setPlainText(transposed_lyrics)
self.verse_text_edit.setFocus() self.verse_text_edit.setFocus()
self.verse_text_edit.moveCursor(QtGui.QTextCursor.End) 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_type_combo_box.setCurrentIndex(verse_type_index)
self.verse_number_box.setValue(int(verse_number)) self.verse_number_box.setValue(int(verse_number))
self.insert_button.setVisible(False) self.insert_button.setVisible(False)
self.transpose_widget.setVisible(False)
else: else:
if not text: if not text:
text = '---[{tag}:1]---\n'.format(tag=VerseType.translated_names[VerseType.Verse]) text = '---[{tag}:1]---\n'.format(tag=VerseType.translated_names[VerseType.Verse])
self.verse_type_combo_box.setCurrentIndex(0) self.verse_type_combo_box.setCurrentIndex(0)
self.verse_number_box.setValue(1) self.verse_number_box.setValue(1)
self.insert_button.setVisible(True) self.insert_button.setVisible(True)
self.transpose_widget.setVisible(True)
self.verse_text_edit.setPlainText(text) self.verse_text_edit.setPlainText(text)
self.verse_text_edit.setFocus() self.verse_text_edit.setFocus()
self.verse_text_edit.moveCursor(QtGui.QTextCursor.End) 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'): if Registry().get('settings').value('songs/enable chords'):
try: try:
transpose_lyrics(self.verse_text_edit.toPlainText(), 1) transpose_lyrics(self.verse_text_edit.toPlainText(), 0)
super(EditVerseForm, self).accept() super(EditVerseForm, self).accept()
except ValueError as ve: except KeyError as ke:
# Transposing failed # Transposing failed
critical_error_message_box(title=translate('SongsPlugin.EditVerseForm', 'Invalid Chord'), critical_error_message_box(title=translate('SongsPlugin.EditVerseForm', 'Invalid Chord'),
message=translate('SongsPlugin.EditVerseForm', message=translate('SongsPlugin.EditVerseForm',
'An invalid chord was detected:\n{err_msg}' 'An invalid chord was detected:\n{err_msg}'
.format(err_msg=ve))) .format(err_msg=ke)))
else: else:
super(EditVerseForm, self).accept() super(EditVerseForm, self).accept()

View File

@ -559,15 +559,17 @@ def transpose_lyrics(lyrics, transpose_value):
verse_list = re.split(r'(---\[.+?:.+?\]---|\[---\])', lyrics) verse_list = re.split(r'(---\[.+?:.+?\]---|\[---\])', lyrics)
transposed_lyrics = '' transposed_lyrics = ''
notation = Registry().get('settings').value('songs/chord notation') notation = Registry().get('settings').value('songs/chord notation')
key = None
for verse in verse_list: for verse in verse_list:
if verse.startswith('---[') or verse == '[---]': if verse.startswith('---[') or verse == '[---]':
transposed_lyrics += verse transposed_lyrics += verse
else: 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 return transposed_lyrics
def transpose_verse(verse_text, transpose_value, notation): def transpose_verse(verse_text, transpose_value, notation, key):
""" """
Transpose Verse Transpose Verse
@ -577,10 +579,13 @@ def transpose_verse(verse_text, transpose_value, notation):
:return: The transposed lyrics :return: The transposed lyrics
""" """
if '[' not in verse_text: if '[' not in verse_text:
return verse_text return verse_text, key
# Split the lyrics based on chord tags # 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) lyric_list = re.split(r'(\[|\]|/)', verse_text)
transposed_lyrics = '' transposed_lyrics = ''
is_bass = False
last_chord = None
in_tag = False in_tag = False
for word in lyric_list: for word in lyric_list:
if not in_tag: if not in_tag:
@ -591,19 +596,25 @@ def transpose_verse(verse_text, transpose_value, notation):
if word == ']': if word == ']':
in_tag = False in_tag = False
transposed_lyrics += word transposed_lyrics += word
elif word == '/' or word == '--}{--': elif word == '/':
is_bass = True
transposed_lyrics += word
elif word == '--}{--':
transposed_lyrics += word transposed_lyrics += word
else: else:
# This MUST be a chord # 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 still inside a chord tag something is wrong!
if in_tag: if in_tag:
return verse_text return verse_text, key
else: 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. Transpose chord according to the notation used.
NOTE: This function has a javascript equivalent in chords.js - make sure to update both! 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'], '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'] '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 = '' transposed_chord = ''
last_chord = '' minor = ''
is_key_change_chord = False
notes_sharp = notes_sharp_notation[notation] notes_sharp = notes_sharp_notation[notation]
notes_flat = notes_flat_notation[notation] notes_flat = notes_flat_notation[notation]
notes_preferred = ['b', '#', '#', '#', '#', '#', '#', '#', '#', '#', '#', '#'] notes_preferred = ['b', '#', '#', 'b', '#', 'b', '#', '#', 'b', '#', 'b', '#']
for i in range(0, len(chord_split)): if chord and chord[0] == '(':
if i > 0:
transposed_chord += '/'
current_chord = chord_split[i]
if current_chord and current_chord[0] == '(':
transposed_chord += '(' transposed_chord += '('
if len(current_chord) > 1: if len(chord) > 1:
current_chord = current_chord[1:] chord = chord[1:]
else: else:
current_chord = '' chord = ''
if len(current_chord) > 0: if chord and chord[0] == '=':
if len(current_chord) > 1: transposed_chord += '='
if '#b'.find(current_chord[1]) == -1: if len(chord) > 1:
note = current_chord[0:1] chord = chord[1:]
rest = current_chord[1:] is_key_change_chord = True
else: else:
note = current_chord[0:2] chord = ''
rest = current_chord[2:] if chord and chord[0] == '|':
transposed_chord += '|'
if len(chord) > 1:
chord = chord[1:]
else: else:
note = current_chord chord = ''
rest = '' if len(chord) > 0:
note_number = notes_flat.index(note) if note not in notes_sharp else notes_sharp.index(note) 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:
minor = ''
note_number = note_numbers[notation][note]
note_number += transpose_value note_number += transpose_value
while note_number > 11: while note_number > 11:
note_number -= 12 note_number -= 12
while note_number < 0: while note_number < 0:
note_number += 12 note_number += 12
if i == 0: if is_bass:
current_chord = notes_sharp[note_number] if notes_preferred[note_number] == '#' else notes_flat[ if last_chord:
note_number] note = scales[notation][last_chord][note_number]
last_chord = current_chord elif key:
note = scales[notation][key][note_number]
else: else:
current_chord = notes_flat[note_number] if last_chord not in notes_sharp else notes_sharp[note_number] note = notes_sharp[note_number] if notes_preferred[note_number] == '#' else notes_flat[note_number]
if not (note not in notes_flat and note not in notes_sharp):
transposed_chord += current_chord + rest
else: else:
transposed_chord += note + rest if not key or is_key_change_chord:
return transposed_chord note = notes_sharp[note_number] if notes_preferred[note_number] == '#' else notes_flat[note_number]
else:
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

View File

@ -67,7 +67,10 @@ class SongsTab(SettingsTab):
self.chords_layout.addWidget(self.chords_info_label) 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 = QtWidgets.QCheckBox(self.mode_group_box)
self.disable_chords_import_check_box.setObjectName('disable_chords_import_check_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.disable_chords_import_check_box)
self.chords_layout.addWidget(self.song_key_warning_check_box)
# Chords notation group box # Chords notation group box
self.chord_notation_label = QtWidgets.QLabel(self.chords_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.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.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.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.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.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) 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.german_notation_radio_button.setText(translate('SongsPlugin.SongsTab', 'German') + ' (C-D-E-F-G-A-H)')
self.neolatin_notation_radio_button.setText( self.neolatin_notation_radio_button.setText(
translate('SongsPlugin.SongsTab', 'Neo-Latin') + ' (Do-Re-Mi-Fa-Sol-La-Si)') 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')) self.footer_group_box.setTitle(translate('SongsPlugin.SongsTab', 'Footer'))
# Keep this in sync with the list in mediaitem.py # Keep this in sync with the list in mediaitem.py
const = '<code>"{}"</code>' const = '<code>"{}"</code>'
@ -224,6 +229,9 @@ class SongsTab(SettingsTab):
def on_disable_chords_import_check_box_changed(self, check_state): def on_disable_chords_import_check_box_changed(self, check_state):
self.disable_chords_import = (check_state == QtCore.Qt.Checked) 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): def on_english_notation_button_clicked(self):
self.chord_notation = 'english' self.chord_notation = 'english'
@ -245,11 +253,13 @@ class SongsTab(SettingsTab):
self.enable_chords = self.settings.value('songs/enable chords') self.enable_chords = self.settings.value('songs/enable chords')
self.chord_notation = self.settings.value('songs/chord notation') self.chord_notation = self.settings.value('songs/chord notation')
self.disable_chords_import = self.settings.value('songs/disable chords import') 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.tool_bar_active_check_box.setChecked(self.tool_bar)
self.update_on_edit_check_box.setChecked(self.update_edit) self.update_on_edit_check_box.setChecked(self.update_edit)
self.add_from_service_check_box.setChecked(self.update_load) self.add_from_service_check_box.setChecked(self.update_load)
self.chords_group_box.setChecked(self.enable_chords) self.chords_group_box.setChecked(self.enable_chords)
self.disable_chords_import_check_box.setChecked(self.disable_chords_import) 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': if self.chord_notation == 'german':
self.german_notation_radio_button.setChecked(True) self.german_notation_radio_button.setChecked(True)
elif self.chord_notation == 'neo-latin': 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/auto play audio', self.auto_play)
self.settings.setValue('songs/enable chords', self.chords_group_box.isChecked()) 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/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/chord notation', self.chord_notation)
self.settings.setValue('songs/songselect username', self.ccli_username.text()) self.settings.setValue('songs/songselect username', self.ccli_username.text())
# Only save password if it's blank or the user acknowleges the warning # Only save password if it's blank or the user acknowleges the warning

View File

@ -22,7 +22,10 @@
The :mod:`openlp.plugins.songs.lib.ui` module provides standard UI components The :mod:`openlp.plugins.songs.lib.ui` module provides standard UI components
for the songs plugin. for the songs plugin.
""" """
from PyQt5 import QtWidgets
from openlp.core.common.i18n import translate from openlp.core.common.i18n import translate
from openlp.core.common.registry import Registry
class SongStrings(object): class SongStrings(object):
@ -41,3 +44,18 @@ class SongStrings(object):
Topic = translate('OpenLP.Ui', 'Topic', 'Singular') Topic = translate('OpenLP.Ui', 'Topic', 'Singular')
Topics = translate('OpenLP.Ui', 'Topics', 'Plural') Topics = translate('OpenLP.Ui', 'Topics', 'Plural')
XMLSyntaxError = translate('OpenLP.Ui', 'XML syntax error') 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.')
)

View File

@ -116,7 +116,7 @@ def test_render_chords(settings):
text_with_rendered_chords = render_chords(text_with_chords) text_with_rendered_chords = render_chords(text_with_chords)
# THEN: We should get html that looks like below # THEN: We should get html that looks like below
expected_html = '<span class="chordline firstchordline">H<span class="chord"><span><strong>C</strong></span>' \ expected_html = '<span class="chordline">H<span class="chord"><span><strong>C</strong></span>' \
'</span>alleluya.<span class="chord"><span><strong>F</strong></span></span><span class="ws">' \ '</span>alleluya.<span class="chord"><span><strong>F</strong></span></span><span class="ws">' \
'&nbsp;&nbsp;</span> <span class="chord"><span><strong>G/B</strong></span></span></span>' '&nbsp;&nbsp;</span> <span class="chord"><span><strong>G/B</strong></span></span></span>'
assert text_with_rendered_chords == expected_html, 'The rendered chords should look as expected' 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) text_with_rendered_chords = render_tags(text_with_chords, can_render_chords=True)
# THEN: We should get html that looks like below # THEN: We should get html that looks like below
expected_html = '<span class="chordline firstchordline">I<span class="chord"><span><strong>D</strong></span>' \ expected_html = '<span class="chordline">I<span class="chord"><span><strong>D</strong></span>' \
'</span>&#x27;M NOT MOVED BY WHAT I SEE HALLE<span class="chord"><span><strong>F</strong>' \ '</span>&#x27;M NOT MOVED BY WHAT I SEE HALLE<span class="chord"><span><strong>F</strong>' \
'</span></span>LUJA<span class="chord"><span><strong>C</strong></span></span>H</span>' '</span></span>LUJA<span class="chord"><span><strong>C</strong></span></span>H</span>'
assert text_with_rendered_chords == expected_html, 'The rendered chords should look as expected' assert text_with_rendered_chords == expected_html, 'The rendered chords should look as expected'

View File

@ -725,10 +725,11 @@ def test_to_dict_text_item(state_media, settings, service_item_env):
'notes': '', 'notes': '',
'slides': [ 'slides': [
{ {
'chords': 'Amazing Grace! how sweet the sound\n' 'chords': '<span class="nochordline">'
'Amazing Grace! how sweet the sound\n'
'That saved a wretch like me;\n' 'That saved a wretch like me;\n'
'I once was lost, but now am found,\n' 'I once was lost, but now am found,\n'
'Was blind, but now I see.', 'Was blind, but now I see.</span>',
'html': 'Amazing Grace! how sweet the sound\n' 'html': 'Amazing Grace! how sweet the sound\n'
'That saved a wretch like me;\n' 'That saved a wretch like me;\n'
'I once was lost, but now am found,\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<br>Written by: John Newton' 'footer': 'Amazing Grace<br>Written by: John Newton'
}, },
{ {
'chords': 'Twas grace that taught my heart to fear,\n' 'chords': '<span class="nochordline">'
'Twas grace that taught my heart to fear,\n'
'And grace my fears relieved;\n' 'And grace my fears relieved;\n'
'How precious did that grace appear,\n' 'How precious did that grace appear,\n'
'The hour I first believed!', 'The hour I first believed!</span>',
'html': 'Twas grace that taught my heart to fear,\n' 'html': 'Twas grace that taught my heart to fear,\n'
'And grace my fears relieved;\n' 'And grace my fears relieved;\n'
'How precious did that grace appear,\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<br>Written by: John Newton' 'footer': 'Amazing Grace<br>Written by: John Newton'
}, },
{ {
'chords': 'Through many dangers, toils and snares\n' 'chords': '<span class="nochordline">'
'Through many dangers, toils and snares\n'
'I have already come;\n' 'I have already come;\n'
'Tis grace that brought me safe thus far,\n' 'Tis grace that brought me safe thus far,\n'
'And grace will lead me home.', 'And grace will lead me home.</span>',
'html': 'Through many dangers, toils and snares\n' 'html': 'Through many dangers, toils and snares\n'
'I have already come;\n' 'I have already come;\n'
'Tis grace that brought me safe thus far,\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<br>Written by: John Newton' 'footer': 'Amazing Grace<br>Written by: John Newton'
}, },
{ {
'chords': 'The Lord has promised good to me,\n' 'chords': '<span class="nochordline">'
'The Lord has promised good to me,\n'
'His word my hope secures;\n' 'His word my hope secures;\n'
'He will my shield and portion be\n' 'He will my shield and portion be\n'
'As long as life endures.', 'As long as life endures.</span>',
'html': 'The Lord has promised good to me,\n' 'html': 'The Lord has promised good to me,\n'
'His word my hope secures;\n' 'His word my hope secures;\n'
'He will my shield and portion be\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<br>Written by: John Newton' 'footer': 'Amazing Grace<br>Written by: John Newton'
}, },
{ {
'chords': 'Yes, when this heart and flesh shall fail,\n' 'chords': '<span class="nochordline">'
'Yes, when this heart and flesh shall fail,\n'
'And mortal life shall cease,\n' 'And mortal life shall cease,\n'
'I shall possess within the veil\n' 'I shall possess within the veil\n'
'A life of joy and peace.', 'A life of joy and peace.</span>',
'html': 'Yes, when this heart and flesh shall fail,\n' 'html': 'Yes, when this heart and flesh shall fail,\n'
'And mortal life shall cease,\n' 'And mortal life shall cease,\n'
'I shall possess within the veil\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<br>Written by: John Newton' 'footer': 'Amazing Grace<br>Written by: John Newton'
}, },
{ {
'chords': 'When weve been there a thousand years,\n' 'chords': '<span class="nochordline">'
'When weve been there a thousand years,\n'
'Bright shining as the sun,\n' 'Bright shining as the sun,\n'
'Weve no less days to sing Gods praise\n' 'Weve no less days to sing Gods praise\n'
'Than when we first begun.', 'Than when we first begun.</span>',
'html': 'When weve been there a thousand years,\n' 'html': 'When weve been there a thousand years,\n'
'Bright shining as the sun,\n' 'Bright shining as the sun,\n'
'Weve no less days to sing Gods praise\n' 'Weve no less days to sing Gods praise\n'

View File

@ -22,7 +22,7 @@
This module contains tests for the editverseform of the Songs plugin. This module contains tests for the editverseform of the Songs plugin.
""" """
import pytest import pytest
from unittest.mock import MagicMock from unittest.mock import MagicMock, patch
from openlp.plugins.songs.forms.editverseform import EditVerseForm 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 # THEN the verse number must not be changed
assert '[---]\nText\n' == edit_verse_form.verse_text_edit.toPlainText(), \ assert '[---]\nText\n' == edit_verse_form.verse_text_edit.toPlainText(), \
'The verse number should be [---]\nText\n' '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()

View File

@ -275,12 +275,17 @@ def test_transpose_chord_up():
""" """
# GIVEN: A Chord # GIVEN: A Chord
chord = 'C' chord = 'C'
key = None
last_chord = None
is_bass = False
# WHEN: Transposing it 1 up # 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 # THEN: The chord should be transposed up one note
assert new_chord == 'C#', 'The chord should be transposed up.' 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(): 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 Test that the transpose_chord() method works when transposing up an advanced chord
""" """
# GIVEN: 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 # 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 # 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(): def test_transpose_chord_down():
@ -303,12 +318,17 @@ def test_transpose_chord_down():
""" """
# GIVEN: A Chord # GIVEN: A Chord
chord = 'C' chord = 'C'
key = None
last_chord = None
is_bass = False
# WHEN: Transposing it 1 down # 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 # THEN: The chord should be transposed down one note
assert new_chord == 'B', 'The chord should be transposed down.' 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(): def test_transpose_chord_error():
@ -320,10 +340,10 @@ def test_transpose_chord_error():
# WHEN: Transposing it 1 down # WHEN: Transposing it 1 down
# THEN: An exception should be raised # THEN: An exception should be raised
with pytest.raises(ValueError) as err: with pytest.raises(KeyError) as err:
transpose_chord(chord, -1, 'english') transpose_chord(chord, -1, 'english', None, None, False)
assert err.value != ValueError('\'T\' is not in list'), \ assert err.value != KeyError('\'T\' is not in list'), \
'ValueError exception should have been thrown for invalid chord' 'KeyError exception should have been thrown for invalid chord'
@patch('openlp.plugins.songs.lib.transpose_verse') @patch('openlp.plugins.songs.lib.transpose_verse')
@ -339,15 +359,15 @@ def test_transpose_lyrics(mocked_transpose_verse, mock_settings):
'---[Verse:2]---\n'\ '---[Verse:2]---\n'\
'I once was lost but now I\'m found.' 'I once was lost but now I\'m found.'
mock_settings.value.return_value = 'english' mock_settings.value.return_value = 'english'
mocked_transpose_verse.return_value = ['', None]
# WHEN: Transposing the lyrics # WHEN: Transposing the lyrics
transpose_lyrics(lyrics, 1) transpose_lyrics(lyrics, 1)
# THEN: transpose_verse should have been called # THEN: transpose_verse should have been called
mocked_transpose_verse.assert_any_call('', 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') 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') 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') mocked_transpose_verse.assert_any_call('\nI once was lost but now I\'m found.', 1, 'english', None)
def test_translated_tag(): def test_translated_tag():

View File

@ -150,7 +150,7 @@ def test_password_change(mocked_settings_set_val, mocked_question, form):
form.save() form.save()
# THEN: footer should not have been saved (one less call than the change test below) # THEN: footer should not have been saved (one less call than the change test below)
mocked_question.assert_called_once() 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') @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() form.save()
# THEN: footer should not have been saved (one less call than the change test below) # THEN: footer should not have been saved (one less call than the change test below)
mocked_question.assert_called_once() 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') @patch('openlp.core.common.settings.Settings.setValue')
@ -178,7 +178,7 @@ def test_footer_nochange(mocked_settings_set_val, form):
# WHEN: save is invoked # WHEN: save is invoked
form.save() form.save()
# THEN: footer should not have been saved (one less call than the change test below) # 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') @patch('openlp.core.common.settings.Settings.setValue')
@ -191,7 +191,7 @@ def test_footer_change(mocked_settings_set_val, form):
# WHEN: save is invoked # WHEN: save is invoked
form.save() form.save()
# THEN: footer should have been saved (one more call to setValue than the nochange test) # 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' assert form.footer_edit_box.toPlainText() == 'A new footer'