forked from openlp/openlp
Added support for chords in Chord Pro format (using brackets), with support for chord transposing and 3 different notations.
Added support for import of song in ChordPro file format Added support for importing chords and verseorder from songbeamer. Add support for export and import of chords in openlyrics Added support for importing chords from opensong. Added support for importing chords from videopsalm. Added support for printing chords. bzr-revno: 2735
This commit is contained in:
commit
eec5542474
@ -246,7 +246,7 @@ class OpenLP(OpenLPMixin, QtWidgets.QApplication):
|
|||||||
Settings().setValue('core/application version', openlp_version)
|
Settings().setValue('core/application version', openlp_version)
|
||||||
# If data_version is different from the current version ask if we should backup the data folder
|
# If data_version is different from the current version ask if we should backup the data folder
|
||||||
elif data_version != openlp_version:
|
elif data_version != openlp_version:
|
||||||
if self.splash.isVisible():
|
if can_show_splash and self.splash.isVisible():
|
||||||
self.splash.hide()
|
self.splash.hide()
|
||||||
if QtWidgets.QMessageBox.question(None, translate('OpenLP', 'Backup'),
|
if QtWidgets.QMessageBox.question(None, translate('OpenLP', 'Backup'),
|
||||||
translate('OpenLP', 'OpenLP has been upgraded, do you want to create\n'
|
translate('OpenLP', 'OpenLP has been upgraded, do you want to create\n'
|
||||||
|
@ -252,4 +252,5 @@ def url_get_file(callback, url, f_path, sha256=None):
|
|||||||
os.remove(f_path)
|
os.remove(f_path)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['get_web_page']
|
__all__ = ['get_web_page']
|
||||||
|
@ -23,10 +23,11 @@
|
|||||||
The :mod:`lib` module contains most of the components and libraries that make
|
The :mod:`lib` module contains most of the components and libraries that make
|
||||||
OpenLP work.
|
OpenLP work.
|
||||||
"""
|
"""
|
||||||
|
import html
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from distutils.version import LooseVersion
|
import re
|
||||||
|
import math
|
||||||
|
|
||||||
from PyQt5 import QtCore, QtGui, Qt, QtWidgets
|
from PyQt5 import QtCore, QtGui, Qt, QtWidgets
|
||||||
|
|
||||||
@ -34,6 +35,8 @@ from openlp.core.common import translate
|
|||||||
|
|
||||||
log = logging.getLogger(__name__ + '.__init__')
|
log = logging.getLogger(__name__ + '.__init__')
|
||||||
|
|
||||||
|
SLIMCHARS = 'fiíIÍjlĺľrtť.,;/ ()|"\'!:\\'
|
||||||
|
|
||||||
|
|
||||||
class ServiceItemContext(object):
|
class ServiceItemContext(object):
|
||||||
"""
|
"""
|
||||||
@ -281,11 +284,12 @@ def check_item_selected(list_widget, message):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def clean_tags(text):
|
def clean_tags(text, remove_chords=False):
|
||||||
"""
|
"""
|
||||||
Remove Tags from text for display
|
Remove Tags from text for display
|
||||||
|
|
||||||
:param text: Text to be cleaned
|
:param text: Text to be cleaned
|
||||||
|
:param remove_chords: Clean ChordPro tags
|
||||||
"""
|
"""
|
||||||
text = text.replace('<br>', '\n')
|
text = text.replace('<br>', '\n')
|
||||||
text = text.replace('{br}', '\n')
|
text = text.replace('{br}', '\n')
|
||||||
@ -293,21 +297,296 @@ def clean_tags(text):
|
|||||||
for tag in FormattingTags.get_html_tags():
|
for tag in FormattingTags.get_html_tags():
|
||||||
text = text.replace(tag['start tag'], '')
|
text = text.replace(tag['start tag'], '')
|
||||||
text = text.replace(tag['end tag'], '')
|
text = text.replace(tag['end tag'], '')
|
||||||
|
# Remove ChordPro tags
|
||||||
|
if remove_chords:
|
||||||
|
text = re.sub(r'\[.+?\]', r'', text)
|
||||||
return text
|
return text
|
||||||
|
|
||||||
|
|
||||||
def expand_tags(text):
|
def expand_tags(text, expand_chord_tags=False, for_printing=False):
|
||||||
"""
|
"""
|
||||||
Expand tags HTML for display
|
Expand tags HTML for display
|
||||||
|
|
||||||
:param text: The text to be expanded.
|
:param text: The text to be expanded.
|
||||||
"""
|
"""
|
||||||
|
if expand_chord_tags:
|
||||||
|
if for_printing:
|
||||||
|
text = expand_chords_for_printing(text, '{br}')
|
||||||
|
else:
|
||||||
|
text = expand_chords(text)
|
||||||
for tag in FormattingTags.get_html_tags():
|
for tag in FormattingTags.get_html_tags():
|
||||||
text = text.replace(tag['start tag'], tag['start html'])
|
text = text.replace(tag['start tag'], tag['start html'])
|
||||||
text = text.replace(tag['end tag'], tag['end html'])
|
text = text.replace(tag['end tag'], tag['end html'])
|
||||||
return text
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def expand_and_align_chords_in_line(match):
|
||||||
|
"""
|
||||||
|
Expand the chords in the line and align them using whitespaces.
|
||||||
|
NOTE: There is equivalent javascript code in chords.js, in the updateSlide function. Make sure to update both!
|
||||||
|
|
||||||
|
:param match:
|
||||||
|
:return: The line with expanded html-chords
|
||||||
|
"""
|
||||||
|
whitespaces = ''
|
||||||
|
chordlen = 0
|
||||||
|
taillen = 0
|
||||||
|
# The match could be "[G]sweet the " from a line like "A[D]mazing [D7]grace! How [G]sweet the [D]sound!"
|
||||||
|
# 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(2)
|
||||||
|
# The remainder of the line, until line end or next chord. Would be " the " in match "[G]sweet the "
|
||||||
|
remainder = match.group(3)
|
||||||
|
# Line end if found, else None
|
||||||
|
end = match.group(4)
|
||||||
|
# Based on char width calculate width of chord
|
||||||
|
for chord_char in chord:
|
||||||
|
if chord_char not in SLIMCHARS:
|
||||||
|
chordlen += 2
|
||||||
|
else:
|
||||||
|
chordlen += 1
|
||||||
|
# Based on char width calculate width of tail
|
||||||
|
for tail_char in tail:
|
||||||
|
if tail_char not in SLIMCHARS:
|
||||||
|
taillen += 2
|
||||||
|
else:
|
||||||
|
taillen += 1
|
||||||
|
# Based on char width calculate width of remainder
|
||||||
|
for remainder_char in remainder:
|
||||||
|
if remainder_char not in SLIMCHARS:
|
||||||
|
taillen += 2
|
||||||
|
else:
|
||||||
|
taillen += 1
|
||||||
|
# If the chord is wider than the tail+remainder and the line goes on, some padding is needed
|
||||||
|
if chordlen >= taillen and end is None:
|
||||||
|
# Decide if the padding should be "_" for drawing out words or spaces
|
||||||
|
if tail:
|
||||||
|
if not remainder:
|
||||||
|
for c in range(math.ceil((chordlen - taillen) / 2) + 2):
|
||||||
|
whitespaces += '_'
|
||||||
|
else:
|
||||||
|
for c in range(chordlen - taillen + 1):
|
||||||
|
whitespaces += ' '
|
||||||
|
else:
|
||||||
|
if not remainder:
|
||||||
|
for c in range(math.floor((chordlen - taillen) / 2)):
|
||||||
|
whitespaces += '_'
|
||||||
|
else:
|
||||||
|
for c in range(chordlen - taillen + 1):
|
||||||
|
whitespaces += ' '
|
||||||
|
else:
|
||||||
|
if not tail and remainder and remainder[0] == ' ':
|
||||||
|
for c in range(chordlen):
|
||||||
|
whitespaces += ' '
|
||||||
|
if whitespaces:
|
||||||
|
if '_' in whitespaces:
|
||||||
|
ws_length = len(whitespaces)
|
||||||
|
if ws_length == 1:
|
||||||
|
whitespaces = '–'
|
||||||
|
else:
|
||||||
|
wsl_mod = ws_length // 2
|
||||||
|
ws_right = ws_left = ' ' * wsl_mod
|
||||||
|
whitespaces = ws_left + '–' + ws_right
|
||||||
|
whitespaces = '<span class="ws">' + whitespaces + '</span>'
|
||||||
|
return '<span class="chord"><span><strong>' + html.escape(chord) + '</strong></span></span>' + html.escape(tail) + \
|
||||||
|
whitespaces + html.escape(remainder)
|
||||||
|
|
||||||
|
|
||||||
|
def expand_chords(text):
|
||||||
|
"""
|
||||||
|
Expand ChordPro tags
|
||||||
|
|
||||||
|
:param text:
|
||||||
|
"""
|
||||||
|
text_lines = text.split('{br}')
|
||||||
|
expanded_text_lines = []
|
||||||
|
chords_on_prev_line = False
|
||||||
|
for line in text_lines:
|
||||||
|
# If a ChordPro is detected in the line, replace it with a html-span tag and wrap the line in a span tag.
|
||||||
|
if '[' in line and ']' in line:
|
||||||
|
if chords_on_prev_line:
|
||||||
|
new_line = '<span class="chordline">'
|
||||||
|
else:
|
||||||
|
new_line = '<span class="chordline firstchordline">'
|
||||||
|
chords_on_prev_line = True
|
||||||
|
# Matches a chord, a tail, a remainder and a line end. See expand_and_align_chords_in_line() for more info.
|
||||||
|
new_line += re.sub(r'\[(.*?)\]([\u0080-\uFFFF,\w]*)'
|
||||||
|
'([\u0080-\uFFFF,\w,\s,\.,\,,\!,\?,\;,\:,\|,\",\',\-,\_]*)(\Z)?',
|
||||||
|
expand_and_align_chords_in_line, line)
|
||||||
|
new_line += '</span>'
|
||||||
|
expanded_text_lines.append(new_line)
|
||||||
|
else:
|
||||||
|
chords_on_prev_line = False
|
||||||
|
expanded_text_lines.append(html.escape(line))
|
||||||
|
return '{br}'.join(expanded_text_lines)
|
||||||
|
|
||||||
|
|
||||||
|
def compare_chord_lyric(chord, lyric):
|
||||||
|
"""
|
||||||
|
Compare the width of chord and lyrics. If chord width is greater than the lyric width the diff is returned.
|
||||||
|
|
||||||
|
:param chord:
|
||||||
|
:param lyric:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
chordlen = 0
|
||||||
|
if chord == ' ':
|
||||||
|
return 0
|
||||||
|
chord = re.sub(r'\{.*?\}', r'', chord)
|
||||||
|
lyric = re.sub(r'\{.*?\}', r'', lyric)
|
||||||
|
for chord_char in chord:
|
||||||
|
if chord_char not in SLIMCHARS:
|
||||||
|
chordlen += 2
|
||||||
|
else:
|
||||||
|
chordlen += 1
|
||||||
|
lyriclen = 0
|
||||||
|
for lyric_char in lyric:
|
||||||
|
if lyric_char not in SLIMCHARS:
|
||||||
|
lyriclen += 2
|
||||||
|
else:
|
||||||
|
lyriclen += 1
|
||||||
|
if chordlen > lyriclen:
|
||||||
|
return chordlen - lyriclen
|
||||||
|
else:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def find_formatting_tags(text, active_formatting_tags):
|
||||||
|
"""
|
||||||
|
Look for formatting tags in lyrics and adds/removes them to/from the given list. Returns the update list.
|
||||||
|
|
||||||
|
:param text:
|
||||||
|
:param active_formatting_tags:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if not re.search(r'\{.*?\}', text):
|
||||||
|
return active_formatting_tags
|
||||||
|
word_it = iter(text)
|
||||||
|
# Loop through lyrics to find any formatting tags
|
||||||
|
for char in word_it:
|
||||||
|
if char == '{':
|
||||||
|
tag = ''
|
||||||
|
char = next(word_it)
|
||||||
|
start_tag = True
|
||||||
|
if char == '/':
|
||||||
|
start_tag = False
|
||||||
|
char = next(word_it)
|
||||||
|
while char != '}':
|
||||||
|
tag += char
|
||||||
|
char = next(word_it)
|
||||||
|
# See if the found tag has an end tag
|
||||||
|
for formatting_tag in FormattingTags.get_html_tags():
|
||||||
|
if formatting_tag['start tag'] == '{' + tag + '}':
|
||||||
|
if formatting_tag['end tag']:
|
||||||
|
if start_tag:
|
||||||
|
# prepend the new tag to the list of active formatting tags
|
||||||
|
active_formatting_tags[:0] = [tag]
|
||||||
|
else:
|
||||||
|
# remove the tag from the list
|
||||||
|
active_formatting_tags.remove(tag)
|
||||||
|
# Break out of the loop matching the found tag against the tag list.
|
||||||
|
break
|
||||||
|
return active_formatting_tags
|
||||||
|
|
||||||
|
|
||||||
|
def expand_chords_for_printing(text, line_split):
|
||||||
|
"""
|
||||||
|
Expand ChordPro tags
|
||||||
|
|
||||||
|
:param text:
|
||||||
|
:param line_split:
|
||||||
|
"""
|
||||||
|
if not re.search(r'\[.*?\]', text):
|
||||||
|
return text
|
||||||
|
text_lines = text.split(line_split)
|
||||||
|
expanded_text_lines = []
|
||||||
|
for line in text_lines:
|
||||||
|
# If a ChordPro is detected in the line, build html tables.
|
||||||
|
new_line = '<table class="line" width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td>'
|
||||||
|
active_formatting_tags = []
|
||||||
|
if re.search(r'\[.*?\]', line):
|
||||||
|
words = line.split(' ')
|
||||||
|
in_chord = False
|
||||||
|
for word in words:
|
||||||
|
chords = []
|
||||||
|
lyrics = []
|
||||||
|
new_line += '<table class="segment" cellpadding="0" cellspacing="0" border="0" align="left">'
|
||||||
|
# If the word contains a chord, we need to handle it.
|
||||||
|
if re.search(r'\[.*?\]', word):
|
||||||
|
chord = ''
|
||||||
|
lyric = ''
|
||||||
|
# Loop over each character of the word
|
||||||
|
for char in word:
|
||||||
|
if char == '[':
|
||||||
|
in_chord = True
|
||||||
|
if lyric != '':
|
||||||
|
if chord == '':
|
||||||
|
chord = ' '
|
||||||
|
chords.append(chord)
|
||||||
|
lyrics.append(lyric)
|
||||||
|
chord = ''
|
||||||
|
lyric = ''
|
||||||
|
elif char == ']' and in_chord:
|
||||||
|
in_chord = False
|
||||||
|
elif in_chord:
|
||||||
|
chord += char
|
||||||
|
else:
|
||||||
|
lyric += char
|
||||||
|
if lyric != '' or chord != '':
|
||||||
|
if chord == '':
|
||||||
|
chord = ' '
|
||||||
|
if lyric == '':
|
||||||
|
lyric = ' '
|
||||||
|
chords.append(chord)
|
||||||
|
lyrics.append(lyric)
|
||||||
|
new_chord_line = '<tr class="chordrow">'
|
||||||
|
new_lyric_line = '</tr><tr>'
|
||||||
|
for i in range(len(lyrics)):
|
||||||
|
spacer = compare_chord_lyric(chords[i], lyrics[i])
|
||||||
|
# Handle formatting tags
|
||||||
|
start_formatting_tags = ''
|
||||||
|
if active_formatting_tags:
|
||||||
|
start_formatting_tags = '{' + '}{'.join(active_formatting_tags) + '}'
|
||||||
|
# Update list of active formatting tags
|
||||||
|
active_formatting_tags = find_formatting_tags(lyrics[i], active_formatting_tags)
|
||||||
|
end_formatting_tags = ''
|
||||||
|
if active_formatting_tags:
|
||||||
|
end_formatting_tags = '{/' + '}{/'.join(active_formatting_tags) + '}'
|
||||||
|
new_chord_line += '<td class="chord">%s</td>' % chords[i]
|
||||||
|
# Check if this is the last column, if so skip spacing calc and instead insert a single space
|
||||||
|
if i + 1 == len(lyrics):
|
||||||
|
new_lyric_line += '<td class="lyrics">{starttags}{lyrics} {endtags}</td>'.format(
|
||||||
|
starttags=start_formatting_tags, lyrics=lyrics[i], endtags=end_formatting_tags)
|
||||||
|
else:
|
||||||
|
spacing = ''
|
||||||
|
if spacer > 0:
|
||||||
|
space = ' ' * int(math.ceil(spacer / 2))
|
||||||
|
spacing = '<span class="chordspacing">%s-%s</span>' % (space, space)
|
||||||
|
new_lyric_line += '<td class="lyrics">{starttags}{lyrics}{spacing}{endtags}</td>'.format(
|
||||||
|
starttags=start_formatting_tags, lyrics=lyrics[i], spacing=spacing,
|
||||||
|
endtags=end_formatting_tags)
|
||||||
|
new_line += new_chord_line + new_lyric_line + '</tr>'
|
||||||
|
else:
|
||||||
|
start_formatting_tags = ''
|
||||||
|
if active_formatting_tags:
|
||||||
|
start_formatting_tags = '{' + '}{'.join(active_formatting_tags) + '}'
|
||||||
|
active_formatting_tags = find_formatting_tags(word, active_formatting_tags)
|
||||||
|
end_formatting_tags = ''
|
||||||
|
if active_formatting_tags:
|
||||||
|
end_formatting_tags = '{/' + '}{/'.join(active_formatting_tags) + '}'
|
||||||
|
new_line += '<tr class="chordrow"><td class="chord"> </td></tr><tr><td class="lyrics">' \
|
||||||
|
'{starttags}{lyrics} {endtags}</td></tr>'.format(
|
||||||
|
starttags=start_formatting_tags, lyrics=word, endtags=end_formatting_tags)
|
||||||
|
new_line += '</table>'
|
||||||
|
else:
|
||||||
|
new_line += line
|
||||||
|
new_line += '</td></tr></table>'
|
||||||
|
expanded_text_lines.append(new_line)
|
||||||
|
# the {br} tag used to split lines is not inserted again since the style of the line-tables makes them redundant.
|
||||||
|
return ''.join(expanded_text_lines)
|
||||||
|
|
||||||
|
|
||||||
def create_separated_list(string_list):
|
def create_separated_list(string_list):
|
||||||
"""
|
"""
|
||||||
Returns a string that represents a join of a list of strings with a localized separator.
|
Returns a string that represents a join of a list of strings with a localized separator.
|
||||||
@ -337,7 +616,7 @@ from .plugin import PluginStatus, StringContent, Plugin
|
|||||||
from .pluginmanager import PluginManager
|
from .pluginmanager import PluginManager
|
||||||
from .settingstab import SettingsTab
|
from .settingstab import SettingsTab
|
||||||
from .serviceitem import ServiceItem, ServiceItemType, ItemCapabilities
|
from .serviceitem import ServiceItem, ServiceItemType, ItemCapabilities
|
||||||
from .htmlbuilder import build_html, build_lyrics_format_css, build_lyrics_outline_css
|
from .htmlbuilder import build_html, build_lyrics_format_css, build_lyrics_outline_css, build_chords_css
|
||||||
from .imagemanager import ImageManager
|
from .imagemanager import ImageManager
|
||||||
from .renderer import Renderer
|
from .renderer import Renderer
|
||||||
from .mediamanageritem import MediaManagerItem
|
from .mediamanageritem import MediaManagerItem
|
||||||
|
@ -172,6 +172,7 @@ def upgrade_db(url, upgrade):
|
|||||||
else:
|
else:
|
||||||
version = int(version_meta.value)
|
version = int(version_meta.value)
|
||||||
if version > upgrade.__version__:
|
if version > upgrade.__version__:
|
||||||
|
session.remove()
|
||||||
return version, upgrade.__version__
|
return version, upgrade.__version__
|
||||||
version += 1
|
version += 1
|
||||||
try:
|
try:
|
||||||
@ -194,7 +195,7 @@ def upgrade_db(url, upgrade):
|
|||||||
session.commit()
|
session.commit()
|
||||||
upgrade_version = upgrade.__version__
|
upgrade_version = upgrade.__version__
|
||||||
version = int(version_meta.value)
|
version = int(version_meta.value)
|
||||||
session.close()
|
session.remove()
|
||||||
return version, upgrade_version
|
return version, upgrade_version
|
||||||
|
|
||||||
|
|
||||||
|
@ -124,6 +124,25 @@ is the function which has to be called from outside. The generated and returned
|
|||||||
position: relative;
|
position: relative;
|
||||||
top: -0.3em;
|
top: -0.3em;
|
||||||
}
|
}
|
||||||
|
/* Chords css */
|
||||||
|
.chordline {
|
||||||
|
line-height: 1.0em;
|
||||||
|
}
|
||||||
|
.chordline span.chord span {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.chordline span.chord span strong {
|
||||||
|
position: absolute;
|
||||||
|
top: -0.8em;
|
||||||
|
left: 0;
|
||||||
|
font-size: 75%;
|
||||||
|
font-weight: normal;
|
||||||
|
line-height: normal;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.firstchordline {
|
||||||
|
line-height: 1.0em;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<script>
|
<script>
|
||||||
var timer = null;
|
var timer = null;
|
||||||
@ -444,6 +463,7 @@ HTML_SRC = Template("""
|
|||||||
position: relative;
|
position: relative;
|
||||||
top: -0.3em;
|
top: -0.3em;
|
||||||
}
|
}
|
||||||
|
/* Chords css */${chords_css}
|
||||||
</style>
|
</style>
|
||||||
<script>
|
<script>
|
||||||
var timer = null;
|
var timer = null;
|
||||||
@ -592,6 +612,30 @@ LYRICS_FORMAT_SRC = Template("""
|
|||||||
height: ${height}px;${font_style}${font_weight}
|
height: ${height}px;${font_style}${font_weight}
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
CHORDS_FORMAT = Template("""
|
||||||
|
.chordline {
|
||||||
|
line-height: ${chord_line_height};
|
||||||
|
}
|
||||||
|
.chordline span.chord span {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.chordline span.chord span strong {
|
||||||
|
position: absolute;
|
||||||
|
top: -0.8em;
|
||||||
|
left: 0;
|
||||||
|
font-size: 75%;
|
||||||
|
font-weight: normal;
|
||||||
|
line-height: normal;
|
||||||
|
display: ${chords_display};
|
||||||
|
}
|
||||||
|
.firstchordline {
|
||||||
|
line-height: ${first_chord_line_height};
|
||||||
|
}
|
||||||
|
.ws {
|
||||||
|
display: ${chords_display};
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}""")
|
||||||
|
|
||||||
|
|
||||||
def build_html(item, screen, is_live, background, image=None, plugins=None):
|
def build_html(item, screen, is_live, background, image=None, plugins=None):
|
||||||
"""
|
"""
|
||||||
@ -636,7 +680,8 @@ def build_html(item, screen, is_live, background, image=None, plugins=None):
|
|||||||
js_additions=js_additions,
|
js_additions=js_additions,
|
||||||
bg_image=bgimage_src,
|
bg_image=bgimage_src,
|
||||||
image=image_src,
|
image=image_src,
|
||||||
html_additions=html_additions)
|
html_additions=html_additions,
|
||||||
|
chords_css=build_chords_css())
|
||||||
|
|
||||||
|
|
||||||
def webkit_version():
|
def webkit_version():
|
||||||
@ -768,3 +813,16 @@ def build_footer_css(item, height):
|
|||||||
return FOOTER_SRC.substitute(left=item.footer.x(), bottom=bottom, width=item.footer.width(),
|
return FOOTER_SRC.substitute(left=item.footer.x(), bottom=bottom, width=item.footer.width(),
|
||||||
family=theme.font_footer_name, size=theme.font_footer_size,
|
family=theme.font_footer_name, size=theme.font_footer_size,
|
||||||
color=theme.font_footer_color, space=whitespace)
|
color=theme.font_footer_color, space=whitespace)
|
||||||
|
|
||||||
|
|
||||||
|
def build_chords_css():
|
||||||
|
if Settings().value('songs/enable chords') and Settings().value('songs/mainview chords'):
|
||||||
|
chord_line_height = '2.0em'
|
||||||
|
chords_display = 'inline'
|
||||||
|
first_chord_line_height = '2.1em'
|
||||||
|
else:
|
||||||
|
chord_line_height = '1.0em'
|
||||||
|
chords_display = 'none'
|
||||||
|
first_chord_line_height = '1.0em'
|
||||||
|
return CHORDS_FORMAT.substitute(chord_line_height=chord_line_height, chords_display=chords_display,
|
||||||
|
first_chord_line_height=first_chord_line_height)
|
||||||
|
@ -27,7 +27,7 @@ from PyQt5 import QtGui, QtCore, QtWebKitWidgets
|
|||||||
|
|
||||||
from openlp.core.common import Registry, RegistryProperties, OpenLPMixin, RegistryMixin, Settings
|
from openlp.core.common import Registry, RegistryProperties, OpenLPMixin, RegistryMixin, Settings
|
||||||
from openlp.core.lib import FormattingTags, ImageSource, ItemCapabilities, ScreenList, ServiceItem, expand_tags, \
|
from openlp.core.lib import FormattingTags, ImageSource, ItemCapabilities, ScreenList, ServiceItem, expand_tags, \
|
||||||
build_lyrics_format_css, build_lyrics_outline_css
|
build_lyrics_format_css, build_lyrics_outline_css, build_chords_css
|
||||||
from openlp.core.common import ThemeLevel
|
from openlp.core.common import ThemeLevel
|
||||||
from openlp.core.ui import MainDisplay
|
from openlp.core.ui import MainDisplay
|
||||||
|
|
||||||
@ -383,13 +383,14 @@ class Renderer(OpenLPMixin, RegistryMixin, RegistryProperties):
|
|||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
*{margin: 0; padding: 0; border: 0;}
|
*{margin: 0; padding: 0; border: 0;}
|
||||||
#main {position: absolute; top: 0px; ${format_css} ${outline_css}}
|
#main {position: absolute; top: 0px; ${format_css} ${outline_css}} ${chords_css}
|
||||||
</style></head>
|
</style></head>
|
||||||
<body><div id="main"></div></body></html>""")
|
<body><div id="main"></div></body></html>""")
|
||||||
self.web.setHtml(html.substitute(format_css=build_lyrics_format_css(theme_data,
|
self.web.setHtml(html.substitute(format_css=build_lyrics_format_css(theme_data,
|
||||||
self.page_width,
|
self.page_width,
|
||||||
self.page_height),
|
self.page_height),
|
||||||
outline_css=build_lyrics_outline_css(theme_data)))
|
outline_css=build_lyrics_outline_css(theme_data),
|
||||||
|
chords_css=build_chords_css()))
|
||||||
self.empty_height = self.web_frame.contentsSize().height()
|
self.empty_height = self.web_frame.contentsSize().height()
|
||||||
|
|
||||||
def _paginate_slide(self, lines, line_end):
|
def _paginate_slide(self, lines, line_end):
|
||||||
|
@ -34,7 +34,7 @@ import ntpath
|
|||||||
from PyQt5 import QtGui
|
from PyQt5 import QtGui
|
||||||
|
|
||||||
from openlp.core.common import RegistryProperties, Settings, translate, AppLocation, md5_hash
|
from openlp.core.common import RegistryProperties, Settings, translate, AppLocation, md5_hash
|
||||||
from openlp.core.lib import ImageSource, build_icon, clean_tags, expand_tags
|
from openlp.core.lib import ImageSource, build_icon, clean_tags, expand_tags, expand_chords, create_thumb
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -117,7 +117,6 @@ class ItemCapabilities(object):
|
|||||||
|
|
||||||
``HasThumbnails``
|
``HasThumbnails``
|
||||||
The item has related thumbnails available
|
The item has related thumbnails available
|
||||||
|
|
||||||
"""
|
"""
|
||||||
CanPreview = 1
|
CanPreview = 1
|
||||||
CanEdit = 2
|
CanEdit = 2
|
||||||
@ -247,6 +246,8 @@ class ServiceItem(RegistryProperties):
|
|||||||
self.renderer.set_item_theme(self.theme)
|
self.renderer.set_item_theme(self.theme)
|
||||||
self.theme_data, self.main, self.footer = self.renderer.pre_render()
|
self.theme_data, self.main, self.footer = self.renderer.pre_render()
|
||||||
if self.service_item_type == ServiceItemType.Text:
|
if self.service_item_type == ServiceItemType.Text:
|
||||||
|
expand_chord_tags = hasattr(self, 'name') and self.name == 'songs' and Settings().value(
|
||||||
|
'songs/enable chords')
|
||||||
log.debug('Formatting slides: {title}'.format(title=self.title))
|
log.debug('Formatting slides: {title}'.format(title=self.title))
|
||||||
# Save rendered pages to this dict. In the case that a slide is used twice we can use the pages saved to
|
# Save rendered pages to this dict. In the case that a slide is used twice we can use the pages saved to
|
||||||
# the dict instead of rendering them again.
|
# the dict instead of rendering them again.
|
||||||
@ -260,13 +261,16 @@ class ServiceItem(RegistryProperties):
|
|||||||
previous_pages[verse_tag] = (slide['raw_slide'], pages)
|
previous_pages[verse_tag] = (slide['raw_slide'], pages)
|
||||||
for page in pages:
|
for page in pages:
|
||||||
page = page.replace('<br>', '{br}')
|
page = page.replace('<br>', '{br}')
|
||||||
html_data = expand_tags(html.escape(page.rstrip()))
|
html_data = expand_tags(page.rstrip(), expand_chord_tags)
|
||||||
self._display_frames.append({
|
new_frame = {
|
||||||
'title': clean_tags(page),
|
'title': clean_tags(page),
|
||||||
'text': clean_tags(page.rstrip()),
|
'text': clean_tags(page.rstrip(), expand_chord_tags),
|
||||||
|
'chords_text': expand_chords(clean_tags(page.rstrip(), False)),
|
||||||
'html': html_data.replace('&nbsp;', ' '),
|
'html': html_data.replace('&nbsp;', ' '),
|
||||||
'verseTag': verse_tag
|
'printing_html': expand_tags(html.escape(page.rstrip()), expand_chord_tags, True),
|
||||||
})
|
'verseTag': verse_tag,
|
||||||
|
}
|
||||||
|
self._display_frames.append(new_frame)
|
||||||
elif self.service_item_type == ServiceItemType.Image or self.service_item_type == ServiceItemType.Command:
|
elif self.service_item_type == ServiceItemType.Image or self.service_item_type == ServiceItemType.Command:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
|
@ -143,6 +143,7 @@ def format_milliseconds(milliseconds):
|
|||||||
seconds=seconds,
|
seconds=seconds,
|
||||||
millis=millis)
|
millis=millis)
|
||||||
|
|
||||||
|
|
||||||
from .mediacontroller import MediaController
|
from .mediacontroller import MediaController
|
||||||
from .playertab import PlayerTab
|
from .playertab import PlayerTab
|
||||||
|
|
||||||
|
@ -95,7 +95,7 @@ class Ui_PrintServiceDialog(object):
|
|||||||
self.main_layout.addWidget(self.preview_widget)
|
self.main_layout.addWidget(self.preview_widget)
|
||||||
self.options_widget = QtWidgets.QWidget(print_service_dialog)
|
self.options_widget = QtWidgets.QWidget(print_service_dialog)
|
||||||
self.options_widget.hide()
|
self.options_widget.hide()
|
||||||
self.options_widget.resize(400, 300)
|
self.options_widget.resize(400, 350)
|
||||||
self.options_widget.setAutoFillBackground(True)
|
self.options_widget.setAutoFillBackground(True)
|
||||||
self.options_layout = QtWidgets.QVBoxLayout(self.options_widget)
|
self.options_layout = QtWidgets.QVBoxLayout(self.options_widget)
|
||||||
self.options_layout.setContentsMargins(8, 8, 8, 8)
|
self.options_layout.setContentsMargins(8, 8, 8, 8)
|
||||||
@ -121,6 +121,8 @@ class Ui_PrintServiceDialog(object):
|
|||||||
self.group_layout.addWidget(self.notes_check_box)
|
self.group_layout.addWidget(self.notes_check_box)
|
||||||
self.meta_data_check_box = QtWidgets.QCheckBox()
|
self.meta_data_check_box = QtWidgets.QCheckBox()
|
||||||
self.group_layout.addWidget(self.meta_data_check_box)
|
self.group_layout.addWidget(self.meta_data_check_box)
|
||||||
|
self.show_chords_check_box = QtWidgets.QCheckBox()
|
||||||
|
self.group_layout.addWidget(self.show_chords_check_box)
|
||||||
self.group_layout.addStretch(1)
|
self.group_layout.addStretch(1)
|
||||||
self.options_group_box.setLayout(self.group_layout)
|
self.options_group_box.setLayout(self.group_layout)
|
||||||
self.options_layout.addWidget(self.options_group_box)
|
self.options_layout.addWidget(self.options_group_box)
|
||||||
@ -144,6 +146,7 @@ class Ui_PrintServiceDialog(object):
|
|||||||
self.page_break_after_text.setText(translate('OpenLP.PrintServiceForm', 'Add page break before each text item'))
|
self.page_break_after_text.setText(translate('OpenLP.PrintServiceForm', 'Add page break before each text item'))
|
||||||
self.notes_check_box.setText(translate('OpenLP.PrintServiceForm', 'Include service item notes'))
|
self.notes_check_box.setText(translate('OpenLP.PrintServiceForm', 'Include service item notes'))
|
||||||
self.meta_data_check_box.setText(translate('OpenLP.PrintServiceForm', 'Include play length of media items'))
|
self.meta_data_check_box.setText(translate('OpenLP.PrintServiceForm', 'Include play length of media items'))
|
||||||
|
self.show_chords_check_box.setText(translate('OpenLP.PrintServiceForm', 'Show chords'))
|
||||||
self.title_line_edit.setText(translate('OpenLP.PrintServiceForm', 'Service Sheet'))
|
self.title_line_edit.setText(translate('OpenLP.PrintServiceForm', 'Service Sheet'))
|
||||||
# Do not change the order.
|
# Do not change the order.
|
||||||
self.zoom_combo_box.addItems([
|
self.zoom_combo_box.addItems([
|
||||||
|
@ -37,7 +37,7 @@ from openlp.core.common import AppLocation
|
|||||||
DEFAULT_CSS = """/*
|
DEFAULT_CSS = """/*
|
||||||
Edit this file to customize the service order print. Note, that not all CSS
|
Edit this file to customize the service order print. Note, that not all CSS
|
||||||
properties are supported. See:
|
properties are supported. See:
|
||||||
http://doc.trolltech.com/4.7/richtext-html-subset.html#css-properties
|
https://doc.qt.io/qt-5/richtext-html-subset.html#css-properties
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.serviceTitle {
|
.serviceTitle {
|
||||||
@ -101,6 +101,19 @@ http://doc.trolltech.com/4.7/richtext-html-subset.html#css-properties
|
|||||||
.newPage {
|
.newPage {
|
||||||
page-break-before: always;
|
page-break-before: always;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table.line {}
|
||||||
|
|
||||||
|
table.segment {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.chord {
|
||||||
|
font-size: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.lyrics {
|
||||||
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
@ -172,6 +185,12 @@ class PrintServiceForm(QtWidgets.QDialog, Ui_PrintServiceDialog, RegistryPropert
|
|||||||
self._add_element('h1', html.escape(self.title_line_edit.text()), html_data.body, classId='serviceTitle')
|
self._add_element('h1', html.escape(self.title_line_edit.text()), html_data.body, classId='serviceTitle')
|
||||||
for index, item in enumerate(self.service_manager.service_items):
|
for index, item in enumerate(self.service_manager.service_items):
|
||||||
self._add_preview_item(html_data.body, item['service_item'], index)
|
self._add_preview_item(html_data.body, item['service_item'], index)
|
||||||
|
if not self.show_chords_check_box.isChecked():
|
||||||
|
# Remove chord row and spacing span elements when not printing chords
|
||||||
|
for chord_row in html_data.find_class('chordrow'):
|
||||||
|
chord_row.drop_tree()
|
||||||
|
for spacing_span in html_data.find_class('chordspacing'):
|
||||||
|
spacing_span.drop_tree()
|
||||||
# Add the custom service notes:
|
# Add the custom service notes:
|
||||||
if self.footer_text_edit.toPlainText():
|
if self.footer_text_edit.toPlainText():
|
||||||
div = self._add_element('div', parent=html_data.body, classId='customNotes')
|
div = self._add_element('div', parent=html_data.body, classId='customNotes')
|
||||||
@ -196,13 +215,13 @@ class PrintServiceForm(QtWidgets.QDialog, Ui_PrintServiceDialog, RegistryPropert
|
|||||||
verse_def = None
|
verse_def = None
|
||||||
verse_html = None
|
verse_html = None
|
||||||
for slide in item.get_frames():
|
for slide in item.get_frames():
|
||||||
if not verse_def or verse_def != slide['verseTag'] or verse_html == slide['html']:
|
if not verse_def or verse_def != slide['verseTag'] or verse_html == slide['printing_html']:
|
||||||
text_div = self._add_element('div', parent=div, classId='itemText')
|
text_div = self._add_element('div', parent=div, classId='itemText')
|
||||||
else:
|
elif 'chordspacing' not in slide['printing_html']:
|
||||||
self._add_element('br', parent=text_div)
|
self._add_element('br', parent=text_div)
|
||||||
self._add_element('span', slide['html'], text_div)
|
self._add_element('span', slide['printing_html'], text_div)
|
||||||
verse_def = slide['verseTag']
|
verse_def = slide['verseTag']
|
||||||
verse_html = slide['html']
|
verse_html = slide['printing_html']
|
||||||
# Break the page before the div element.
|
# Break the page before the div element.
|
||||||
if index != 0 and self.page_break_after_text.isChecked():
|
if index != 0 and self.page_break_after_text.isChecked():
|
||||||
div.set('class', 'item newPage')
|
div.set('class', 'item newPage')
|
||||||
|
@ -429,4 +429,5 @@ class BibleManager(OpenLPMixin, RegistryProperties):
|
|||||||
for bible in self.db_cache:
|
for bible in self.db_cache:
|
||||||
self.db_cache[bible].finalise()
|
self.db_cache[bible].finalise()
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['BibleFormat']
|
__all__ = ['BibleFormat']
|
||||||
|
@ -197,6 +197,7 @@ class PPTViewer(QtWidgets.QWidget):
|
|||||||
def openDialog(self):
|
def openDialog(self):
|
||||||
self.pptEdit.setText(QtWidgets.QFileDialog.getOpenFileName(self, 'Open file')[0])
|
self.pptEdit.setText(QtWidgets.QFileDialog.getOpenFileName(self, 'Open file')[0])
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
pptdll = cdll.LoadLibrary(r'pptviewlib.dll')
|
pptdll = cdll.LoadLibrary(r'pptviewlib.dll')
|
||||||
pptdll.SetDebug(1)
|
pptdll.SetDebug(1)
|
||||||
|
46
openlp/plugins/remotes/html/chords.html
Normal file
46
openlp/plugins/remotes/html/chords.html
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<!--
|
||||||
|
###############################################################################
|
||||||
|
# OpenLP - Open Source Lyrics Projection #
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Copyright (c) 2008-2017 OpenLP Developers #
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# This program is free software; you can redistribute it and/or modify it #
|
||||||
|
# under the terms of the GNU General Public License as published by the Free #
|
||||||
|
# Software Foundation; version 2 of the License. #
|
||||||
|
# #
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT #
|
||||||
|
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
|
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
|
||||||
|
# more details. #
|
||||||
|
# #
|
||||||
|
# You should have received a copy of the GNU General Public License along #
|
||||||
|
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||||
|
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||||
|
###############################################################################
|
||||||
|
-->
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>${chords_title}</title>
|
||||||
|
<link rel="stylesheet" href="/css/stage.css" />
|
||||||
|
<link rel="stylesheet" href="/css/chords.css" />
|
||||||
|
<link rel="shortcut icon" type="image/x-icon" href="/images/favicon.ico">
|
||||||
|
<script type="text/javascript" src="/assets/jquery.min.js"></script>
|
||||||
|
<script type="text/javascript" src="/js/chords.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<input type="hidden" id="next-text" value="${next}" />
|
||||||
|
<div id="right">
|
||||||
|
<div id="clock"></div>
|
||||||
|
<div id="chords" class="button">Toggle Chords</div>
|
||||||
|
<div id="notes"></div>
|
||||||
|
</div>
|
||||||
|
<div id="header">
|
||||||
|
<div id="verseorder"></div>
|
||||||
|
<div id="transpose">Transpose:</div> <div class="button" id="transposedown">-</div> <div id="transposevalue">0</div> <div class="button" id="transposeup">+</div> <div id="capodisplay">(Capo)</div>
|
||||||
|
</div>
|
||||||
|
<div id="currentslide"></div>
|
||||||
|
<div id="nextslide"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
96
openlp/plugins/remotes/html/css/chords.css
Normal file
96
openlp/plugins/remotes/html/css/chords.css
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
/******************************************************************************
|
||||||
|
* OpenLP - Open Source Lyrics Projection *
|
||||||
|
* --------------------------------------------------------------------------- *
|
||||||
|
* Copyright (c) 2008-2017 OpenLP Developers *
|
||||||
|
* --------------------------------------------------------------------------- *
|
||||||
|
* This program is free software; you can redistribute it and/or modify it *
|
||||||
|
* under the terms of the GNU General Public License as published by the Free *
|
||||||
|
* Software Foundation; version 2 of the License. *
|
||||||
|
* *
|
||||||
|
* This program is distributed in the hope that it will be useful, but WITHOUT *
|
||||||
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or *
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for *
|
||||||
|
* more details. *
|
||||||
|
* *
|
||||||
|
* You should have received a copy of the GNU General Public License along *
|
||||||
|
* with this program; if not, write to the Free Software Foundation, Inc., 59 *
|
||||||
|
* Temple Place, Suite 330, Boston, MA 02111-1307 USA *
|
||||||
|
******************************************************************************/
|
||||||
|
|
||||||
|
#header {
|
||||||
|
padding-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#transpose,
|
||||||
|
#transposevalue,
|
||||||
|
#capodisplay {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 30pt;
|
||||||
|
color: gray;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
display: inline-block;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 1px solid gray;
|
||||||
|
border-radius: .3em;
|
||||||
|
padding: 0 .2em;
|
||||||
|
min-width: 1.2em;
|
||||||
|
line-height: 1.2em;
|
||||||
|
font-size: 25pt;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
text-shadow: 0px 1px 0px white;
|
||||||
|
color: black;
|
||||||
|
background: linear-gradient(to bottom, white 5%, gray 100%);
|
||||||
|
background-color: gray;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.button:hover {
|
||||||
|
background: linear-gradient(to bottom, white 10%, gray 150%);
|
||||||
|
color: darkslategray ;
|
||||||
|
background-color: gray;
|
||||||
|
}
|
||||||
|
.button:active {
|
||||||
|
position:relative;
|
||||||
|
top:1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Extending existing definition in stage.css */
|
||||||
|
#verseorder {
|
||||||
|
line-height: 1.5;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chordline {
|
||||||
|
line-height: 2.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chordline1 {
|
||||||
|
line-height: 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
.chordline span.chord span {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chordline span.chord span strong {
|
||||||
|
position: absolute;
|
||||||
|
top: -0.8em;
|
||||||
|
left: 0;
|
||||||
|
font-size: 30pt;
|
||||||
|
font-weight: normal;
|
||||||
|
line-height: normal;
|
||||||
|
color: yellow;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nextslide .chordline span.chord span strong {
|
||||||
|
color: gray;
|
||||||
|
}
|
@ -21,6 +21,10 @@ body {
|
|||||||
background-color: black;
|
background-color: black;
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
-webkit-user-select: none; /* Chrome/Safari */
|
||||||
|
-moz-user-select: none; /* Firefox */
|
||||||
|
-ms-user-select: none; /* IE 10+ */
|
||||||
|
user-select: none; /* Future */
|
||||||
}
|
}
|
||||||
|
|
||||||
#currentslide {
|
#currentslide {
|
||||||
|
331
openlp/plugins/remotes/html/js/chords.js
Normal file
331
openlp/plugins/remotes/html/js/chords.js
Normal file
@ -0,0 +1,331 @@
|
|||||||
|
/******************************************************************************
|
||||||
|
* OpenLP - Open Source Lyrics Projection *
|
||||||
|
* --------------------------------------------------------------------------- *
|
||||||
|
* Copyright (c) 2008-2017 OpenLP Developers *
|
||||||
|
* --------------------------------------------------------------------------- *
|
||||||
|
* This program is free software; you can redistribute it and/or modify it *
|
||||||
|
* under the terms of the GNU General Public License as published by the Free *
|
||||||
|
* Software Foundation; version 2 of the License. *
|
||||||
|
* *
|
||||||
|
* This program is distributed in the hope that it will be useful, but WITHOUT *
|
||||||
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or *
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for *
|
||||||
|
* more details. *
|
||||||
|
* *
|
||||||
|
* You should have received a copy of the GNU General Public License along *
|
||||||
|
* with this program; if not, write to the Free Software Foundation, Inc., 59 *
|
||||||
|
* Temple Place, Suite 330, Boston, MA 02111-1307 USA *
|
||||||
|
******************************************************************************/
|
||||||
|
var lastChord;
|
||||||
|
|
||||||
|
var notesSharpNotation = {}
|
||||||
|
var notesFlatNotation = {}
|
||||||
|
|
||||||
|
// See https://en.wikipedia.org/wiki/Musical_note#12-tone_chromatic_scale
|
||||||
|
notesSharpNotation['german'] = ['C','C#','D','D#','E','F','F#','G','G#','A','A#','H'];
|
||||||
|
notesFlatNotation['german'] = ['C','Db','D','Eb','Fb','F','Gb','G','Ab','A','B','H'];
|
||||||
|
notesSharpNotation['english'] = ['C','C#','D','D#','E','F','F#','G','G#','A','A#','B'];
|
||||||
|
notesFlatNotation['english'] = ['C','Db','D','Eb','Fb','F','Gb','G','Ab','A','Bb','B'];
|
||||||
|
notesSharpNotation['neo-latin'] = ['Do','Do#','Re','Re#','Mi','Fa','Fa#','Sol','Sol#','La','La#','Si'];
|
||||||
|
notesFlatNotation['neo-latin'] = ['Do','Reb','Re','Mib','Fab','Fa','Solb','Sol','Lab','La','Sib','Si'];
|
||||||
|
|
||||||
|
function getTransposeValue(songId) {
|
||||||
|
if (localStorage.getItem(songId + '_transposeValue')) {return localStorage.getItem(songId + '_transposeValue');}
|
||||||
|
else {return 0;}
|
||||||
|
}
|
||||||
|
|
||||||
|
function storeTransposeValue(songId,transposeValueToSet) {
|
||||||
|
localStorage.setItem(songId + '_transposeValue', transposeValueToSet);
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: This function has a python equivalent in openlp/plugins/songs/lib/__init__.py - make sure to update both!
|
||||||
|
function transposeChord(chord, transposeValue, notation) {
|
||||||
|
var chordSplit = chord.replace('♭', 'b').split(/[\/]/);
|
||||||
|
var transposedChord = '', note, notenumber, rest, currentChord;
|
||||||
|
var notesSharp = notesSharpNotation[notation];
|
||||||
|
var notesFlat = notesFlatNotation[notation];
|
||||||
|
var notesPreferred = ['b','#','#','#','#','#','#','#','#','#','#','#'];
|
||||||
|
for (i = 0; i <= chordSplit.length - 1; i++) {
|
||||||
|
if (i > 0) {
|
||||||
|
transposedChord += '/';
|
||||||
|
}
|
||||||
|
currentchord = chordSplit[i];
|
||||||
|
if (currentchord.length > 0 && currentchord.charAt(0) === '(') {
|
||||||
|
transposedChord += '(';
|
||||||
|
if (currentchord.length > 1) {
|
||||||
|
currentchord = currentchord.substr(1);
|
||||||
|
} else {
|
||||||
|
currentchord = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (currentchord.length > 0) {
|
||||||
|
if (currentchord.length > 1) {
|
||||||
|
if ('#b'.indexOf(currentchord.charAt(1)) === -1) {
|
||||||
|
note = currentchord.substr(0, 1);
|
||||||
|
rest = currentchord.substr(1);
|
||||||
|
} else {
|
||||||
|
note = currentchord.substr(0, 2);
|
||||||
|
rest = currentchord.substr(2);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
note = currentchord;
|
||||||
|
rest = "";
|
||||||
|
}
|
||||||
|
notenumber = (notesSharp.indexOf(note) === -1 ? notesFlat.indexOf(note) : notesSharp.indexOf(note));
|
||||||
|
notenumber += parseInt(transposeValue);
|
||||||
|
while (notenumber > 11) {
|
||||||
|
notenumber -= 12;
|
||||||
|
}
|
||||||
|
while (notenumber < 0) {
|
||||||
|
notenumber += 12;
|
||||||
|
}
|
||||||
|
if (i === 0) {
|
||||||
|
currentChord = notesPreferred[notenumber] === '#' ? notesSharp[notenumber] : notesFlat[notenumber];
|
||||||
|
lastChord = currentChord;
|
||||||
|
} else {
|
||||||
|
currentChord = notesSharp.indexOf(lastChord) === -1 ? notesFlat[notenumber] : notesSharp[notenumber];
|
||||||
|
}
|
||||||
|
if (!(notesFlat.indexOf(note) === -1 && notesSharp.indexOf(note) === -1)) {
|
||||||
|
transposedChord += currentChord + rest;
|
||||||
|
} else {
|
||||||
|
transposedChord += note + rest;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return transposedChord;
|
||||||
|
}
|
||||||
|
|
||||||
|
var OpenLPChordOverflowFillCount = 0;
|
||||||
|
window.OpenLP = {
|
||||||
|
showchords:true,
|
||||||
|
loadService: function (event) {
|
||||||
|
$.getJSON(
|
||||||
|
"/api/service/list",
|
||||||
|
function (data, status) {
|
||||||
|
OpenLP.nextSong = "";
|
||||||
|
$("#notes").html("");
|
||||||
|
for (idx in data.results.items) {
|
||||||
|
idx = parseInt(idx, 10);
|
||||||
|
if (data.results.items[idx]["selected"]) {
|
||||||
|
$("#notes").html(data.results.items[idx]["notes"].replace(/\n/g, "<br />"));
|
||||||
|
if (data.results.items.length > idx + 1) {
|
||||||
|
OpenLP.nextSong = data.results.items[idx + 1]["title"];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OpenLP.updateSlide();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
loadSlides: function (event) {
|
||||||
|
$.getJSON(
|
||||||
|
"/api/controller/live/text",
|
||||||
|
function (data, status) {
|
||||||
|
OpenLP.currentSlides = data.results.slides;
|
||||||
|
$('#transposevalue').text(getTransposeValue(OpenLP.currentSlides[0].text.split("\n")[0]));
|
||||||
|
OpenLP.currentSlide = 0;
|
||||||
|
OpenLP.currentTags = Array();
|
||||||
|
var div = $("#verseorder");
|
||||||
|
div.html("");
|
||||||
|
var tag = "";
|
||||||
|
var tags = 0;
|
||||||
|
var lastChange = 0;
|
||||||
|
$.each(data.results.slides, function(idx, slide) {
|
||||||
|
var prevtag = tag;
|
||||||
|
tag = slide["tag"];
|
||||||
|
if (tag != prevtag) {
|
||||||
|
// If the tag has changed, add new one to the list
|
||||||
|
lastChange = idx;
|
||||||
|
tags = tags + 1;
|
||||||
|
div.append(" <span>");
|
||||||
|
$("#verseorder span").last().attr("id", "tag" + tags).text(tag);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if ((slide["chords_text"] == data.results.slides[lastChange]["chords_text"]) &&
|
||||||
|
(data.results.slides.length > idx + (idx - lastChange))) {
|
||||||
|
// If the tag hasn't changed, check to see if the same verse
|
||||||
|
// has been repeated consecutively. Note the verse may have been
|
||||||
|
// split over several slides, so search through. If so, repeat the tag.
|
||||||
|
var match = true;
|
||||||
|
for (var idx2 = 0; idx2 < idx - lastChange; idx2++) {
|
||||||
|
if(data.results.slides[lastChange + idx2]["chords_text"] != data.results.slides[idx + idx2]["chords_text"]) {
|
||||||
|
match = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (match) {
|
||||||
|
lastChange = idx;
|
||||||
|
tags = tags + 1;
|
||||||
|
div.append(" <span>");
|
||||||
|
$("#verseorder span").last().attr("id", "tag" + tags).text(tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OpenLP.currentTags[idx] = tags;
|
||||||
|
if (slide["selected"])
|
||||||
|
OpenLP.currentSlide = idx;
|
||||||
|
})
|
||||||
|
OpenLP.loadService();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
updateSlide: function() {
|
||||||
|
// Show the current slide on top. Any trailing slides for the same verse
|
||||||
|
// are shown too underneath in grey.
|
||||||
|
// Then leave a blank line between following verses
|
||||||
|
var transposeValue = getTransposeValue(OpenLP.currentSlides[0].text.split("\n")[0]);
|
||||||
|
var chordclass=/class="[a-z\s]*chord[a-z\s]*"\s*style="display:\s?none"/g;
|
||||||
|
var chordclassshow='class="chord"';
|
||||||
|
var regchord=/<span class="chord"><span><strong>([\(\w#b♭\+\*\d/\)-]+)<\/strong><\/span><\/span>([\u0080-\uFFFF,\w]*)(<span class="ws">.+?<\/span>)?([\u0080-\uFFFF,\w,\s,\.,\,,\!,\?,\;,\:,\|,\",\',\-,\_]*)(<br>)?/g;
|
||||||
|
// NOTE: There is equivalent python code in openlp/core/lib/__init__.py, in the expand_and_align_chords_in_line function. Make sure to update both!
|
||||||
|
var replaceChords=function(mstr,$chord,$tail,$skips,$remainder,$end) {
|
||||||
|
var w='';
|
||||||
|
var $chordlen = 0;
|
||||||
|
var $taillen = 0;
|
||||||
|
var slimchars='fiíIÍjlĺľrtť.,;/ ()|"\'!:\\';
|
||||||
|
// Transpose chord as dictated by the transpose value in local storage
|
||||||
|
if (transposeValue != 0) {
|
||||||
|
$chord = transposeChord($chord, transposeValue, OpenLP.chordNotation);
|
||||||
|
}
|
||||||
|
for (var i = 0; i < $chord.length; i++) if (slimchars.indexOf($chord.charAt(i)) === -1) {$chordlen += 2;} else {$chordlen += 1;}
|
||||||
|
for (var i = 0; i < $tail.length; i++) if (slimchars.indexOf($tail.charAt(i)) === -1) {$taillen += 2;} else {$taillen += 1;}
|
||||||
|
for (var i = 0; i < $remainder.length; i++) if (slimchars.indexOf($tail.charAt(i)) === -1) {$taillen += 2;} else {$taillen += 1;}
|
||||||
|
if ($chordlen >= $taillen && !$end) {
|
||||||
|
if ($tail.length){
|
||||||
|
if (!$remainder.length) {
|
||||||
|
for (c = 0; c < Math.ceil(($chordlen - $taillen) / 2) + 1; c++) {w += '_';}
|
||||||
|
} else {
|
||||||
|
for (c = 0; c < $chordlen - $taillen + 2; c++) {w += ' ';}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!$remainder.length) {
|
||||||
|
for (c = 0; c < Math.floor(($chordlen - $taillen) / 2) + 1; c++) {w += '_';}
|
||||||
|
} else {
|
||||||
|
for (c = 0; c < $chordlen - $taillen + 1; c++) {w += ' ';}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
if (!$tail && $remainder.charAt(0) == ' ') {for (c = 0; c < $chordlen; c++) {w += ' ';}}
|
||||||
|
}
|
||||||
|
if (w!='') {
|
||||||
|
if (w[0] == '_') {
|
||||||
|
ws_length = w.length;
|
||||||
|
if (ws_length==1) {
|
||||||
|
w = '–';
|
||||||
|
} else {
|
||||||
|
wsl_mod = Math.floor(ws_length / 2);
|
||||||
|
ws_right = ws_left = new Array(wsl_mod + 1).join(' ');
|
||||||
|
w = ws_left + '–' + ws_right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w='<span class="ws">' + w + '</span>';
|
||||||
|
}
|
||||||
|
return $.grep(['<span class="chord"><span><strong>', $chord, '</strong></span></span>', $tail, w, $remainder, $end], Boolean).join('');
|
||||||
|
};
|
||||||
|
$("#verseorder span").removeClass("currenttag");
|
||||||
|
$("#tag" + OpenLP.currentTags[OpenLP.currentSlide]).addClass("currenttag");
|
||||||
|
var slide = OpenLP.currentSlides[OpenLP.currentSlide];
|
||||||
|
var text = "";
|
||||||
|
// use title if available
|
||||||
|
if (slide["title"]) {
|
||||||
|
text = slide["title"];
|
||||||
|
} else {
|
||||||
|
text = slide["chords_text"];
|
||||||
|
if(OpenLP.showchords) {
|
||||||
|
text = text.replace(chordclass,chordclassshow);
|
||||||
|
text = text.replace(regchord, replaceChords);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// use thumbnail if available
|
||||||
|
if (slide["img"]) {
|
||||||
|
text += "<br /><img src='" + slide["img"].replace("/thumbnails/", "/thumbnails320x240/") + "'><br />";
|
||||||
|
}
|
||||||
|
// use notes if available
|
||||||
|
if (slide["slide_notes"]) {
|
||||||
|
text += '<br />' + slide["slide_notes"];
|
||||||
|
}
|
||||||
|
text = text.replace(/\n/g, "<br />");
|
||||||
|
$("#currentslide").html(text);
|
||||||
|
text = "";
|
||||||
|
if (OpenLP.currentSlide < OpenLP.currentSlides.length - 1) {
|
||||||
|
for (var idx = OpenLP.currentSlide + 1; idx < OpenLP.currentSlides.length; idx++) {
|
||||||
|
if (OpenLP.currentTags[idx] != OpenLP.currentTags[idx - 1])
|
||||||
|
text = text + "<p class=\"nextslide\">";
|
||||||
|
if (OpenLP.currentSlides[idx]["title"]) {
|
||||||
|
text = text + OpenLP.currentSlides[idx]["title"];
|
||||||
|
} else {
|
||||||
|
text = text + OpenLP.currentSlides[idx]["chords_text"];
|
||||||
|
if(OpenLP.showchords) {
|
||||||
|
text = text.replace(chordclass,chordclassshow);
|
||||||
|
text = text.replace(regchord, replaceChords);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (OpenLP.currentTags[idx] != OpenLP.currentTags[idx - 1])
|
||||||
|
text = text + "</p>";
|
||||||
|
else
|
||||||
|
text = text + "<br />";
|
||||||
|
}
|
||||||
|
text = text.replace(/\n/g, "<br />");
|
||||||
|
$("#nextslide").html(text);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
text = "<p class=\"nextslide\">" + $("#next-text").val() + ": " + OpenLP.nextSong + "</p>";
|
||||||
|
$("#nextslide").html(text);
|
||||||
|
}
|
||||||
|
if(!OpenLP.showchords) {
|
||||||
|
$(".chordline").toggleClass('chordline1');
|
||||||
|
$(".chord").toggle();
|
||||||
|
$(".ws").toggle();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateClock: function(data) {
|
||||||
|
var div = $("#clock");
|
||||||
|
var t = new Date();
|
||||||
|
var h = t.getHours();
|
||||||
|
if (data.results.twelve && h > 12)
|
||||||
|
h = h - 12;
|
||||||
|
if (h < 10) h = '0' + h + '';
|
||||||
|
var m = t.getMinutes();
|
||||||
|
if (m < 10)
|
||||||
|
m = '0' + m + '';
|
||||||
|
div.html(h + ":" + m);
|
||||||
|
},
|
||||||
|
pollServer: function () {
|
||||||
|
$.getJSON(
|
||||||
|
"/api/poll",
|
||||||
|
function (data, status) {
|
||||||
|
OpenLP.updateClock(data);
|
||||||
|
OpenLP.chordNotation = data.results.chordNotation;
|
||||||
|
if (OpenLP.currentItem != data.results.item || OpenLP.currentService != data.results.service) {
|
||||||
|
OpenLP.currentItem = data.results.item;
|
||||||
|
OpenLP.currentService = data.results.service;
|
||||||
|
OpenLP.loadSlides();
|
||||||
|
}
|
||||||
|
else if (OpenLP.currentSlide != data.results.slide) {
|
||||||
|
OpenLP.currentSlide = parseInt(data.results.slide, 10);
|
||||||
|
OpenLP.updateSlide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$.ajaxSetup({ cache: false });
|
||||||
|
setInterval("OpenLP.pollServer();", 500);
|
||||||
|
OpenLP.pollServer();
|
||||||
|
$(document).ready(function() {
|
||||||
|
$('#transposeup').click(function(e) {
|
||||||
|
$('#transposevalue').text(parseInt($('#transposevalue').text()) + 1);
|
||||||
|
storeTransposeValue(OpenLP.currentSlides[0].text.split("\n")[0], $('#transposevalue').text());
|
||||||
|
OpenLP.loadSlides();
|
||||||
|
});
|
||||||
|
$('#transposedown').click(function(e) {
|
||||||
|
$('#transposevalue').text(parseInt($('#transposevalue').text()) - 1);
|
||||||
|
storeTransposeValue(OpenLP.currentSlides[0].text.split("\n")[0], $('#transposevalue').text());
|
||||||
|
OpenLP.loadSlides();
|
||||||
|
});
|
||||||
|
$('#chords').click(function () {
|
||||||
|
OpenLP.showchords = OpenLP.showchords ? false : true;
|
||||||
|
OpenLP.loadSlides();
|
||||||
|
});
|
||||||
|
});
|
@ -152,6 +152,7 @@ class HttpRouter(RegistryProperties):
|
|||||||
('^/$', {'function': self.serve_file, 'secure': False}),
|
('^/$', {'function': self.serve_file, 'secure': False}),
|
||||||
('^/(stage)$', {'function': self.serve_file, 'secure': False}),
|
('^/(stage)$', {'function': self.serve_file, 'secure': False}),
|
||||||
('^/(stage)/(.*)$', {'function': self.stages, 'secure': False}),
|
('^/(stage)/(.*)$', {'function': self.stages, 'secure': False}),
|
||||||
|
('^/(chords)$', {'function': self.serve_file, 'secure': False}),
|
||||||
('^/(main)$', {'function': self.serve_file, 'secure': False}),
|
('^/(main)$', {'function': self.serve_file, 'secure': False}),
|
||||||
(r'^/(\w+)/thumbnails([^/]+)?/(.*)$', {'function': self.serve_thumbnail, 'secure': False}),
|
(r'^/(\w+)/thumbnails([^/]+)?/(.*)$', {'function': self.serve_thumbnail, 'secure': False}),
|
||||||
(r'^/api/poll$', {'function': self.poll, 'secure': False}),
|
(r'^/api/poll$', {'function': self.poll, 'secure': False}),
|
||||||
@ -318,10 +319,12 @@ class HttpRouter(RegistryProperties):
|
|||||||
"""
|
"""
|
||||||
remote = translate('RemotePlugin.Mobile', 'Remote')
|
remote = translate('RemotePlugin.Mobile', 'Remote')
|
||||||
stage = translate('RemotePlugin.Mobile', 'Stage View')
|
stage = translate('RemotePlugin.Mobile', 'Stage View')
|
||||||
|
chords = translate('RemotePlugin.Mobile', 'Chords View')
|
||||||
live = translate('RemotePlugin.Mobile', 'Live View')
|
live = translate('RemotePlugin.Mobile', 'Live View')
|
||||||
self.template_vars = {
|
self.template_vars = {
|
||||||
'app_title': "{main} {remote}".format(main=UiStrings().OLPV2x, remote=remote),
|
'app_title': "{main} {remote}".format(main=UiStrings().OLPV2x, remote=remote),
|
||||||
'stage_title': "{main} {stage}".format(main=UiStrings().OLPV2x, stage=stage),
|
'stage_title': "{main} {stage}".format(main=UiStrings().OLPV2x, stage=stage),
|
||||||
|
'chords_title': "{main} {chords}".format(main=UiStrings().OLPV2x, chords=chords),
|
||||||
'live_title': "{main} {live}".format(main=UiStrings().OLPV2x, live=live),
|
'live_title': "{main} {live}".format(main=UiStrings().OLPV2x, live=live),
|
||||||
'service_manager': translate('RemotePlugin.Mobile', 'Service Manager'),
|
'service_manager': translate('RemotePlugin.Mobile', 'Service Manager'),
|
||||||
'slide_controller': translate('RemotePlugin.Mobile', 'Slide Controller'),
|
'slide_controller': translate('RemotePlugin.Mobile', 'Slide Controller'),
|
||||||
@ -482,7 +485,8 @@ class HttpRouter(RegistryProperties):
|
|||||||
'display': self.live_controller.desktop_screen.isChecked(),
|
'display': self.live_controller.desktop_screen.isChecked(),
|
||||||
'version': 2,
|
'version': 2,
|
||||||
'isSecure': Settings().value(self.settings_section + '/authentication enabled'),
|
'isSecure': Settings().value(self.settings_section + '/authentication enabled'),
|
||||||
'isAuthorised': self.authorised
|
'isAuthorised': self.authorised,
|
||||||
|
'chordNotation': Settings().value('songs/chord notation'),
|
||||||
}
|
}
|
||||||
self.do_json_header()
|
self.do_json_header()
|
||||||
return json.dumps({'results': result}).encode()
|
return json.dumps({'results': result}).encode()
|
||||||
@ -554,6 +558,7 @@ class HttpRouter(RegistryProperties):
|
|||||||
item['tag'] = str(frame['verseTag'])
|
item['tag'] = str(frame['verseTag'])
|
||||||
else:
|
else:
|
||||||
item['tag'] = str(index + 1)
|
item['tag'] = str(index + 1)
|
||||||
|
item['chords_text'] = str(frame['chords_text'])
|
||||||
item['text'] = str(frame['text'])
|
item['text'] = str(frame['text'])
|
||||||
item['html'] = str(frame['html'])
|
item['html'] = str(frame['html'])
|
||||||
# Handle images, unless a custom thumbnail is given or if thumbnails is disabled
|
# Handle images, unless a custom thumbnail is given or if thumbnails is disabled
|
||||||
|
@ -81,6 +81,12 @@ class RemoteTab(SettingsTab):
|
|||||||
self.stage_url.setObjectName('stage_url')
|
self.stage_url.setObjectName('stage_url')
|
||||||
self.stage_url.setOpenExternalLinks(True)
|
self.stage_url.setOpenExternalLinks(True)
|
||||||
self.http_setting_layout.addRow(self.stage_url_label, self.stage_url)
|
self.http_setting_layout.addRow(self.stage_url_label, self.stage_url)
|
||||||
|
self.chords_url_label = QtWidgets.QLabel(self.http_settings_group_box)
|
||||||
|
self.chords_url_label.setObjectName('chords_url_label')
|
||||||
|
self.chords_url = QtWidgets.QLabel(self.http_settings_group_box)
|
||||||
|
self.chords_url.setObjectName('chords_url')
|
||||||
|
self.chords_url.setOpenExternalLinks(True)
|
||||||
|
self.http_setting_layout.addRow(self.chords_url_label, self.chords_url)
|
||||||
self.live_url_label = QtWidgets.QLabel(self.http_settings_group_box)
|
self.live_url_label = QtWidgets.QLabel(self.http_settings_group_box)
|
||||||
self.live_url_label.setObjectName('live_url_label')
|
self.live_url_label.setObjectName('live_url_label')
|
||||||
self.live_url = QtWidgets.QLabel(self.http_settings_group_box)
|
self.live_url = QtWidgets.QLabel(self.http_settings_group_box)
|
||||||
@ -148,6 +154,7 @@ class RemoteTab(SettingsTab):
|
|||||||
self.port_label.setText(translate('RemotePlugin.RemoteTab', 'Port number:'))
|
self.port_label.setText(translate('RemotePlugin.RemoteTab', 'Port number:'))
|
||||||
self.remote_url_label.setText(translate('RemotePlugin.RemoteTab', 'Remote URL:'))
|
self.remote_url_label.setText(translate('RemotePlugin.RemoteTab', 'Remote URL:'))
|
||||||
self.stage_url_label.setText(translate('RemotePlugin.RemoteTab', 'Stage view URL:'))
|
self.stage_url_label.setText(translate('RemotePlugin.RemoteTab', 'Stage view URL:'))
|
||||||
|
self.chords_url_label.setText(translate('RemotePlugin.RemoteTab', 'Chords view URL:'))
|
||||||
self.live_url_label.setText(translate('RemotePlugin.RemoteTab', 'Live view URL:'))
|
self.live_url_label.setText(translate('RemotePlugin.RemoteTab', 'Live view URL:'))
|
||||||
self.twelve_hour_check_box.setText(translate('RemotePlugin.RemoteTab', 'Display stage time in 12h format'))
|
self.twelve_hour_check_box.setText(translate('RemotePlugin.RemoteTab', 'Display stage time in 12h format'))
|
||||||
self.thumbnails_check_box.setText(translate('RemotePlugin.RemoteTab',
|
self.thumbnails_check_box.setText(translate('RemotePlugin.RemoteTab',
|
||||||
|
@ -25,6 +25,7 @@ from PyQt5 import QtWidgets
|
|||||||
from openlp.core.ui.lib import SpellTextEdit
|
from openlp.core.ui.lib import SpellTextEdit
|
||||||
from openlp.core.lib import build_icon, translate
|
from openlp.core.lib import build_icon, translate
|
||||||
from openlp.core.lib.ui import UiStrings, create_button_box
|
from openlp.core.lib.ui import UiStrings, create_button_box
|
||||||
|
from openlp.core.common import Settings
|
||||||
from openlp.plugins.songs.lib import VerseType
|
from openlp.plugins.songs.lib import VerseType
|
||||||
|
|
||||||
|
|
||||||
@ -63,6 +64,21 @@ class Ui_EditVerseDialog(object):
|
|||||||
self.verse_type_layout.addWidget(self.insert_button)
|
self.verse_type_layout.addWidget(self.insert_button)
|
||||||
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 Settings().value('songs/enable chords'):
|
||||||
|
self.transpose_layout = QtWidgets.QHBoxLayout()
|
||||||
|
self.transpose_layout.setObjectName('transpose_layout')
|
||||||
|
self.transpose_label = QtWidgets.QLabel(edit_verse_dialog)
|
||||||
|
self.transpose_label.setObjectName('transpose_label')
|
||||||
|
self.transpose_layout.addWidget(self.transpose_label)
|
||||||
|
self.transpose_up_button = QtWidgets.QPushButton(edit_verse_dialog)
|
||||||
|
self.transpose_up_button.setIcon(build_icon(':/services/service_up.png'))
|
||||||
|
self.transpose_up_button.setObjectName('transpose_up')
|
||||||
|
self.transpose_layout.addWidget(self.transpose_up_button)
|
||||||
|
self.transpose_down_button = QtWidgets.QPushButton(edit_verse_dialog)
|
||||||
|
self.transpose_down_button.setIcon(build_icon(':/services/service_down.png'))
|
||||||
|
self.transpose_down_button.setObjectName('transpose_down')
|
||||||
|
self.transpose_layout.addWidget(self.transpose_down_button)
|
||||||
|
self.dialog_layout.addLayout(self.transpose_layout)
|
||||||
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.retranslateUi(edit_verse_dialog)
|
self.retranslateUi(edit_verse_dialog)
|
||||||
@ -82,3 +98,7 @@ class Ui_EditVerseDialog(object):
|
|||||||
self.insert_button.setText(translate('SongsPlugin.EditVerseForm', '&Insert'))
|
self.insert_button.setText(translate('SongsPlugin.EditVerseForm', '&Insert'))
|
||||||
self.insert_button.setToolTip(translate('SongsPlugin.EditVerseForm',
|
self.insert_button.setToolTip(translate('SongsPlugin.EditVerseForm',
|
||||||
'Split a slide into two by inserting a verse splitter.'))
|
'Split a slide into two by inserting a verse splitter.'))
|
||||||
|
if Settings().value('songs/enable chords'):
|
||||||
|
self.transpose_label.setText(translate('SongsPlugin.EditVerseForm', 'Transpose:'))
|
||||||
|
self.transpose_up_button.setText(translate('SongsPlugin.EditVerseForm', 'Up'))
|
||||||
|
self.transpose_down_button.setText(translate('SongsPlugin.EditVerseForm', 'Down'))
|
||||||
|
@ -25,7 +25,9 @@ import logging
|
|||||||
|
|
||||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||||
|
|
||||||
from openlp.plugins.songs.lib import VerseType
|
from openlp.plugins.songs.lib import VerseType, transpose_lyrics
|
||||||
|
from openlp.core.lib.ui import critical_error_message_box
|
||||||
|
from openlp.core.common import translate, Settings
|
||||||
from .editversedialog import Ui_EditVerseDialog
|
from .editversedialog import Ui_EditVerseDialog
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
@ -48,6 +50,9 @@ class EditVerseForm(QtWidgets.QDialog, Ui_EditVerseDialog):
|
|||||||
self.split_button.clicked.connect(self.on_split_button_clicked)
|
self.split_button.clicked.connect(self.on_split_button_clicked)
|
||||||
self.verse_text_edit.cursorPositionChanged.connect(self.on_cursor_position_changed)
|
self.verse_text_edit.cursorPositionChanged.connect(self.on_cursor_position_changed)
|
||||||
self.verse_type_combo_box.currentIndexChanged.connect(self.on_verse_type_combo_box_changed)
|
self.verse_type_combo_box.currentIndexChanged.connect(self.on_verse_type_combo_box_changed)
|
||||||
|
if Settings().value('songs/enable chords'):
|
||||||
|
self.transpose_down_button.clicked.connect(self.on_transepose_down_button_clicked)
|
||||||
|
self.transpose_up_button.clicked.connect(self.on_transepose_up_button_clicked)
|
||||||
|
|
||||||
def insert_verse(self, verse_tag, verse_num=1):
|
def insert_verse(self, verse_tag, verse_num=1):
|
||||||
"""
|
"""
|
||||||
@ -95,6 +100,41 @@ class EditVerseForm(QtWidgets.QDialog, Ui_EditVerseDialog):
|
|||||||
"""
|
"""
|
||||||
self.update_suggested_verse_number()
|
self.update_suggested_verse_number()
|
||||||
|
|
||||||
|
def on_transepose_up_button_clicked(self):
|
||||||
|
"""
|
||||||
|
The transpose up button clicked
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
transposed_lyrics = transpose_lyrics(self.verse_text_edit.toPlainText(), 1)
|
||||||
|
self.verse_text_edit.setPlainText(transposed_lyrics)
|
||||||
|
except ValueError as ve:
|
||||||
|
# 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)))
|
||||||
|
return
|
||||||
|
self.verse_text_edit.setFocus()
|
||||||
|
self.verse_text_edit.moveCursor(QtGui.QTextCursor.End)
|
||||||
|
|
||||||
|
def on_transepose_down_button_clicked(self):
|
||||||
|
"""
|
||||||
|
The transpose down button clicked
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
transposed_lyrics = transpose_lyrics(self.verse_text_edit.toPlainText(), -1)
|
||||||
|
self.verse_text_edit.setPlainText(transposed_lyrics)
|
||||||
|
except ValueError as ve:
|
||||||
|
# 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)))
|
||||||
|
return
|
||||||
|
self.verse_text_edit.setPlainText(transposed_lyrics)
|
||||||
|
self.verse_text_edit.setFocus()
|
||||||
|
self.verse_text_edit.moveCursor(QtGui.QTextCursor.End)
|
||||||
|
|
||||||
def update_suggested_verse_number(self):
|
def update_suggested_verse_number(self):
|
||||||
"""
|
"""
|
||||||
Adjusts the verse number SpinBox in regard to the selected verse type and the cursor's position.
|
Adjusts the verse number SpinBox in regard to the selected verse type and the cursor's position.
|
||||||
@ -169,3 +209,20 @@ class EditVerseForm(QtWidgets.QDialog, Ui_EditVerseDialog):
|
|||||||
if not text.startswith('---['):
|
if not text.startswith('---['):
|
||||||
text = '---[{tag}:1]---\n{text}'.format(tag=VerseType.translated_names[VerseType.Verse], text=text)
|
text = '---[{tag}:1]---\n{text}'.format(tag=VerseType.translated_names[VerseType.Verse], text=text)
|
||||||
return text
|
return text
|
||||||
|
|
||||||
|
def accept(self):
|
||||||
|
"""
|
||||||
|
Test if any invalid chords has been entered before closing the verse editor
|
||||||
|
"""
|
||||||
|
if Settings().value('songs/enable chords'):
|
||||||
|
try:
|
||||||
|
transposed_lyrics = transpose_lyrics(self.verse_text_edit.toPlainText(), 1)
|
||||||
|
super(EditVerseForm, self).accept()
|
||||||
|
except ValueError as ve:
|
||||||
|
# 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)))
|
||||||
|
else:
|
||||||
|
super(EditVerseForm, self).accept()
|
||||||
|
@ -29,7 +29,7 @@ import re
|
|||||||
|
|
||||||
from PyQt5 import QtWidgets
|
from PyQt5 import QtWidgets
|
||||||
|
|
||||||
from openlp.core.common import AppLocation, CONTROL_CHARS
|
from openlp.core.common import AppLocation, CONTROL_CHARS, Settings
|
||||||
from openlp.core.lib import translate, clean_tags
|
from openlp.core.lib import translate, clean_tags
|
||||||
from openlp.plugins.songs.lib.db import Author, MediaFile, Song, Topic
|
from openlp.plugins.songs.lib.db import Author, MediaFile, Song, Topic
|
||||||
from openlp.plugins.songs.lib.ui import SongStrings
|
from openlp.plugins.songs.lib.ui import SongStrings
|
||||||
@ -380,7 +380,7 @@ def clean_song(manager, song):
|
|||||||
if isinstance(song.lyrics, bytes):
|
if isinstance(song.lyrics, bytes):
|
||||||
song.lyrics = str(song.lyrics, encoding='utf8')
|
song.lyrics = str(song.lyrics, encoding='utf8')
|
||||||
verses = SongXML().get_verses(song.lyrics)
|
verses = SongXML().get_verses(song.lyrics)
|
||||||
song.search_lyrics = ' '.join([clean_string(clean_tags(verse[1])) for verse in verses])
|
song.search_lyrics = ' '.join([clean_string(clean_tags(verse[1], True)) for verse in verses])
|
||||||
# The song does not have any author, add one.
|
# The song does not have any author, add one.
|
||||||
if not song.authors_songs:
|
if not song.authors_songs:
|
||||||
name = SongStrings.AuthorUnknown
|
name = SongStrings.AuthorUnknown
|
||||||
@ -541,3 +541,123 @@ def delete_song(song_id, song_plugin):
|
|||||||
except OSError:
|
except OSError:
|
||||||
log.exception('Could not remove directory: {path}'.format(path=save_path))
|
log.exception('Could not remove directory: {path}'.format(path=save_path))
|
||||||
song_plugin.manager.delete_object(Song, song_id)
|
song_plugin.manager.delete_object(Song, song_id)
|
||||||
|
|
||||||
|
|
||||||
|
def transpose_lyrics(lyrics, transepose_value):
|
||||||
|
"""
|
||||||
|
Transepose lyrics
|
||||||
|
|
||||||
|
:param lyrcs: The lyrics to be transposed
|
||||||
|
:param transepose_value: The value to transpose the lyrics with
|
||||||
|
:return: The transposed lyrics
|
||||||
|
"""
|
||||||
|
# Split text by verse delimiter - both normal and optional
|
||||||
|
verse_list = re.split('(---\[.+?:.+?\]---|\[---\])', lyrics)
|
||||||
|
transposed_lyrics = ''
|
||||||
|
notation = Settings().value('songs/chord notation')
|
||||||
|
for verse in verse_list:
|
||||||
|
if verse.startswith('---[') or verse == '[---]':
|
||||||
|
transposed_lyrics += verse
|
||||||
|
else:
|
||||||
|
transposed_lyrics += transpose_verse(verse, transepose_value, notation)
|
||||||
|
return transposed_lyrics
|
||||||
|
|
||||||
|
|
||||||
|
def transpose_verse(verse_text, transepose_value, notation):
|
||||||
|
"""
|
||||||
|
Transepose lyrics
|
||||||
|
|
||||||
|
:param lyrcs: The lyrics to be transposed
|
||||||
|
:param transepose_value: The value to transpose the lyrics with
|
||||||
|
:return: The transposed lyrics
|
||||||
|
"""
|
||||||
|
if '[' not in verse_text:
|
||||||
|
return verse_text
|
||||||
|
# Split the lyrics based on chord tags
|
||||||
|
lyric_list = re.split('(\[|\]|/)', verse_text)
|
||||||
|
transposed_lyrics = ''
|
||||||
|
in_tag = False
|
||||||
|
for word in lyric_list:
|
||||||
|
if not in_tag:
|
||||||
|
transposed_lyrics += word
|
||||||
|
if word == '[':
|
||||||
|
in_tag = True
|
||||||
|
else:
|
||||||
|
if word == ']':
|
||||||
|
in_tag = False
|
||||||
|
transposed_lyrics += word
|
||||||
|
elif word == '/':
|
||||||
|
transposed_lyrics += word
|
||||||
|
else:
|
||||||
|
# This MUST be a chord
|
||||||
|
transposed_lyrics += transpose_chord(word, transepose_value, notation)
|
||||||
|
# If still inside a chord tag something is wrong!
|
||||||
|
if in_tag:
|
||||||
|
return verse_text
|
||||||
|
else:
|
||||||
|
return transposed_lyrics
|
||||||
|
|
||||||
|
|
||||||
|
def transpose_chord(chord, transpose_value, notation):
|
||||||
|
"""
|
||||||
|
Transpose chord according to the notation used.
|
||||||
|
NOTE: This function has a javascript equivalent in chords.js - make sure to update both!
|
||||||
|
|
||||||
|
:param chord: The chord to transpose.
|
||||||
|
:param transpose_value: The value the chord should be transposed.
|
||||||
|
:param notation: The notation to use when transposing.
|
||||||
|
:return: The transposed chord.
|
||||||
|
"""
|
||||||
|
# See https://en.wikipedia.org/wiki/Musical_note#12-tone_chromatic_scale
|
||||||
|
notes_sharp_notation = {}
|
||||||
|
notes_flat_notation = {}
|
||||||
|
notes_sharp_notation['german'] = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'H']
|
||||||
|
notes_flat_notation['german'] = ['C', 'Db', 'D', 'Eb', 'Fb', 'F', 'Gb', 'G', 'Ab', 'A', 'B', 'H']
|
||||||
|
notes_sharp_notation['english'] = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']
|
||||||
|
notes_flat_notation['english'] = ['C', 'Db', 'D', 'Eb', 'Fb', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B']
|
||||||
|
notes_sharp_notation['neo-latin'] = ['Do', 'Do#', 'Re', 'Re#', 'Mi', 'Fa', 'Fa#', 'Sol', 'Sol#', 'La', 'La#', 'Si']
|
||||||
|
notes_flat_notation['neo-latin'] = ['Do', 'Reb', 'Re', 'Mib', 'Fab', 'Fa', 'Solb', 'Sol', 'Lab', 'La', 'Sib', 'Si']
|
||||||
|
chord_split = chord.replace('♭', 'b').split('/')
|
||||||
|
transposed_chord = ''
|
||||||
|
last_chord = ''
|
||||||
|
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 += '/'
|
||||||
|
currentchord = chord_split[i]
|
||||||
|
if currentchord and currentchord[0] == '(':
|
||||||
|
transposed_chord += '('
|
||||||
|
if len(currentchord) > 1:
|
||||||
|
currentchord = currentchord[1:]
|
||||||
|
else:
|
||||||
|
currentchord = ''
|
||||||
|
if len(currentchord) > 0:
|
||||||
|
if len(currentchord) > 1:
|
||||||
|
if '#b'.find(currentchord[1]) == -1:
|
||||||
|
note = currentchord[0:1]
|
||||||
|
rest = currentchord[1:]
|
||||||
|
else:
|
||||||
|
note = currentchord[0:2]
|
||||||
|
rest = currentchord[2:]
|
||||||
|
else:
|
||||||
|
note = currentchord
|
||||||
|
rest = ''
|
||||||
|
notenumber = notes_flat.index(note) if note not in notes_sharp else notes_sharp.index(note)
|
||||||
|
notenumber += transpose_value
|
||||||
|
while notenumber > 11:
|
||||||
|
notenumber -= 12
|
||||||
|
while notenumber < 0:
|
||||||
|
notenumber += 12
|
||||||
|
if i == 0:
|
||||||
|
current_chord = notes_sharp[notenumber] if notes_preferred[notenumber] == '#' else notes_flat[
|
||||||
|
notenumber]
|
||||||
|
last_chord = current_chord
|
||||||
|
else:
|
||||||
|
current_chord = notes_flat[notenumber] if last_chord not in notes_sharp else notes_sharp[notenumber]
|
||||||
|
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
|
||||||
|
@ -48,6 +48,7 @@ from .importers.powerpraise import PowerPraiseImport
|
|||||||
from .importers.presentationmanager import PresentationManagerImport
|
from .importers.presentationmanager import PresentationManagerImport
|
||||||
from .importers.lyrix import LyrixImport
|
from .importers.lyrix import LyrixImport
|
||||||
from .importers.videopsalm import VideoPsalmImport
|
from .importers.videopsalm import VideoPsalmImport
|
||||||
|
from .importers.chordpro import ChordProImport
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -155,29 +156,30 @@ class SongFormat(object):
|
|||||||
OpenLP2 = 1
|
OpenLP2 = 1
|
||||||
Generic = 2
|
Generic = 2
|
||||||
CCLI = 3
|
CCLI = 3
|
||||||
DreamBeam = 4
|
ChordPro = 4
|
||||||
EasySlides = 5
|
DreamBeam = 5
|
||||||
EasyWorshipDB = 6
|
EasySlides = 6
|
||||||
EasyWorshipService = 7
|
EasyWorshipDB = 7
|
||||||
FoilPresenter = 8
|
EasyWorshipService = 8
|
||||||
Lyrix = 9
|
FoilPresenter = 9
|
||||||
MediaShout = 10
|
Lyrix = 10
|
||||||
OpenSong = 11
|
MediaShout = 11
|
||||||
OPSPro = 12
|
OpenSong = 12
|
||||||
PowerPraise = 13
|
OPSPro = 13
|
||||||
PowerSong = 14
|
PowerPraise = 14
|
||||||
PresentationManager = 15
|
PowerSong = 15
|
||||||
ProPresenter = 16
|
PresentationManager = 16
|
||||||
SongBeamer = 17
|
ProPresenter = 17
|
||||||
SongPro = 18
|
SongBeamer = 18
|
||||||
SongShowPlus = 19
|
SongPro = 19
|
||||||
SongsOfFellowship = 20
|
SongShowPlus = 20
|
||||||
SundayPlus = 21
|
SongsOfFellowship = 21
|
||||||
VideoPsalm = 22
|
SundayPlus = 22
|
||||||
WordsOfWorship = 23
|
VideoPsalm = 23
|
||||||
WorshipAssistant = 24
|
WordsOfWorship = 24
|
||||||
WorshipCenterPro = 25
|
WorshipAssistant = 25
|
||||||
ZionWorx = 26
|
WorshipCenterPro = 26
|
||||||
|
ZionWorx = 27
|
||||||
|
|
||||||
# Set optional attribute defaults
|
# Set optional attribute defaults
|
||||||
__defaults__ = {
|
__defaults__ = {
|
||||||
@ -224,6 +226,13 @@ class SongFormat(object):
|
|||||||
'filter': '{text} (*.usr *.txt *.bin)'.format(text=translate('SongsPlugin.ImportWizardForm',
|
'filter': '{text} (*.usr *.txt *.bin)'.format(text=translate('SongsPlugin.ImportWizardForm',
|
||||||
'CCLI SongSelect Files'))
|
'CCLI SongSelect Files'))
|
||||||
},
|
},
|
||||||
|
ChordPro: {
|
||||||
|
'class': ChordProImport,
|
||||||
|
'name': 'ChordPro',
|
||||||
|
'prefix': 'chordPro',
|
||||||
|
'filter': '{text} (*.cho *.crd *.chordpro *.chopro *.txt)'.format(
|
||||||
|
text=translate('SongsPlugin.ImportWizardForm', 'ChordPro Files'))
|
||||||
|
},
|
||||||
DreamBeam: {
|
DreamBeam: {
|
||||||
'class': DreamBeamImport,
|
'class': DreamBeamImport,
|
||||||
'name': 'DreamBeam',
|
'name': 'DreamBeam',
|
||||||
@ -427,6 +436,7 @@ class SongFormat(object):
|
|||||||
SongFormat.OpenLP2,
|
SongFormat.OpenLP2,
|
||||||
SongFormat.Generic,
|
SongFormat.Generic,
|
||||||
SongFormat.CCLI,
|
SongFormat.CCLI,
|
||||||
|
SongFormat.ChordPro,
|
||||||
SongFormat.DreamBeam,
|
SongFormat.DreamBeam,
|
||||||
SongFormat.EasySlides,
|
SongFormat.EasySlides,
|
||||||
SongFormat.EasyWorshipDB,
|
SongFormat.EasyWorshipDB,
|
||||||
|
178
openlp/plugins/songs/lib/importers/chordpro.py
Normal file
178
openlp/plugins/songs/lib/importers/chordpro.py
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# OpenLP - Open Source Lyrics Projection #
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Copyright (c) 2008-2016 OpenLP Developers #
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# This program is free software; you can redistribute it and/or modify it #
|
||||||
|
# under the terms of the GNU General Public License as published by the Free #
|
||||||
|
# Software Foundation; version 2 of the License. #
|
||||||
|
# #
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT #
|
||||||
|
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
|
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
|
||||||
|
# more details. #
|
||||||
|
# #
|
||||||
|
# You should have received a copy of the GNU General Public License along #
|
||||||
|
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||||
|
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||||
|
###############################################################################
|
||||||
|
"""
|
||||||
|
The :mod:`chordpro` module provides the functionality for importing
|
||||||
|
ChordPro files into the current database.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
|
||||||
|
from openlp.core.common import Settings
|
||||||
|
|
||||||
|
from .songimport import SongImport
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ChordProImport(SongImport):
|
||||||
|
"""
|
||||||
|
The :class:`ChordProImport` class provides OpenLP with the
|
||||||
|
ability to import ChordPro files.
|
||||||
|
This importer is based on the information available on these webpages:
|
||||||
|
http://webchord.sourceforge.net/tech.html
|
||||||
|
http://www.vromans.org/johan/projects/Chordii/chordpro/
|
||||||
|
http://www.tenbyten.com/software/songsgen/help/HtmlHelp/files_reference.htm
|
||||||
|
http://linkesoft.com/songbook/chordproformat.html
|
||||||
|
"""
|
||||||
|
def do_import(self):
|
||||||
|
self.import_wizard.progress_bar.setMaximum(len(self.import_source))
|
||||||
|
for filename in self.import_source:
|
||||||
|
if self.stop_import_flag:
|
||||||
|
return
|
||||||
|
song_file = open(filename, 'rt')
|
||||||
|
self.do_import_file(song_file)
|
||||||
|
song_file.close()
|
||||||
|
|
||||||
|
def do_import_file(self, song_file):
|
||||||
|
"""
|
||||||
|
Imports the songs in the given file
|
||||||
|
:param song_file: The file object to be imported from.
|
||||||
|
"""
|
||||||
|
self.set_defaults()
|
||||||
|
# Loop over the lines of the file
|
||||||
|
file_content = song_file.read()
|
||||||
|
current_verse = ''
|
||||||
|
current_verse_type = 'v'
|
||||||
|
skip_block = False
|
||||||
|
for line in file_content.splitlines():
|
||||||
|
line = line.rstrip()
|
||||||
|
# Detect tags
|
||||||
|
if line.startswith('{'):
|
||||||
|
tag_name, tag_value = self.parse_tag(line)
|
||||||
|
# Detect which tag
|
||||||
|
if tag_name in ['title', 't']:
|
||||||
|
self.title = tag_value
|
||||||
|
elif tag_name in ['subtitle', 'su', 'st']:
|
||||||
|
self.alternate_title = tag_value
|
||||||
|
elif tag_name in ['comment', 'c', 'comment_italic', 'ci', 'comment_box', 'cb']:
|
||||||
|
# Detect if the comment is used as a chorus repeat marker
|
||||||
|
if tag_value.lower().startswith('chorus'):
|
||||||
|
if current_verse.strip():
|
||||||
|
# Add collected verse to the lyrics
|
||||||
|
# Strip out chords if set up to
|
||||||
|
if not Settings().value('songs/enable chords') or Settings().value(
|
||||||
|
'songs/disable chords import'):
|
||||||
|
current_verse = re.sub(r'\[.*?\]', '', current_verse)
|
||||||
|
self.add_verse(current_verse.rstrip(), current_verse_type)
|
||||||
|
current_verse_type = 'v'
|
||||||
|
current_verse = ''
|
||||||
|
self.repeat_verse('c1')
|
||||||
|
else:
|
||||||
|
self.add_comment(tag_value)
|
||||||
|
elif tag_name in ['start_of_chorus', 'soc']:
|
||||||
|
current_verse_type = 'c'
|
||||||
|
elif tag_name in ['end_of_chorus', 'eoc']:
|
||||||
|
# Add collected chorus to the lyrics
|
||||||
|
# Strip out chords if set up to
|
||||||
|
if not Settings().value('songs/enable chords') or Settings().value('songs/disable chords import'):
|
||||||
|
current_verse = re.sub(r'\[.*?\]', '', current_verse)
|
||||||
|
self.add_verse(current_verse.rstrip(), current_verse_type)
|
||||||
|
current_verse_type = 'v'
|
||||||
|
current_verse = ''
|
||||||
|
elif tag_name in ['start_of_tab', 'sot']:
|
||||||
|
if current_verse.strip():
|
||||||
|
# Add collected verse to the lyrics
|
||||||
|
# Strip out chords if set up to
|
||||||
|
if not Settings().value('songs/enable chords') or Settings().value(
|
||||||
|
'songs/disable chords import'):
|
||||||
|
current_verse = re.sub(r'\[.*?\]', '', current_verse)
|
||||||
|
self.add_verse(current_verse.rstrip(), current_verse_type)
|
||||||
|
current_verse_type = 'v'
|
||||||
|
current_verse = ''
|
||||||
|
skip_block = True
|
||||||
|
elif tag_name in ['end_of_tab', 'eot']:
|
||||||
|
skip_block = False
|
||||||
|
elif tag_name in ['new_song', 'ns']:
|
||||||
|
# A new song starts below this tag
|
||||||
|
if self.verses and self.title:
|
||||||
|
if current_verse.strip():
|
||||||
|
# Strip out chords if set up to
|
||||||
|
if not Settings().value('songs/enable chords') or Settings().value(
|
||||||
|
'songs/disable chords import'):
|
||||||
|
current_verse = re.sub(r'\[.*?\]', '', current_verse)
|
||||||
|
self.add_verse(current_verse.rstrip(), current_verse_type)
|
||||||
|
if not self.finish():
|
||||||
|
self.log_error(song_file.name)
|
||||||
|
self.set_defaults()
|
||||||
|
current_verse_type = 'v'
|
||||||
|
current_verse = ''
|
||||||
|
else:
|
||||||
|
# Unsupported tag
|
||||||
|
log.debug('unsupported tag: %s' % line)
|
||||||
|
elif line.startswith('#'):
|
||||||
|
# Found a comment line, which is ignored...
|
||||||
|
continue
|
||||||
|
elif line == "['|]":
|
||||||
|
# Found a vertical bar
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
if skip_block:
|
||||||
|
continue
|
||||||
|
elif line == '' and current_verse.strip() and current_verse_type != 'c':
|
||||||
|
# Add collected verse to the lyrics
|
||||||
|
# Strip out chords if set up to
|
||||||
|
if not Settings().value('songs/enable chords') or Settings().value('songs/disable chords import'):
|
||||||
|
current_verse = re.sub(r'\[.*?\]', '', current_verse)
|
||||||
|
self.add_verse(current_verse.rstrip(), current_verse_type)
|
||||||
|
current_verse_type = 'v'
|
||||||
|
current_verse = ''
|
||||||
|
else:
|
||||||
|
if current_verse.strip() == '':
|
||||||
|
current_verse = line + '\n'
|
||||||
|
else:
|
||||||
|
current_verse += line + '\n'
|
||||||
|
if current_verse.strip():
|
||||||
|
# Strip out chords if set up to
|
||||||
|
if not Settings().value('songs/enable chords') or Settings().value(
|
||||||
|
'songs/disable chords import'):
|
||||||
|
current_verse = re.sub(r'\[.*?\]', '', current_verse)
|
||||||
|
self.add_verse(current_verse.rstrip(), current_verse_type)
|
||||||
|
if not self.finish():
|
||||||
|
self.log_error(song_file.name)
|
||||||
|
|
||||||
|
def parse_tag(self, line):
|
||||||
|
"""
|
||||||
|
:param line: Line with the tag to be parsed
|
||||||
|
:return: A tuple with tag name and tag value (if any)
|
||||||
|
"""
|
||||||
|
# Strip the first '}'
|
||||||
|
line = line[1:].strip()
|
||||||
|
colon_idx = line.find(':')
|
||||||
|
# check if this is a tag without value
|
||||||
|
if colon_idx < 0:
|
||||||
|
# strip the final '}' and return the tag name
|
||||||
|
return line[:-1], None
|
||||||
|
tag_name = line[:colon_idx]
|
||||||
|
tag_value = line[colon_idx + 1:-1].strip()
|
||||||
|
return tag_name, tag_value
|
@ -26,7 +26,7 @@ import re
|
|||||||
from lxml import objectify
|
from lxml import objectify
|
||||||
from lxml.etree import Error, LxmlError
|
from lxml.etree import Error, LxmlError
|
||||||
|
|
||||||
from openlp.core.common import translate
|
from openlp.core.common import translate, Settings
|
||||||
from openlp.plugins.songs.lib import VerseType
|
from openlp.plugins.songs.lib import VerseType
|
||||||
from openlp.plugins.songs.lib.importers.songimport import SongImport
|
from openlp.plugins.songs.lib.importers.songimport import SongImport
|
||||||
from openlp.plugins.songs.lib.ui import SongStrings
|
from openlp.plugins.songs.lib.ui import SongStrings
|
||||||
@ -87,7 +87,7 @@ class OpenSongImport(SongImport):
|
|||||||
All verses are imported and tagged appropriately.
|
All verses are imported and tagged appropriately.
|
||||||
|
|
||||||
Guitar chords can be provided "above" the lyrics (the line is preceded by a period "."), and one or more "_" can
|
Guitar chords can be provided "above" the lyrics (the line is preceded by a period "."), and one or more "_" can
|
||||||
be used to signify long-drawn-out words. Chords and "_" are removed by this importer. For example::
|
be used to signify long-drawn-out words. For example::
|
||||||
|
|
||||||
. A7 Bm
|
. A7 Bm
|
||||||
1 Some____ Words
|
1 Some____ Words
|
||||||
@ -195,14 +195,34 @@ class OpenSongImport(SongImport):
|
|||||||
lyrics = str(root.lyrics)
|
lyrics = str(root.lyrics)
|
||||||
else:
|
else:
|
||||||
lyrics = ''
|
lyrics = ''
|
||||||
|
chords = []
|
||||||
for this_line in lyrics.split('\n'):
|
for this_line in lyrics.split('\n'):
|
||||||
if not this_line.strip():
|
if not this_line.strip():
|
||||||
continue
|
continue
|
||||||
# skip this line if it is a comment
|
# skip this line if it is a comment
|
||||||
if this_line.startswith(';'):
|
if this_line.startswith(';'):
|
||||||
continue
|
continue
|
||||||
# skip guitar chords and page and column breaks
|
# skip page and column breaks
|
||||||
if this_line.startswith('.') or this_line.startswith('---') or this_line.startswith('-!!'):
|
if this_line.startswith('---') or this_line.startswith('-!!'):
|
||||||
|
continue
|
||||||
|
# guitar chords marker
|
||||||
|
if this_line.startswith('.'):
|
||||||
|
# Find the position of the chords so they can be inserted in the lyrics
|
||||||
|
chords = []
|
||||||
|
this_line = this_line[1:]
|
||||||
|
chord = ''
|
||||||
|
i = 0
|
||||||
|
while i < len(this_line):
|
||||||
|
if this_line[i] != ' ':
|
||||||
|
chord_pos = i
|
||||||
|
chord += this_line[i]
|
||||||
|
i += 1
|
||||||
|
while i < len(this_line) and this_line[i] != ' ':
|
||||||
|
chord += this_line[i]
|
||||||
|
i += 1
|
||||||
|
chords.append((chord_pos, chord))
|
||||||
|
chord = ''
|
||||||
|
i += 1
|
||||||
continue
|
continue
|
||||||
# verse/chorus/etc. marker
|
# verse/chorus/etc. marker
|
||||||
if this_line.startswith('['):
|
if this_line.startswith('['):
|
||||||
@ -228,12 +248,20 @@ class OpenSongImport(SongImport):
|
|||||||
# number at start of line.. it's verse number
|
# number at start of line.. it's verse number
|
||||||
if this_line[0].isdigit():
|
if this_line[0].isdigit():
|
||||||
verse_num = this_line[0]
|
verse_num = this_line[0]
|
||||||
this_line = this_line[1:].strip()
|
this_line = this_line[1:]
|
||||||
verses.setdefault(verse_tag, {})
|
verses.setdefault(verse_tag, {})
|
||||||
verses[verse_tag].setdefault(verse_num, {})
|
verses[verse_tag].setdefault(verse_num, {})
|
||||||
if inst not in verses[verse_tag][verse_num]:
|
if inst not in verses[verse_tag][verse_num]:
|
||||||
verses[verse_tag][verse_num][inst] = []
|
verses[verse_tag][verse_num][inst] = []
|
||||||
our_verse_order.append([verse_tag, verse_num, inst])
|
our_verse_order.append([verse_tag, verse_num, inst])
|
||||||
|
# If chords exists insert them
|
||||||
|
if chords and Settings().value('songs/enable chords') and not Settings().value(
|
||||||
|
'songs/disable chords import'):
|
||||||
|
offset = 0
|
||||||
|
for (column, chord) in chords:
|
||||||
|
this_line = '{pre}[{chord}]{post}'.format(pre=this_line[:offset + column], chord=chord,
|
||||||
|
post=this_line[offset + column:])
|
||||||
|
offset += len(chord) + 2
|
||||||
# Tidy text and remove the ____s from extended words
|
# Tidy text and remove the ____s from extended words
|
||||||
this_line = self.tidy_text(this_line)
|
this_line = self.tidy_text(this_line)
|
||||||
this_line = this_line.replace('_', '')
|
this_line = this_line.replace('_', '')
|
||||||
|
@ -25,10 +25,12 @@ The :mod:`songbeamer` module provides the functionality for importing SongBeamer
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import base64
|
||||||
|
import math
|
||||||
|
|
||||||
from openlp.core.common import get_file_encoding
|
|
||||||
from openlp.plugins.songs.lib import VerseType
|
from openlp.plugins.songs.lib import VerseType
|
||||||
from openlp.plugins.songs.lib.importers.songimport import SongImport
|
from openlp.plugins.songs.lib.importers.songimport import SongImport
|
||||||
|
from openlp.core.common import Settings, is_win, is_macosx, get_file_encoding
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -60,6 +62,13 @@ class SongBeamerTypes(object):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class VerseTagMode(object):
|
||||||
|
Unknown = 0
|
||||||
|
ContainsTags = 1
|
||||||
|
ContainsNoTags = 2
|
||||||
|
ContainsNoTagsRestart = 3
|
||||||
|
|
||||||
|
|
||||||
class SongBeamerImport(SongImport):
|
class SongBeamerImport(SongImport):
|
||||||
"""
|
"""
|
||||||
Import Song Beamer files(s). Song Beamer file format is text based in the beginning are one or more control tags
|
Import Song Beamer files(s). Song Beamer file format is text based in the beginning are one or more control tags
|
||||||
@ -109,7 +118,7 @@ class SongBeamerImport(SongImport):
|
|||||||
self.set_defaults()
|
self.set_defaults()
|
||||||
self.current_verse = ''
|
self.current_verse = ''
|
||||||
self.current_verse_type = VerseType.tags[VerseType.Verse]
|
self.current_verse_type = VerseType.tags[VerseType.Verse]
|
||||||
read_verses = False
|
self.chord_table = None
|
||||||
file_name = os.path.split(import_file)[1]
|
file_name = os.path.split(import_file)[1]
|
||||||
if os.path.isfile(import_file):
|
if os.path.isfile(import_file):
|
||||||
# Detect the encoding
|
# Detect the encoding
|
||||||
@ -125,33 +134,103 @@ class SongBeamerImport(SongImport):
|
|||||||
continue
|
continue
|
||||||
self.title = file_name.split('.sng')[0]
|
self.title = file_name.split('.sng')[0]
|
||||||
read_verses = False
|
read_verses = False
|
||||||
for line in song_data:
|
# The first verse separator doesn't count, but the others does, so line count starts at -1
|
||||||
# Just make sure that the line is of the type 'Unicode'.
|
line_number = -1
|
||||||
line = str(line).strip()
|
verse_tags_mode = VerseTagMode.Unknown
|
||||||
|
first_verse = True
|
||||||
|
idx = -1
|
||||||
|
while idx + 1 < len(song_data):
|
||||||
|
idx = idx + 1
|
||||||
|
line = song_data[idx].rstrip()
|
||||||
|
stripped_line = line.strip()
|
||||||
if line.startswith('#') and not read_verses:
|
if line.startswith('#') and not read_verses:
|
||||||
self.parseTags(line)
|
self.parse_tags(line)
|
||||||
elif line.startswith('--'):
|
elif stripped_line.startswith('---'):
|
||||||
# --- and -- allowed for page-breaks (difference in Songbeamer only in printout)
|
# '---' is a verse breaker
|
||||||
if self.current_verse:
|
if self.current_verse:
|
||||||
self.replace_html_tags()
|
self.replace_html_tags()
|
||||||
self.add_verse(self.current_verse, self.current_verse_type)
|
self.add_verse(self.current_verse, self.current_verse_type)
|
||||||
self.current_verse = ''
|
self.current_verse = ''
|
||||||
self.current_verse_type = VerseType.tags[VerseType.Verse]
|
self.current_verse_type = VerseType.tags[VerseType.Verse]
|
||||||
|
first_verse = False
|
||||||
read_verses = True
|
read_verses = True
|
||||||
verse_start = True
|
verse_start = True
|
||||||
|
# Songbeamer allows chord on line "-1", meaning the first line has only chords
|
||||||
|
if line_number == -1:
|
||||||
|
first_line = self.insert_chords(line_number, '')
|
||||||
|
if first_line:
|
||||||
|
self.current_verse = first_line.strip() + '\n'
|
||||||
|
line_number += 1
|
||||||
|
elif stripped_line.startswith('--'):
|
||||||
|
# '--' is a page breaker, we convert to optional page break
|
||||||
|
self.current_verse += '[---]\n'
|
||||||
|
line_number += 1
|
||||||
elif read_verses:
|
elif read_verses:
|
||||||
if verse_start:
|
if verse_start:
|
||||||
verse_start = False
|
verse_start = False
|
||||||
if not self.check_verse_marks(line):
|
verse_mark = self.check_verse_marks(line)
|
||||||
self.current_verse = line + '\n'
|
# To ensure that linenumbers are mapped correctly when inserting chords, we attempt to detect
|
||||||
|
# if verse tags are inserted manually or by SongBeamer. If they are inserted manually the lines
|
||||||
|
# should be counted, otherwise not. If all verses start with a tag we assume it is inserted by
|
||||||
|
# SongBeamer.
|
||||||
|
if first_verse and verse_tags_mode == VerseTagMode.Unknown:
|
||||||
|
if verse_mark:
|
||||||
|
verse_tags_mode = VerseTagMode.ContainsTags
|
||||||
|
else:
|
||||||
|
verse_tags_mode = VerseTagMode.ContainsNoTags
|
||||||
|
elif verse_tags_mode != VerseTagMode.ContainsNoTagsRestart:
|
||||||
|
if not verse_mark and verse_tags_mode == VerseTagMode.ContainsTags:
|
||||||
|
# A verse mark was expected but not found, which means that verse marks has not been
|
||||||
|
# inserted by songbeamer, but are manually added headings. So restart the loop, and
|
||||||
|
# count tags as lines.
|
||||||
|
self.set_defaults()
|
||||||
|
self.title = file_name.split('.sng')[0]
|
||||||
|
verse_tags_mode = VerseTagMode.ContainsNoTagsRestart
|
||||||
|
read_verses = False
|
||||||
|
# The first verseseparator doesn't count, but the others does, so linecount starts at -1
|
||||||
|
line_number = -1
|
||||||
|
first_verse = True
|
||||||
|
idx = -1
|
||||||
|
continue
|
||||||
|
if not verse_mark:
|
||||||
|
line = self.insert_chords(line_number, line)
|
||||||
|
self.current_verse += line.strip() + '\n'
|
||||||
|
line_number += 1
|
||||||
|
elif verse_tags_mode in [VerseTagMode.ContainsNoTags, VerseTagMode.ContainsNoTagsRestart]:
|
||||||
|
line_number += 1
|
||||||
else:
|
else:
|
||||||
self.current_verse += line + '\n'
|
line = self.insert_chords(line_number, line)
|
||||||
|
self.current_verse += line.strip() + '\n'
|
||||||
|
line_number += 1
|
||||||
if self.current_verse:
|
if self.current_verse:
|
||||||
self.replace_html_tags()
|
self.replace_html_tags()
|
||||||
self.add_verse(self.current_verse, self.current_verse_type)
|
self.add_verse(self.current_verse, self.current_verse_type)
|
||||||
if not self.finish():
|
if not self.finish():
|
||||||
self.log_error(import_file)
|
self.log_error(import_file)
|
||||||
|
|
||||||
|
def insert_chords(self, line_number, line):
|
||||||
|
"""
|
||||||
|
Insert chords into text if any exists and chords import is enabled
|
||||||
|
|
||||||
|
:param linenumber: Number of the current line
|
||||||
|
:param line: The line of lyrics to insert chords
|
||||||
|
"""
|
||||||
|
if self.chord_table and Settings().value('songs/enable chords') and not Settings().value(
|
||||||
|
'songs/disable chords import') and line_number in self.chord_table:
|
||||||
|
line_idx = sorted(self.chord_table[line_number].keys(), reverse=True)
|
||||||
|
for idx in line_idx:
|
||||||
|
# In SongBeamer the column position of the chord can be a decimal, we just round it up.
|
||||||
|
int_idx = int(math.ceil(idx))
|
||||||
|
if int_idx < 0:
|
||||||
|
int_idx = 0
|
||||||
|
elif int_idx > len(line):
|
||||||
|
# If a chord is placed beyond the current end of the line, extend the line with spaces.
|
||||||
|
line += ' ' * (int_idx - len(line))
|
||||||
|
chord = self.chord_table[line_number][idx]
|
||||||
|
chord = chord.replace('<', '♭')
|
||||||
|
line = line[:int_idx] + '[' + chord + ']' + line[int_idx:]
|
||||||
|
return line
|
||||||
|
|
||||||
def replace_html_tags(self):
|
def replace_html_tags(self):
|
||||||
"""
|
"""
|
||||||
This can be called to replace SongBeamer's specific (html) tags with OpenLP's specific (html) tags.
|
This can be called to replace SongBeamer's specific (html) tags with OpenLP's specific (html) tags.
|
||||||
@ -159,7 +238,7 @@ class SongBeamerImport(SongImport):
|
|||||||
for pair in SongBeamerImport.HTML_TAG_PAIRS:
|
for pair in SongBeamerImport.HTML_TAG_PAIRS:
|
||||||
self.current_verse = pair[0].sub(pair[1], self.current_verse)
|
self.current_verse = pair[0].sub(pair[1], self.current_verse)
|
||||||
|
|
||||||
def parseTags(self, line):
|
def parse_tags(self, line):
|
||||||
"""
|
"""
|
||||||
Parses a meta data line.
|
Parses a meta data line.
|
||||||
|
|
||||||
@ -176,8 +255,10 @@ class SongBeamerImport(SongImport):
|
|||||||
self.add_copyright(tag_val[1])
|
self.add_copyright(tag_val[1])
|
||||||
elif tag_val[0] == '#AddCopyrightInfo':
|
elif tag_val[0] == '#AddCopyrightInfo':
|
||||||
pass
|
pass
|
||||||
|
elif tag_val[0] == '#AudioFile':
|
||||||
|
self.parse_audio_file(tag_val[1])
|
||||||
elif tag_val[0] == '#Author':
|
elif tag_val[0] == '#Author':
|
||||||
self.parse_author(tag_val[1])
|
self.parse_author(tag_val[1], 'words')
|
||||||
elif tag_val[0] == '#BackgroundImage':
|
elif tag_val[0] == '#BackgroundImage':
|
||||||
pass
|
pass
|
||||||
elif tag_val[0] == '#Bible':
|
elif tag_val[0] == '#Bible':
|
||||||
@ -187,13 +268,16 @@ class SongBeamerImport(SongImport):
|
|||||||
elif tag_val[0] == '#CCLI':
|
elif tag_val[0] == '#CCLI':
|
||||||
self.ccli_number = tag_val[1]
|
self.ccli_number = tag_val[1]
|
||||||
elif tag_val[0] == '#Chords':
|
elif tag_val[0] == '#Chords':
|
||||||
pass
|
self.chord_table = self.parse_chords(tag_val[1])
|
||||||
elif tag_val[0] == '#ChurchSongID':
|
elif tag_val[0] == '#ChurchSongID':
|
||||||
pass
|
pass
|
||||||
elif tag_val[0] == '#ColorChords':
|
elif tag_val[0] == '#ColorChords':
|
||||||
pass
|
pass
|
||||||
elif tag_val[0] == '#Comments':
|
elif tag_val[0] == '#Comments':
|
||||||
self.comments = tag_val[1]
|
try:
|
||||||
|
self.comments = base64.b64decode(tag_val[1]).decode(self.input_file_encoding)
|
||||||
|
except ValueError:
|
||||||
|
self.comments = tag_val[1]
|
||||||
elif tag_val[0] == '#Editor':
|
elif tag_val[0] == '#Editor':
|
||||||
pass
|
pass
|
||||||
elif tag_val[0] == '#Font':
|
elif tag_val[0] == '#Font':
|
||||||
@ -217,7 +301,7 @@ class SongBeamerImport(SongImport):
|
|||||||
elif tag_val[0] == '#LangCount':
|
elif tag_val[0] == '#LangCount':
|
||||||
pass
|
pass
|
||||||
elif tag_val[0] == '#Melody':
|
elif tag_val[0] == '#Melody':
|
||||||
self.parse_author(tag_val[1])
|
self.parse_author(tag_val[1], 'music')
|
||||||
elif tag_val[0] == '#NatCopyright':
|
elif tag_val[0] == '#NatCopyright':
|
||||||
pass
|
pass
|
||||||
elif tag_val[0] == '#OTitle':
|
elif tag_val[0] == '#OTitle':
|
||||||
@ -243,7 +327,7 @@ class SongBeamerImport(SongImport):
|
|||||||
elif tag_val[0] == '#TextAlign':
|
elif tag_val[0] == '#TextAlign':
|
||||||
pass
|
pass
|
||||||
elif tag_val[0] == '#Title':
|
elif tag_val[0] == '#Title':
|
||||||
self.title = str(tag_val[1]).strip()
|
self.title = tag_val[1].strip()
|
||||||
elif tag_val[0] == '#TitleAlign':
|
elif tag_val[0] == '#TitleAlign':
|
||||||
pass
|
pass
|
||||||
elif tag_val[0] == '#TitleFontSize':
|
elif tag_val[0] == '#TitleFontSize':
|
||||||
@ -263,25 +347,80 @@ class SongBeamerImport(SongImport):
|
|||||||
elif tag_val[0] == '#Version':
|
elif tag_val[0] == '#Version':
|
||||||
pass
|
pass
|
||||||
elif tag_val[0] == '#VerseOrder':
|
elif tag_val[0] == '#VerseOrder':
|
||||||
# TODO: add the verse order.
|
verse_order = tag_val[1].strip()
|
||||||
pass
|
for verse_mark in verse_order.split(','):
|
||||||
|
new_verse_mark = self.convert_verse_marks(verse_mark)
|
||||||
|
if new_verse_mark:
|
||||||
|
self.verse_order_list.append(new_verse_mark)
|
||||||
|
|
||||||
def check_verse_marks(self, line):
|
def check_verse_marks(self, line):
|
||||||
"""
|
"""
|
||||||
Check and add the verse's MarkType. Returns ``True`` if the given line contains a correct verse mark otherwise
|
Check and add the verse's MarkType. Returns ``True`` if the given line contains a correct verse mark otherwise
|
||||||
``False``.
|
``False``.
|
||||||
|
|
||||||
:param line: The line to check for marks (unicode).
|
:param line: The line to check for marks.
|
||||||
"""
|
"""
|
||||||
marks = line.split(' ')
|
new_verse_mark = self.convert_verse_marks(line)
|
||||||
if len(marks) <= 2 and marks[0].lower() in SongBeamerTypes.MarkTypes:
|
if new_verse_mark:
|
||||||
self.current_verse_type = SongBeamerTypes.MarkTypes[marks[0].lower()]
|
self.current_verse_type = new_verse_mark
|
||||||
if len(marks) == 2:
|
|
||||||
# If we have a digit, we append it to current_verse_type.
|
|
||||||
if marks[1].isdigit():
|
|
||||||
self.current_verse_type += marks[1]
|
|
||||||
return True
|
|
||||||
elif marks[0].lower().startswith('$$m='): # this verse-mark cannot be numbered
|
|
||||||
self.current_verse_type = SongBeamerTypes.MarkTypes['$$m=']
|
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def convert_verse_marks(self, line):
|
||||||
|
"""
|
||||||
|
Convert the verse's MarkType. Returns the OpenLP versemark if the given line contains a correct SongBeamer verse
|
||||||
|
mark otherwise ``None``.
|
||||||
|
|
||||||
|
:param line: The line to check for marks.
|
||||||
|
"""
|
||||||
|
new_verse_mark = None
|
||||||
|
marks = line.split(' ')
|
||||||
|
if len(marks) <= 2 and marks[0].lower() in SongBeamerTypes.MarkTypes:
|
||||||
|
new_verse_mark = SongBeamerTypes.MarkTypes[marks[0].lower()]
|
||||||
|
if len(marks) == 2:
|
||||||
|
# If we have a digit, we append it to the converted verse mark
|
||||||
|
if marks[1].isdigit():
|
||||||
|
new_verse_mark += marks[1]
|
||||||
|
elif marks[0].lower().startswith('$$m='): # this verse-mark cannot be numbered
|
||||||
|
new_verse_mark = SongBeamerTypes.MarkTypes['$$m=']
|
||||||
|
return new_verse_mark
|
||||||
|
|
||||||
|
def parse_chords(self, chords):
|
||||||
|
"""
|
||||||
|
Parse chords. The chords are in a base64 encode string. The decoded string is an index of chord placement
|
||||||
|
separated by "\r", like this: "<linecolumn>,<linenumber>,<chord>\r"
|
||||||
|
|
||||||
|
:param chords: Chords in a base64 encoded string
|
||||||
|
"""
|
||||||
|
chord_list = base64.b64decode(chords).decode(self.input_file_encoding).split('\r')
|
||||||
|
chord_table = {}
|
||||||
|
for chord_index in chord_list:
|
||||||
|
if not chord_index:
|
||||||
|
continue
|
||||||
|
[col_str, line_str, chord] = chord_index.split(',')
|
||||||
|
col = float(col_str)
|
||||||
|
line = int(line_str)
|
||||||
|
if line not in chord_table:
|
||||||
|
chord_table[line] = {}
|
||||||
|
chord_table[line][col] = chord
|
||||||
|
return chord_table
|
||||||
|
|
||||||
|
def parse_audio_file(self, audio_file_path):
|
||||||
|
"""
|
||||||
|
Parse audio file. The path is relative to the SongsBeamer Songs folder.
|
||||||
|
|
||||||
|
:param audio_file_path: Path to the audio file
|
||||||
|
"""
|
||||||
|
# The path is relative to SongBeamers Song folder
|
||||||
|
if is_win():
|
||||||
|
user_doc_folder = os.path.expandvars('$DOCUMENTS')
|
||||||
|
elif is_macosx():
|
||||||
|
user_doc_folder = os.path.join(os.path.expanduser('~'), 'Documents')
|
||||||
|
else:
|
||||||
|
# SongBeamer only runs on mac and win...
|
||||||
|
return
|
||||||
|
audio_file_path = os.path.normpath(os.path.join(user_doc_folder, 'SongBeamer', 'Songs', audio_file_path))
|
||||||
|
if os.path.isfile(audio_file_path):
|
||||||
|
self.add_media_file(audio_file_path)
|
||||||
|
else:
|
||||||
|
log.debug('Could not import mediafile "%s" since it does not exists!' % audio_file_path)
|
||||||
|
@ -242,7 +242,7 @@ class SongImport(QtCore.QObject):
|
|||||||
self.copyright += ' '
|
self.copyright += ' '
|
||||||
self.copyright += copyright
|
self.copyright += copyright
|
||||||
|
|
||||||
def parse_author(self, text):
|
def parse_author(self, text, type=None):
|
||||||
"""
|
"""
|
||||||
Add the author. OpenLP stores them individually so split by 'and', '&' and comma. However need to check
|
Add the author. OpenLP stores them individually so split by 'and', '&' and comma. However need to check
|
||||||
for 'Mr and Mrs Smith' and turn it to 'Mr Smith' and 'Mrs Smith'.
|
for 'Mr and Mrs Smith' and turn it to 'Mr Smith' and 'Mrs Smith'.
|
||||||
@ -256,7 +256,10 @@ class SongImport(QtCore.QObject):
|
|||||||
if author2.endswith('.'):
|
if author2.endswith('.'):
|
||||||
author2 = author2[:-1]
|
author2 = author2[:-1]
|
||||||
if author2:
|
if author2:
|
||||||
self.add_author(author2)
|
if type:
|
||||||
|
self.add_author(author2, type)
|
||||||
|
else:
|
||||||
|
self.add_author(author2)
|
||||||
|
|
||||||
def add_author(self, author, type=None):
|
def add_author(self, author, type=None):
|
||||||
"""
|
"""
|
||||||
@ -304,12 +307,23 @@ class SongImport(QtCore.QObject):
|
|||||||
if verse_def not in self.verse_order_list_generated:
|
if verse_def not in self.verse_order_list_generated:
|
||||||
self.verse_order_list_generated.append(verse_def)
|
self.verse_order_list_generated.append(verse_def)
|
||||||
|
|
||||||
def repeat_verse(self):
|
def repeat_verse(self, verse_def=None):
|
||||||
"""
|
"""
|
||||||
Repeat the previous verse in the verse order
|
Repeat the verse with the given verse_def or default to repeating the previous verse in the verse order
|
||||||
|
|
||||||
|
:param verse_def: verse_def of the verse to be repeated
|
||||||
"""
|
"""
|
||||||
if self.verse_order_list_generated:
|
if self.verse_order_list_generated:
|
||||||
self.verse_order_list_generated.append(self.verse_order_list_generated[-1])
|
if verse_def:
|
||||||
|
# If the given verse_def is only one char (like 'v' or 'c'), postfix it with '1'
|
||||||
|
if len(verse_def) == 1:
|
||||||
|
verse_def += '1'
|
||||||
|
if verse_def in self.verse_order_list_generated:
|
||||||
|
self.verse_order_list_generated.append(verse_def)
|
||||||
|
else:
|
||||||
|
log.warning('Trying to add unknown verse_def "%s"' % verse_def)
|
||||||
|
else:
|
||||||
|
self.verse_order_list_generated.append(self.verse_order_list_generated[-1])
|
||||||
self.verse_order_list_generated_useful = True
|
self.verse_order_list_generated_useful = True
|
||||||
|
|
||||||
def check_complete(self):
|
def check_complete(self):
|
||||||
|
@ -26,8 +26,9 @@ exproted from Lyrix."""
|
|||||||
import logging
|
import logging
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
from openlp.core.common import translate
|
from openlp.core.common import translate, Settings
|
||||||
from openlp.plugins.songs.lib.importers.songimport import SongImport
|
from openlp.plugins.songs.lib.importers.songimport import SongImport
|
||||||
from openlp.plugins.songs.lib.db import AuthorType
|
from openlp.plugins.songs.lib.db import AuthorType
|
||||||
|
|
||||||
@ -123,7 +124,11 @@ class VideoPsalmImport(SongImport):
|
|||||||
for verse in song['Verses']:
|
for verse in song['Verses']:
|
||||||
if 'Text' not in verse:
|
if 'Text' not in verse:
|
||||||
continue
|
continue
|
||||||
self.add_verse(verse['Text'], 'v')
|
verse_text = verse['Text']
|
||||||
|
# Strip out chords if set up to
|
||||||
|
if not Settings().value('songs/enable chords') or Settings().value('songs/disable chords import'):
|
||||||
|
verse_text = re.sub(r'\[.*?\]', '', verse_text)
|
||||||
|
self.add_verse(verse_text, 'v')
|
||||||
if not self.finish():
|
if not self.finish():
|
||||||
self.log_error('Could not import {title}'.format(title=self.title))
|
self.log_error('Could not import {title}'.format(title=self.title))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
@ -61,7 +61,7 @@ import re
|
|||||||
|
|
||||||
from lxml import etree, objectify
|
from lxml import etree, objectify
|
||||||
|
|
||||||
from openlp.core.common import translate
|
from openlp.core.common import translate, Settings
|
||||||
from openlp.core.common.versionchecker import get_application_version
|
from openlp.core.common.versionchecker import get_application_version
|
||||||
from openlp.core.lib import FormattingTags
|
from openlp.core.lib import FormattingTags
|
||||||
from openlp.plugins.songs.lib import VerseType, clean_song
|
from openlp.plugins.songs.lib import VerseType, clean_song
|
||||||
@ -154,7 +154,7 @@ class OpenLyrics(object):
|
|||||||
OpenLP does not support the attribute *lang*.
|
OpenLP does not support the attribute *lang*.
|
||||||
|
|
||||||
``<chord>``
|
``<chord>``
|
||||||
This property is not supported.
|
This property is fully supported.
|
||||||
|
|
||||||
``<comments>``
|
``<comments>``
|
||||||
The ``<comments>`` property is fully supported. But comments in lyrics are not supported.
|
The ``<comments>`` property is fully supported. But comments in lyrics are not supported.
|
||||||
@ -323,7 +323,19 @@ class OpenLyrics(object):
|
|||||||
# Do not add the break attribute to the last lines element.
|
# Do not add the break attribute to the last lines element.
|
||||||
if index < len(optional_verses) - 1:
|
if index < len(optional_verses) - 1:
|
||||||
lines_element.set('break', 'optional')
|
lines_element.set('break', 'optional')
|
||||||
return self._extract_xml(song_xml).decode()
|
xml_text = self._extract_xml(song_xml).decode()
|
||||||
|
return self._chordpro_to_openlyrics(xml_text)
|
||||||
|
|
||||||
|
def _chordpro_to_openlyrics(self, text):
|
||||||
|
"""
|
||||||
|
Convert chords from Chord Pro format to Open Lyrics format
|
||||||
|
|
||||||
|
:param text: the lyric with chords
|
||||||
|
:return: the lyrics with the converted chords
|
||||||
|
"""
|
||||||
|
# Process chords.
|
||||||
|
new_text = re.sub(r'\[(\w.*?)\]', r'<chord name="\1"/>', text)
|
||||||
|
return new_text
|
||||||
|
|
||||||
def _get_missing_tags(self, text):
|
def _get_missing_tags(self, text):
|
||||||
"""
|
"""
|
||||||
@ -595,8 +607,7 @@ class OpenLyrics(object):
|
|||||||
|
|
||||||
def _process_lines_mixed_content(self, element, newlines=True):
|
def _process_lines_mixed_content(self, element, newlines=True):
|
||||||
"""
|
"""
|
||||||
Converts the xml text with mixed content to OpenLP representation. Chords are skipped and formatting tags are
|
Converts the xml text with mixed content to OpenLP representation. Chords and formatting tags are converted.
|
||||||
converted.
|
|
||||||
|
|
||||||
:param element: The property object (lxml.etree.Element).
|
:param element: The property object (lxml.etree.Element).
|
||||||
:param newlines: The switch to enable/disable processing of line breaks <br/>. The <br/> is used since
|
:param newlines: The switch to enable/disable processing of line breaks <br/>. The <br/> is used since
|
||||||
@ -608,12 +619,14 @@ class OpenLyrics(object):
|
|||||||
# TODO: Verify format() with template variables
|
# TODO: Verify format() with template variables
|
||||||
if element.tag == NSMAP % 'comment':
|
if element.tag == NSMAP % 'comment':
|
||||||
if element.tail:
|
if element.tail:
|
||||||
# Append tail text at chord element.
|
# Append tail text at comment element.
|
||||||
text += element.tail
|
text += element.tail
|
||||||
return text
|
return text
|
||||||
# Skip <chord> element - not yet supported.
|
# Convert chords to ChordPro format which OpenLP uses internally
|
||||||
# TODO: Verify format() with template variables
|
# TODO: Verify format() with template variables
|
||||||
elif element.tag == NSMAP % 'chord':
|
elif element.tag == NSMAP % 'chord':
|
||||||
|
if Settings().value('songs/enable chords') and not Settings().value('songs/disable chords import'):
|
||||||
|
text += '[{chord}]'.format(chord=element.get('name'))
|
||||||
if element.tail:
|
if element.tail:
|
||||||
# Append tail text at chord element.
|
# Append tail text at chord element.
|
||||||
text += element.tail
|
text += element.tail
|
||||||
@ -666,7 +679,7 @@ class OpenLyrics(object):
|
|||||||
text = self._process_lines_mixed_content(element)
|
text = self._process_lines_mixed_content(element)
|
||||||
# OpenLyrics version <= 0.7 contains <line> elements to represent lines. First child element is tested.
|
# OpenLyrics version <= 0.7 contains <line> elements to represent lines. First child element is tested.
|
||||||
else:
|
else:
|
||||||
# Loop over the "line" elements removing comments and chords.
|
# Loop over the "line" elements removing comments
|
||||||
for line in element:
|
for line in element:
|
||||||
# Skip comment lines.
|
# Skip comment lines.
|
||||||
# TODO: Verify format() with template variables
|
# TODO: Verify format() with template variables
|
||||||
|
@ -60,6 +60,35 @@ class SongsTab(SettingsTab):
|
|||||||
self.display_copyright_check_box.setObjectName('copyright_check_box')
|
self.display_copyright_check_box.setObjectName('copyright_check_box')
|
||||||
self.mode_layout.addWidget(self.display_copyright_check_box)
|
self.mode_layout.addWidget(self.display_copyright_check_box)
|
||||||
self.left_layout.addWidget(self.mode_group_box)
|
self.left_layout.addWidget(self.mode_group_box)
|
||||||
|
# Chords group box
|
||||||
|
self.chords_group_box = QtWidgets.QGroupBox(self.left_column)
|
||||||
|
self.chords_group_box.setObjectName('chords_group_box')
|
||||||
|
self.chords_group_box.setCheckable(True)
|
||||||
|
self.chords_layout = QtWidgets.QVBoxLayout(self.chords_group_box)
|
||||||
|
self.chords_layout.setObjectName('chords_layout')
|
||||||
|
self.chords_info_label = QtWidgets.QLabel(self.chords_group_box)
|
||||||
|
self.chords_info_label.setWordWrap(True)
|
||||||
|
self.chords_layout.addWidget(self.chords_info_label)
|
||||||
|
self.mainview_chords_check_box = QtWidgets.QCheckBox(self.mode_group_box)
|
||||||
|
self.mainview_chords_check_box.setObjectName('mainview_chords_check_box')
|
||||||
|
self.chords_layout.addWidget(self.mainview_chords_check_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.chords_layout.addWidget(self.disable_chords_import_check_box)
|
||||||
|
# Chords notation group box
|
||||||
|
self.chord_notation_label = QtWidgets.QLabel(self.chords_group_box)
|
||||||
|
self.chord_notation_label.setWordWrap(True)
|
||||||
|
self.chords_layout.addWidget(self.chord_notation_label)
|
||||||
|
self.english_notation_radio_button = QtWidgets.QRadioButton(self.chords_group_box)
|
||||||
|
self.english_notation_radio_button.setObjectName('english_notation_radio_button')
|
||||||
|
self.chords_layout.addWidget(self.english_notation_radio_button)
|
||||||
|
self.german_notation_radio_button = QtWidgets.QRadioButton(self.chords_group_box)
|
||||||
|
self.german_notation_radio_button.setObjectName('german_notation_radio_button')
|
||||||
|
self.chords_layout.addWidget(self.german_notation_radio_button)
|
||||||
|
self.neolatin_notation_radio_button = QtWidgets.QRadioButton(self.chords_group_box)
|
||||||
|
self.neolatin_notation_radio_button.setObjectName('neolatin_notation_radio_button')
|
||||||
|
self.chords_layout.addWidget(self.neolatin_notation_radio_button)
|
||||||
|
self.left_layout.addWidget(self.chords_group_box)
|
||||||
self.left_layout.addStretch()
|
self.left_layout.addStretch()
|
||||||
self.right_layout.addStretch()
|
self.right_layout.addStretch()
|
||||||
self.tool_bar_active_check_box.stateChanged.connect(self.on_tool_bar_active_check_box_changed)
|
self.tool_bar_active_check_box.stateChanged.connect(self.on_tool_bar_active_check_box_changed)
|
||||||
@ -68,6 +97,11 @@ class SongsTab(SettingsTab):
|
|||||||
self.display_songbook_check_box.stateChanged.connect(self.on_songbook_check_box_changed)
|
self.display_songbook_check_box.stateChanged.connect(self.on_songbook_check_box_changed)
|
||||||
self.display_written_by_check_box.stateChanged.connect(self.on_written_by_check_box_changed)
|
self.display_written_by_check_box.stateChanged.connect(self.on_written_by_check_box_changed)
|
||||||
self.display_copyright_check_box.stateChanged.connect(self.on_copyright_check_box_changed)
|
self.display_copyright_check_box.stateChanged.connect(self.on_copyright_check_box_changed)
|
||||||
|
self.mainview_chords_check_box.stateChanged.connect(self.on_mainview_chords_check_box_changed)
|
||||||
|
self.disable_chords_import_check_box.stateChanged.connect(self.on_disable_chords_import_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)
|
||||||
|
|
||||||
def retranslateUi(self):
|
def retranslateUi(self):
|
||||||
self.mode_group_box.setTitle(translate('SongsPlugin.SongsTab', 'Song related settings'))
|
self.mode_group_box.setTitle(translate('SongsPlugin.SongsTab', 'Song related settings'))
|
||||||
@ -82,6 +116,17 @@ class SongsTab(SettingsTab):
|
|||||||
self.display_copyright_check_box.setText(translate('SongsPlugin.SongsTab',
|
self.display_copyright_check_box.setText(translate('SongsPlugin.SongsTab',
|
||||||
'Display "{symbol}" symbol before copyright '
|
'Display "{symbol}" symbol before copyright '
|
||||||
'info').format(symbol=SongStrings.CopyrightSymbol))
|
'info').format(symbol=SongStrings.CopyrightSymbol))
|
||||||
|
self.chords_info_label.setText(translate('SongsPlugin.SongsTab', 'If enabled all text between "[" and "]" will '
|
||||||
|
'be regarded as chords.'))
|
||||||
|
self.chords_group_box.setTitle(translate('SongsPlugin.SongsTab', 'Chords'))
|
||||||
|
self.mainview_chords_check_box.setText(translate('SongsPlugin.SongsTab', 'Display chords in the main view'))
|
||||||
|
self.disable_chords_import_check_box.setText(translate('SongsPlugin.SongsTab',
|
||||||
|
'Ignore chords when importing songs'))
|
||||||
|
self.chord_notation_label.setText(translate('SongsPlugin.SongsTab', 'Chord notation to use:'))
|
||||||
|
self.english_notation_radio_button.setText(translate('SongsPlugin.SongsTab', 'English') + ' (C-D-E-F-G-A-B)')
|
||||||
|
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)')
|
||||||
|
|
||||||
def on_search_as_type_check_box_changed(self, check_state):
|
def on_search_as_type_check_box_changed(self, check_state):
|
||||||
self.song_search = (check_state == QtCore.Qt.Checked)
|
self.song_search = (check_state == QtCore.Qt.Checked)
|
||||||
@ -104,6 +149,21 @@ class SongsTab(SettingsTab):
|
|||||||
def on_copyright_check_box_changed(self, check_state):
|
def on_copyright_check_box_changed(self, check_state):
|
||||||
self.display_copyright_symbol = (check_state == QtCore.Qt.Checked)
|
self.display_copyright_symbol = (check_state == QtCore.Qt.Checked)
|
||||||
|
|
||||||
|
def on_mainview_chords_check_box_changed(self, check_state):
|
||||||
|
self.mainview_chords = (check_state == QtCore.Qt.Checked)
|
||||||
|
|
||||||
|
def on_disable_chords_import_check_box_changed(self, check_state):
|
||||||
|
self.disable_chords_import = (check_state == QtCore.Qt.Checked)
|
||||||
|
|
||||||
|
def on_english_notation_button_clicked(self):
|
||||||
|
self.chord_notation = 'english'
|
||||||
|
|
||||||
|
def on_german_notation_button_clicked(self):
|
||||||
|
self.chord_notation = 'german'
|
||||||
|
|
||||||
|
def on_neolatin_notation_button_clicked(self):
|
||||||
|
self.chord_notation = 'neo-latin'
|
||||||
|
|
||||||
def load(self):
|
def load(self):
|
||||||
settings = Settings()
|
settings = Settings()
|
||||||
settings.beginGroup(self.settings_section)
|
settings.beginGroup(self.settings_section)
|
||||||
@ -113,12 +173,25 @@ class SongsTab(SettingsTab):
|
|||||||
self.display_songbook = settings.value('display songbook')
|
self.display_songbook = settings.value('display songbook')
|
||||||
self.display_written_by = settings.value('display written by')
|
self.display_written_by = settings.value('display written by')
|
||||||
self.display_copyright_symbol = settings.value('display copyright symbol')
|
self.display_copyright_symbol = settings.value('display copyright symbol')
|
||||||
|
self.enable_chords = settings.value('enable chords')
|
||||||
|
self.chord_notation = settings.value('chord notation')
|
||||||
|
self.mainview_chords = settings.value('mainview chords')
|
||||||
|
self.disable_chords_import = settings.value('disable chords import')
|
||||||
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.display_songbook_check_box.setChecked(self.display_songbook)
|
self.display_songbook_check_box.setChecked(self.display_songbook)
|
||||||
self.display_written_by_check_box.setChecked(self.display_written_by)
|
self.display_written_by_check_box.setChecked(self.display_written_by)
|
||||||
self.display_copyright_check_box.setChecked(self.display_copyright_symbol)
|
self.display_copyright_check_box.setChecked(self.display_copyright_symbol)
|
||||||
|
self.chords_group_box.setChecked(self.enable_chords)
|
||||||
|
self.mainview_chords_check_box.setChecked(self.mainview_chords)
|
||||||
|
self.disable_chords_import_check_box.setChecked(self.disable_chords_import)
|
||||||
|
if self.chord_notation == 'german':
|
||||||
|
self.german_notation_radio_button.setChecked(True)
|
||||||
|
elif self.chord_notation == 'neo-latin':
|
||||||
|
self.neolatin_notation_radio_button.setChecked(True)
|
||||||
|
else:
|
||||||
|
self.english_notation_radio_button.setChecked(True)
|
||||||
settings.endGroup()
|
settings.endGroup()
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
@ -130,6 +203,10 @@ class SongsTab(SettingsTab):
|
|||||||
settings.setValue('display songbook', self.display_songbook)
|
settings.setValue('display songbook', self.display_songbook)
|
||||||
settings.setValue('display written by', self.display_written_by)
|
settings.setValue('display written by', self.display_written_by)
|
||||||
settings.setValue('display copyright symbol', self.display_copyright_symbol)
|
settings.setValue('display copyright symbol', self.display_copyright_symbol)
|
||||||
|
settings.setValue('enable chords', self.chords_group_box.isChecked())
|
||||||
|
settings.setValue('mainview chords', self.mainview_chords)
|
||||||
|
settings.setValue('disable chords import', self.disable_chords_import)
|
||||||
|
settings.setValue('chord notation', self.chord_notation)
|
||||||
settings.endGroup()
|
settings.endGroup()
|
||||||
if self.tab_visited:
|
if self.tab_visited:
|
||||||
self.settings_form.register_post_process('songs_config_updated')
|
self.settings_form.register_post_process('songs_config_updated')
|
||||||
|
@ -66,7 +66,11 @@ __default_settings__ = {
|
|||||||
'songs/last directory export': '',
|
'songs/last directory export': '',
|
||||||
'songs/songselect username': '',
|
'songs/songselect username': '',
|
||||||
'songs/songselect password': '',
|
'songs/songselect password': '',
|
||||||
'songs/songselect searches': ''
|
'songs/songselect searches': '',
|
||||||
|
'songs/enable chords': True,
|
||||||
|
'songs/chord notation': 'english', # Can be english, german or neo-latin
|
||||||
|
'songs/mainview chords': False,
|
||||||
|
'songs/disable chords import': False,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -250,5 +250,6 @@ def main():
|
|||||||
print_qt_image_formats()
|
print_qt_image_formats()
|
||||||
print_enchant_backends_and_languages()
|
print_enchant_backends_and_languages()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
@ -217,5 +217,6 @@ def main():
|
|||||||
else:
|
else:
|
||||||
parser.print_help()
|
parser.print_help()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
[pep8]
|
[pep8]
|
||||||
exclude=resources.py,vlc.py
|
exclude=resources.py,vlc.py
|
||||||
max-line-length = 120
|
max-line-length = 120
|
||||||
ignore = E402
|
ignore = E402,E722
|
||||||
|
@ -121,11 +121,11 @@ class TestCategoryActionList(TestCase):
|
|||||||
self.list.add(self.action2)
|
self.list.add(self.action2)
|
||||||
|
|
||||||
# WHEN: Iterating over the list
|
# WHEN: Iterating over the list
|
||||||
l = [a for a in self.list]
|
list = [a for a in self.list]
|
||||||
# THEN: Make sure they are returned in correct order
|
# THEN: Make sure they are returned in correct order
|
||||||
self.assertEquals(len(self.list), 2)
|
self.assertEquals(len(self.list), 2)
|
||||||
self.assertIs(l[0], self.action1)
|
self.assertIs(list[0], self.action1)
|
||||||
self.assertIs(l[1], self.action2)
|
self.assertIs(list[1], self.action2)
|
||||||
|
|
||||||
def test_remove(self):
|
def test_remove(self):
|
||||||
"""
|
"""
|
||||||
|
@ -8,7 +8,7 @@ from PyQt5 import QtCore, QtWebKit
|
|||||||
|
|
||||||
from openlp.core.common import Settings
|
from openlp.core.common import Settings
|
||||||
from openlp.core.lib.htmlbuilder import build_html, build_background_css, build_lyrics_css, build_lyrics_outline_css, \
|
from openlp.core.lib.htmlbuilder import build_html, build_background_css, build_lyrics_css, build_lyrics_outline_css, \
|
||||||
build_lyrics_format_css, build_footer_css, webkit_version
|
build_lyrics_format_css, build_footer_css, webkit_version, build_chords_css
|
||||||
from openlp.core.lib.theme import HorizontalType, VerticalType
|
from openlp.core.lib.theme import HorizontalType, VerticalType
|
||||||
|
|
||||||
from tests.helpers.testmixin import TestMixin
|
from tests.helpers.testmixin import TestMixin
|
||||||
@ -60,6 +60,29 @@ HTML = """
|
|||||||
position: relative;
|
position: relative;
|
||||||
top: -0.3em;
|
top: -0.3em;
|
||||||
}
|
}
|
||||||
|
/* Chords css */
|
||||||
|
.chordline {
|
||||||
|
line-height: 1.0em;
|
||||||
|
}
|
||||||
|
.chordline span.chord span {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.chordline span.chord span strong {
|
||||||
|
position: absolute;
|
||||||
|
top: -0.8em;
|
||||||
|
left: 0;
|
||||||
|
font-size: 75%;
|
||||||
|
font-weight: normal;
|
||||||
|
line-height: normal;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.firstchordline {
|
||||||
|
line-height: 1.0em;
|
||||||
|
}
|
||||||
|
.ws {
|
||||||
|
display: none;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<script>
|
<script>
|
||||||
var timer = null;
|
var timer = null;
|
||||||
@ -211,6 +234,34 @@ FOOTER_CSS_BASE = """
|
|||||||
FOOTER_CSS = FOOTER_CSS_BASE % ('nowrap')
|
FOOTER_CSS = FOOTER_CSS_BASE % ('nowrap')
|
||||||
FOOTER_CSS_WRAP = FOOTER_CSS_BASE % ('normal')
|
FOOTER_CSS_WRAP = FOOTER_CSS_BASE % ('normal')
|
||||||
FOOTER_CSS_INVALID = ''
|
FOOTER_CSS_INVALID = ''
|
||||||
|
CHORD_CSS_ENABLED = """
|
||||||
|
.chordline {
|
||||||
|
line-height: 2.0em;
|
||||||
|
}
|
||||||
|
.chordline span.chord span {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.chordline span.chord span strong {
|
||||||
|
position: absolute;
|
||||||
|
top: -0.8em;
|
||||||
|
left: 0;
|
||||||
|
font-size: 75%;
|
||||||
|
font-weight: normal;
|
||||||
|
line-height: normal;
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
.firstchordline {
|
||||||
|
line-height: 2.1em;
|
||||||
|
}
|
||||||
|
.ws {
|
||||||
|
display: inline;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}"""
|
||||||
|
|
||||||
|
__default_settings__ = {
|
||||||
|
'songs/mainview chords': False,
|
||||||
|
'songs/enable chords': True
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class Htmbuilder(TestCase, TestMixin):
|
class Htmbuilder(TestCase, TestMixin):
|
||||||
@ -222,6 +273,7 @@ class Htmbuilder(TestCase, TestMixin):
|
|||||||
Create the UI
|
Create the UI
|
||||||
"""
|
"""
|
||||||
self.build_settings()
|
self.build_settings()
|
||||||
|
Settings().extend_default_settings(__default_settings__)
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
"""
|
"""
|
||||||
@ -403,3 +455,17 @@ class Htmbuilder(TestCase, TestMixin):
|
|||||||
# WHEN: Retrieving the webkit version
|
# WHEN: Retrieving the webkit version
|
||||||
# THEN: Webkit versions should match
|
# THEN: Webkit versions should match
|
||||||
self.assertEquals(webkit_version(), webkit_ver, "The returned webkit version doesn't match the installed one")
|
self.assertEquals(webkit_version(), webkit_ver, "The returned webkit version doesn't match the installed one")
|
||||||
|
|
||||||
|
def test_build_chords_css(self):
|
||||||
|
"""
|
||||||
|
Test the build_chords_css() function
|
||||||
|
"""
|
||||||
|
# GIVEN: A setting that activates chords on the mainview
|
||||||
|
Settings().setValue('songs/enable chords', True)
|
||||||
|
Settings().setValue('songs/mainview chords', True)
|
||||||
|
|
||||||
|
# WHEN: Building the chord CSS
|
||||||
|
chord_css = build_chords_css()
|
||||||
|
|
||||||
|
# THEN: The build css should look as expected
|
||||||
|
self.assertEqual(CHORD_CSS_ENABLED, chord_css, 'The chord CSS should look as expected')
|
||||||
|
@ -29,8 +29,10 @@ from unittest.mock import MagicMock, patch
|
|||||||
|
|
||||||
from PyQt5 import QtCore, QtGui
|
from PyQt5 import QtCore, QtGui
|
||||||
|
|
||||||
|
from openlp.core.lib import FormattingTags, expand_chords_for_printing
|
||||||
from openlp.core.lib import build_icon, check_item_selected, clean_tags, create_thumb, create_separated_list, \
|
from openlp.core.lib import build_icon, check_item_selected, clean_tags, create_thumb, create_separated_list, \
|
||||||
expand_tags, get_text_file_string, image_to_byte, resize_image, str_to_bool, validate_thumb
|
expand_tags, get_text_file_string, image_to_byte, resize_image, str_to_bool, validate_thumb, expand_chords, \
|
||||||
|
compare_chord_lyric, find_formatting_tags
|
||||||
|
|
||||||
TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'resources'))
|
TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'resources'))
|
||||||
|
|
||||||
@ -745,3 +747,116 @@ class TestLib(TestCase):
|
|||||||
# THEN: We should have "Author 1, Author 2 and Author 3"
|
# THEN: We should have "Author 1, Author 2 and Author 3"
|
||||||
self.assertEqual(string_result, 'Author 1, Author 2 and Author 3', 'The string should be "Author 1, '
|
self.assertEqual(string_result, 'Author 1, Author 2 and Author 3', 'The string should be "Author 1, '
|
||||||
'Author 2, and Author 3".')
|
'Author 2, and Author 3".')
|
||||||
|
|
||||||
|
def test_expand_chords(self):
|
||||||
|
"""
|
||||||
|
Test that the expanding of chords works as expected.
|
||||||
|
"""
|
||||||
|
# GIVEN: A lyrics-line with chords
|
||||||
|
text_with_chords = 'H[C]alleluya.[F] [G]'
|
||||||
|
|
||||||
|
# WHEN: Expanding the chords
|
||||||
|
text_with_expanded_chords = expand_chords(text_with_chords)
|
||||||
|
|
||||||
|
# THEN: We should get html that looks like below
|
||||||
|
expected_html = '<span class="chordline firstchordline">H<span class="chord"><span><strong>C</strong></span>' \
|
||||||
|
'</span>alleluya.<span class="chord"><span><strong>F</strong></span></span><span class="ws">' \
|
||||||
|
' </span> <span class="chord"><span><strong>G</strong></span></span></span>'
|
||||||
|
self.assertEqual(expected_html, text_with_expanded_chords, 'The expanded chords should look as expected!')
|
||||||
|
|
||||||
|
def test_expand_chords2(self):
|
||||||
|
"""
|
||||||
|
Test that the expanding of chords works as expected when special chars are involved.
|
||||||
|
"""
|
||||||
|
import html
|
||||||
|
# GIVEN: A lyrics-line with chords
|
||||||
|
text_with_chords = "I[D]'M NOT MOVED BY WHAT I SEE HALLE[F]LUJA[C]H"
|
||||||
|
|
||||||
|
# WHEN: Expanding the chords
|
||||||
|
text_with_expanded_chords = expand_tags(text_with_chords, True)
|
||||||
|
|
||||||
|
# THEN: We should get html that looks like below
|
||||||
|
expected_html = '<span class="chordline firstchordline">I<span class="chord"><span><strong>D</strong></span>' \
|
||||||
|
'</span>'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>'
|
||||||
|
self.assertEqual(expected_html, text_with_expanded_chords, 'The expanded chords should look as expected!')
|
||||||
|
|
||||||
|
def test_compare_chord_lyric_short_chord(self):
|
||||||
|
"""
|
||||||
|
Test that the chord/lyric comparing works.
|
||||||
|
"""
|
||||||
|
# GIVEN: A chord and some lyric
|
||||||
|
chord = 'C'
|
||||||
|
lyrics = 'alleluya'
|
||||||
|
|
||||||
|
# WHEN: Comparing the chord and lyrics
|
||||||
|
ret = compare_chord_lyric(chord, lyrics)
|
||||||
|
|
||||||
|
# THEN: The returned value should 0 because the lyric is longer than the chord
|
||||||
|
self.assertEquals(0, ret, 'The returned value should 0 because the lyric is longer than the chord')
|
||||||
|
|
||||||
|
def test_compare_chord_lyric_long_chord(self):
|
||||||
|
"""
|
||||||
|
Test that the chord/lyric comparing works.
|
||||||
|
"""
|
||||||
|
# GIVEN: A chord and some lyric
|
||||||
|
chord = 'Gsus'
|
||||||
|
lyrics = 'me'
|
||||||
|
|
||||||
|
# WHEN: Comparing the chord and lyrics
|
||||||
|
ret = compare_chord_lyric(chord, lyrics)
|
||||||
|
|
||||||
|
# THEN: The returned value should 4 because the chord is longer than the lyric
|
||||||
|
self.assertEquals(4, ret, 'The returned value should 4 because the chord is longer than the lyric')
|
||||||
|
|
||||||
|
def test_find_formatting_tags(self):
|
||||||
|
"""
|
||||||
|
Test that find_formatting_tags works as expected
|
||||||
|
"""
|
||||||
|
# GIVEN: Lyrics with formatting tags and a empty list of formatting tags
|
||||||
|
lyrics = '{st}Amazing {r}grace{/r} how sweet the sound'
|
||||||
|
tags = []
|
||||||
|
FormattingTags.load_tags()
|
||||||
|
|
||||||
|
# WHEN: Detecting active formatting tags
|
||||||
|
active_tags = find_formatting_tags(lyrics, tags)
|
||||||
|
|
||||||
|
# THEN: The list of active tags should contain only 'st'
|
||||||
|
self.assertListEqual(['st'], active_tags, 'The list of active tags should contain only "st"')
|
||||||
|
|
||||||
|
def test_expand_chords_for_printing(self):
|
||||||
|
"""
|
||||||
|
Test that the expanding of chords for printing works as expected.
|
||||||
|
"""
|
||||||
|
# GIVEN: A lyrics-line with chords
|
||||||
|
text_with_chords = '{st}[D]Amazing {r}gr[D7]ace{/r} how [G]sweet the [D]sound [F]{/st}'
|
||||||
|
FormattingTags.load_tags()
|
||||||
|
|
||||||
|
# WHEN: Expanding the chords
|
||||||
|
text_with_expanded_chords = expand_chords_for_printing(text_with_chords, '{br}')
|
||||||
|
|
||||||
|
# THEN: We should get html that looks like below
|
||||||
|
expected_html = '<table class="line" width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td><table ' \
|
||||||
|
'class="segment" cellpadding="0" cellspacing="0" border="0" align="left"><tr class="chordrow">'\
|
||||||
|
'<td class="chord"> </td><td class="chord">D</td></tr><tr><td class="lyrics">{st}{/st}' \
|
||||||
|
'</td><td class="lyrics">{st}Amazing {/st}</td></tr></table><table class="segment" ' \
|
||||||
|
'cellpadding="0" cellspacing="0" border="0" align="left"><tr class="chordrow">' \
|
||||||
|
'<td class="chord"> </td><td class="chord">D7</td></tr><tr><td class="lyrics">{st}{r}gr' \
|
||||||
|
'{/r}{/st}</td><td class="lyrics">{r}{st}ace{/r} {/st}</td></tr></table><table ' \
|
||||||
|
'class="segment" cellpadding="0" cellspacing="0" border="0" align="left"><tr class="chordrow">'\
|
||||||
|
'<td class="chord"> </td></tr><tr><td class="lyrics">{st} {/st}</td></tr></table>' \
|
||||||
|
'<table class="segment" cellpadding="0" cellspacing="0" border="0" align="left"><tr ' \
|
||||||
|
'class="chordrow"><td class="chord"> </td></tr><tr><td class="lyrics">{st}how {/st}' \
|
||||||
|
'</td></tr></table><table class="segment" cellpadding="0" cellspacing="0" border="0" ' \
|
||||||
|
'align="left"><tr class="chordrow"><td class="chord">G</td></tr><tr><td class="lyrics">{st}' \
|
||||||
|
'sweet {/st}</td></tr></table><table class="segment" cellpadding="0" cellspacing="0" ' \
|
||||||
|
'border="0" align="left"><tr class="chordrow"><td class="chord"> </td></tr><tr><td ' \
|
||||||
|
'class="lyrics">{st}the {/st}</td></tr></table><table class="segment" cellpadding="0" ' \
|
||||||
|
'cellspacing="0" border="0" align="left"><tr class="chordrow"><td class="chord">D</td></tr>' \
|
||||||
|
'<tr><td class="lyrics">{st}sound {/st}</td></tr></table><table class="segment" ' \
|
||||||
|
'cellpadding="0" cellspacing="0" border="0" align="left"><tr class="chordrow"><td ' \
|
||||||
|
'class="chord"> </td></tr><tr><td class="lyrics">{st} {/st}</td></tr></table>' \
|
||||||
|
'<table class="segment" cellpadding="0" cellspacing="0" border="0" align="left"><tr ' \
|
||||||
|
'class="chordrow"><td class="chord">F</td></tr><tr><td class="lyrics">{st}{/st} </td>' \
|
||||||
|
'</tr></table></td></tr></table>'
|
||||||
|
self.assertEqual(expected_html, text_with_expanded_chords, 'The expanded chords should look as expected!')
|
||||||
|
@ -27,7 +27,7 @@ from unittest import TestCase
|
|||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
from openlp.core.common import Registry, md5_hash
|
from openlp.core.common import Registry, md5_hash
|
||||||
from openlp.core.lib import ItemCapabilities, ServiceItem, ServiceItemType
|
from openlp.core.lib import ItemCapabilities, ServiceItem, ServiceItemType, FormattingTags
|
||||||
|
|
||||||
from tests.utils import assert_length, convert_file_service_item
|
from tests.utils import assert_length, convert_file_service_item
|
||||||
|
|
||||||
@ -38,6 +38,23 @@ VERSE = 'The Lord said to {r}Noah{/r}: \n'\
|
|||||||
'Get those children out of the muddy, muddy \n'\
|
'Get those children out of the muddy, muddy \n'\
|
||||||
'{r}C{/r}{b}h{/b}{bl}i{/bl}{y}l{/y}{g}d{/g}{pk}'\
|
'{r}C{/r}{b}h{/b}{bl}i{/bl}{y}l{/y}{g}d{/g}{pk}'\
|
||||||
'r{/pk}{o}e{/o}{pp}n{/pp} of the Lord\n'
|
'r{/pk}{o}e{/o}{pp}n{/pp} of the Lord\n'
|
||||||
|
CLEANED_VERSE = 'The Lord said to Noah: \n'\
|
||||||
|
'There\'s gonna be a floody, floody\n'\
|
||||||
|
'The Lord said to Noah:\n'\
|
||||||
|
'There\'s gonna be a floody, floody\n'\
|
||||||
|
'Get those children out of the muddy, muddy \n'\
|
||||||
|
'Children of the Lord\n'
|
||||||
|
RENDERED_VERSE = 'The Lord said to <span style="-webkit-text-fill-color:red">Noah</span>: \n'\
|
||||||
|
'There's gonna be a <sup>floody</sup>, <sub>floody</sub>\n'\
|
||||||
|
'The Lord said to <span style="-webkit-text-fill-color:green">Noah</span>:\n'\
|
||||||
|
'There's gonna be a <strong>floody</strong>, <em>floody</em>\n'\
|
||||||
|
'Get those children out of the muddy, muddy \n'\
|
||||||
|
'<span style="-webkit-text-fill-color:red">C</span><span style="-webkit-text-fill-color:black">h' \
|
||||||
|
'</span><span style="-webkit-text-fill-color:blue">i</span>'\
|
||||||
|
'<span style="-webkit-text-fill-color:yellow">l</span><span style="-webkit-text-fill-color:green">d'\
|
||||||
|
'</span><span style="-webkit-text-fill-color:#FFC0CB">r</span>'\
|
||||||
|
'<span style="-webkit-text-fill-color:#FFA500">e</span><span style="-webkit-text-fill-color:#800080">'\
|
||||||
|
'n</span> of the Lord\n'
|
||||||
FOOTER = ['Arky Arky (Unknown)', 'Public Domain', 'CCLI 123456']
|
FOOTER = ['Arky Arky (Unknown)', 'Public Domain', 'CCLI 123456']
|
||||||
TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'resources', 'service'))
|
TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'resources', 'service'))
|
||||||
|
|
||||||
@ -74,6 +91,7 @@ class TestServiceItem(TestCase):
|
|||||||
# GIVEN: A new service item and a mocked add icon function
|
# GIVEN: A new service item and a mocked add icon function
|
||||||
service_item = ServiceItem(None)
|
service_item = ServiceItem(None)
|
||||||
service_item.add_icon = MagicMock()
|
service_item.add_icon = MagicMock()
|
||||||
|
FormattingTags.load_tags()
|
||||||
|
|
||||||
# WHEN: We add a custom from a saved service
|
# WHEN: We add a custom from a saved service
|
||||||
line = convert_file_service_item(TEST_PATH, 'serviceitem_custom_1.osj')
|
line = convert_file_service_item(TEST_PATH, 'serviceitem_custom_1.osj')
|
||||||
@ -89,9 +107,9 @@ class TestServiceItem(TestCase):
|
|||||||
|
|
||||||
# THEN: The frames should also be valid
|
# THEN: The frames should also be valid
|
||||||
self.assertEqual('Test Custom', service_item.get_display_title(), 'The title should be "Test Custom"')
|
self.assertEqual('Test Custom', service_item.get_display_title(), 'The title should be "Test Custom"')
|
||||||
self.assertEqual(VERSE[:-1], service_item.get_frames()[0]['text'],
|
self.assertEqual(CLEANED_VERSE[:-1], service_item.get_frames()[0]['text'],
|
||||||
'The returned text matches the input, except the last line feed')
|
'The returned text matches the input, except the last line feed')
|
||||||
self.assertEqual(VERSE.split('\n', 1)[0], service_item.get_rendered_frame(1),
|
self.assertEqual(RENDERED_VERSE.split('\n', 1)[0], service_item.get_rendered_frame(1),
|
||||||
'The first line has been returned')
|
'The first line has been returned')
|
||||||
self.assertEqual('Slide 1', service_item.get_frame_title(0), '"Slide 1" has been returned as the title')
|
self.assertEqual('Slide 1', service_item.get_frame_title(0), '"Slide 1" has been returned as the title')
|
||||||
self.assertEqual('Slide 2', service_item.get_frame_title(1), '"Slide 2" has been returned as the title')
|
self.assertEqual('Slide 2', service_item.get_frame_title(1), '"Slide 2" has been returned as the title')
|
||||||
@ -300,6 +318,7 @@ class TestServiceItem(TestCase):
|
|||||||
# GIVEN: A new service item and a mocked add icon function
|
# GIVEN: A new service item and a mocked add icon function
|
||||||
service_item = ServiceItem(None)
|
service_item = ServiceItem(None)
|
||||||
service_item.add_icon = MagicMock()
|
service_item.add_icon = MagicMock()
|
||||||
|
FormattingTags.load_tags()
|
||||||
|
|
||||||
# WHEN: We add a custom from a saved service
|
# WHEN: We add a custom from a saved service
|
||||||
line = convert_file_service_item(TEST_PATH, 'serviceitem-song-linked-audio.osj')
|
line = convert_file_service_item(TEST_PATH, 'serviceitem-song-linked-audio.osj')
|
||||||
@ -315,9 +334,9 @@ class TestServiceItem(TestCase):
|
|||||||
|
|
||||||
# THEN: The frames should also be valid
|
# THEN: The frames should also be valid
|
||||||
self.assertEqual('Amazing Grace', service_item.get_display_title(), 'The title should be "Amazing Grace"')
|
self.assertEqual('Amazing Grace', service_item.get_display_title(), 'The title should be "Amazing Grace"')
|
||||||
self.assertEqual(VERSE[:-1], service_item.get_frames()[0]['text'],
|
self.assertEqual(CLEANED_VERSE[:-1], service_item.get_frames()[0]['text'],
|
||||||
'The returned text matches the input, except the last line feed')
|
'The returned text matches the input, except the last line feed')
|
||||||
self.assertEqual(VERSE.split('\n', 1)[0], service_item.get_rendered_frame(1),
|
self.assertEqual(RENDERED_VERSE.split('\n', 1)[0], service_item.get_rendered_frame(1),
|
||||||
'The first line has been returned')
|
'The first line has been returned')
|
||||||
self.assertEqual('Amazing Grace! how sweet the s', service_item.get_frame_title(0),
|
self.assertEqual('Amazing Grace! how sweet the s', service_item.get_frame_title(0),
|
||||||
'"Amazing Grace! how sweet the s" has been returned as the title')
|
'"Amazing Grace! how sweet the s" has been returned as the title')
|
||||||
|
52
tests/functional/openlp_plugins/songs/test_chordproimport.py
Normal file
52
tests/functional/openlp_plugins/songs/test_chordproimport.py
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# OpenLP - Open Source Lyrics Projection #
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Copyright (c) 2008-2017 OpenLP Developers #
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# This program is free software; you can redistribute it and/or modify it #
|
||||||
|
# under the terms of the GNU General Public License as published by the Free #
|
||||||
|
# Software Foundation; version 2 of the License. #
|
||||||
|
# #
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT #
|
||||||
|
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
|
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
|
||||||
|
# more details. #
|
||||||
|
# #
|
||||||
|
# You should have received a copy of the GNU General Public License along #
|
||||||
|
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||||
|
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||||
|
###############################################################################
|
||||||
|
"""
|
||||||
|
This module contains tests for the OpenSong song importer.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
|
||||||
|
from tests.helpers.songfileimport import SongImportTestHelper
|
||||||
|
from unittest.mock import patch, MagicMock
|
||||||
|
|
||||||
|
TEST_PATH = os.path.abspath(
|
||||||
|
os.path.join(os.path.dirname(__file__), '..', '..', '..', 'resources', 'chordprosongs'))
|
||||||
|
|
||||||
|
|
||||||
|
class TestChordProFileImport(SongImportTestHelper):
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.importer_class_name = 'ChordProImport'
|
||||||
|
self.importer_module_name = 'chordpro'
|
||||||
|
super(TestChordProFileImport, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
@patch('openlp.plugins.songs.lib.importers.chordpro.Settings')
|
||||||
|
def test_song_import(self, mocked_settings):
|
||||||
|
"""
|
||||||
|
Test that loading an ChordPro file works correctly on various files
|
||||||
|
"""
|
||||||
|
# Mock out the settings - always return False
|
||||||
|
mocked_returned_settings = MagicMock()
|
||||||
|
mocked_returned_settings.value.side_effect = lambda value: True if value == 'songs/enable chords' else False
|
||||||
|
mocked_settings.return_value = mocked_returned_settings
|
||||||
|
# Do the test import
|
||||||
|
self.file_import([os.path.join(TEST_PATH, 'swing-low.chordpro')],
|
||||||
|
self.load_external_result_data(os.path.join(TEST_PATH, 'swing-low.json')))
|
@ -48,7 +48,8 @@ class TestDB(TestCase):
|
|||||||
"""
|
"""
|
||||||
Clean up after tests
|
Clean up after tests
|
||||||
"""
|
"""
|
||||||
shutil.rmtree(self.tmp_folder)
|
# Ignore errors since windows can have problems with locked files
|
||||||
|
shutil.rmtree(self.tmp_folder, ignore_errors=True)
|
||||||
|
|
||||||
def test_add_author(self):
|
def test_add_author(self):
|
||||||
"""
|
"""
|
||||||
|
@ -114,6 +114,7 @@ class TestFieldDesc:
|
|||||||
self.field_type = field_type
|
self.field_type = field_type
|
||||||
self.size = size
|
self.size = size
|
||||||
|
|
||||||
|
|
||||||
TEST_DATA_ENCODING = 'cp1252'
|
TEST_DATA_ENCODING = 'cp1252'
|
||||||
CODE_PAGE_MAPPINGS = [
|
CODE_PAGE_MAPPINGS = [
|
||||||
(852, 'cp1250'), (737, 'cp1253'), (775, 'cp1257'), (855, 'cp1251'), (857, 'cp1254'),
|
(852, 'cp1250'), (737, 'cp1253'), (775, 'cp1257'), (855, 'cp1251'), (857, 'cp1254'),
|
||||||
|
@ -25,7 +25,7 @@ This module contains tests for the lib submodule of the Songs plugin.
|
|||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
from unittest.mock import patch, MagicMock, PropertyMock
|
from unittest.mock import patch, MagicMock, PropertyMock
|
||||||
|
|
||||||
from openlp.plugins.songs.lib import VerseType, clean_string, clean_title, strip_rtf
|
from openlp.plugins.songs.lib import VerseType, clean_string, clean_title, strip_rtf, transpose_chord, transpose_lyrics
|
||||||
from openlp.plugins.songs.lib.songcompare import songs_probably_equal, _remove_typos, _op_length
|
from openlp.plugins.songs.lib.songcompare import songs_probably_equal, _remove_typos, _op_length
|
||||||
|
|
||||||
|
|
||||||
@ -206,7 +206,7 @@ class TestLib(TestCase):
|
|||||||
assert result[0][3] == 0, 'The start indices should be kept.'
|
assert result[0][3] == 0, 'The start indices should be kept.'
|
||||||
assert result[0][4] == 21, 'The stop indices should be kept.'
|
assert result[0][4] == 21, 'The stop indices should be kept.'
|
||||||
|
|
||||||
def test_remove_typos_beginning_negated(self):
|
def test_remove_typos_middle_negated(self):
|
||||||
"""
|
"""
|
||||||
Test the _remove_typos function with a large difference in the middle.
|
Test the _remove_typos function with a large difference in the middle.
|
||||||
"""
|
"""
|
||||||
@ -264,6 +264,85 @@ class TestLib(TestCase):
|
|||||||
# THEN: The stripped text matches thed expected result
|
# THEN: The stripped text matches thed expected result
|
||||||
assert result == exp_result, 'The result should be %s' % exp_result
|
assert result == exp_result, 'The result should be %s' % exp_result
|
||||||
|
|
||||||
|
def test_transpose_chord_up(self):
|
||||||
|
"""
|
||||||
|
Test that the transpose_chord() method works when transposing up
|
||||||
|
"""
|
||||||
|
# GIVEN: A Chord
|
||||||
|
chord = 'C'
|
||||||
|
|
||||||
|
# WHEN: Transposing it 1 up
|
||||||
|
new_chord = transpose_chord(chord, 1, 'english')
|
||||||
|
|
||||||
|
# THEN: The chord should be transposed up one note
|
||||||
|
self.assertEqual(new_chord, 'C#', 'The chord should be transposed up.')
|
||||||
|
|
||||||
|
def test_transpose_chord_up_adv(self):
|
||||||
|
"""
|
||||||
|
Test that the transpose_chord() method works when transposing up an advanced chord
|
||||||
|
"""
|
||||||
|
# GIVEN: An advanced Chord
|
||||||
|
chord = '(C/D#)'
|
||||||
|
|
||||||
|
# WHEN: Transposing it 1 up
|
||||||
|
new_chord = transpose_chord(chord, 1, 'english')
|
||||||
|
|
||||||
|
# THEN: The chord should be transposed up one note
|
||||||
|
self.assertEqual(new_chord, '(C#/E)', 'The chord should be transposed up.')
|
||||||
|
|
||||||
|
def test_transpose_chord_down(self):
|
||||||
|
"""
|
||||||
|
Test that the transpose_chord() method works when transposing down
|
||||||
|
"""
|
||||||
|
# GIVEN: A Chord
|
||||||
|
chord = 'C'
|
||||||
|
|
||||||
|
# WHEN: Transposing it 1 down
|
||||||
|
new_chord = transpose_chord(chord, -1, 'english')
|
||||||
|
|
||||||
|
# THEN: The chord should be transposed down one note
|
||||||
|
self.assertEqual(new_chord, 'B', 'The chord should be transposed down.')
|
||||||
|
|
||||||
|
def test_transpose_chord_error(self):
|
||||||
|
"""
|
||||||
|
Test that the transpose_chord() raises exception on invalid chord
|
||||||
|
"""
|
||||||
|
# GIVEN: A invalid Chord
|
||||||
|
chord = 'T'
|
||||||
|
|
||||||
|
# WHEN: Transposing it 1 down
|
||||||
|
# THEN: An exception should be raised
|
||||||
|
with self.assertRaises(ValueError) as err:
|
||||||
|
new_chord = transpose_chord(chord, -1, 'english')
|
||||||
|
self.assertEqual(err.exception.args[0], '\'T\' is not in list',
|
||||||
|
'ValueError exception should have been thrown for invalid chord')
|
||||||
|
|
||||||
|
@patch('openlp.plugins.songs.lib.transpose_verse')
|
||||||
|
@patch('openlp.plugins.songs.lib.Settings')
|
||||||
|
def test_transpose_lyrics(self, mocked_settings, mocked_transpose_verse):
|
||||||
|
"""
|
||||||
|
Test that the transpose_lyrics() splits verses correctly
|
||||||
|
"""
|
||||||
|
# GIVEN: Lyrics with verse splitters and a mocked settings
|
||||||
|
lyrics = '---[Verse:1]---\n'\
|
||||||
|
'Amazing grace how sweet the sound\n'\
|
||||||
|
'[---]\n'\
|
||||||
|
'That saved a wretch like me.\n'\
|
||||||
|
'---[Verse:2]---\n'\
|
||||||
|
'I once was lost but now I\'m found.'
|
||||||
|
mocked_returned_settings = MagicMock()
|
||||||
|
mocked_returned_settings.value.return_value = 'english'
|
||||||
|
mocked_settings.return_value = mocked_returned_settings
|
||||||
|
|
||||||
|
# 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')
|
||||||
|
|
||||||
|
|
||||||
class TestVerseType(TestCase):
|
class TestVerseType(TestCase):
|
||||||
"""
|
"""
|
||||||
|
@ -42,10 +42,16 @@ class TestOpenSongFileImport(SongImportTestHelper):
|
|||||||
self.importer_module_name = 'opensong'
|
self.importer_module_name = 'opensong'
|
||||||
super(TestOpenSongFileImport, self).__init__(*args, **kwargs)
|
super(TestOpenSongFileImport, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
def test_song_import(self):
|
@patch('openlp.plugins.songs.lib.importers.opensong.Settings')
|
||||||
|
def test_song_import(self, mocked_settings):
|
||||||
"""
|
"""
|
||||||
Test that loading an OpenSong file works correctly on various files
|
Test that loading an OpenSong file works correctly on various files
|
||||||
"""
|
"""
|
||||||
|
# Mock out the settings - always return False
|
||||||
|
mocked_returned_settings = MagicMock()
|
||||||
|
mocked_returned_settings.value.side_effect = lambda value: True if value == 'songs/enable chords' else False
|
||||||
|
mocked_settings.return_value = mocked_returned_settings
|
||||||
|
# Do the test import
|
||||||
self.file_import([os.path.join(TEST_PATH, 'Amazing Grace')],
|
self.file_import([os.path.join(TEST_PATH, 'Amazing Grace')],
|
||||||
self.load_external_result_data(os.path.join(TEST_PATH, 'Amazing Grace.json')))
|
self.load_external_result_data(os.path.join(TEST_PATH, 'Amazing Grace.json')))
|
||||||
self.file_import([os.path.join(TEST_PATH, 'Beautiful Garden Of Prayer')],
|
self.file_import([os.path.join(TEST_PATH, 'Beautiful Garden Of Prayer')],
|
||||||
|
@ -42,12 +42,21 @@ class TestSongBeamerFileImport(SongImportTestHelper):
|
|||||||
self.importer_module_name = 'songbeamer'
|
self.importer_module_name = 'songbeamer'
|
||||||
super(TestSongBeamerFileImport, self).__init__(*args, **kwargs)
|
super(TestSongBeamerFileImport, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
def test_song_import(self):
|
@patch('openlp.plugins.songs.lib.importers.songbeamer.Settings')
|
||||||
|
def test_song_import(self, mocked_settings):
|
||||||
"""
|
"""
|
||||||
Test that loading an OpenSong file works correctly on various files
|
Test that loading an SongBeamer file works correctly on various files
|
||||||
"""
|
"""
|
||||||
|
# Mock out the settings - always return False
|
||||||
|
mocked_returned_settings = MagicMock()
|
||||||
|
mocked_returned_settings.value.side_effect = lambda value: True if value == 'songs/enable chords' else False
|
||||||
|
mocked_settings.return_value = mocked_returned_settings
|
||||||
|
self.file_import([os.path.join(TEST_PATH, 'Amazing Grace.sng')],
|
||||||
|
self.load_external_result_data(os.path.join(TEST_PATH, 'Amazing Grace.json')))
|
||||||
self.file_import([os.path.join(TEST_PATH, 'Lobsinget dem Herrn.sng')],
|
self.file_import([os.path.join(TEST_PATH, 'Lobsinget dem Herrn.sng')],
|
||||||
self.load_external_result_data(os.path.join(TEST_PATH, 'Lobsinget dem Herrn.json')))
|
self.load_external_result_data(os.path.join(TEST_PATH, 'Lobsinget dem Herrn.json')))
|
||||||
|
self.file_import([os.path.join(TEST_PATH, 'When I Call On You.sng')],
|
||||||
|
self.load_external_result_data(os.path.join(TEST_PATH, 'When I Call On You.json')))
|
||||||
|
|
||||||
def test_cp1252_encoded_file(self):
|
def test_cp1252_encoded_file(self):
|
||||||
"""
|
"""
|
||||||
@ -66,6 +75,16 @@ class TestSongBeamerImport(TestCase):
|
|||||||
Create the registry
|
Create the registry
|
||||||
"""
|
"""
|
||||||
Registry.create()
|
Registry.create()
|
||||||
|
self.song_import_patcher = patch('openlp.plugins.songs.lib.importers.songbeamer.SongImport')
|
||||||
|
self.song_import_patcher.start()
|
||||||
|
mocked_manager = MagicMock()
|
||||||
|
self.importer = SongBeamerImport(mocked_manager, filenames=[])
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
"""
|
||||||
|
Clean up
|
||||||
|
"""
|
||||||
|
self.song_import_patcher.stop()
|
||||||
|
|
||||||
def test_create_importer(self):
|
def test_create_importer(self):
|
||||||
"""
|
"""
|
||||||
@ -85,43 +104,38 @@ class TestSongBeamerImport(TestCase):
|
|||||||
"""
|
"""
|
||||||
Test SongBeamerImport.do_import handles different invalid import_source values
|
Test SongBeamerImport.do_import handles different invalid import_source values
|
||||||
"""
|
"""
|
||||||
# GIVEN: A mocked out SongImport class, and a mocked out "manager"
|
# GIVEN: A mocked out import wizard
|
||||||
with patch('openlp.plugins.songs.lib.importers.songbeamer.SongImport'):
|
mocked_import_wizard = MagicMock()
|
||||||
mocked_manager = MagicMock()
|
self.importer.import_wizard = mocked_import_wizard
|
||||||
mocked_import_wizard = MagicMock()
|
self.importer.stop_import_flag = True
|
||||||
importer = SongBeamerImport(mocked_manager, filenames=[])
|
|
||||||
importer.import_wizard = mocked_import_wizard
|
|
||||||
importer.stop_import_flag = True
|
|
||||||
|
|
||||||
# WHEN: Import source is not a list
|
# WHEN: Import source is not a list
|
||||||
for source in ['not a list', 0]:
|
for source in ['not a list', 0]:
|
||||||
importer.import_source = source
|
self.importer.import_source = source
|
||||||
|
|
||||||
# THEN: do_import should return none and the progress bar maximum should not be set.
|
# THEN: do_import should return none and the progress bar maximum should not be set.
|
||||||
self.assertIsNone(importer.do_import(), 'do_import should return None when import_source is not a list')
|
self.assertIsNone(self.importer.do_import(),
|
||||||
self.assertEqual(mocked_import_wizard.progress_bar.setMaximum.called, False,
|
'do_import should return None when import_source is not a list')
|
||||||
'setMaxium on import_wizard.progress_bar should not have been called')
|
self.assertEqual(mocked_import_wizard.progress_bar.setMaximum.called, False,
|
||||||
|
'setMaxium on import_wizard.progress_bar should not have been called')
|
||||||
|
|
||||||
def test_valid_import_source(self):
|
def test_valid_import_source(self):
|
||||||
"""
|
"""
|
||||||
Test SongBeamerImport.do_import handles different invalid import_source values
|
Test SongBeamerImport.do_import handles different invalid import_source values
|
||||||
"""
|
"""
|
||||||
# GIVEN: A mocked out SongImport class, and a mocked out "manager"
|
# GIVEN: A mocked out import wizard
|
||||||
with patch('openlp.plugins.songs.lib.importers.songbeamer.SongImport'):
|
mocked_import_wizard = MagicMock()
|
||||||
mocked_manager = MagicMock()
|
self.importer.import_wizard = mocked_import_wizard
|
||||||
mocked_import_wizard = MagicMock()
|
self.importer.stop_import_flag = True
|
||||||
importer = SongBeamerImport(mocked_manager, filenames=[])
|
|
||||||
importer.import_wizard = mocked_import_wizard
|
|
||||||
importer.stop_import_flag = True
|
|
||||||
|
|
||||||
# WHEN: Import source is a list
|
# WHEN: Import source is a list
|
||||||
importer.import_source = ['List', 'of', 'files']
|
self.importer.import_source = ['List', 'of', 'files']
|
||||||
|
|
||||||
# THEN: do_import should return none and the progress bar setMaximum should be called with the length of
|
# THEN: do_import should return none and the progress bar setMaximum should be called with the length of
|
||||||
# import_source.
|
# import_source.
|
||||||
self.assertIsNone(importer.do_import(),
|
self.assertIsNone(self.importer.do_import(),
|
||||||
'do_import should return None when import_source is a list and stop_import_flag is True')
|
'do_import should return None when import_source is a list and stop_import_flag is True')
|
||||||
mocked_import_wizard.progress_bar.setMaximum.assert_called_with(len(importer.import_source))
|
mocked_import_wizard.progress_bar.setMaximum.assert_called_with(len(self.importer.import_source))
|
||||||
|
|
||||||
def test_check_verse_marks(self):
|
def test_check_verse_marks(self):
|
||||||
"""
|
"""
|
||||||
@ -130,75 +144,76 @@ class TestSongBeamerImport(TestCase):
|
|||||||
|
|
||||||
# GIVEN: line with unnumbered verse-type
|
# GIVEN: line with unnumbered verse-type
|
||||||
line = 'Refrain'
|
line = 'Refrain'
|
||||||
self.current_verse_type = None
|
self.importer.current_verse_type = None
|
||||||
# WHEN: line is being checked for verse marks
|
# WHEN: line is being checked for verse marks
|
||||||
result = SongBeamerImport.check_verse_marks(self, line)
|
result = self.importer.check_verse_marks(line)
|
||||||
# THEN: we should get back true and c as self.current_verse_type
|
# THEN: we should get back true and c as self.importer.current_verse_type
|
||||||
self.assertTrue(result, 'Versemark for <Refrain> should be found, value true')
|
self.assertTrue(result, 'Versemark for <Refrain> should be found, value true')
|
||||||
self.assertEqual(self.current_verse_type, 'c', '<Refrain> should be interpreted as <c>')
|
self.assertEqual(self.importer.current_verse_type, 'c', '<Refrain> should be interpreted as <c>')
|
||||||
|
|
||||||
# GIVEN: line with unnumbered verse-type and trailing space
|
# GIVEN: line with unnumbered verse-type and trailing space
|
||||||
line = 'ReFrain '
|
line = 'ReFrain '
|
||||||
self.current_verse_type = None
|
self.importer.current_verse_type = None
|
||||||
# WHEN: line is being checked for verse marks
|
# WHEN: line is being checked for verse marks
|
||||||
result = SongBeamerImport.check_verse_marks(self, line)
|
result = self.importer.check_verse_marks(line)
|
||||||
# THEN: we should get back true and c as self.current_verse_type
|
# THEN: we should get back true and c as self.importer.current_verse_type
|
||||||
self.assertTrue(result, 'Versemark for <ReFrain > should be found, value true')
|
self.assertTrue(result, 'Versemark for <ReFrain > should be found, value true')
|
||||||
self.assertEqual(self.current_verse_type, 'c', '<ReFrain > should be interpreted as <c>')
|
self.assertEqual(self.importer.current_verse_type, 'c', '<ReFrain > should be interpreted as <c>')
|
||||||
|
|
||||||
# GIVEN: line with numbered verse-type
|
# GIVEN: line with numbered verse-type
|
||||||
line = 'VersE 1'
|
line = 'VersE 1'
|
||||||
self.current_verse_type = None
|
self.importer.current_verse_type = None
|
||||||
# WHEN: line is being checked for verse marks
|
# WHEN: line is being checked for verse marks
|
||||||
result = SongBeamerImport.check_verse_marks(self, line)
|
result = self.importer.check_verse_marks(line)
|
||||||
# THEN: we should get back true and v1 as self.current_verse_type
|
# THEN: we should get back true and v1 as self.importer.current_verse_type
|
||||||
self.assertTrue(result, 'Versemark for <VersE 1> should be found, value true')
|
self.assertTrue(result, 'Versemark for <VersE 1> should be found, value true')
|
||||||
self.assertEqual(self.current_verse_type, 'v1', u'<VersE 1> should be interpreted as <v1>')
|
self.assertEqual(self.importer.current_verse_type, 'v1', u'<VersE 1> should be interpreted as <v1>')
|
||||||
|
|
||||||
# GIVEN: line with special unnumbered verse-mark (used in Songbeamer to allow usage of non-supported tags)
|
# GIVEN: line with special unnumbered verse-mark (used in Songbeamer to allow usage of non-supported tags)
|
||||||
line = '$$M=special'
|
line = '$$M=special'
|
||||||
self.current_verse_type = None
|
self.importer.current_verse_type = None
|
||||||
# WHEN: line is being checked for verse marks
|
# WHEN: line is being checked for verse marks
|
||||||
result = SongBeamerImport.check_verse_marks(self, line)
|
result = self.importer.check_verse_marks(line)
|
||||||
# THEN: we should get back true and o as self.current_verse_type
|
# THEN: we should get back true and o as self.importer.current_verse_type
|
||||||
self.assertTrue(result, 'Versemark for <$$M=special> should be found, value true')
|
self.assertTrue(result, 'Versemark for <$$M=special> should be found, value true')
|
||||||
self.assertEqual(self.current_verse_type, 'o', u'<$$M=special> should be interpreted as <o>')
|
self.assertEqual(self.importer.current_verse_type, 'o', u'<$$M=special> should be interpreted as <o>')
|
||||||
|
|
||||||
# GIVEN: line with song-text with 3 words
|
# GIVEN: line with song-text with 3 words
|
||||||
line = 'Jesus my saviour'
|
line = 'Jesus my saviour'
|
||||||
self.current_verse_type = None
|
self.importer.current_verse_type = None
|
||||||
# WHEN: line is being checked for verse marks
|
# WHEN: line is being checked for verse marks
|
||||||
result = SongBeamerImport.check_verse_marks(self, line)
|
result = self.importer.check_verse_marks(line)
|
||||||
# THEN: we should get back false and none as self.current_verse_type
|
# THEN: we should get back false and none as self.importer.current_verse_type
|
||||||
self.assertFalse(result, 'No versemark for <Jesus my saviour> should be found, value false')
|
self.assertFalse(result, 'No versemark for <Jesus my saviour> should be found, value false')
|
||||||
self.assertIsNone(self.current_verse_type, '<Jesus my saviour> should be interpreted as none versemark')
|
self.assertIsNone(self.importer.current_verse_type,
|
||||||
|
'<Jesus my saviour> should be interpreted as none versemark')
|
||||||
|
|
||||||
# GIVEN: line with song-text with 2 words
|
# GIVEN: line with song-text with 2 words
|
||||||
line = 'Praise him'
|
line = 'Praise him'
|
||||||
self.current_verse_type = None
|
self.importer.current_verse_type = None
|
||||||
# WHEN: line is being checked for verse marks
|
# WHEN: line is being checked for verse marks
|
||||||
result = SongBeamerImport.check_verse_marks(self, line)
|
result = self.importer.check_verse_marks(line)
|
||||||
# THEN: we should get back false and none as self.current_verse_type
|
# THEN: we should get back false and none as self.importer.current_verse_type
|
||||||
self.assertFalse(result, 'No versemark for <Praise him> should be found, value false')
|
self.assertFalse(result, 'No versemark for <Praise him> should be found, value false')
|
||||||
self.assertIsNone(self.current_verse_type, '<Praise him> should be interpreted as none versemark')
|
self.assertIsNone(self.importer.current_verse_type, '<Praise him> should be interpreted as none versemark')
|
||||||
|
|
||||||
# GIVEN: line with only a space (could occur, nothing regular)
|
# GIVEN: line with only a space (could occur, nothing regular)
|
||||||
line = ' '
|
line = ' '
|
||||||
self.current_verse_type = None
|
self.importer.current_verse_type = None
|
||||||
# WHEN: line is being checked for verse marks
|
# WHEN: line is being checked for verse marks
|
||||||
result = SongBeamerImport.check_verse_marks(self, line)
|
result = self.importer.check_verse_marks(line)
|
||||||
# THEN: we should get back false and none as self.current_verse_type
|
# THEN: we should get back false and none as self.importer.current_verse_type
|
||||||
self.assertFalse(result, 'No versemark for < > should be found, value false')
|
self.assertFalse(result, 'No versemark for < > should be found, value false')
|
||||||
self.assertIsNone(self.current_verse_type, '< > should be interpreted as none versemark')
|
self.assertIsNone(self.importer.current_verse_type, '< > should be interpreted as none versemark')
|
||||||
|
|
||||||
# GIVEN: blank line (could occur, nothing regular)
|
# GIVEN: blank line (could occur, nothing regular)
|
||||||
line = ''
|
line = ''
|
||||||
self.current_verse_type = None
|
self.importer.current_verse_type = None
|
||||||
# WHEN: line is being checked for verse marks
|
# WHEN: line is being checked for verse marks
|
||||||
result = SongBeamerImport.check_verse_marks(self, line)
|
result = self.importer.check_verse_marks(line)
|
||||||
# THEN: we should get back false and none as self.current_verse_type
|
# THEN: we should get back false and none as self.importer.current_verse_type
|
||||||
self.assertFalse(result, 'No versemark for <> should be found, value false')
|
self.assertFalse(result, 'No versemark for <> should be found, value false')
|
||||||
self.assertIsNone(self.current_verse_type, '<> should be interpreted as none versemark')
|
self.assertIsNone(self.importer.current_verse_type, '<> should be interpreted as none versemark')
|
||||||
|
|
||||||
def test_verse_marks_defined_in_lowercase(self):
|
def test_verse_marks_defined_in_lowercase(self):
|
||||||
"""
|
"""
|
||||||
|
@ -25,6 +25,7 @@ This module contains tests for the VideoPsalm song importer.
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from tests.helpers.songfileimport import SongImportTestHelper
|
from tests.helpers.songfileimport import SongImportTestHelper
|
||||||
|
from unittest.mock import patch, MagicMock
|
||||||
|
|
||||||
TEST_PATH = os.path.abspath(
|
TEST_PATH = os.path.abspath(
|
||||||
os.path.join(os.path.dirname(__file__), '..', '..', '..', 'resources', 'videopsalmsongs'))
|
os.path.join(os.path.dirname(__file__), '..', '..', '..', 'resources', 'videopsalmsongs'))
|
||||||
@ -37,10 +38,16 @@ class TestVideoPsalmFileImport(SongImportTestHelper):
|
|||||||
self.importer_module_name = 'videopsalm'
|
self.importer_module_name = 'videopsalm'
|
||||||
super(TestVideoPsalmFileImport, self).__init__(*args, **kwargs)
|
super(TestVideoPsalmFileImport, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
def test_song_import(self):
|
@patch('openlp.plugins.songs.lib.importers.videopsalm.Settings')
|
||||||
|
def test_song_import(self, mocked_settings):
|
||||||
"""
|
"""
|
||||||
Test that loading an VideoPsalm file works correctly on various files
|
Test that loading an VideoPsalm file works correctly on various files
|
||||||
"""
|
"""
|
||||||
|
# Mock out the settings - always return False
|
||||||
|
mocked_returned_settings = MagicMock()
|
||||||
|
mocked_returned_settings.value.side_effect = lambda value: True if value == 'songs/enable chords' else False
|
||||||
|
mocked_settings.return_value = mocked_returned_settings
|
||||||
|
# Do the test import
|
||||||
self.file_import(os.path.join(TEST_PATH, 'videopsalm-as-safe-a-stronghold.json'),
|
self.file_import(os.path.join(TEST_PATH, 'videopsalm-as-safe-a-stronghold.json'),
|
||||||
self.load_external_result_data(os.path.join(TEST_PATH, 'as-safe-a-stronghold.json')))
|
self.load_external_result_data(os.path.join(TEST_PATH, 'as-safe-a-stronghold.json')))
|
||||||
self.file_import(os.path.join(TEST_PATH, 'videopsalm-as-safe-a-stronghold2.json'),
|
self.file_import(os.path.join(TEST_PATH, 'videopsalm-as-safe-a-stronghold2.json'),
|
||||||
|
29
tests/resources/chordprosongs/swing-low.chordpro
Normal file
29
tests/resources/chordprosongs/swing-low.chordpro
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{title:Swing Low Sweet Chariot}
|
||||||
|
{st:Traditional}
|
||||||
|
|
||||||
|
{start_of_chorus}
|
||||||
|
Swing [D]low, sweet [G]chari[D]ot,
|
||||||
|
Comin' for to carry me [A7]home.
|
||||||
|
Swing [D7]low, sweet [G]chari[D]ot,
|
||||||
|
Comin' for to [A7]carry me [D]home.
|
||||||
|
{end_of_chorus}
|
||||||
|
|
||||||
|
I looked over Jordan, and what did I see,
|
||||||
|
Comin' for to carry me home.
|
||||||
|
A band of angels comin' after me,
|
||||||
|
Comin' for to carry me home.
|
||||||
|
|
||||||
|
{c:Chorus}
|
||||||
|
|
||||||
|
If you get there before I do,
|
||||||
|
Comin' for to carry me home.
|
||||||
|
Just tell my friends that I'm a comin' too.
|
||||||
|
Comin' for to carry me home.
|
||||||
|
|
||||||
|
{c:Chorus}
|
||||||
|
|
||||||
|
I'm sometimes up and sometimes down,
|
||||||
|
Comin' for to carry me home.
|
||||||
|
But still my soul feels heavenly bound.
|
||||||
|
Comin' for to carry me home.
|
||||||
|
{c:Chorus}
|
22
tests/resources/chordprosongs/swing-low.json
Normal file
22
tests/resources/chordprosongs/swing-low.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"title": "Swing Low Sweet Chariot",
|
||||||
|
"alternative_title": "Traditional",
|
||||||
|
"verses": [
|
||||||
|
[
|
||||||
|
"Swing [D]low, sweet [G]chari[D]ot,\nComin' for to carry me [A7]home.\nSwing [D7]low, sweet [G]chari[D]ot,\nComin' for to [A7]carry me [D]home.",
|
||||||
|
"c"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"I looked over Jordan, and what did I see,\n Comin' for to carry me home.\nA band of angels comin' after me,\n Comin' for to carry me home.",
|
||||||
|
"v"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"If you get there before I do,\n Comin' for to carry me home.\nJust tell my friends that I'm a comin' too.\n Comin' for to carry me home.",
|
||||||
|
"v"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"I'm sometimes up and sometimes down,\n Comin' for to carry me home.\nBut still my soul feels heavenly bound.\n Comin' for to carry me home.",
|
||||||
|
"v"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
@ -19,23 +19,23 @@
|
|||||||
"verse_order_list": [],
|
"verse_order_list": [],
|
||||||
"verses": [
|
"verses": [
|
||||||
[
|
[
|
||||||
"Amazing grace! How sweet the sound!\nThat saved a wretch like me!\nI once was lost, but now am found;\nWas blind, but now I see.",
|
"A[D]mazing [D7]grace! How [G]sweet the [D]sound!\nThat [Bm]saved a [E]wretch like [A]me![A7]\nI [D]once was [D7]lost, but [G]now am [D]found;\nWas [Bm]blind, but [A]now I [G]see.[D]",
|
||||||
"v1"
|
"v1"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"'Twas grace that taught my heart to fear,\nAnd grace my fears relieved.\nHow precious did that grace appear,\nThe hour I first believed.",
|
"'Twas [D]grace that [D7]taught my [G]heart to [D]fear,\nAnd [Bm]grace my [E]fears re[A]lieved.[A7]\nHow [D]precious [D7]did that [G]grace ap[D]pear,\nThe [Bm]hour I [A]first be[G]lieved.[D]",
|
||||||
"v2"
|
"v2"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"The Lord has promised good to me,\nHis Word my hope secures.\nHe will my shield and portion be\nAs long as life endures.",
|
"The [D]Lord has [D7]promised [G]good to [D]me,\nHis [Bm]Word my [E]hope se[A]cures.[A7]\nHe [D]will my [D7]shield and [G]portion [D]be\nAs [Bm]long as [A]life en[G]dures.[D]",
|
||||||
"v3"
|
"v3"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"Thro' many dangers, toils and snares\nI have already come.\n'Tis grace that brought me safe thus far,\nAnd grace will lead me home.",
|
"Thro' [D]many [D7]dangers, [G]toils and [D]snares\nI [Bm]have al[E]ready [A]come.[A7]\n'Tis [D]grace that [D7]brought me [G]safe thus [D]far,\nAnd [Bm]grace will [A]lead me [G]home.[D]",
|
||||||
"v4"
|
"v4"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"When we've been there ten thousand years,\nBright shining as the sun,\nWe've no less days to sing God's praise,\nThan when we first begun.",
|
"When [D]we've been [D7]there ten [G]thousand [D]years,\nBright [Bm]shining [E]as the [A]sun,[A7]\nWe've [D]no less [D7]days to [G]sing God's [D]praise,\nThan [Bm]when we [A]first be[G]gun.[D]",
|
||||||
"v5"
|
"v5"
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
@ -19,23 +19,23 @@
|
|||||||
"verse_order_list": [],
|
"verse_order_list": [],
|
||||||
"verses": [
|
"verses": [
|
||||||
[
|
[
|
||||||
"Amazing grace! How sweet the sound!\nThat saved a wretch like me!\nI once was lost, but now am found;\nWas blind, but now I see.",
|
"A[D]mazing [D7]grace! How [G]sweet the [D]sound!\nThat [Bm]saved a [E]wretch like [A]me![A7]\nI [D]once was [D7]lost, but [G]now am [D]found;\nWas [Bm]blind, but [A]now I [G]see.[D]",
|
||||||
"v1"
|
"v1"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"'Twas grace that taught my heart to fear,\nAnd grace my fears relieved.\nHow precious did that grace appear,\nThe hour I first believed.",
|
"'Twas [D]grace that [D7]taught my [G]heart to [D]fear,\nAnd [Bm]grace my [E]fears re[A]lieved.[A7]\nHow [D]precious [D7]did that [G]grace ap[D]pear,\nThe [Bm]hour I [A]first be[G]lieved.[D]",
|
||||||
"v2"
|
"v2"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"The Lord has promised good to me,\nHis Word my hope secures.\nHe will my shield and portion be\nAs long as life endures.",
|
"The [D]Lord has [D7]promised [G]good to [D]me,\nHis [Bm]Word my [E]hope se[A]cures.[A7]\nHe [D]will my [D7]shield and [G]portion [D]be\nAs [Bm]long as [A]life en[G]dures.[D]",
|
||||||
"v3"
|
"v3"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"Thro' many dangers, toils and snares\nI have already come.\n'Tis grace that brought me safe thus far,\nAnd grace will lead me home.",
|
"Thro' [D]many [D7]dangers, [G]toils and [D]snares\nI [Bm]have al[E]ready [A]come.[A7]\n'Tis [D]grace that [D7]brought me [G]safe thus [D]far,\nAnd [Bm]grace will [A]lead me [G]home.[D]",
|
||||||
"v4"
|
"v4"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"When we've been there ten thousand years,\nBright shining as the sun,\nWe've no less days to sing God's praise,\nThan when we first begun.",
|
"When [D]we've been [D7]there ten [G]thousand [D]years,\nBright [Bm]shining [E]as the [A]sun,[A7]\nWe've [D]no less [D7]days to [G]sing God's [D]praise,\nThan [Bm]when we [A]first be[G]gun.[D]",
|
||||||
"v5"
|
"v5"
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
29
tests/resources/songbeamersongs/Amazing Grace.json
Normal file
29
tests/resources/songbeamersongs/Amazing Grace.json
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"authors": [
|
||||||
|
["John Newton", "words"]
|
||||||
|
],
|
||||||
|
"title": "Amazing grace",
|
||||||
|
"verse_order_list": ["v1", "v2", "v3", "v4", "v5"],
|
||||||
|
"verses": [
|
||||||
|
[
|
||||||
|
"[D]Amazing gr[D7]ace how [G]sweet the [D]sound\nThat saved a [E7]wretch like [A7]me\nI [D]once was [D7]lost but [G]now im [D]found\nWas b[E7]lind but no[A7]w i [D]see [A7]\n",
|
||||||
|
"v1"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"T'was [D]grace that [D7]taught my [G]heart to [D]fear\nAnd grace my [E7]fears [A7]relieved\nHow [D]precious [D7]did that [G]Grace [D]appear\nThe [E7]hour I [A7]first bel[D]ieved.[A7]\n",
|
||||||
|
"v2"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Through [D]many [D7]dangers, [G]toils and [D]snares\nI have [E7]already [A7]come;\n'Tis [D]Grace that [D7]brought me [G]safe thus [D]far\nand [E7]Grace will [A7]lead me [D]home. [A7]\n",
|
||||||
|
"v3"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"When we[D]'ve been here[D7] ten thous[G]and y[D]ears\nBright shining [E7]as the [A7]sun.\nWe've [D]no less [D7]days to s[G]ing God's p[D]raise\nThan w[E7]hen we've [A7]first [D]begun.[A7]\n",
|
||||||
|
"v4"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"[D]Amazing gr[D7]ace how [G]sweet the [D]sound\nThat saved a [E7]wretch like [A7]me\nI [D]once was [D7]lost but [G]now im [D]found\nWas b[E7]lind but no[A7]w i [D]see [A]\n",
|
||||||
|
"v5"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
37
tests/resources/songbeamersongs/Amazing Grace.sng
Normal file
37
tests/resources/songbeamersongs/Amazing Grace.sng
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
#LangCount=1
|
||||||
|
#Title=Amazing grace
|
||||||
|
#Chords=MCwwLEQNMTAsMCxENw0xOCwwLEcNMjgsMCxEDTEzLDEsRTcNMjUsMSxBNw0yLDIsRA0xMSwyLEQ3DTIwLDIsRw0yNywyLEQNNSwzLEU3DTE2LDMsQTcNMjAsMyxEDTI0LDMsQTcNNiw1LEQNMTcsNSxENw0yNyw1LEcNMzYsNSxEDTEzLDYsRTcNMTksNixBNw00LDcsRA0xMyw3LEQ3DTIyLDcsRw0yOCw3LEQNNCw4LEU3DTExLDgsQTcNMjAsOCxEDTI2LDgsQTcNOCwxMCxEDTEzLDEwLEQ3DTIyLDEwLEcNMzIsMTAsRA03LDExLEU3DTE1LDExLEE3DTUsMTIsRA0xNiwxMixENw0yNywxMixHDTM3LDEyLEQNNCwxMyxFNw0xNSwxMyxBNw0yMywxMyxEDTI5LDEzLEE3DTcsMTUsRA0yMCwxNSxENw0zMCwxNSxHDTM1LDE1LEQNMTUsMTYsRTcNMjIsMTYsQTcNNiwxNyxEDTE0LDE3LEQ3DTIzLDE3LEcNMzQsMTcsRA02LDE4LEU3DTE2LDE4LEE3DTIyLDE4LEQNMjgsMTgsQTcNMCwyMCxEDTEwLDIwLEQ3DTE4LDIwLEcNMjgsMjAsRA0xMywyMSxFNw0yNSwyMSxBNw0yLDIyLEQNMTEsMjIsRDcNMjAsMjIsRw0yNywyMixEDTUsMjMsRTcNMTYsMjMsQTcNMjAsMjMsRA0yNCwyMyxBDQ==
|
||||||
|
#Author=John Newton
|
||||||
|
#Editor=SongBeamer 4.37a
|
||||||
|
#Version=3
|
||||||
|
#VerseOrder=Verse 1,Verse 2,Verse 3,Verse 4,Verse 5
|
||||||
|
---
|
||||||
|
Verse 1
|
||||||
|
Amazing grace how sweet the sound
|
||||||
|
That saved a wretch like me
|
||||||
|
I once was lost but now im found
|
||||||
|
Was blind but now i see
|
||||||
|
---
|
||||||
|
Verse 2
|
||||||
|
T'was grace that taught my heart to fear
|
||||||
|
And grace my fears relieved
|
||||||
|
How precious did that Grace appear
|
||||||
|
The hour I first believed.
|
||||||
|
---
|
||||||
|
Verse 3
|
||||||
|
Through many dangers, toils and snares
|
||||||
|
I have already come;
|
||||||
|
'Tis Grace that brought me safe thus far
|
||||||
|
and Grace will lead me home.
|
||||||
|
---
|
||||||
|
Verse 4
|
||||||
|
When we've been here ten thousand years
|
||||||
|
Bright shining as the sun.
|
||||||
|
We've no less days to sing God's praise
|
||||||
|
Than when we've first begun.
|
||||||
|
---
|
||||||
|
Verse 5
|
||||||
|
Amazing grace how sweet the sound
|
||||||
|
That saved a wretch like me
|
||||||
|
I once was lost but now im found
|
||||||
|
Was blind but now i see
|
@ -8,5 +8,8 @@
|
|||||||
],
|
],
|
||||||
"song_book_name": "Glaubenslieder I",
|
"song_book_name": "Glaubenslieder I",
|
||||||
"song_number": "1",
|
"song_number": "1",
|
||||||
"authors": ["Carl Brockhaus", "Johann Jakob Vetter"]
|
"authors": [
|
||||||
|
["Carl Brockhaus", "words"],
|
||||||
|
["Johann Jakob Vetter", "music"]
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
14
tests/resources/songbeamersongs/When I Call On You.json
Normal file
14
tests/resources/songbeamersongs/When I Call On You.json
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"title": "When I Call On You",
|
||||||
|
"verse_order_list": [],
|
||||||
|
"verses": [
|
||||||
|
[
|
||||||
|
"[G]When I call on Y[Em]ou,\n[G]You are always [D]there,\n[G]When I call on Y[Em]ou.\n[G]You are always [D]there. [D]\n",
|
||||||
|
"v"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"[G]Oh it [Em]makes me [G]feel like [D]dancing,\n[G]Oh it [Em]makes me [G]feel like [D]dancing,\n[Em]Oh - oh -[D]oh - oh -[G]oh.\n",
|
||||||
|
"v"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
14
tests/resources/songbeamersongs/When I Call On You.sng
Normal file
14
tests/resources/songbeamersongs/When I Call On You.sng
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
#LangCount=1
|
||||||
|
#Title=When I Call On You
|
||||||
|
#Chords=MCwwLEcNMTUuNSwwLEVtDTAsMSxHDTE1LDEsRA0wLDIsRw0xNS41LDIsRW0NMCwzLEcNMTUsMyxEDTIzLjUsMyxEDTAsNSxHDTYsNSxFbQ0xNSw1LEcNMjUsNSxEDTAsNixHDTYsNixFbQ0xNSw2LEcNMjUsNixEDTAsNyxFbQ05LDcsRA0xOCw3LEcN
|
||||||
|
#Editor=SongBeamer 4.47
|
||||||
|
#Version=3
|
||||||
|
---
|
||||||
|
When I call on You,
|
||||||
|
You are always there,
|
||||||
|
When I call on You.
|
||||||
|
You are always there.
|
||||||
|
---
|
||||||
|
Oh it makes me feel like dancing,
|
||||||
|
Oh it makes me feel like dancing,
|
||||||
|
Oh - oh -oh - oh -oh.
|
@ -1,6 +1,8 @@
|
|||||||
{
|
{
|
||||||
"title": "Some Song",
|
"title": "Some Song",
|
||||||
"authors": ["Author"],
|
"authors": [
|
||||||
|
["Author", "words"]
|
||||||
|
],
|
||||||
"verses" : [
|
"verses" : [
|
||||||
["Here are a couple of \"weird\" chars’’’.\n", "v"],
|
["Here are a couple of \"weird\" chars’’’.\n", "v"],
|
||||||
["Here is another one….\n\n", "v"]
|
["Here is another one….\n\n", "v"]
|
||||||
|
Loading…
Reference in New Issue
Block a user