forked from openlp/openlp
Head
This commit is contained in:
commit
6fe9dfeb04
|
@ -9,3 +9,4 @@ OpenLP 2.5.1
|
|||
* Fix a problem with the new QMediaPlayer not controlling the playlist anymore
|
||||
* Added importing of author types to the OpenLP 2 song importer
|
||||
* Refactored the merge script and gave it some options
|
||||
* Fix a problem with loading Qt's translation files, bug #1676163
|
||||
|
|
|
@ -246,7 +246,7 @@ class OpenLP(OpenLPMixin, QtWidgets.QApplication):
|
|||
Settings().setValue('core/application version', openlp_version)
|
||||
# If data_version is different from the current version ask if we should backup the data folder
|
||||
elif data_version != openlp_version:
|
||||
if self.splash.isVisible():
|
||||
if can_show_splash and self.splash.isVisible():
|
||||
self.splash.hide()
|
||||
if QtWidgets.QMessageBox.question(None, translate('OpenLP', 'Backup'),
|
||||
translate('OpenLP', 'OpenLP has been upgraded, do you want to create\n'
|
||||
|
@ -428,13 +428,12 @@ def main(args=None):
|
|||
sys.exit()
|
||||
# i18n Set Language
|
||||
language = LanguageManager.get_language()
|
||||
application_translator, default_translator = LanguageManager.get_translator(language)
|
||||
if not application_translator.isEmpty():
|
||||
application.installTranslator(application_translator)
|
||||
if not default_translator.isEmpty():
|
||||
application.installTranslator(default_translator)
|
||||
else:
|
||||
log.debug('Could not find default_translator.')
|
||||
translators = LanguageManager.get_translators(language)
|
||||
for translator in translators:
|
||||
if not translator.isEmpty():
|
||||
application.installTranslator(translator)
|
||||
if not translators:
|
||||
log.debug('Could not find translators.')
|
||||
if args and not args.no_error_form:
|
||||
sys.excepthook = application.hook_exception
|
||||
sys.exit(application.run(qt_args))
|
||||
|
|
|
@ -24,7 +24,7 @@ The :mod:`common` module contains most of the components and libraries that make
|
|||
OpenLP work.
|
||||
"""
|
||||
import hashlib
|
||||
|
||||
import importlib
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
|
@ -32,6 +32,7 @@ import sys
|
|||
import traceback
|
||||
from chardet.universaldetector import UniversalDetector
|
||||
from ipaddress import IPv4Address, IPv6Address, AddressValueError
|
||||
from pathlib import Path
|
||||
from shutil import which
|
||||
from subprocess import check_output, CalledProcessError, STDOUT
|
||||
|
||||
|
@ -79,6 +80,49 @@ def check_directory_exists(directory, do_not_log=False):
|
|||
log.exception('failed to check if directory exists or create directory')
|
||||
|
||||
|
||||
def extension_loader(glob_pattern, excluded_files=[]):
|
||||
"""
|
||||
A utility function to find and load OpenLP extensions, such as plugins, presentation and media controllers and
|
||||
importers.
|
||||
|
||||
:param glob_pattern: A glob pattern used to find the extension(s) to be imported. Should be relative to the
|
||||
application directory. i.e. openlp/plugins/*/*plugin.py
|
||||
:type glob_pattern: str
|
||||
|
||||
:param excluded_files: A list of file names to exclude that the glob pattern may find.
|
||||
:type excluded_files: list of strings
|
||||
|
||||
:return: None
|
||||
:rtype: None
|
||||
"""
|
||||
app_dir = Path(AppLocation.get_directory(AppLocation.AppDir)).parent
|
||||
for extension_path in app_dir.glob(glob_pattern):
|
||||
extension_path = extension_path.relative_to(app_dir)
|
||||
if extension_path.name in excluded_files:
|
||||
continue
|
||||
module_name = path_to_module(extension_path)
|
||||
try:
|
||||
importlib.import_module(module_name)
|
||||
except (ImportError, OSError):
|
||||
# On some platforms importing vlc.py might cause OSError exceptions. (e.g. Mac OS X)
|
||||
log.warning('Failed to import {module_name} on path {extension_path}'
|
||||
.format(module_name=module_name, extension_path=str(extension_path)))
|
||||
|
||||
|
||||
def path_to_module(path):
|
||||
"""
|
||||
Convert a path to a module name (i.e openlp.core.common)
|
||||
|
||||
:param path: The path to convert to a module name.
|
||||
:type path: Path
|
||||
|
||||
:return: The module name.
|
||||
:rtype: str
|
||||
"""
|
||||
module_path = path.with_suffix('')
|
||||
return '.'.join(module_path.parts)
|
||||
|
||||
|
||||
def get_frozen_path(frozen_option, non_frozen_option):
|
||||
"""
|
||||
Return a path based on the system status.
|
||||
|
@ -398,7 +442,7 @@ def check_binary_exists(program_path):
|
|||
"""
|
||||
Function that checks whether a binary exists.
|
||||
|
||||
:param program_path:The full path to the binary to check.
|
||||
:param program_path: The full path to the binary to check.
|
||||
:return: program output to be parsed
|
||||
"""
|
||||
log.debug('testing program_path: {text}'.format(text=program_path))
|
||||
|
|
|
@ -252,4 +252,5 @@ def url_get_file(callback, url, f_path, sha256=None):
|
|||
os.remove(f_path)
|
||||
return True
|
||||
|
||||
|
||||
__all__ = ['get_web_page']
|
||||
|
|
|
@ -45,7 +45,7 @@ class LanguageManager(object):
|
|||
auto_language = False
|
||||
|
||||
@staticmethod
|
||||
def get_translator(language):
|
||||
def get_translators(language):
|
||||
"""
|
||||
Set up a translator to use in this instance of OpenLP
|
||||
|
||||
|
@ -59,9 +59,12 @@ class LanguageManager(object):
|
|||
# A translator for buttons and other default strings provided by Qt.
|
||||
if not is_win() and not is_macosx():
|
||||
lang_path = QtCore.QLibraryInfo.location(QtCore.QLibraryInfo.TranslationsPath)
|
||||
# As of Qt5, the core translations come in 2 files per language
|
||||
default_translator = QtCore.QTranslator()
|
||||
default_translator.load('qt_%s' % language, lang_path)
|
||||
return app_translator, default_translator
|
||||
base_translator = QtCore.QTranslator()
|
||||
base_translator.load('qtbase_%s' % language, lang_path)
|
||||
return app_translator, default_translator, base_translator
|
||||
|
||||
@staticmethod
|
||||
def find_qm_files():
|
||||
|
@ -71,8 +74,8 @@ class LanguageManager(object):
|
|||
log.debug('Translation files: {files}'.format(files=AppLocation.get_directory(AppLocation.LanguageDir)))
|
||||
trans_dir = QtCore.QDir(AppLocation.get_directory(AppLocation.LanguageDir))
|
||||
file_names = trans_dir.entryList(['*.qm'], QtCore.QDir.Files, QtCore.QDir.Name)
|
||||
# Remove qm files from the list which start with "qt_".
|
||||
file_names = [file_ for file_ in file_names if not file_.startswith('qt_')]
|
||||
# Remove qm files from the list which start with "qt".
|
||||
file_names = [file_ for file_ in file_names if not file_.startswith('qt')]
|
||||
return list(map(trans_dir.filePath, file_names))
|
||||
|
||||
@staticmethod
|
||||
|
|
|
@ -23,10 +23,11 @@
|
|||
The :mod:`lib` module contains most of the components and libraries that make
|
||||
OpenLP work.
|
||||
"""
|
||||
|
||||
import html
|
||||
import logging
|
||||
import os
|
||||
from distutils.version import LooseVersion
|
||||
import re
|
||||
import math
|
||||
|
||||
from PyQt5 import QtCore, QtGui, Qt, QtWidgets
|
||||
|
||||
|
@ -34,6 +35,8 @@ from openlp.core.common import translate
|
|||
|
||||
log = logging.getLogger(__name__ + '.__init__')
|
||||
|
||||
SLIMCHARS = 'fiíIÍjlĺľrtť.,;/ ()|"\'!:\\'
|
||||
|
||||
|
||||
class ServiceItemContext(object):
|
||||
"""
|
||||
|
@ -281,11 +284,12 @@ def check_item_selected(list_widget, message):
|
|||
return True
|
||||
|
||||
|
||||
def clean_tags(text):
|
||||
def clean_tags(text, remove_chords=False):
|
||||
"""
|
||||
Remove Tags from text for display
|
||||
|
||||
:param text: Text to be cleaned
|
||||
:param remove_chords: Clean ChordPro tags
|
||||
"""
|
||||
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():
|
||||
text = text.replace(tag['start tag'], '')
|
||||
text = text.replace(tag['end tag'], '')
|
||||
# Remove ChordPro tags
|
||||
if remove_chords:
|
||||
text = re.sub(r'\[.+?\]', r'', text)
|
||||
return text
|
||||
|
||||
|
||||
def expand_tags(text):
|
||||
def expand_tags(text, expand_chord_tags=False, for_printing=False):
|
||||
"""
|
||||
Expand tags HTML for display
|
||||
|
||||
: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():
|
||||
text = text.replace(tag['start tag'], tag['start html'])
|
||||
text = text.replace(tag['end tag'], tag['end html'])
|
||||
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):
|
||||
"""
|
||||
Returns a string that represents a join of a list of strings with a localized separator.
|
||||
|
@ -337,10 +616,10 @@ from .plugin import PluginStatus, StringContent, Plugin
|
|||
from .pluginmanager import PluginManager
|
||||
from .settingstab import SettingsTab
|
||||
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 .renderer import Renderer
|
||||
from .mediamanageritem import MediaManagerItem
|
||||
from .projector.db import ProjectorDB, Projector
|
||||
from .projector.pjlink1 import PJLink1
|
||||
from .projector.pjlink1 import PJLink
|
||||
from .projector.constants import PJLINK_PORT, ERROR_MSG, ERROR_STRING
|
||||
|
|
|
@ -172,6 +172,7 @@ def upgrade_db(url, upgrade):
|
|||
else:
|
||||
version = int(version_meta.value)
|
||||
if version > upgrade.__version__:
|
||||
session.remove()
|
||||
return version, upgrade.__version__
|
||||
version += 1
|
||||
try:
|
||||
|
@ -194,7 +195,7 @@ def upgrade_db(url, upgrade):
|
|||
session.commit()
|
||||
upgrade_version = upgrade.__version__
|
||||
version = int(version_meta.value)
|
||||
session.close()
|
||||
session.remove()
|
||||
return version, upgrade_version
|
||||
|
||||
|
||||
|
|
|
@ -124,6 +124,25 @@ is the function which has to be called from outside. The generated and returned
|
|||
position: relative;
|
||||
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>
|
||||
<script>
|
||||
var timer = null;
|
||||
|
@ -444,6 +463,7 @@ HTML_SRC = Template("""
|
|||
position: relative;
|
||||
top: -0.3em;
|
||||
}
|
||||
/* Chords css */${chords_css}
|
||||
</style>
|
||||
<script>
|
||||
var timer = null;
|
||||
|
@ -592,6 +612,30 @@ LYRICS_FORMAT_SRC = Template("""
|
|||
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):
|
||||
"""
|
||||
|
@ -636,7 +680,8 @@ def build_html(item, screen, is_live, background, image=None, plugins=None):
|
|||
js_additions=js_additions,
|
||||
bg_image=bgimage_src,
|
||||
image=image_src,
|
||||
html_additions=html_additions)
|
||||
html_additions=html_additions,
|
||||
chords_css=build_chords_css())
|
||||
|
||||
|
||||
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(),
|
||||
family=theme.font_footer_name, size=theme.font_footer_size,
|
||||
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)
|
||||
|
|
|
@ -23,10 +23,9 @@
|
|||
Provide plugin management
|
||||
"""
|
||||
import os
|
||||
import imp
|
||||
|
||||
from openlp.core.lib import Plugin, PluginStatus
|
||||
from openlp.core.common import AppLocation, RegistryProperties, OpenLPMixin, RegistryMixin
|
||||
from openlp.core.common import AppLocation, RegistryProperties, OpenLPMixin, RegistryMixin, extension_loader
|
||||
|
||||
|
||||
class PluginManager(RegistryMixin, OpenLPMixin, RegistryProperties):
|
||||
|
@ -70,32 +69,8 @@ class PluginManager(RegistryMixin, OpenLPMixin, RegistryProperties):
|
|||
"""
|
||||
Scan a directory for objects inheriting from the ``Plugin`` class.
|
||||
"""
|
||||
start_depth = len(os.path.abspath(self.base_path).split(os.sep))
|
||||
present_plugin_dir = os.path.join(self.base_path, 'presentations')
|
||||
self.log_debug('finding plugins in {path} at depth {depth:d}'.format(path=self.base_path, depth=start_depth))
|
||||
for root, dirs, files in os.walk(self.base_path):
|
||||
for name in files:
|
||||
if name.endswith('.py') and not name.startswith('__'):
|
||||
path = os.path.abspath(os.path.join(root, name))
|
||||
this_depth = len(path.split(os.sep))
|
||||
if this_depth - start_depth > 2:
|
||||
# skip anything lower down
|
||||
break
|
||||
module_name = name[:-3]
|
||||
# import the modules
|
||||
self.log_debug('Importing {name} from {root}. Depth {depth:d}'.format(name=module_name,
|
||||
root=root,
|
||||
depth=this_depth))
|
||||
try:
|
||||
# Use the "imp" library to try to get around a problem with the PyUNO library which
|
||||
# monkey-patches the __import__ function to do some magic. This causes issues with our tests.
|
||||
# First, try to find the module we want to import, searching the directory in root
|
||||
fp, path_name, description = imp.find_module(module_name, [root])
|
||||
# Then load the module (do the actual import) using the details from find_module()
|
||||
imp.load_module(module_name, fp, path_name, description)
|
||||
except ImportError as e:
|
||||
self.log_exception('Failed to import module {name} on path {path}: '
|
||||
'{args}'.format(name=module_name, path=path, args=e.args[0]))
|
||||
glob_pattern = os.path.join('openlp', 'plugins', '*', '*plugin.py')
|
||||
extension_loader(glob_pattern)
|
||||
plugin_classes = Plugin.__subclasses__()
|
||||
plugin_objects = []
|
||||
for p in plugin_classes:
|
||||
|
|
|
@ -48,7 +48,8 @@ __all__ = ['S_OK', 'E_GENERAL', 'E_NOT_CONNECTED', 'E_FAN', 'E_LAMP', 'E_TEMP',
|
|||
'S_INFO', 'S_NETWORK_SENDING', 'S_NETWORK_RECEIVED',
|
||||
'ERROR_STRING', 'CR', 'LF', 'PJLINK_ERST_STATUS', 'PJLINK_POWR_STATUS',
|
||||
'PJLINK_PORT', 'PJLINK_MAX_PACKET', 'TIMEOUT', 'ERROR_MSG', 'PJLINK_ERRORS',
|
||||
'STATUS_STRING', 'PJLINK_VALID_CMD', 'CONNECTION_ERRORS']
|
||||
'STATUS_STRING', 'PJLINK_VALID_CMD', 'CONNECTION_ERRORS',
|
||||
'PJLINK_DEFAULT_SOURCES', 'PJLINK_DEFAULT_CODES', 'PJLINK_DEFAULT_ITEMS']
|
||||
|
||||
# Set common constants.
|
||||
CR = chr(0x0D) # \r
|
||||
|
@ -56,20 +57,35 @@ LF = chr(0x0A) # \n
|
|||
PJLINK_PORT = 4352
|
||||
TIMEOUT = 30.0
|
||||
PJLINK_MAX_PACKET = 136
|
||||
PJLINK_VALID_CMD = {'1': ['PJLINK', # Initial connection
|
||||
'POWR', # Power option
|
||||
'INPT', # Video sources option
|
||||
'AVMT', # Shutter option
|
||||
'ERST', # Error status option
|
||||
'LAMP', # Lamp(s) query (Includes fans)
|
||||
'INST', # Input sources available query
|
||||
'NAME', # Projector name query
|
||||
'INF1', # Manufacturer name query
|
||||
'INF2', # Product name query
|
||||
'INFO', # Other information query
|
||||
'CLSS' # PJLink class support query
|
||||
]}
|
||||
|
||||
# NOTE: Change format to account for some commands are both class 1 and 2
|
||||
PJLINK_VALID_CMD = {
|
||||
'ACKN': ['2', ], # UDP Reply to 'SRCH'
|
||||
'AVMT': ['1', ], # Shutter option
|
||||
'CLSS': ['1', ], # PJLink class support query
|
||||
'ERST': ['1', '2'], # Error status option
|
||||
'FILT': ['2', ], # Get current filter usage time
|
||||
'FREZ': ['2', ], # Set freeze/unfreeze picture being projected
|
||||
'INF1': ['1', ], # Manufacturer name query
|
||||
'INF2': ['1', ], # Product name query
|
||||
'INFO': ['1', ], # Other information query
|
||||
'INNM': ['2', ], # Get Video source input terminal name
|
||||
'INPT': ['1', ], # Video sources option
|
||||
'INST': ['1', ], # Input sources available query
|
||||
'IRES': ['2', ], # Get Video source resolution
|
||||
'LAMP': ['1', ], # Lamp(s) query (Includes fans)
|
||||
'LKUP': ['2', ], # UPD Linkup status notification
|
||||
'MVOL': ['2', ], # Set microphone volume
|
||||
'NAME': ['1', ], # Projector name query
|
||||
'PJLINK': ['1', ], # Initial connection
|
||||
'POWR': ['1', ], # Power option
|
||||
'RFIL': ['2', ], # Get replacement air filter model number
|
||||
'RLMP': ['2', ], # Get lamp replacement model number
|
||||
'RRES': ['2', ], # Get projector recommended video resolution
|
||||
'SNUM': ['2', ], # Get projector serial number
|
||||
'SRCH': ['2', ], # UDP broadcast search for available projectors on local network
|
||||
'SVER': ['2', ], # Get projector software version
|
||||
'SVOL': ['2', ] # Set speaker volume
|
||||
}
|
||||
# Error and status codes
|
||||
S_OK = E_OK = 0 # E_OK included since I sometimes forget
|
||||
# Error codes. Start at 200 so we don't duplicate system error codes.
|
||||
|
@ -321,53 +337,54 @@ PJLINK_DEFAULT_SOURCES = {
|
|||
'2': translate('OpenLP.DB', 'Video'),
|
||||
'3': translate('OpenLP.DB', 'Digital'),
|
||||
'4': translate('OpenLP.DB', 'Storage'),
|
||||
'5': translate('OpenLP.DB', 'Network')
|
||||
'5': translate('OpenLP.DB', 'Network'),
|
||||
'6': translate('OpenLP.DB', 'Internal')
|
||||
}
|
||||
|
||||
PJLINK_DEFAULT_CODES = {
|
||||
'11': translate('OpenLP.DB', 'RGB 1'),
|
||||
'12': translate('OpenLP.DB', 'RGB 2'),
|
||||
'13': translate('OpenLP.DB', 'RGB 3'),
|
||||
'14': translate('OpenLP.DB', 'RGB 4'),
|
||||
'15': translate('OpenLP.DB', 'RGB 5'),
|
||||
'16': translate('OpenLP.DB', 'RGB 6'),
|
||||
'17': translate('OpenLP.DB', 'RGB 7'),
|
||||
'18': translate('OpenLP.DB', 'RGB 8'),
|
||||
'19': translate('OpenLP.DB', 'RGB 9'),
|
||||
'21': translate('OpenLP.DB', 'Video 1'),
|
||||
'22': translate('OpenLP.DB', 'Video 2'),
|
||||
'23': translate('OpenLP.DB', 'Video 3'),
|
||||
'24': translate('OpenLP.DB', 'Video 4'),
|
||||
'25': translate('OpenLP.DB', 'Video 5'),
|
||||
'26': translate('OpenLP.DB', 'Video 6'),
|
||||
'27': translate('OpenLP.DB', 'Video 7'),
|
||||
'28': translate('OpenLP.DB', 'Video 8'),
|
||||
'29': translate('OpenLP.DB', 'Video 9'),
|
||||
'31': translate('OpenLP.DB', 'Digital 1'),
|
||||
'32': translate('OpenLP.DB', 'Digital 2'),
|
||||
'33': translate('OpenLP.DB', 'Digital 3'),
|
||||
'34': translate('OpenLP.DB', 'Digital 4'),
|
||||
'35': translate('OpenLP.DB', 'Digital 5'),
|
||||
'36': translate('OpenLP.DB', 'Digital 6'),
|
||||
'37': translate('OpenLP.DB', 'Digital 7'),
|
||||
'38': translate('OpenLP.DB', 'Digital 8'),
|
||||
'39': translate('OpenLP.DB', 'Digital 9'),
|
||||
'41': translate('OpenLP.DB', 'Storage 1'),
|
||||
'42': translate('OpenLP.DB', 'Storage 2'),
|
||||
'43': translate('OpenLP.DB', 'Storage 3'),
|
||||
'44': translate('OpenLP.DB', 'Storage 4'),
|
||||
'45': translate('OpenLP.DB', 'Storage 5'),
|
||||
'46': translate('OpenLP.DB', 'Storage 6'),
|
||||
'47': translate('OpenLP.DB', 'Storage 7'),
|
||||
'48': translate('OpenLP.DB', 'Storage 8'),
|
||||
'49': translate('OpenLP.DB', 'Storage 9'),
|
||||
'51': translate('OpenLP.DB', 'Network 1'),
|
||||
'52': translate('OpenLP.DB', 'Network 2'),
|
||||
'53': translate('OpenLP.DB', 'Network 3'),
|
||||
'54': translate('OpenLP.DB', 'Network 4'),
|
||||
'55': translate('OpenLP.DB', 'Network 5'),
|
||||
'56': translate('OpenLP.DB', 'Network 6'),
|
||||
'57': translate('OpenLP.DB', 'Network 7'),
|
||||
'58': translate('OpenLP.DB', 'Network 8'),
|
||||
'59': translate('OpenLP.DB', 'Network 9')
|
||||
PJLINK_DEFAULT_ITEMS = {
|
||||
'1': translate('OpenLP.DB', '1'),
|
||||
'2': translate('OpenLP.DB', '2'),
|
||||
'3': translate('OpenLP.DB', '3'),
|
||||
'4': translate('OpenLP.DB', '4'),
|
||||
'5': translate('OpenLP.DB', '5'),
|
||||
'6': translate('OpenLP.DB', '6'),
|
||||
'7': translate('OpenLP.DB', '7'),
|
||||
'8': translate('OpenLP.DB', '8'),
|
||||
'9': translate('OpenLP.DB', '9'),
|
||||
'A': translate('OpenLP.DB', 'A'),
|
||||
'B': translate('OpenLP.DB', 'B'),
|
||||
'C': translate('OpenLP.DB', 'C'),
|
||||
'D': translate('OpenLP.DB', 'D'),
|
||||
'E': translate('OpenLP.DB', 'E'),
|
||||
'F': translate('OpenLP.DB', 'F'),
|
||||
'G': translate('OpenLP.DB', 'G'),
|
||||
'H': translate('OpenLP.DB', 'H'),
|
||||
'I': translate('OpenLP.DB', 'I'),
|
||||
'J': translate('OpenLP.DB', 'J'),
|
||||
'K': translate('OpenLP.DB', 'K'),
|
||||
'L': translate('OpenLP.DB', 'L'),
|
||||
'M': translate('OpenLP.DB', 'M'),
|
||||
'N': translate('OpenLP.DB', 'N'),
|
||||
'O': translate('OpenLP.DB', 'O'),
|
||||
'P': translate('OpenLP.DB', 'P'),
|
||||
'Q': translate('OpenLP.DB', 'Q'),
|
||||
'R': translate('OpenLP.DB', 'R'),
|
||||
'S': translate('OpenLP.DB', 'S'),
|
||||
'T': translate('OpenLP.DB', 'T'),
|
||||
'U': translate('OpenLP.DB', 'U'),
|
||||
'V': translate('OpenLP.DB', 'V'),
|
||||
'W': translate('OpenLP.DB', 'W'),
|
||||
'X': translate('OpenLP.DB', 'X'),
|
||||
'Y': translate('OpenLP.DB', 'Y'),
|
||||
'Z': translate('OpenLP.DB', 'Z')
|
||||
}
|
||||
|
||||
# Due to the expanded nature of PJLink class 2 video sources,
|
||||
# translate the individual types then build the video source
|
||||
# dictionary from the translations.
|
||||
PJLINK_DEFAULT_CODES = dict()
|
||||
for source in PJLINK_DEFAULT_SOURCES:
|
||||
for item in PJLINK_DEFAULT_ITEMS:
|
||||
label = "{source}{item}".format(source=source, item=item)
|
||||
PJLINK_DEFAULT_CODES[label] = "{source} {item}".format(source=PJLINK_DEFAULT_SOURCES[source],
|
||||
item=PJLINK_DEFAULT_ITEMS[item])
|
||||
|
|
|
@ -150,11 +150,15 @@ class Projector(CommonBase, Base):
|
|||
name: Column(String(20))
|
||||
location: Column(String(30))
|
||||
notes: Column(String(200))
|
||||
pjlink_name: Column(String(128)) # From projector (future)
|
||||
manufacturer: Column(String(128)) # From projector (future)
|
||||
model: Column(String(128)) # From projector (future)
|
||||
other: Column(String(128)) # From projector (future)
|
||||
sources: Column(String(128)) # From projector (future)
|
||||
pjlink_name: Column(String(128)) # From projector
|
||||
manufacturer: Column(String(128)) # From projector
|
||||
model: Column(String(128)) # From projector
|
||||
other: Column(String(128)) # From projector
|
||||
sources: Column(String(128)) # From projector
|
||||
serial_no: Column(String(30)) # From projector (Class 2)
|
||||
sw_version: Column(String(30)) # From projector (Class 2)
|
||||
model_filter: Column(String(30)) # From projector (Class 2)
|
||||
model_lamp: Column(String(30)) # From projector (Class 2)
|
||||
|
||||
ProjectorSource relates
|
||||
"""
|
||||
|
@ -164,20 +168,25 @@ class Projector(CommonBase, Base):
|
|||
"""
|
||||
return '< Projector(id="{data}", ip="{ip}", port="{port}", pin="{pin}", name="{name}", ' \
|
||||
'location="{location}", notes="{notes}", pjlink_name="{pjlink_name}", ' \
|
||||
'manufacturer="{manufacturer}", model="{model}", other="{other}", ' \
|
||||
'sources="{sources}", source_list="{source_list}") >'.format(data=self.id,
|
||||
ip=self.ip,
|
||||
port=self.port,
|
||||
pin=self.pin,
|
||||
name=self.name,
|
||||
location=self.location,
|
||||
notes=self.notes,
|
||||
pjlink_name=self.pjlink_name,
|
||||
manufacturer=self.manufacturer,
|
||||
model=self.model,
|
||||
other=self.other,
|
||||
sources=self.sources,
|
||||
source_list=self.source_list)
|
||||
'manufacturer="{manufacturer}", model="{model}", serial_no="{serial}", other="{other}", ' \
|
||||
'sources="{sources}", source_list="{source_list}", model_filter="{mfilter}", ' \
|
||||
'model_lamp="{mlamp}", sw_version="{sw_ver}") >'.format(data=self.id,
|
||||
ip=self.ip,
|
||||
port=self.port,
|
||||
pin=self.pin,
|
||||
name=self.name,
|
||||
location=self.location,
|
||||
notes=self.notes,
|
||||
pjlink_name=self.pjlink_name,
|
||||
manufacturer=self.manufacturer,
|
||||
model=self.model,
|
||||
other=self.other,
|
||||
sources=self.sources,
|
||||
source_list=self.source_list,
|
||||
serial=self.serial_no,
|
||||
mfilter=self.model_filter,
|
||||
mlamp=self.model_lamp,
|
||||
sw_ver=self.sw_version)
|
||||
ip = Column(String(100))
|
||||
port = Column(String(8))
|
||||
pin = Column(String(20))
|
||||
|
@ -189,6 +198,10 @@ class Projector(CommonBase, Base):
|
|||
model = Column(String(128))
|
||||
other = Column(String(128))
|
||||
sources = Column(String(128))
|
||||
serial_no = Column(String(30))
|
||||
sw_version = Column(String(30))
|
||||
model_filter = Column(String(30))
|
||||
model_lamp = Column(String(30))
|
||||
source_list = relationship('ProjectorSource',
|
||||
order_by='ProjectorSource.code',
|
||||
backref='projector',
|
||||
|
@ -359,6 +372,10 @@ class ProjectorDB(Manager):
|
|||
old_projector.model = projector.model
|
||||
old_projector.other = projector.other
|
||||
old_projector.sources = projector.sources
|
||||
old_projector.serial_no = projector.serial_no
|
||||
old_projector.sw_version = projector.sw_version
|
||||
old_projector.model_filter = projector.model_filter
|
||||
old_projector.model_lamp = projector.model_lamp
|
||||
return self.save_object(old_projector)
|
||||
|
||||
def delete_projector(self, projector):
|
||||
|
|
|
@ -42,7 +42,7 @@ log = logging.getLogger(__name__)
|
|||
|
||||
log.debug('pjlink1 loaded')
|
||||
|
||||
__all__ = ['PJLink1']
|
||||
__all__ = ['PJLink']
|
||||
|
||||
from codecs import decode
|
||||
|
||||
|
@ -53,20 +53,22 @@ from openlp.core.lib.projector.constants import CONNECTION_ERRORS, CR, ERROR_MSG
|
|||
E_AUTHENTICATION, E_CONNECTION_REFUSED, E_GENERAL, E_INVALID_DATA, E_NETWORK, E_NOT_CONNECTED, \
|
||||
E_PARAMETER, E_PROJECTOR, E_SOCKET_TIMEOUT, E_UNAVAILABLE, E_UNDEFINED, PJLINK_ERRORS, \
|
||||
PJLINK_ERST_STATUS, PJLINK_MAX_PACKET, PJLINK_PORT, PJLINK_POWR_STATUS, PJLINK_VALID_CMD, \
|
||||
STATUS_STRING, S_CONNECTED, S_CONNECTING, S_NETWORK_RECEIVED, S_NETWORK_SENDING, S_NOT_CONNECTED, \
|
||||
S_OFF, S_OK, S_ON, S_STATUS
|
||||
PJLINK_DEFAULT_CODES, STATUS_STRING, S_CONNECTED, S_CONNECTING, S_NETWORK_RECEIVED, S_NETWORK_SENDING, \
|
||||
S_NOT_CONNECTED, S_OFF, S_OK, S_ON, S_STATUS
|
||||
|
||||
# Shortcuts
|
||||
SocketError = QtNetwork.QAbstractSocket.SocketError
|
||||
SocketSTate = QtNetwork.QAbstractSocket.SocketState
|
||||
|
||||
PJLINK_PREFIX = '%'
|
||||
PJLINK_CLASS = '1'
|
||||
PJLINK_HEADER = '{prefix}{linkclass}'.format(prefix=PJLINK_PREFIX, linkclass=PJLINK_CLASS)
|
||||
PJLINK_CLASS = '1' # Default to class 1 until we query the projector
|
||||
# Add prefix here, but defer linkclass expansion until later when we have the actual
|
||||
# PJLink class for the command
|
||||
PJLINK_HEADER = '{prefix}{{linkclass}}'.format(prefix=PJLINK_PREFIX)
|
||||
PJLINK_SUFFIX = CR
|
||||
|
||||
|
||||
class PJLink1(QtNetwork.QTcpSocket):
|
||||
class PJLink(QtNetwork.QTcpSocket):
|
||||
"""
|
||||
Socket service for connecting to a PJLink-capable projector.
|
||||
"""
|
||||
|
@ -78,6 +80,33 @@ class PJLink1(QtNetwork.QTcpSocket):
|
|||
projectorNoAuthentication = QtCore.pyqtSignal(str) # PIN set and no authentication needed
|
||||
projectorReceivedData = QtCore.pyqtSignal() # Notify when received data finished processing
|
||||
projectorUpdateIcons = QtCore.pyqtSignal() # Update the status icons on toolbar
|
||||
# New commands available in PJLink Class 2
|
||||
pjlink_future = [
|
||||
'ACKN', # UDP Reply to 'SRCH'
|
||||
'FILT', # Get current filter usage time
|
||||
'FREZ', # Set freeze/unfreeze picture being projected
|
||||
'INNM', # Get Video source input terminal name
|
||||
'IRES', # Get Video source resolution
|
||||
'LKUP', # UPD Linkup status notification
|
||||
'MVOL', # Set microphone volume
|
||||
'RFIL', # Get replacement air filter model number
|
||||
'RLMP', # Get lamp replacement model number
|
||||
'RRES', # Get projector recommended video resolution
|
||||
'SNUM', # Get projector serial number
|
||||
'SRCH', # UDP broadcast search for available projectors on local network
|
||||
'SVER', # Get projector software version
|
||||
'SVOL', # Set speaker volume
|
||||
'TESTMEONLY' # For testing when other commands have been implemented
|
||||
]
|
||||
|
||||
pjlink_udp_commands = [
|
||||
'ACKN',
|
||||
'ERST', # Class 1 or 2
|
||||
'INPT', # Class 1 or 2
|
||||
'LKUP',
|
||||
'POWR', # Class 1 or 2
|
||||
'SRCH'
|
||||
]
|
||||
|
||||
def __init__(self, name=None, ip=None, port=PJLINK_PORT, pin=None, *args, **kwargs):
|
||||
"""
|
||||
|
@ -100,7 +129,7 @@ class PJLink1(QtNetwork.QTcpSocket):
|
|||
self.ip = ip
|
||||
self.port = port
|
||||
self.pin = pin
|
||||
super(PJLink1, self).__init__()
|
||||
super(PJLink, self).__init__()
|
||||
self.dbid = None
|
||||
self.location = None
|
||||
self.notes = None
|
||||
|
@ -133,7 +162,7 @@ class PJLink1(QtNetwork.QTcpSocket):
|
|||
# Socket timer for some possible brain-dead projectors or network cable pulled
|
||||
self.socket_timer = None
|
||||
# Map command to function
|
||||
self.pjlink1_functions = {
|
||||
self.pjlink_functions = {
|
||||
'AVMT': self.process_avmt,
|
||||
'CLSS': self.process_clss,
|
||||
'ERST': self.process_erst,
|
||||
|
@ -244,8 +273,6 @@ class PJLink1(QtNetwork.QTcpSocket):
|
|||
self.send_command('INF2', queue=True)
|
||||
if self.pjlink_name is None:
|
||||
self.send_command('NAME', queue=True)
|
||||
if self.power == S_ON and self.source_available is None:
|
||||
self.send_command('INST', queue=True)
|
||||
|
||||
def _get_status(self, status):
|
||||
"""
|
||||
|
@ -259,7 +286,7 @@ class PJLink1(QtNetwork.QTcpSocket):
|
|||
elif status in STATUS_STRING:
|
||||
return STATUS_STRING[status], ERROR_MSG[status]
|
||||
else:
|
||||
return status, translate('OpenLP.PJLink1', 'Unknown status')
|
||||
return status, translate('OpenLP.PJLink', 'Unknown status')
|
||||
|
||||
def change_status(self, status, msg=None):
|
||||
"""
|
||||
|
@ -269,7 +296,7 @@ class PJLink1(QtNetwork.QTcpSocket):
|
|||
:param status: Status code
|
||||
:param msg: Optional message
|
||||
"""
|
||||
message = translate('OpenLP.PJLink1', 'No message') if msg is None else msg
|
||||
message = translate('OpenLP.PJLink', 'No message') if msg is None else msg
|
||||
(code, message) = self._get_status(status)
|
||||
if msg is not None:
|
||||
message = msg
|
||||
|
@ -322,7 +349,7 @@ class PJLink1(QtNetwork.QTcpSocket):
|
|||
elif len(read) < 8:
|
||||
log.warning('({ip}) Not enough data read)'.format(ip=self.ip))
|
||||
return
|
||||
data = decode(read, 'ascii')
|
||||
data = decode(read, 'utf-8')
|
||||
# Possibility of extraneous data on input when reading.
|
||||
# Clean out extraneous characters in buffer.
|
||||
dontcare = self.readLine(self.max_size)
|
||||
|
@ -403,25 +430,24 @@ class PJLink1(QtNetwork.QTcpSocket):
|
|||
return
|
||||
self.socket_timer.stop()
|
||||
self.projectorNetwork.emit(S_NETWORK_RECEIVED)
|
||||
data_in = decode(read, 'ascii')
|
||||
# NOTE: Class2 has changed to some values being UTF-8
|
||||
data_in = decode(read, 'utf-8')
|
||||
data = data_in.strip()
|
||||
if len(data) < 7:
|
||||
# Not enough data for a packet
|
||||
log.debug('({ip}) get_data(): Packet length < 7: "{data}"'.format(ip=self.ip, data=data))
|
||||
self.send_busy = False
|
||||
self.projectorReceivedData.emit()
|
||||
return
|
||||
log.debug('({ip}) get_data(): Checking new data "{data}"'.format(ip=self.ip, data=data))
|
||||
if data.upper().startswith('PJLINK'):
|
||||
# Reconnected from remote host disconnect ?
|
||||
self.check_login(data)
|
||||
self.send_busy = False
|
||||
self.projectorReceivedData.emit()
|
||||
self.receive_data_signal()
|
||||
return
|
||||
elif '=' not in data:
|
||||
log.warning('({ip}) get_data(): Invalid packet received'.format(ip=self.ip))
|
||||
self.send_busy = False
|
||||
self.projectorReceivedData.emit()
|
||||
self.receive_data_signal()
|
||||
return
|
||||
log.debug('({ip}) get_data(): Checking new data "{data}"'.format(ip=self.ip, data=data))
|
||||
# At this point, we should have something to work with
|
||||
if data.upper().startswith('PJLINK'):
|
||||
# Reconnected from remote host disconnect ?
|
||||
self.check_login(data)
|
||||
self.receive_data_signal()
|
||||
return
|
||||
data_split = data.split('=')
|
||||
try:
|
||||
|
@ -430,15 +456,15 @@ class PJLink1(QtNetwork.QTcpSocket):
|
|||
log.warning('({ip}) get_data(): Invalid packet - expected header + command + data'.format(ip=self.ip))
|
||||
log.warning('({ip}) get_data(): Received data: "{data}"'.format(ip=self.ip, data=data_in.strip()))
|
||||
self.change_status(E_INVALID_DATA)
|
||||
self.send_busy = False
|
||||
self.projectorReceivedData.emit()
|
||||
self.receive_data_signal()
|
||||
return
|
||||
|
||||
if not (self.pjlink_class in PJLINK_VALID_CMD and cmd in PJLINK_VALID_CMD[self.pjlink_class]):
|
||||
if not (cmd in PJLINK_VALID_CMD and class_ in PJLINK_VALID_CMD[cmd]):
|
||||
log.warning('({ip}) get_data(): Invalid packet - unknown command "{data}"'.format(ip=self.ip, data=cmd))
|
||||
self.send_busy = False
|
||||
self.projectorReceivedData.emit()
|
||||
self.receive_data_signal()
|
||||
return
|
||||
if int(self.pjlink_class) < int(class_):
|
||||
log.warn('({ip}) get_data(): Projector returned class reply higher '
|
||||
'than projector stated class'.format(ip=self.ip))
|
||||
return self.process_command(cmd, data)
|
||||
|
||||
@QtCore.pyqtSlot(QtNetwork.QAbstractSocket.SocketError)
|
||||
|
@ -487,8 +513,10 @@ class PJLink1(QtNetwork.QTcpSocket):
|
|||
data=opts,
|
||||
salt='' if salt is None
|
||||
else ' with hash'))
|
||||
# TODO: Check for class of command rather than default to projector PJLink class
|
||||
header = PJLINK_HEADER.format(linkclass=self.pjlink_class)
|
||||
out = '{salt}{header}{command} {options}{suffix}'.format(salt="" if salt is None else salt,
|
||||
header=PJLINK_HEADER,
|
||||
header=header,
|
||||
command=cmd,
|
||||
options=opts,
|
||||
suffix=CR)
|
||||
|
@ -510,11 +538,12 @@ class PJLink1(QtNetwork.QTcpSocket):
|
|||
self._send_command()
|
||||
|
||||
@QtCore.pyqtSlot()
|
||||
def _send_command(self, data=None):
|
||||
def _send_command(self, data=None, utf8=False):
|
||||
"""
|
||||
Socket interface to send data. If data=None, then check queue.
|
||||
|
||||
:param data: Immediate data to send
|
||||
:param utf8: Send as UTF-8 string otherwise send as ASCII string
|
||||
"""
|
||||
log.debug('({ip}) _send_string()'.format(ip=self.ip))
|
||||
log.debug('({ip}) _send_string(): Connection status: {data}'.format(ip=self.ip, data=self.state()))
|
||||
|
@ -542,12 +571,12 @@ class PJLink1(QtNetwork.QTcpSocket):
|
|||
log.debug('({ip}) _send_string(): Queue = {data}'.format(ip=self.ip, data=self.send_queue))
|
||||
self.socket_timer.start()
|
||||
self.projectorNetwork.emit(S_NETWORK_SENDING)
|
||||
sent = self.write(out.encode('ascii'))
|
||||
sent = self.write(out.encode('{string_encoding}'.format(string_encoding='utf-8' if utf8 else 'ascii')))
|
||||
self.waitForBytesWritten(2000) # 2 seconds should be enough
|
||||
if sent == -1:
|
||||
# Network error?
|
||||
self.change_status(E_NETWORK,
|
||||
translate('OpenLP.PJLink1', 'Error while sending data to projector'))
|
||||
translate('OpenLP.PJLink', 'Error while sending data to projector'))
|
||||
|
||||
def process_command(self, cmd, data):
|
||||
"""
|
||||
|
@ -556,7 +585,13 @@ class PJLink1(QtNetwork.QTcpSocket):
|
|||
:param cmd: Command to process
|
||||
:param data: Data being processed
|
||||
"""
|
||||
log.debug('({ip}) Processing command "{data}"'.format(ip=self.ip, data=cmd))
|
||||
log.debug('({ip}) Processing command "{cmd}" with data "{data}"'.format(ip=self.ip,
|
||||
cmd=cmd,
|
||||
data=data))
|
||||
# Check if we have a future command not available yet
|
||||
if cmd in self.pjlink_future:
|
||||
self._not_implemented(cmd)
|
||||
return
|
||||
if data in PJLINK_ERRORS:
|
||||
# Oops - projector error
|
||||
log.error('({ip}) Projector returned error "{data}"'.format(ip=self.ip, data=data))
|
||||
|
@ -568,9 +603,8 @@ class PJLink1(QtNetwork.QTcpSocket):
|
|||
self.projectorAuthentication.emit(self.name)
|
||||
elif data.upper() == 'ERR1':
|
||||
# Undefined command
|
||||
self.change_status(E_UNDEFINED, '{error} "{data}"'.format(error=translate('OpenLP.PJLink1',
|
||||
'Undefined command:'),
|
||||
data=cmd))
|
||||
self.change_status(E_UNDEFINED, '{error}: "{data}"'.format(error=ERROR_MSG[E_UNDEFINED],
|
||||
data=cmd))
|
||||
elif data.upper() == 'ERR2':
|
||||
# Invalid parameter
|
||||
self.change_status(E_PARAMETER)
|
||||
|
@ -591,8 +625,9 @@ class PJLink1(QtNetwork.QTcpSocket):
|
|||
self.projectorReceivedData.emit()
|
||||
return
|
||||
|
||||
if cmd in self.pjlink1_functions:
|
||||
self.pjlink1_functions[cmd](data)
|
||||
if cmd in self.pjlink_functions:
|
||||
log.debug('({ip}) Calling function for {cmd}'.format(ip=self.ip, cmd=cmd))
|
||||
self.pjlink_functions[cmd](data)
|
||||
else:
|
||||
log.warning('({ip}) Invalid command {data}'.format(ip=self.ip, data=cmd))
|
||||
self.send_busy = False
|
||||
|
@ -628,6 +663,7 @@ class PJLink1(QtNetwork.QTcpSocket):
|
|||
|
||||
:param data: Power status
|
||||
"""
|
||||
log.debug('({ip}: Processing POWR command'.format(ip=self.ip))
|
||||
if data in PJLINK_POWR_STATUS:
|
||||
power = PJLINK_POWR_STATUS[data]
|
||||
update_icons = self.power != power
|
||||
|
@ -962,3 +998,19 @@ class PJLink1(QtNetwork.QTcpSocket):
|
|||
log.debug('({ip}) Setting AVMT to "10" (shutter open)'.format(ip=self.ip))
|
||||
self.send_command(cmd='AVMT', opts='10')
|
||||
self.poll_loop()
|
||||
|
||||
def receive_data_signal(self):
|
||||
"""
|
||||
Clear any busy flags and send data received signal
|
||||
"""
|
||||
self.send_busy = False
|
||||
self.projectorReceivedData.emit()
|
||||
return
|
||||
|
||||
def _not_implemented(self, cmd):
|
||||
"""
|
||||
Log when a future PJLink command has not been implemented yet.
|
||||
"""
|
||||
log.warn("({ip}) Future command '{cmd}' has not been implemented yet".format(ip=self.ip,
|
||||
cmd=cmd))
|
||||
return
|
||||
|
|
|
@ -27,7 +27,7 @@ from PyQt5 import QtGui, QtCore, QtWebKitWidgets
|
|||
|
||||
from openlp.core.common import Registry, RegistryProperties, OpenLPMixin, RegistryMixin, Settings
|
||||
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.ui import MainDisplay
|
||||
|
||||
|
@ -383,13 +383,14 @@ class Renderer(OpenLPMixin, RegistryMixin, RegistryProperties):
|
|||
</script>
|
||||
<style>
|
||||
*{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>
|
||||
<body><div id="main"></div></body></html>""")
|
||||
self.web.setHtml(html.substitute(format_css=build_lyrics_format_css(theme_data,
|
||||
self.page_width,
|
||||
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()
|
||||
|
||||
def _paginate_slide(self, lines, line_end):
|
||||
|
|
|
@ -34,7 +34,7 @@ import ntpath
|
|||
from PyQt5 import QtGui
|
||||
|
||||
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__)
|
||||
|
||||
|
@ -117,7 +117,6 @@ class ItemCapabilities(object):
|
|||
|
||||
``HasThumbnails``
|
||||
The item has related thumbnails available
|
||||
|
||||
"""
|
||||
CanPreview = 1
|
||||
CanEdit = 2
|
||||
|
@ -247,6 +246,8 @@ class ServiceItem(RegistryProperties):
|
|||
self.renderer.set_item_theme(self.theme)
|
||||
self.theme_data, self.main, self.footer = self.renderer.pre_render()
|
||||
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))
|
||||
# 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.
|
||||
|
@ -260,13 +261,16 @@ class ServiceItem(RegistryProperties):
|
|||
previous_pages[verse_tag] = (slide['raw_slide'], pages)
|
||||
for page in pages:
|
||||
page = page.replace('<br>', '{br}')
|
||||
html_data = expand_tags(html.escape(page.rstrip()))
|
||||
self._display_frames.append({
|
||||
html_data = expand_tags(page.rstrip(), expand_chord_tags)
|
||||
new_frame = {
|
||||
'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;', ' '),
|
||||
'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:
|
||||
pass
|
||||
else:
|
||||
|
|
|
@ -163,7 +163,7 @@ class Theme(object):
|
|||
jsn = get_text_file_string(json_file)
|
||||
jsn = json.loads(jsn)
|
||||
self.expand_json(jsn)
|
||||
self.background_filename = None
|
||||
self.background_filename = ''
|
||||
|
||||
def expand_json(self, var, prev=None):
|
||||
"""
|
||||
|
|
|
@ -25,13 +25,13 @@ The :mod:`advancedtab` provides an advanced settings facility.
|
|||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
||||
from openlp.core.common import AppLocation, Settings, SlideLimits, UiStrings, translate
|
||||
from openlp.core.lib import SettingsTab, build_icon
|
||||
from openlp.core.common.languagemanager import format_time
|
||||
from openlp.core.lib import SettingsTab, build_icon
|
||||
from openlp.core.ui.lib import PathEdit, PathType
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
@ -153,32 +153,17 @@ class AdvancedTab(SettingsTab):
|
|||
self.data_directory_group_box.setObjectName('data_directory_group_box')
|
||||
self.data_directory_layout = QtWidgets.QFormLayout(self.data_directory_group_box)
|
||||
self.data_directory_layout.setObjectName('data_directory_layout')
|
||||
self.data_directory_current_label = QtWidgets.QLabel(self.data_directory_group_box)
|
||||
self.data_directory_current_label.setObjectName('data_directory_current_label')
|
||||
self.data_directory_label = QtWidgets.QLabel(self.data_directory_group_box)
|
||||
self.data_directory_label.setObjectName('data_directory_label')
|
||||
self.data_directory_new_label = QtWidgets.QLabel(self.data_directory_group_box)
|
||||
self.data_directory_new_label.setObjectName('data_directory_current_label')
|
||||
self.new_data_directory_edit = QtWidgets.QLineEdit(self.data_directory_group_box)
|
||||
self.new_data_directory_edit.setObjectName('new_data_directory_edit')
|
||||
self.new_data_directory_edit.setReadOnly(True)
|
||||
self.data_directory_path_edit = PathEdit(self.data_directory_group_box, path_type=PathType.Directories,
|
||||
default_path=AppLocation.get_directory(AppLocation.DataDir))
|
||||
self.data_directory_layout.addRow(self.data_directory_new_label, self.data_directory_path_edit)
|
||||
self.new_data_directory_has_files_label = QtWidgets.QLabel(self.data_directory_group_box)
|
||||
self.new_data_directory_has_files_label.setObjectName('new_data_directory_has_files_label')
|
||||
self.new_data_directory_has_files_label.setWordWrap(True)
|
||||
self.data_directory_browse_button = QtWidgets.QToolButton(self.data_directory_group_box)
|
||||
self.data_directory_browse_button.setObjectName('data_directory_browse_button')
|
||||
self.data_directory_browse_button.setIcon(build_icon(':/general/general_open.png'))
|
||||
self.data_directory_default_button = QtWidgets.QToolButton(self.data_directory_group_box)
|
||||
self.data_directory_default_button.setObjectName('data_directory_default_button')
|
||||
self.data_directory_default_button.setIcon(build_icon(':/general/general_revert.png'))
|
||||
self.data_directory_cancel_button = QtWidgets.QToolButton(self.data_directory_group_box)
|
||||
self.data_directory_cancel_button.setObjectName('data_directory_cancel_button')
|
||||
self.data_directory_cancel_button.setIcon(build_icon(':/general/general_delete.png'))
|
||||
self.new_data_directory_label_layout = QtWidgets.QHBoxLayout()
|
||||
self.new_data_directory_label_layout.setObjectName('new_data_directory_label_layout')
|
||||
self.new_data_directory_label_layout.addWidget(self.new_data_directory_edit)
|
||||
self.new_data_directory_label_layout.addWidget(self.data_directory_browse_button)
|
||||
self.new_data_directory_label_layout.addWidget(self.data_directory_default_button)
|
||||
self.data_directory_copy_check_layout = QtWidgets.QHBoxLayout()
|
||||
self.data_directory_copy_check_layout.setObjectName('data_directory_copy_check_layout')
|
||||
self.data_directory_copy_check_box = QtWidgets.QCheckBox(self.data_directory_group_box)
|
||||
|
@ -186,8 +171,6 @@ class AdvancedTab(SettingsTab):
|
|||
self.data_directory_copy_check_layout.addWidget(self.data_directory_copy_check_box)
|
||||
self.data_directory_copy_check_layout.addStretch()
|
||||
self.data_directory_copy_check_layout.addWidget(self.data_directory_cancel_button)
|
||||
self.data_directory_layout.addRow(self.data_directory_current_label, self.data_directory_label)
|
||||
self.data_directory_layout.addRow(self.data_directory_new_label, self.new_data_directory_label_layout)
|
||||
self.data_directory_layout.addRow(self.data_directory_copy_check_layout)
|
||||
self.data_directory_layout.addRow(self.new_data_directory_has_files_label)
|
||||
self.left_layout.addWidget(self.data_directory_group_box)
|
||||
|
@ -239,8 +222,7 @@ class AdvancedTab(SettingsTab):
|
|||
self.service_name_edit.textChanged.connect(self.update_service_name_example)
|
||||
self.service_name_revert_button.clicked.connect(self.on_service_name_revert_button_clicked)
|
||||
self.alternate_rows_check_box.toggled.connect(self.on_alternate_rows_check_box_toggled)
|
||||
self.data_directory_browse_button.clicked.connect(self.on_data_directory_browse_button_clicked)
|
||||
self.data_directory_default_button.clicked.connect(self.on_data_directory_default_button_clicked)
|
||||
self.data_directory_path_edit.pathChanged.connect(self.on_data_directory_path_edit_path_changed)
|
||||
self.data_directory_cancel_button.clicked.connect(self.on_data_directory_cancel_button_clicked)
|
||||
self.data_directory_copy_check_box.toggled.connect(self.on_data_directory_copy_check_box_toggled)
|
||||
self.end_slide_radio_button.clicked.connect(self.on_end_slide_button_clicked)
|
||||
|
@ -317,12 +299,7 @@ class AdvancedTab(SettingsTab):
|
|||
self.service_name_example_label.setText(translate('OpenLP.AdvancedTab', 'Example:'))
|
||||
self.hide_mouse_group_box.setTitle(translate('OpenLP.AdvancedTab', 'Mouse Cursor'))
|
||||
self.hide_mouse_check_box.setText(translate('OpenLP.AdvancedTab', 'Hide mouse cursor when over display window'))
|
||||
self.data_directory_current_label.setText(translate('OpenLP.AdvancedTab', 'Current path:'))
|
||||
self.data_directory_new_label.setText(translate('OpenLP.AdvancedTab', 'Custom path:'))
|
||||
self.data_directory_browse_button.setToolTip(translate('OpenLP.AdvancedTab',
|
||||
'Browse for new data file location.'))
|
||||
self.data_directory_default_button.setToolTip(
|
||||
translate('OpenLP.AdvancedTab', 'Set the data location to the default.'))
|
||||
self.data_directory_new_label.setText(translate('OpenLP.AdvancedTab', 'Path:'))
|
||||
self.data_directory_cancel_button.setText(translate('OpenLP.AdvancedTab', 'Cancel'))
|
||||
self.data_directory_cancel_button.setToolTip(
|
||||
translate('OpenLP.AdvancedTab', 'Cancel OpenLP data directory location change.'))
|
||||
|
@ -396,8 +373,7 @@ class AdvancedTab(SettingsTab):
|
|||
self.new_data_directory_has_files_label.hide()
|
||||
self.data_directory_cancel_button.hide()
|
||||
# Since data location can be changed, make sure the path is present.
|
||||
self.current_data_path = AppLocation.get_data_path()
|
||||
self.data_directory_label.setText(os.path.abspath(self.current_data_path))
|
||||
self.data_directory_path_edit.path = AppLocation.get_data_path()
|
||||
# Don't allow data directory move if running portable.
|
||||
if settings.value('advanced/is portable'):
|
||||
self.data_directory_group_box.hide()
|
||||
|
@ -509,24 +485,10 @@ class AdvancedTab(SettingsTab):
|
|||
self.service_name_edit.setText(UiStrings().DefaultServiceName)
|
||||
self.service_name_edit.setFocus()
|
||||
|
||||
def on_data_directory_browse_button_clicked(self):
|
||||
def on_data_directory_path_edit_path_changed(self, new_data_path):
|
||||
"""
|
||||
Browse for a new data directory location.
|
||||
"""
|
||||
old_root_path = str(self.data_directory_label.text())
|
||||
# Get the new directory location.
|
||||
new_data_path = QtWidgets.QFileDialog.getExistingDirectory(self, translate('OpenLP.AdvancedTab',
|
||||
'Select Data Directory Location'),
|
||||
old_root_path,
|
||||
options=QtWidgets.QFileDialog.ShowDirsOnly)
|
||||
# Set the new data path.
|
||||
if new_data_path:
|
||||
new_data_path = os.path.normpath(new_data_path)
|
||||
if self.current_data_path.lower() == new_data_path.lower():
|
||||
self.on_data_directory_cancel_button_clicked()
|
||||
return
|
||||
else:
|
||||
return
|
||||
# Make sure they want to change the data.
|
||||
answer = QtWidgets.QMessageBox.question(self, translate('OpenLP.AdvancedTab', 'Confirm Data Directory Change'),
|
||||
translate('OpenLP.AdvancedTab', 'Are you sure you want to change the '
|
||||
|
@ -537,42 +499,14 @@ class AdvancedTab(SettingsTab):
|
|||
QtWidgets.QMessageBox.No),
|
||||
QtWidgets.QMessageBox.No)
|
||||
if answer != QtWidgets.QMessageBox.Yes:
|
||||
self.data_directory_path_edit.path = AppLocation.get_data_path()
|
||||
return
|
||||
# Check if data already exists here.
|
||||
self.check_data_overwrite(new_data_path)
|
||||
# Save the new location.
|
||||
self.main_window.set_new_data_path(new_data_path)
|
||||
self.new_data_directory_edit.setText(new_data_path)
|
||||
self.data_directory_cancel_button.show()
|
||||
|
||||
def on_data_directory_default_button_clicked(self):
|
||||
"""
|
||||
Re-set the data directory location to the 'default' location.
|
||||
"""
|
||||
new_data_path = AppLocation.get_directory(AppLocation.DataDir)
|
||||
if self.current_data_path.lower() != new_data_path.lower():
|
||||
# Make sure they want to change the data location back to the
|
||||
# default.
|
||||
answer = QtWidgets.QMessageBox.question(self, translate('OpenLP.AdvancedTab', 'Reset Data Directory'),
|
||||
translate('OpenLP.AdvancedTab', 'Are you sure you want to change '
|
||||
'the location of the OpenLP data '
|
||||
'directory to the default location?'
|
||||
'\n\nThis location will be used '
|
||||
'after OpenLP is closed.'),
|
||||
QtWidgets.QMessageBox.StandardButtons(QtWidgets.QMessageBox.Yes |
|
||||
QtWidgets.QMessageBox.No),
|
||||
QtWidgets.QMessageBox.No)
|
||||
if answer != QtWidgets.QMessageBox.Yes:
|
||||
return
|
||||
self.check_data_overwrite(new_data_path)
|
||||
# Save the new location.
|
||||
self.main_window.set_new_data_path(new_data_path)
|
||||
self.new_data_directory_edit.setText(os.path.abspath(new_data_path))
|
||||
self.data_directory_cancel_button.show()
|
||||
else:
|
||||
# We cancel the change in case user changed their mind.
|
||||
self.on_data_directory_cancel_button_clicked()
|
||||
|
||||
def on_data_directory_copy_check_box_toggled(self):
|
||||
"""
|
||||
Copy existing data when you change your data directory.
|
||||
|
@ -589,7 +523,6 @@ class AdvancedTab(SettingsTab):
|
|||
Check if there's already data in the target directory.
|
||||
"""
|
||||
test_path = os.path.join(data_path, 'songs')
|
||||
self.data_directory_copy_check_box.show()
|
||||
if os.path.exists(test_path):
|
||||
self.data_exists = True
|
||||
# Check is they want to replace existing data.
|
||||
|
@ -603,6 +536,7 @@ class AdvancedTab(SettingsTab):
|
|||
QtWidgets.QMessageBox.StandardButtons(QtWidgets.QMessageBox.Yes |
|
||||
QtWidgets.QMessageBox.No),
|
||||
QtWidgets.QMessageBox.No)
|
||||
self.data_directory_copy_check_box.show()
|
||||
if answer == QtWidgets.QMessageBox.Yes:
|
||||
self.data_directory_copy_check_box.setChecked(True)
|
||||
self.new_data_directory_has_files_label.show()
|
||||
|
@ -618,7 +552,7 @@ class AdvancedTab(SettingsTab):
|
|||
"""
|
||||
Cancel the data directory location change
|
||||
"""
|
||||
self.new_data_directory_edit.clear()
|
||||
self.data_directory_path_edit.path = AppLocation.get_data_path()
|
||||
self.data_directory_copy_check_box.setChecked(False)
|
||||
self.main_window.set_new_data_path(None)
|
||||
self.main_window.set_copy_data(False)
|
||||
|
|
|
@ -27,8 +27,8 @@ import logging
|
|||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
||||
from openlp.core.common import Registry, Settings, UiStrings, translate, get_images_filter
|
||||
from openlp.core.lib import SettingsTab, ScreenList, build_icon
|
||||
from openlp.core.ui.lib.colorbutton import ColorButton
|
||||
from openlp.core.lib import SettingsTab, ScreenList
|
||||
from openlp.core.ui.lib import ColorButton, PathEdit
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
@ -172,20 +172,8 @@ class GeneralTab(SettingsTab):
|
|||
self.logo_layout.setObjectName('logo_layout')
|
||||
self.logo_file_label = QtWidgets.QLabel(self.logo_group_box)
|
||||
self.logo_file_label.setObjectName('logo_file_label')
|
||||
self.logo_file_edit = QtWidgets.QLineEdit(self.logo_group_box)
|
||||
self.logo_file_edit.setObjectName('logo_file_edit')
|
||||
self.logo_browse_button = QtWidgets.QToolButton(self.logo_group_box)
|
||||
self.logo_browse_button.setObjectName('logo_browse_button')
|
||||
self.logo_browse_button.setIcon(build_icon(':/general/general_open.png'))
|
||||
self.logo_revert_button = QtWidgets.QToolButton(self.logo_group_box)
|
||||
self.logo_revert_button.setObjectName('logo_revert_button')
|
||||
self.logo_revert_button.setIcon(build_icon(':/general/general_revert.png'))
|
||||
self.logo_file_layout = QtWidgets.QHBoxLayout()
|
||||
self.logo_file_layout.setObjectName('logo_file_layout')
|
||||
self.logo_file_layout.addWidget(self.logo_file_edit)
|
||||
self.logo_file_layout.addWidget(self.logo_browse_button)
|
||||
self.logo_file_layout.addWidget(self.logo_revert_button)
|
||||
self.logo_layout.addRow(self.logo_file_label, self.logo_file_layout)
|
||||
self.logo_file_path_edit = PathEdit(self.logo_group_box, default_path=':/graphics/openlp-splash-screen.png')
|
||||
self.logo_layout.addRow(self.logo_file_label, self.logo_file_path_edit)
|
||||
self.logo_color_label = QtWidgets.QLabel(self.logo_group_box)
|
||||
self.logo_color_label.setObjectName('logo_color_label')
|
||||
self.logo_color_button = ColorButton(self.logo_group_box)
|
||||
|
@ -196,8 +184,6 @@ class GeneralTab(SettingsTab):
|
|||
self.logo_layout.addRow(self.logo_hide_on_startup_check_box)
|
||||
self.right_layout.addWidget(self.logo_group_box)
|
||||
self.logo_color_button.colorChanged.connect(self.on_logo_background_color_changed)
|
||||
self.logo_browse_button.clicked.connect(self.on_logo_browse_button_clicked)
|
||||
self.logo_revert_button.clicked.connect(self.on_logo_revert_button_clicked)
|
||||
# Application Settings
|
||||
self.settings_group_box = QtWidgets.QGroupBox(self.right_column)
|
||||
self.settings_group_box.setObjectName('settings_group_box')
|
||||
|
@ -254,8 +240,6 @@ class GeneralTab(SettingsTab):
|
|||
self.logo_group_box.setTitle(translate('OpenLP.GeneralTab', 'Logo'))
|
||||
self.logo_color_label.setText(UiStrings().BackgroundColorColon)
|
||||
self.logo_file_label.setText(translate('OpenLP.GeneralTab', 'Logo file:'))
|
||||
self.logo_browse_button.setToolTip(translate('OpenLP.GeneralTab', 'Browse for an image file to display.'))
|
||||
self.logo_revert_button.setToolTip(translate('OpenLP.GeneralTab', 'Revert to the default OpenLP logo.'))
|
||||
self.logo_hide_on_startup_check_box.setText(translate('OpenLP.GeneralTab', 'Don\'t show logo on startup'))
|
||||
self.check_for_updates_check_box.setText(translate('OpenLP.GeneralTab', 'Check for updates to OpenLP'))
|
||||
self.settings_group_box.setTitle(translate('OpenLP.GeneralTab', 'Application Settings'))
|
||||
|
@ -282,6 +266,9 @@ class GeneralTab(SettingsTab):
|
|||
self.audio_group_box.setTitle(translate('OpenLP.GeneralTab', 'Background Audio'))
|
||||
self.start_paused_check_box.setText(translate('OpenLP.GeneralTab', 'Start background audio paused'))
|
||||
self.repeat_list_check_box.setText(translate('OpenLP.GeneralTab', 'Repeat track list'))
|
||||
self.logo_file_path_edit.dialog_caption = dialog_caption = translate('OpenLP.AdvancedTab', 'Select Logo File')
|
||||
self.logo_file_path_edit.filters = '{text};;{names} (*)'.format(
|
||||
text=get_images_filter(), names=UiStrings().AllFiles)
|
||||
|
||||
def load(self):
|
||||
"""
|
||||
|
@ -304,7 +291,7 @@ class GeneralTab(SettingsTab):
|
|||
self.auto_open_check_box.setChecked(settings.value('auto open'))
|
||||
self.show_splash_check_box.setChecked(settings.value('show splash'))
|
||||
self.logo_background_color = settings.value('logo background color')
|
||||
self.logo_file_edit.setText(settings.value('logo file'))
|
||||
self.logo_file_path_edit.path = settings.value('logo file')
|
||||
self.logo_hide_on_startup_check_box.setChecked(settings.value('logo hide on startup'))
|
||||
self.logo_color_button.color = self.logo_background_color
|
||||
self.check_for_updates_check_box.setChecked(settings.value('update check'))
|
||||
|
@ -338,7 +325,7 @@ class GeneralTab(SettingsTab):
|
|||
settings.setValue('auto open', self.auto_open_check_box.isChecked())
|
||||
settings.setValue('show splash', self.show_splash_check_box.isChecked())
|
||||
settings.setValue('logo background color', self.logo_background_color)
|
||||
settings.setValue('logo file', self.logo_file_edit.text())
|
||||
settings.setValue('logo file', self.logo_file_path_edit.path)
|
||||
settings.setValue('logo hide on startup', self.logo_hide_on_startup_check_box.isChecked())
|
||||
settings.setValue('update check', self.check_for_updates_check_box.isChecked())
|
||||
settings.setValue('save prompt', self.save_check_service_check_box.isChecked())
|
||||
|
@ -404,25 +391,6 @@ class GeneralTab(SettingsTab):
|
|||
"""
|
||||
self.display_changed = True
|
||||
|
||||
def on_logo_browse_button_clicked(self):
|
||||
"""
|
||||
Select the logo file
|
||||
"""
|
||||
file_filters = '{text};;{names} (*.*)'.format(text=get_images_filter(), names=UiStrings().AllFiles)
|
||||
filename, filter_used = QtWidgets.QFileDialog.getOpenFileName(self,
|
||||
translate('OpenLP.AdvancedTab', 'Open File'), '',
|
||||
file_filters)
|
||||
if filename:
|
||||
self.logo_file_edit.setText(filename)
|
||||
self.logo_file_edit.setFocus()
|
||||
|
||||
def on_logo_revert_button_clicked(self):
|
||||
"""
|
||||
Revert the logo file back to the default setting.
|
||||
"""
|
||||
self.logo_file_edit.setText(':/graphics/openlp-splash-screen.png')
|
||||
self.logo_file_edit.setFocus()
|
||||
|
||||
def on_logo_background_color_changed(self, color):
|
||||
"""
|
||||
Select the background color for logo.
|
||||
|
|
|
@ -21,14 +21,16 @@
|
|||
###############################################################################
|
||||
|
||||
from .colorbutton import ColorButton
|
||||
from .listwidgetwithdnd import ListWidgetWithDnD
|
||||
from .treewidgetwithdnd import TreeWidgetWithDnD
|
||||
from .toolbar import OpenLPToolbar
|
||||
from .dockwidget import OpenLPDockWidget
|
||||
from .wizard import OpenLPWizard, WizardStrings
|
||||
from .mediadockmanager import MediaDockManager
|
||||
from .listpreviewwidget import ListPreviewWidget
|
||||
from .listwidgetwithdnd import ListWidgetWithDnD
|
||||
from .mediadockmanager import MediaDockManager
|
||||
from .dockwidget import OpenLPDockWidget
|
||||
from .toolbar import OpenLPToolbar
|
||||
from .wizard import OpenLPWizard, WizardStrings
|
||||
from .pathedit import PathEdit, PathType
|
||||
from .spelltextedit import SpellTextEdit
|
||||
from .treewidgetwithdnd import TreeWidgetWithDnD
|
||||
|
||||
__all__ = ['ColorButton', 'ListPreviewWidget', 'ListWidgetWithDnD', 'OpenLPToolbar', 'OpenLPDockWidget',
|
||||
'OpenLPWizard', 'WizardStrings', 'MediaDockManager', 'ListPreviewWidget', 'SpellTextEdit']
|
||||
__all__ = ['ColorButton', 'ListPreviewWidget', 'ListWidgetWithDnD', 'MediaDockManager', 'OpenLPDockWidget',
|
||||
'OpenLPToolbar', 'OpenLPWizard', 'PathEdit', 'PathType', 'SpellTextEdit', 'TreeWidgetWithDnD',
|
||||
'WizardStrings']
|
||||
|
|
|
@ -39,7 +39,7 @@ class ColorButton(QtWidgets.QPushButton):
|
|||
"""
|
||||
Initialise the ColorButton
|
||||
"""
|
||||
super(ColorButton, self).__init__()
|
||||
super().__init__(parent)
|
||||
self.parent = parent
|
||||
self.change_color('#ffffff')
|
||||
self.setToolTip(translate('OpenLP.ColorButton', 'Click to select a color.'))
|
||||
|
|
|
@ -0,0 +1,205 @@
|
|||
# -*- 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 #
|
||||
###############################################################################
|
||||
from enum import Enum
|
||||
import os.path
|
||||
|
||||
from PyQt5 import QtCore, QtWidgets
|
||||
|
||||
from openlp.core.common import UiStrings, translate
|
||||
from openlp.core.lib import build_icon
|
||||
|
||||
|
||||
class PathType(Enum):
|
||||
Files = 1
|
||||
Directories = 2
|
||||
|
||||
|
||||
class PathEdit(QtWidgets.QWidget):
|
||||
"""
|
||||
The :class:`~openlp.core.ui.lib.pathedit.PathEdit` class subclasses QWidget to create a custom widget for use when
|
||||
a file or directory needs to be selected.
|
||||
"""
|
||||
pathChanged = QtCore.pyqtSignal(str)
|
||||
|
||||
def __init__(self, parent=None, path_type=PathType.Files, default_path=None, dialog_caption=None, show_revert=True):
|
||||
"""
|
||||
Initalise the PathEdit widget
|
||||
|
||||
:param parent: The parent of the widget. This is just passed to the super method.
|
||||
:type parent: QWidget or None
|
||||
|
||||
:param dialog_caption: Used to customise the caption in the QFileDialog.
|
||||
:param dialog_caption: str
|
||||
|
||||
:param default_path: The default path. This is set as the path when the revert button is clicked
|
||||
:type default_path: str
|
||||
|
||||
:param show_revert: Used to determin if the 'revert button' should be visible.
|
||||
:type show_revert: bool
|
||||
|
||||
:return: None
|
||||
:rtype: None
|
||||
"""
|
||||
super().__init__(parent)
|
||||
self.default_path = default_path
|
||||
self.dialog_caption = dialog_caption
|
||||
self._path_type = path_type
|
||||
self._path = None
|
||||
self.filters = '{all_files} (*)'.format(all_files=UiStrings().AllFiles)
|
||||
self._setup(show_revert)
|
||||
|
||||
def _setup(self, show_revert):
|
||||
"""
|
||||
Set up the widget
|
||||
:param show_revert: Show or hide the revert button
|
||||
:type show_revert: bool
|
||||
|
||||
:return: None
|
||||
:rtype: None
|
||||
"""
|
||||
widget_layout = QtWidgets.QHBoxLayout()
|
||||
widget_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.line_edit = QtWidgets.QLineEdit(self)
|
||||
self.line_edit.setText(self._path)
|
||||
widget_layout.addWidget(self.line_edit)
|
||||
self.browse_button = QtWidgets.QToolButton(self)
|
||||
self.browse_button.setIcon(build_icon(':/general/general_open.png'))
|
||||
widget_layout.addWidget(self.browse_button)
|
||||
self.revert_button = QtWidgets.QToolButton(self)
|
||||
self.revert_button.setIcon(build_icon(':/general/general_revert.png'))
|
||||
self.revert_button.setVisible(show_revert)
|
||||
widget_layout.addWidget(self.revert_button)
|
||||
self.setLayout(widget_layout)
|
||||
# Signals and Slots
|
||||
self.browse_button.clicked.connect(self.on_browse_button_clicked)
|
||||
self.revert_button.clicked.connect(self.on_revert_button_clicked)
|
||||
self.line_edit.editingFinished.connect(self.on_line_edit_editing_finished)
|
||||
self.update_button_tool_tips()
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
"""
|
||||
A property getter method to return the selected path.
|
||||
|
||||
:return: The selected path
|
||||
:rtype: str
|
||||
"""
|
||||
return self._path
|
||||
|
||||
@path.setter
|
||||
def path(self, path):
|
||||
"""
|
||||
A Property setter method to set the selected path
|
||||
|
||||
:param path: The path to set the widget to
|
||||
:type path: str
|
||||
"""
|
||||
self._path = path
|
||||
self.line_edit.setText(path)
|
||||
self.line_edit.setToolTip(path)
|
||||
|
||||
@property
|
||||
def path_type(self):
|
||||
"""
|
||||
A property getter method to return the path_type. Path type allows you to sepecify if the user is restricted to
|
||||
selecting a file or directory.
|
||||
|
||||
:return: The type selected
|
||||
:rtype: Enum of PathEdit
|
||||
"""
|
||||
return self._path_type
|
||||
|
||||
@path_type.setter
|
||||
def path_type(self, path_type):
|
||||
"""
|
||||
A Property setter method to set the path type
|
||||
|
||||
:param path: The type of path to select
|
||||
:type path: Enum of PathEdit
|
||||
"""
|
||||
self._path_type = path_type
|
||||
self.update_button_tool_tips()
|
||||
|
||||
def update_button_tool_tips(self):
|
||||
"""
|
||||
Called to update the tooltips on the buttons. This is changing path types, and when the widget is initalised
|
||||
:return: None
|
||||
"""
|
||||
if self._path_type == PathType.Directories:
|
||||
self.browse_button.setToolTip(translate('OpenLP.PathEdit', 'Browse for directory.'))
|
||||
self.revert_button.setToolTip(translate('OpenLP.PathEdit', 'Revert to default directory.'))
|
||||
else:
|
||||
self.browse_button.setToolTip(translate('OpenLP.PathEdit', 'Browse for file.'))
|
||||
self.revert_button.setToolTip(translate('OpenLP.PathEdit', 'Revert to default file.'))
|
||||
|
||||
def on_browse_button_clicked(self):
|
||||
"""
|
||||
A handler to handle a click on the browse button.
|
||||
|
||||
Show the QFileDialog and process the input from the user
|
||||
:return: None
|
||||
"""
|
||||
caption = self.dialog_caption
|
||||
path = ''
|
||||
if self._path_type == PathType.Directories:
|
||||
if not caption:
|
||||
caption = translate('OpenLP.PathEdit', 'Select Directory')
|
||||
path = QtWidgets.QFileDialog.getExistingDirectory(self, caption,
|
||||
self._path, QtWidgets.QFileDialog.ShowDirsOnly)
|
||||
elif self._path_type == PathType.Files:
|
||||
if not caption:
|
||||
caption = self.dialog_caption = translate('OpenLP.PathEdit', 'Select File')
|
||||
path, filter_used = QtWidgets.QFileDialog.getOpenFileName(self, caption, self._path, self.filters)
|
||||
if path:
|
||||
path = os.path.normpath(path)
|
||||
self.on_new_path(path)
|
||||
|
||||
def on_revert_button_clicked(self):
|
||||
"""
|
||||
A handler to handle a click on the revert button.
|
||||
|
||||
Set the new path to the value of the default_path instance variable.
|
||||
:return: None
|
||||
"""
|
||||
self.on_new_path(self.default_path)
|
||||
|
||||
def on_line_edit_editing_finished(self):
|
||||
"""
|
||||
A handler to handle when the line edit has finished being edited.
|
||||
:return: None
|
||||
"""
|
||||
self.on_new_path(self.line_edit.text())
|
||||
|
||||
def on_new_path(self, path):
|
||||
"""
|
||||
A method called to validate and set a new path.
|
||||
|
||||
Emits the pathChanged Signal
|
||||
|
||||
:param path: The new path
|
||||
:type path: str
|
||||
|
||||
:return: None
|
||||
"""
|
||||
if self._path != path:
|
||||
self.path = path
|
||||
self.pathChanged.emit(path)
|
|
@ -143,6 +143,7 @@ def format_milliseconds(milliseconds):
|
|||
seconds=seconds,
|
||||
millis=millis)
|
||||
|
||||
|
||||
from .mediacontroller import MediaController
|
||||
from .playertab import PlayerTab
|
||||
|
||||
|
|
|
@ -28,7 +28,8 @@ import os
|
|||
import datetime
|
||||
from PyQt5 import QtCore, QtWidgets
|
||||
|
||||
from openlp.core.common import OpenLPMixin, Registry, RegistryMixin, RegistryProperties, Settings, UiStrings, translate
|
||||
from openlp.core.common import OpenLPMixin, Registry, RegistryMixin, RegistryProperties, Settings, UiStrings, \
|
||||
extension_loader, translate
|
||||
from openlp.core.lib import ItemCapabilities
|
||||
from openlp.core.lib.ui import critical_error_message_box
|
||||
from openlp.core.common import AppLocation
|
||||
|
@ -39,6 +40,7 @@ from openlp.core.ui.media import MediaState, MediaInfo, MediaType, get_media_pla
|
|||
parse_optical_path
|
||||
from openlp.core.ui.lib.toolbar import OpenLPToolbar
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
TICK_TIME = 200
|
||||
|
@ -172,19 +174,9 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties):
|
|||
Check to see if we have any media Player's available.
|
||||
"""
|
||||
log.debug('_check_available_media_players')
|
||||
controller_dir = os.path.join(AppLocation.get_directory(AppLocation.AppDir), 'core', 'ui', 'media')
|
||||
for filename in os.listdir(controller_dir):
|
||||
if filename.endswith('player.py') and filename != 'mediaplayer.py':
|
||||
path = os.path.join(controller_dir, filename)
|
||||
if os.path.isfile(path):
|
||||
module_name = 'openlp.core.ui.media.' + os.path.splitext(filename)[0]
|
||||
log.debug('Importing controller %s', module_name)
|
||||
try:
|
||||
__import__(module_name, globals(), locals(), [])
|
||||
# On some platforms importing vlc.py might cause
|
||||
# also OSError exceptions. (e.g. Mac OS X)
|
||||
except (ImportError, OSError):
|
||||
log.warning('Failed to import %s on path %s', module_name, path)
|
||||
controller_dir = os.path.join('openlp', 'core', 'ui', 'media')
|
||||
glob_pattern = os.path.join(controller_dir, '*player.py')
|
||||
extension_loader(glob_pattern, ['mediaplayer.py'])
|
||||
player_classes = MediaPlayer.__subclasses__()
|
||||
for player_class in player_classes:
|
||||
self.register_players(player_class(self))
|
||||
|
|
|
@ -95,7 +95,7 @@ class Ui_PrintServiceDialog(object):
|
|||
self.main_layout.addWidget(self.preview_widget)
|
||||
self.options_widget = QtWidgets.QWidget(print_service_dialog)
|
||||
self.options_widget.hide()
|
||||
self.options_widget.resize(400, 300)
|
||||
self.options_widget.resize(400, 350)
|
||||
self.options_widget.setAutoFillBackground(True)
|
||||
self.options_layout = QtWidgets.QVBoxLayout(self.options_widget)
|
||||
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.meta_data_check_box = QtWidgets.QCheckBox()
|
||||
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.options_group_box.setLayout(self.group_layout)
|
||||
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.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.show_chords_check_box.setText(translate('OpenLP.PrintServiceForm', 'Show chords'))
|
||||
self.title_line_edit.setText(translate('OpenLP.PrintServiceForm', 'Service Sheet'))
|
||||
# Do not change the order.
|
||||
self.zoom_combo_box.addItems([
|
||||
|
|
|
@ -37,7 +37,7 @@ from openlp.core.common import AppLocation
|
|||
DEFAULT_CSS = """/*
|
||||
Edit this file to customize the service order print. Note, that not all CSS
|
||||
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 {
|
||||
|
@ -101,6 +101,19 @@ http://doc.trolltech.com/4.7/richtext-html-subset.html#css-properties
|
|||
.newPage {
|
||||
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')
|
||||
for index, item in enumerate(self.service_manager.service_items):
|
||||
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:
|
||||
if self.footer_text_edit.toPlainText():
|
||||
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_html = None
|
||||
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')
|
||||
else:
|
||||
elif 'chordspacing' not in slide['printing_html']:
|
||||
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_html = slide['html']
|
||||
verse_html = slide['printing_html']
|
||||
# Break the page before the div element.
|
||||
if index != 0 and self.page_break_after_text.isChecked():
|
||||
div.set('class', 'item newPage')
|
||||
|
|
|
@ -38,7 +38,7 @@ from openlp.core.lib.projector.constants import ERROR_MSG, ERROR_STRING, E_AUTHE
|
|||
E_NETWORK, E_NOT_CONNECTED, E_UNKNOWN_SOCKET_ERROR, STATUS_STRING, S_CONNECTED, S_CONNECTING, S_COOLDOWN, \
|
||||
S_INITIALIZE, S_NOT_CONNECTED, S_OFF, S_ON, S_STANDBY, S_WARMUP
|
||||
from openlp.core.lib.projector.db import ProjectorDB
|
||||
from openlp.core.lib.projector.pjlink1 import PJLink1
|
||||
from openlp.core.lib.projector.pjlink1 import PJLink
|
||||
from openlp.core.ui.projector.editform import ProjectorEditForm
|
||||
from openlp.core.ui.projector.sourceselectform import SourceSelectTabs, SourceSelectSingle
|
||||
|
||||
|
@ -690,19 +690,19 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, UiProjecto
|
|||
Helper app to build a projector instance
|
||||
|
||||
:param projector: Dict of projector database information
|
||||
:returns: PJLink1() instance
|
||||
:returns: PJLink() instance
|
||||
"""
|
||||
log.debug('_add_projector()')
|
||||
return PJLink1(dbid=projector.id,
|
||||
ip=projector.ip,
|
||||
port=int(projector.port),
|
||||
name=projector.name,
|
||||
location=projector.location,
|
||||
notes=projector.notes,
|
||||
pin=None if projector.pin == '' else projector.pin,
|
||||
poll_time=self.poll_time,
|
||||
socket_timeout=self.socket_timeout
|
||||
)
|
||||
return PJLink(dbid=projector.id,
|
||||
ip=projector.ip,
|
||||
port=int(projector.port),
|
||||
name=projector.name,
|
||||
location=projector.location,
|
||||
notes=projector.notes,
|
||||
pin=None if projector.pin == '' else projector.pin,
|
||||
poll_time=self.poll_time,
|
||||
socket_timeout=self.socket_timeout
|
||||
)
|
||||
|
||||
def add_projector(self, projector, start=False):
|
||||
"""
|
||||
|
@ -961,7 +961,7 @@ class ProjectorItem(QtCore.QObject):
|
|||
"""
|
||||
Initialization for ProjectorItem instance
|
||||
|
||||
:param link: PJLink1 instance for QListWidgetItem
|
||||
:param link: PJLink instance for QListWidgetItem
|
||||
"""
|
||||
self.link = link
|
||||
self.thread = None
|
||||
|
|
|
@ -921,7 +921,8 @@ class SlideController(DisplayController, RegistryProperties):
|
|||
Registry().execute('{name}_stop'.format(name=old_item.name.lower()), [old_item, self.is_live])
|
||||
if old_item.is_media() and not self.service_item.is_media():
|
||||
self.on_media_close()
|
||||
Registry().execute('slidecontroller_{item}_started'.format(item=self.type_prefix), [self.service_item])
|
||||
if self.is_live:
|
||||
Registry().execute('slidecontroller_{item}_started'.format(item=self.type_prefix), [self.service_item])
|
||||
|
||||
def on_slide_selected_index(self, message):
|
||||
"""
|
||||
|
|
|
@ -69,10 +69,16 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties):
|
|||
self.video_color_button.colorChanged.connect(self.on_video_color_changed)
|
||||
self.gradient_start_button.colorChanged.connect(self.on_gradient_start_color_changed)
|
||||
self.gradient_end_button.colorChanged.connect(self.on_gradient_end_color_changed)
|
||||
self.image_browse_button.clicked.connect(self.on_image_browse_button_clicked)
|
||||
self.image_file_edit.editingFinished.connect(self.on_image_file_edit_editing_finished)
|
||||
self.video_browse_button.clicked.connect(self.on_video_browse_button_clicked)
|
||||
self.video_file_edit.editingFinished.connect(self.on_video_file_edit_editing_finished)
|
||||
self.image_path_edit.filters = \
|
||||
'{name};;{text} (*)'.format(name=get_images_filter(), text=UiStrings().AllFiles)
|
||||
self.image_path_edit.pathChanged.connect(self.on_image_path_edit_path_changed)
|
||||
# TODO: Should work
|
||||
visible_formats = '({name})'.format(name='; '.join(VIDEO_EXT))
|
||||
actual_formats = '({name})'.format(name=' '.join(VIDEO_EXT))
|
||||
video_filter = '{trans} {visible} {actual}'.format(trans=translate('OpenLP', 'Video Files'),
|
||||
visible=visible_formats, actual=actual_formats)
|
||||
self.video_path_edit.filters = '{video};;{ui} (*)'.format(video=video_filter, ui=UiStrings().AllFiles)
|
||||
self.video_path_edit.pathChanged.connect(self.on_video_path_edit_path_changed)
|
||||
self.main_color_button.colorChanged.connect(self.on_main_color_changed)
|
||||
self.outline_color_button.colorChanged.connect(self.on_outline_color_changed)
|
||||
self.shadow_color_button.colorChanged.connect(self.on_shadow_color_changed)
|
||||
|
@ -112,7 +118,8 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties):
|
|||
self.background_page.registerField('color', self.color_button)
|
||||
self.background_page.registerField('gradient_start', self.gradient_start_button)
|
||||
self.background_page.registerField('gradient_end', self.gradient_end_button)
|
||||
self.background_page.registerField('background_image', self.image_file_edit)
|
||||
self.background_page.registerField('background_image', self.image_path_edit,
|
||||
'path', self.image_path_edit.pathChanged)
|
||||
self.background_page.registerField('gradient', self.gradient_combo_box)
|
||||
self.main_area_page.registerField('main_color_button', self.main_color_button)
|
||||
self.main_area_page.registerField('main_size_spin_box', self.main_size_spin_box)
|
||||
|
@ -309,11 +316,11 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties):
|
|||
self.setField('background_type', 1)
|
||||
elif self.theme.background_type == BackgroundType.to_string(BackgroundType.Image):
|
||||
self.image_color_button.color = self.theme.background_border_color
|
||||
self.image_file_edit.setText(self.theme.background_filename)
|
||||
self.image_path_edit.path = self.theme.background_filename
|
||||
self.setField('background_type', 2)
|
||||
elif self.theme.background_type == BackgroundType.to_string(BackgroundType.Video):
|
||||
self.video_color_button.color = self.theme.background_border_color
|
||||
self.video_file_edit.setText(self.theme.background_filename)
|
||||
self.video_path_edit.path = self.theme.background_filename
|
||||
self.setField('background_type', 4)
|
||||
elif self.theme.background_type == BackgroundType.to_string(BackgroundType.Transparent):
|
||||
self.setField('background_type', 3)
|
||||
|
@ -441,48 +448,20 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties):
|
|||
"""
|
||||
self.theme.background_end_color = color
|
||||
|
||||
def on_image_browse_button_clicked(self):
|
||||
def on_image_path_edit_path_changed(self, filename):
|
||||
"""
|
||||
Background Image button pushed.
|
||||
"""
|
||||
images_filter = get_images_filter()
|
||||
images_filter = '{name};;{text} (*.*)'.format(name=images_filter, text=UiStrings().AllFiles)
|
||||
filename, filter_used = QtWidgets.QFileDialog.getOpenFileName(
|
||||
self, translate('OpenLP.ThemeWizard', 'Select Image'),
|
||||
self.image_file_edit.text(), images_filter)
|
||||
if filename:
|
||||
self.theme.background_filename = filename
|
||||
self.theme.background_filename = filename
|
||||
self.set_background_page_values()
|
||||
|
||||
def on_image_file_edit_editing_finished(self):
|
||||
"""
|
||||
Background image path edited
|
||||
"""
|
||||
self.theme.background_filename = str(self.image_file_edit.text())
|
||||
|
||||
def on_video_browse_button_clicked(self):
|
||||
def on_video_path_edit_path_changed(self, filename):
|
||||
"""
|
||||
Background video button pushed.
|
||||
"""
|
||||
# TODO: Should work
|
||||
visible_formats = '({name})'.format(name='; '.join(VIDEO_EXT))
|
||||
actual_formats = '({name})'.format(name=' '.join(VIDEO_EXT))
|
||||
video_filter = '{trans} {visible} {actual}'.format(trans=translate('OpenLP', 'Video Files'),
|
||||
visible=visible_formats, actual=actual_formats)
|
||||
video_filter = '{video};;{ui} (*.*)'.format(video=video_filter, ui=UiStrings().AllFiles)
|
||||
filename, filter_used = QtWidgets.QFileDialog.getOpenFileName(
|
||||
self, translate('OpenLP.ThemeWizard', 'Select Video'),
|
||||
self.video_file_edit.text(), video_filter)
|
||||
if filename:
|
||||
self.theme.background_filename = filename
|
||||
self.theme.background_filename = filename
|
||||
self.set_background_page_values()
|
||||
|
||||
def on_video_file_edit_editing_finished(self):
|
||||
"""
|
||||
Background video path edited
|
||||
"""
|
||||
self.theme.background_filename = str(self.image_file_edit.text())
|
||||
|
||||
def on_main_color_changed(self, color):
|
||||
"""
|
||||
Set the main colour value
|
||||
|
|
|
@ -28,7 +28,7 @@ from openlp.core.common import UiStrings, translate, is_macosx
|
|||
from openlp.core.lib import build_icon
|
||||
from openlp.core.lib.theme import HorizontalType, BackgroundType, BackgroundGradientType
|
||||
from openlp.core.lib.ui import add_welcome_page, create_valign_selection_widgets
|
||||
from openlp.core.ui.lib.colorbutton import ColorButton
|
||||
from openlp.core.ui.lib import ColorButton, PathEdit
|
||||
|
||||
|
||||
class Ui_ThemeWizard(object):
|
||||
|
@ -116,16 +116,10 @@ class Ui_ThemeWizard(object):
|
|||
self.image_layout.addRow(self.image_color_label, self.image_color_button)
|
||||
self.image_label = QtWidgets.QLabel(self.image_widget)
|
||||
self.image_label.setObjectName('image_label')
|
||||
self.image_file_layout = QtWidgets.QHBoxLayout()
|
||||
self.image_file_layout.setObjectName('image_file_layout')
|
||||
self.image_file_edit = QtWidgets.QLineEdit(self.image_widget)
|
||||
self.image_file_edit.setObjectName('image_file_edit')
|
||||
self.image_file_layout.addWidget(self.image_file_edit)
|
||||
self.image_browse_button = QtWidgets.QToolButton(self.image_widget)
|
||||
self.image_browse_button.setObjectName('image_browse_button')
|
||||
self.image_browse_button.setIcon(build_icon(':/general/general_open.png'))
|
||||
self.image_file_layout.addWidget(self.image_browse_button)
|
||||
self.image_layout.addRow(self.image_label, self.image_file_layout)
|
||||
self.image_path_edit = PathEdit(self.image_widget,
|
||||
dialog_caption=translate('OpenLP.ThemeWizard', 'Select Image'),
|
||||
show_revert=False)
|
||||
self.image_layout.addRow(self.image_label, self.image_path_edit)
|
||||
self.image_layout.setItem(2, QtWidgets.QFormLayout.LabelRole, self.spacer)
|
||||
self.background_stack.addWidget(self.image_widget)
|
||||
self.transparent_widget = QtWidgets.QWidget(self.background_page)
|
||||
|
@ -147,16 +141,10 @@ class Ui_ThemeWizard(object):
|
|||
self.video_layout.addRow(self.video_color_label, self.video_color_button)
|
||||
self.video_label = QtWidgets.QLabel(self.video_widget)
|
||||
self.video_label.setObjectName('video_label')
|
||||
self.video_file_layout = QtWidgets.QHBoxLayout()
|
||||
self.video_file_layout.setObjectName('video_file_layout')
|
||||
self.video_file_edit = QtWidgets.QLineEdit(self.video_widget)
|
||||
self.video_file_edit.setObjectName('video_file_edit')
|
||||
self.video_file_layout.addWidget(self.video_file_edit)
|
||||
self.video_browse_button = QtWidgets.QToolButton(self.video_widget)
|
||||
self.video_browse_button.setObjectName('video_browse_button')
|
||||
self.video_browse_button.setIcon(build_icon(':/general/general_open.png'))
|
||||
self.video_file_layout.addWidget(self.video_browse_button)
|
||||
self.video_layout.addRow(self.video_label, self.video_file_layout)
|
||||
self.video_path_edit = PathEdit(self.video_widget,
|
||||
dialog_caption=translate('OpenLP.ThemeWizard', 'Select Video'),
|
||||
show_revert=False)
|
||||
self.video_layout.addRow(self.video_label, self.video_path_edit)
|
||||
self.video_layout.setItem(2, QtWidgets.QFormLayout.LabelRole, self.spacer)
|
||||
self.background_stack.addWidget(self.video_widget)
|
||||
theme_wizard.addPage(self.background_page)
|
||||
|
|
|
@ -429,4 +429,5 @@ class BibleManager(OpenLPMixin, RegistryProperties):
|
|||
for bible in self.db_cache:
|
||||
self.db_cache[bible].finalise()
|
||||
|
||||
|
||||
__all__ = ['BibleFormat']
|
||||
|
|
|
@ -58,7 +58,8 @@ from PyQt5 import QtCore
|
|||
|
||||
from openlp.core.lib import ScreenList
|
||||
from openlp.core.common import get_uno_command, get_uno_instance
|
||||
from .presentationcontroller import PresentationController, PresentationDocument, TextType
|
||||
from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument, \
|
||||
TextType
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
|
|
@ -29,7 +29,7 @@ from subprocess import check_output, CalledProcessError
|
|||
from openlp.core.common import AppLocation, check_binary_exists
|
||||
from openlp.core.common import Settings, is_win
|
||||
from openlp.core.lib import ScreenList
|
||||
from .presentationcontroller import PresentationController, PresentationDocument
|
||||
from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument
|
||||
|
||||
if is_win():
|
||||
from subprocess import STARTUPINFO, STARTF_USESHOWWINDOW
|
||||
|
|
|
@ -43,7 +43,7 @@ if is_win():
|
|||
from openlp.core.lib import ScreenList
|
||||
from openlp.core.lib.ui import UiStrings, critical_error_message_box, translate
|
||||
from openlp.core.common import trace_error_handler, Registry
|
||||
from .presentationcontroller import PresentationController, PresentationDocument
|
||||
from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
@ -35,7 +35,7 @@ if is_win():
|
|||
|
||||
from openlp.core.common import AppLocation
|
||||
from openlp.core.lib import ScreenList
|
||||
from .presentationcontroller import PresentationController, PresentationDocument
|
||||
from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
|
|
@ -197,6 +197,7 @@ class PPTViewer(QtWidgets.QWidget):
|
|||
def openDialog(self):
|
||||
self.pptEdit.setText(QtWidgets.QFileDialog.getOpenFileName(self, 'Open file')[0])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
pptdll = cdll.LoadLibrary(r'pptviewlib.dll')
|
||||
pptdll.SetDebug(1)
|
||||
|
|
|
@ -25,7 +25,8 @@ from PyQt5 import QtGui, QtWidgets
|
|||
from openlp.core.common import Settings, UiStrings, translate
|
||||
from openlp.core.lib import SettingsTab, build_icon
|
||||
from openlp.core.lib.ui import critical_error_message_box
|
||||
from .pdfcontroller import PdfController
|
||||
from openlp.core.ui.lib import PathEdit
|
||||
from openlp.plugins.presentations.lib.pdfcontroller import PdfController
|
||||
|
||||
|
||||
class PresentationTab(SettingsTab):
|
||||
|
@ -88,26 +89,15 @@ class PresentationTab(SettingsTab):
|
|||
self.pdf_program_check_box = QtWidgets.QCheckBox(self.pdf_group_box)
|
||||
self.pdf_program_check_box.setObjectName('pdf_program_check_box')
|
||||
self.pdf_layout.addRow(self.pdf_program_check_box)
|
||||
self.pdf_program_path_layout = QtWidgets.QHBoxLayout()
|
||||
self.pdf_program_path_layout.setObjectName('pdf_program_path_layout')
|
||||
self.pdf_program_path = QtWidgets.QLineEdit(self.pdf_group_box)
|
||||
self.pdf_program_path.setObjectName('pdf_program_path')
|
||||
self.pdf_program_path.setReadOnly(True)
|
||||
self.pdf_program_path.setPalette(self.get_grey_text_palette(True))
|
||||
self.pdf_program_path_layout.addWidget(self.pdf_program_path)
|
||||
self.pdf_program_browse_button = QtWidgets.QToolButton(self.pdf_group_box)
|
||||
self.pdf_program_browse_button.setObjectName('pdf_program_browse_button')
|
||||
self.pdf_program_browse_button.setIcon(build_icon(':/general/general_open.png'))
|
||||
self.pdf_program_browse_button.setEnabled(False)
|
||||
self.pdf_program_path_layout.addWidget(self.pdf_program_browse_button)
|
||||
self.pdf_layout.addRow(self.pdf_program_path_layout)
|
||||
self.program_path_edit = PathEdit(self.pdf_group_box)
|
||||
self.pdf_layout.addRow(self.program_path_edit)
|
||||
self.left_layout.addWidget(self.pdf_group_box)
|
||||
self.left_layout.addStretch()
|
||||
self.right_column.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred)
|
||||
self.right_layout.addStretch()
|
||||
# Signals and slots
|
||||
self.pdf_program_browse_button.clicked.connect(self.on_pdf_program_browse_button_clicked)
|
||||
self.pdf_program_check_box.clicked.connect(self.on_pdf_program_check_box_clicked)
|
||||
self.program_path_edit.pathChanged.connect(self.on_program_path_edit_path_changed)
|
||||
self.pdf_program_check_box.clicked.connect(self.program_path_edit.setEnabled)
|
||||
|
||||
def retranslateUi(self):
|
||||
"""
|
||||
|
@ -132,6 +122,8 @@ class PresentationTab(SettingsTab):
|
|||
'(This may fix PowerPoint scaling issues in Windows 8 and 10)'))
|
||||
self.pdf_program_check_box.setText(
|
||||
translate('PresentationPlugin.PresentationTab', 'Use given full path for mudraw or ghostscript binary:'))
|
||||
self.program_path_edit.dialog_caption = translate('PresentationPlugin.PresentationTab',
|
||||
'Select mudraw or ghostscript binary')
|
||||
|
||||
def set_controller_text(self, checkbox, controller):
|
||||
if checkbox.isEnabled():
|
||||
|
@ -161,11 +153,10 @@ class PresentationTab(SettingsTab):
|
|||
# load pdf-program settings
|
||||
enable_pdf_program = Settings().value(self.settings_section + '/enable_pdf_program')
|
||||
self.pdf_program_check_box.setChecked(enable_pdf_program)
|
||||
self.pdf_program_path.setPalette(self.get_grey_text_palette(not enable_pdf_program))
|
||||
self.pdf_program_browse_button.setEnabled(enable_pdf_program)
|
||||
self.program_path_edit.setEnabled(enable_pdf_program)
|
||||
pdf_program = Settings().value(self.settings_section + '/pdf_program')
|
||||
if pdf_program:
|
||||
self.pdf_program_path.setText(pdf_program)
|
||||
self.program_path_edit.path = pdf_program
|
||||
|
||||
def save(self):
|
||||
"""
|
||||
|
@ -201,7 +192,7 @@ class PresentationTab(SettingsTab):
|
|||
Settings().setValue(setting_key, self.ppt_window_check_box.checkState())
|
||||
changed = True
|
||||
# Save pdf-settings
|
||||
pdf_program = self.pdf_program_path.text()
|
||||
pdf_program = self.program_path_edit.path
|
||||
enable_pdf_program = self.pdf_program_check_box.checkState()
|
||||
# If the given program is blank disable using the program
|
||||
if pdf_program == '':
|
||||
|
@ -228,42 +219,12 @@ class PresentationTab(SettingsTab):
|
|||
checkbox.setEnabled(controller.is_available())
|
||||
self.set_controller_text(checkbox, controller)
|
||||
|
||||
def on_pdf_program_browse_button_clicked(self):
|
||||
def on_program_path_edit_path_changed(self, filename):
|
||||
"""
|
||||
Select the mudraw or ghostscript binary that should be used.
|
||||
"""
|
||||
filename, filter_used = QtWidgets.QFileDialog.getOpenFileName(
|
||||
self, translate('PresentationPlugin.PresentationTab', 'Select mudraw or ghostscript binary.'),
|
||||
self.pdf_program_path.text())
|
||||
if filename:
|
||||
program_type = PdfController.process_check_binary(filename)
|
||||
if not program_type:
|
||||
if not PdfController.process_check_binary(filename):
|
||||
critical_error_message_box(UiStrings().Error,
|
||||
translate('PresentationPlugin.PresentationTab',
|
||||
'The program is not ghostscript or mudraw which is required.'))
|
||||
else:
|
||||
self.pdf_program_path.setText(filename)
|
||||
|
||||
def on_pdf_program_check_box_clicked(self, checked):
|
||||
"""
|
||||
When checkbox for manual entering pdf-program is clicked,
|
||||
enable or disable the textbox for the programpath and the browse-button.
|
||||
|
||||
:param checked: If the box is checked or not.
|
||||
"""
|
||||
self.pdf_program_path.setPalette(self.get_grey_text_palette(not checked))
|
||||
self.pdf_program_browse_button.setEnabled(checked)
|
||||
|
||||
def get_grey_text_palette(self, greyed):
|
||||
"""
|
||||
Returns a QPalette with greyed out text as used for placeholderText.
|
||||
|
||||
:param greyed: Determines whether the palette should be grayed.
|
||||
:return: The created palette.
|
||||
"""
|
||||
palette = QtGui.QPalette()
|
||||
color = self.palette().color(QtGui.QPalette.Active, QtGui.QPalette.Text)
|
||||
if greyed:
|
||||
color.setAlpha(128)
|
||||
palette.setColor(QtGui.QPalette.Active, QtGui.QPalette.Text, color)
|
||||
return palette
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||
|
||||
###############################################################################
|
||||
|
@ -20,19 +20,18 @@
|
|||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
"""
|
||||
The :mod:`presentationplugin` module provides the ability for OpenLP to display presentations from a variety of document
|
||||
formats.
|
||||
The :mod:`openlp.plugins.presentations.presentationplugin` module provides the ability for OpenLP to display
|
||||
presentations from a variety of document formats.
|
||||
"""
|
||||
import os
|
||||
import logging
|
||||
|
||||
from PyQt5 import QtCore
|
||||
|
||||
from openlp.core.common import AppLocation, translate
|
||||
from openlp.core.common import AppLocation, extension_loader, translate
|
||||
from openlp.core.lib import Plugin, StringContent, build_icon
|
||||
from openlp.plugins.presentations.lib import PresentationController, PresentationMediaItem, PresentationTab
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
@ -122,17 +121,9 @@ class PresentationPlugin(Plugin):
|
|||
Check to see if we have any presentation software available. If not do not install the plugin.
|
||||
"""
|
||||
log.debug('check_pre_conditions')
|
||||
controller_dir = os.path.join(AppLocation.get_directory(AppLocation.PluginsDir), 'presentations', 'lib')
|
||||
for filename in os.listdir(controller_dir):
|
||||
if filename.endswith('controller.py') and filename != 'presentationcontroller.py':
|
||||
path = os.path.join(controller_dir, filename)
|
||||
if os.path.isfile(path):
|
||||
module_name = 'openlp.plugins.presentations.lib.' + os.path.splitext(filename)[0]
|
||||
log.debug('Importing controller {name}'.format(name=module_name))
|
||||
try:
|
||||
__import__(module_name, globals(), locals(), [])
|
||||
except ImportError:
|
||||
log.warning('Failed to import {name} on path {path}'.format(name=module_name, path=path))
|
||||
controller_dir = os.path.join('openlp', 'plugins', 'presentations', 'lib')
|
||||
glob_pattern = os.path.join(controller_dir, '*controller.py')
|
||||
extension_loader(glob_pattern, ['presentationcontroller.py'])
|
||||
controller_classes = PresentationController.__subclasses__()
|
||||
for controller_class in controller_classes:
|
||||
controller = controller_class(self)
|
||||
|
|
|
@ -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>
|
|
@ -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;
|
||||
font-family: sans-serif;
|
||||
overflow: hidden;
|
||||
-webkit-user-select: none; /* Chrome/Safari */
|
||||
-moz-user-select: none; /* Firefox */
|
||||
-ms-user-select: none; /* IE 10+ */
|
||||
user-select: none; /* Future */
|
||||
}
|
||||
|
||||
#currentslide {
|
||||
|
|
|
@ -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}),
|
||||
('^/(stage)$', {'function': self.serve_file, 'secure': False}),
|
||||
('^/(stage)/(.*)$', {'function': self.stages, 'secure': False}),
|
||||
('^/(chords)$', {'function': self.serve_file, 'secure': False}),
|
||||
('^/(main)$', {'function': self.serve_file, 'secure': False}),
|
||||
(r'^/(\w+)/thumbnails([^/]+)?/(.*)$', {'function': self.serve_thumbnail, 'secure': False}),
|
||||
(r'^/api/poll$', {'function': self.poll, 'secure': False}),
|
||||
|
@ -318,10 +319,12 @@ class HttpRouter(RegistryProperties):
|
|||
"""
|
||||
remote = translate('RemotePlugin.Mobile', 'Remote')
|
||||
stage = translate('RemotePlugin.Mobile', 'Stage View')
|
||||
chords = translate('RemotePlugin.Mobile', 'Chords View')
|
||||
live = translate('RemotePlugin.Mobile', 'Live View')
|
||||
self.template_vars = {
|
||||
'app_title': "{main} {remote}".format(main=UiStrings().OLPV2x, remote=remote),
|
||||
'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),
|
||||
'service_manager': translate('RemotePlugin.Mobile', 'Service Manager'),
|
||||
'slide_controller': translate('RemotePlugin.Mobile', 'Slide Controller'),
|
||||
|
@ -482,7 +485,8 @@ class HttpRouter(RegistryProperties):
|
|||
'display': self.live_controller.desktop_screen.isChecked(),
|
||||
'version': 2,
|
||||
'isSecure': Settings().value(self.settings_section + '/authentication enabled'),
|
||||
'isAuthorised': self.authorised
|
||||
'isAuthorised': self.authorised,
|
||||
'chordNotation': Settings().value('songs/chord notation'),
|
||||
}
|
||||
self.do_json_header()
|
||||
return json.dumps({'results': result}).encode()
|
||||
|
@ -554,6 +558,7 @@ class HttpRouter(RegistryProperties):
|
|||
item['tag'] = str(frame['verseTag'])
|
||||
else:
|
||||
item['tag'] = str(index + 1)
|
||||
item['chords_text'] = str(frame['chords_text'])
|
||||
item['text'] = str(frame['text'])
|
||||
item['html'] = str(frame['html'])
|
||||
# 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.setOpenExternalLinks(True)
|
||||
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.setObjectName('live_url_label')
|
||||
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.remote_url_label.setText(translate('RemotePlugin.RemoteTab', 'Remote 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.twelve_hour_check_box.setText(translate('RemotePlugin.RemoteTab', 'Display stage time in 12h format'))
|
||||
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.lib import build_icon, translate
|
||||
from openlp.core.lib.ui import UiStrings, create_button_box
|
||||
from openlp.core.common import Settings
|
||||
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.addStretch()
|
||||
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.dialog_layout.addWidget(self.button_box)
|
||||
self.retranslateUi(edit_verse_dialog)
|
||||
|
@ -82,3 +98,7 @@ class Ui_EditVerseDialog(object):
|
|||
self.insert_button.setText(translate('SongsPlugin.EditVerseForm', '&Insert'))
|
||||
self.insert_button.setToolTip(translate('SongsPlugin.EditVerseForm',
|
||||
'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 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
|
||||
|
||||
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.verse_text_edit.cursorPositionChanged.connect(self.on_cursor_position_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):
|
||||
"""
|
||||
|
@ -95,6 +100,41 @@ class EditVerseForm(QtWidgets.QDialog, Ui_EditVerseDialog):
|
|||
"""
|
||||
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):
|
||||
"""
|
||||
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('---['):
|
||||
text = '---[{tag}:1]---\n{text}'.format(tag=VerseType.translated_names[VerseType.Verse], text=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()
|
||||
|
|
|
@ -20,7 +20,6 @@
|
|||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
import logging
|
||||
import os
|
||||
|
||||
from PyQt5 import QtCore, QtWidgets
|
||||
from sqlalchemy.sql import and_
|
||||
|
@ -184,7 +183,7 @@ class SongMaintenanceForm(QtWidgets.QDialog, Ui_SongMaintenanceDialog, RegistryP
|
|||
Author.display_name == new_author.display_name
|
||||
)
|
||||
)
|
||||
return self.__check_object_exists(authors, new_author, edit)
|
||||
return self._check_object_exists(authors, new_author, edit)
|
||||
|
||||
def check_topic_exists(self, new_topic, edit=False):
|
||||
"""
|
||||
|
@ -194,7 +193,7 @@ class SongMaintenanceForm(QtWidgets.QDialog, Ui_SongMaintenanceDialog, RegistryP
|
|||
:param edit: Are we editing the song?
|
||||
"""
|
||||
topics = self.manager.get_all_objects(Topic, Topic.name == new_topic.name)
|
||||
return self.__check_object_exists(topics, new_topic, edit)
|
||||
return self._check_object_exists(topics, new_topic, edit)
|
||||
|
||||
def check_song_book_exists(self, new_book, edit=False):
|
||||
"""
|
||||
|
@ -205,9 +204,9 @@ class SongMaintenanceForm(QtWidgets.QDialog, Ui_SongMaintenanceDialog, RegistryP
|
|||
"""
|
||||
books = self.manager.get_all_objects(
|
||||
Book, and_(Book.name == new_book.name, Book.publisher == new_book.publisher))
|
||||
return self.__check_object_exists(books, new_book, edit)
|
||||
return self._check_object_exists(books, new_book, edit)
|
||||
|
||||
def __check_object_exists(self, existing_objects, new_object, edit):
|
||||
def _check_object_exists(self, existing_objects, new_object, edit):
|
||||
"""
|
||||
Utility method to check for an existing object.
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ import re
|
|||
|
||||
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.plugins.songs.lib.db import Author, MediaFile, Song, Topic
|
||||
from openlp.plugins.songs.lib.ui import SongStrings
|
||||
|
@ -380,7 +380,7 @@ def clean_song(manager, song):
|
|||
if isinstance(song.lyrics, bytes):
|
||||
song.lyrics = str(song.lyrics, encoding='utf8')
|
||||
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.
|
||||
if not song.authors_songs:
|
||||
name = SongStrings.AuthorUnknown
|
||||
|
@ -512,10 +512,13 @@ def strip_rtf(text, default_encoding=None):
|
|||
elif not ignorable:
|
||||
ebytes.append(int(hex_, 16))
|
||||
elif tchar:
|
||||
if curskip > 0:
|
||||
curskip -= 1
|
||||
elif not ignorable:
|
||||
if not ignorable:
|
||||
ebytes += tchar.encode()
|
||||
if len(ebytes) >= curskip:
|
||||
ebytes = ebytes[curskip:]
|
||||
else:
|
||||
curskip -= len(ebytes)
|
||||
ebytes = ""
|
||||
text = ''.join(out)
|
||||
return text, default_encoding
|
||||
|
||||
|
@ -541,3 +544,123 @@ def delete_song(song_id, song_plugin):
|
|||
except OSError:
|
||||
log.exception('Could not remove directory: {path}'.format(path=save_path))
|
||||
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.lyrix import LyrixImport
|
||||
from .importers.videopsalm import VideoPsalmImport
|
||||
from .importers.chordpro import ChordProImport
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
@ -155,29 +156,31 @@ class SongFormat(object):
|
|||
OpenLP2 = 1
|
||||
Generic = 2
|
||||
CCLI = 3
|
||||
DreamBeam = 4
|
||||
EasySlides = 5
|
||||
EasyWorshipDB = 6
|
||||
EasyWorshipService = 7
|
||||
FoilPresenter = 8
|
||||
Lyrix = 9
|
||||
MediaShout = 10
|
||||
OpenSong = 11
|
||||
OPSPro = 12
|
||||
PowerPraise = 13
|
||||
PowerSong = 14
|
||||
PresentationManager = 15
|
||||
ProPresenter = 16
|
||||
SongBeamer = 17
|
||||
SongPro = 18
|
||||
SongShowPlus = 19
|
||||
SongsOfFellowship = 20
|
||||
SundayPlus = 21
|
||||
VideoPsalm = 22
|
||||
WordsOfWorship = 23
|
||||
WorshipAssistant = 24
|
||||
WorshipCenterPro = 25
|
||||
ZionWorx = 26
|
||||
ChordPro = 4
|
||||
DreamBeam = 5
|
||||
EasySlides = 6
|
||||
EasyWorshipDB = 7
|
||||
EasyWorshipSqliteDB = 8
|
||||
EasyWorshipService = 9
|
||||
FoilPresenter = 10
|
||||
Lyrix = 11
|
||||
MediaShout = 12
|
||||
OpenSong = 13
|
||||
OPSPro = 14
|
||||
PowerPraise = 15
|
||||
PowerSong = 16
|
||||
PresentationManager = 17
|
||||
ProPresenter = 18
|
||||
SongBeamer = 19
|
||||
SongPro = 20
|
||||
SongShowPlus = 21
|
||||
SongsOfFellowship = 22
|
||||
SundayPlus = 23
|
||||
VideoPsalm = 24
|
||||
WordsOfWorship = 25
|
||||
WorshipAssistant = 26
|
||||
WorshipCenterPro = 27
|
||||
ZionWorx = 28
|
||||
|
||||
# Set optional attribute defaults
|
||||
__defaults__ = {
|
||||
|
@ -224,6 +227,13 @@ class SongFormat(object):
|
|||
'filter': '{text} (*.usr *.txt *.bin)'.format(text=translate('SongsPlugin.ImportWizardForm',
|
||||
'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: {
|
||||
'class': DreamBeamImport,
|
||||
'name': 'DreamBeam',
|
||||
|
@ -242,9 +252,17 @@ class SongFormat(object):
|
|||
'name': 'EasyWorship Song Database',
|
||||
'prefix': 'ew',
|
||||
'selectMode': SongFormatSelect.SingleFile,
|
||||
'filter': '{text} (*.db)'.format(text=translate('SongsPlugin.ImportWizardForm',
|
||||
'filter': '{text} (*.DB)'.format(text=translate('SongsPlugin.ImportWizardForm',
|
||||
'EasyWorship Song Database'))
|
||||
},
|
||||
EasyWorshipSqliteDB: {
|
||||
'class': EasyWorshipSongImport,
|
||||
'name': 'EasyWorship 6 Song Database',
|
||||
'prefix': 'ew',
|
||||
'selectMode': SongFormatSelect.SingleFolder,
|
||||
'filter': '{text} (*.db)'.format(text=translate('SongsPlugin.ImportWizardForm',
|
||||
'EasyWorship 6 Song Data Directory'))
|
||||
},
|
||||
EasyWorshipService: {
|
||||
'class': EasyWorshipSongImport,
|
||||
'name': 'EasyWorship Service File',
|
||||
|
@ -427,9 +445,11 @@ class SongFormat(object):
|
|||
SongFormat.OpenLP2,
|
||||
SongFormat.Generic,
|
||||
SongFormat.CCLI,
|
||||
SongFormat.ChordPro,
|
||||
SongFormat.DreamBeam,
|
||||
SongFormat.EasySlides,
|
||||
SongFormat.EasyWorshipDB,
|
||||
SongFormat.EasyWorshipSqliteDB,
|
||||
SongFormat.EasyWorshipService,
|
||||
SongFormat.FoilPresenter,
|
||||
SongFormat.Lyrix,
|
||||
|
|
|
@ -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
|
|
@ -28,6 +28,7 @@ import struct
|
|||
import re
|
||||
import zlib
|
||||
import logging
|
||||
import sqlite3
|
||||
|
||||
from openlp.core.lib import translate
|
||||
from openlp.plugins.songs.lib import VerseType
|
||||
|
@ -77,8 +78,10 @@ class EasyWorshipSongImport(SongImport):
|
|||
"""
|
||||
if self.import_source.lower().endswith('ews'):
|
||||
self.import_ews()
|
||||
else:
|
||||
elif self.import_source.endswith('DB'):
|
||||
self.import_db()
|
||||
else:
|
||||
self.import_sqlite_db()
|
||||
|
||||
def import_ews(self):
|
||||
"""
|
||||
|
@ -125,8 +128,8 @@ class EasyWorshipSongImport(SongImport):
|
|||
else:
|
||||
log.debug('Given ews file is of unknown version.')
|
||||
return
|
||||
entry_count = self.get_i32(file_pos)
|
||||
entry_length = self.get_i16(file_pos + 4)
|
||||
entry_count = self.ews_get_i32(file_pos)
|
||||
entry_length = self.ews_get_i16(file_pos + 4)
|
||||
file_pos += 6
|
||||
self.import_wizard.progress_bar.setMaximum(entry_count)
|
||||
# Loop over songs
|
||||
|
@ -144,13 +147,13 @@ class EasyWorshipSongImport(SongImport):
|
|||
# 0x08 = Audio, 0x09 = Web
|
||||
# 1410 Song number cstring 10
|
||||
self.set_defaults()
|
||||
self.title = self.get_string(file_pos + 0, 50)
|
||||
authors = self.get_string(file_pos + 307, 50)
|
||||
copyright = self.get_string(file_pos + 358, 100)
|
||||
admin = self.get_string(file_pos + 459, 50)
|
||||
cont_ptr = self.get_i32(file_pos + 800)
|
||||
cont_type = self.get_i32(file_pos + 820)
|
||||
self.ccli_number = self.get_string(file_pos + 1410, 10)
|
||||
self.title = self.ews_get_string(file_pos + 0, 50)
|
||||
authors = self.ews_get_string(file_pos + 307, 50)
|
||||
copyright = self.ews_get_string(file_pos + 358, 100)
|
||||
admin = self.ews_get_string(file_pos + 459, 50)
|
||||
cont_ptr = self.ews_get_i32(file_pos + 800)
|
||||
cont_type = self.ews_get_i32(file_pos + 820)
|
||||
self.ccli_number = self.ews_get_string(file_pos + 1410, 10)
|
||||
# Only handle content type 1 (songs)
|
||||
if cont_type != 1:
|
||||
file_pos += entry_length
|
||||
|
@ -164,9 +167,9 @@ class EasyWorshipSongImport(SongImport):
|
|||
# Checksum int32be 4 Alder-32 checksum.
|
||||
# (unknown) 4 0x51 0x4b 0x03 0x04
|
||||
# Content length int32le 4 Length of content after decompression
|
||||
content_length = self.get_i32(cont_ptr)
|
||||
deflated_content = self.get_bytes(cont_ptr + 4, content_length - 10)
|
||||
deflated_length = self.get_i32(cont_ptr + 4 + content_length - 6)
|
||||
content_length = self.ews_get_i32(cont_ptr)
|
||||
deflated_content = self.ews_get_bytes(cont_ptr + 4, content_length - 10)
|
||||
deflated_length = self.ews_get_i32(cont_ptr + 4 + content_length - 6)
|
||||
inflated_content = zlib.decompress(deflated_content, 15, deflated_length)
|
||||
if copyright:
|
||||
self.copyright = copyright
|
||||
|
@ -196,7 +199,7 @@ class EasyWorshipSongImport(SongImport):
|
|||
Import the songs from the database
|
||||
"""
|
||||
# Open the DB and MB files if they exist
|
||||
import_source_mb = self.import_source.replace('.DB', '.MB').replace('.db', '.mb')
|
||||
import_source_mb = self.import_source.replace('.DB', '.MB')
|
||||
if not os.path.isfile(self.import_source):
|
||||
self.log_error(self.import_source, translate('SongsPlugin.EasyWorshipSongImport',
|
||||
'This file does not exist.'))
|
||||
|
@ -260,16 +263,16 @@ class EasyWorshipSongImport(SongImport):
|
|||
for i, field_name in enumerate(field_names):
|
||||
field_type, field_size = struct.unpack_from('BB', field_info, i * 2)
|
||||
field_descriptions.append(FieldDescEntry(field_name, field_type, field_size))
|
||||
self.set_record_struct(field_descriptions)
|
||||
self.db_set_record_struct(field_descriptions)
|
||||
# Pick out the field description indexes we will need
|
||||
try:
|
||||
success = True
|
||||
fi_title = self.find_field(b'Title')
|
||||
fi_author = self.find_field(b'Author')
|
||||
fi_copy = self.find_field(b'Copyright')
|
||||
fi_admin = self.find_field(b'Administrator')
|
||||
fi_words = self.find_field(b'Words')
|
||||
fi_ccli = self.find_field(b'Song Number')
|
||||
fi_title = self.db_find_field(b'Title')
|
||||
fi_author = self.db_find_field(b'Author')
|
||||
fi_copy = self.db_find_field(b'Copyright')
|
||||
fi_admin = self.db_find_field(b'Administrator')
|
||||
fi_words = self.db_find_field(b'Words')
|
||||
fi_ccli = self.db_find_field(b'Song Number')
|
||||
except IndexError:
|
||||
# This is the wrong table
|
||||
success = False
|
||||
|
@ -297,13 +300,13 @@ class EasyWorshipSongImport(SongImport):
|
|||
raw_record = db_file.read(record_size)
|
||||
self.fields = self.record_structure.unpack(raw_record)
|
||||
self.set_defaults()
|
||||
self.title = self.get_field(fi_title).decode(self.encoding)
|
||||
self.title = self.db_get_field(fi_title).decode(self.encoding)
|
||||
# Get remaining fields.
|
||||
copy = self.get_field(fi_copy)
|
||||
admin = self.get_field(fi_admin)
|
||||
ccli = self.get_field(fi_ccli)
|
||||
authors = self.get_field(fi_author)
|
||||
words = self.get_field(fi_words)
|
||||
copy = self.db_get_field(fi_copy)
|
||||
admin = self.db_get_field(fi_admin)
|
||||
ccli = self.db_get_field(fi_ccli)
|
||||
authors = self.db_get_field(fi_author)
|
||||
words = self.db_get_field(fi_words)
|
||||
if copy:
|
||||
self.copyright = copy.decode(self.encoding)
|
||||
if admin:
|
||||
|
@ -337,6 +340,82 @@ class EasyWorshipSongImport(SongImport):
|
|||
db_file.close()
|
||||
self.memo_file.close()
|
||||
|
||||
def _find_file(self, base_path, path_list):
|
||||
"""
|
||||
Find the specified file, with the option of the file being at any level in the specified directory structure.
|
||||
|
||||
:param base_path: the location search in
|
||||
:param path_list: the targeted file, preceded by directories that may be their parents relative to the base_path
|
||||
:return: path for targeted file
|
||||
"""
|
||||
target_file = ''
|
||||
while len(path_list) > 0:
|
||||
target_file = os.path.join(path_list[-1], target_file)
|
||||
path_list = path_list[:len(path_list) - 1]
|
||||
full_path = os.path.join(base_path, target_file)
|
||||
full_path = full_path[:len(full_path) - 1]
|
||||
if os.path.isfile(full_path):
|
||||
return full_path
|
||||
return ''
|
||||
|
||||
def import_sqlite_db(self):
|
||||
"""
|
||||
Import the songs from an EasyWorship 6 SQLite database
|
||||
"""
|
||||
songs_db_path = self._find_file(self.import_source, ["Databases", "Data", "Songs.db"])
|
||||
song_words_db_path = self._find_file(self.import_source, ["Databases", "Data", "SongWords.db"])
|
||||
invalid_dir_msg = 'This does not appear to be a valid Easy Worship 6 database directory.'
|
||||
# check to see if needed files are there
|
||||
if not os.path.isfile(songs_db_path):
|
||||
self.log_error(songs_db_path, translate('SongsPlugin.EasyWorshipSongImport', invalid_dir_msg))
|
||||
return
|
||||
if not os.path.isfile(song_words_db_path):
|
||||
self.log_error(song_words_db_path, translate('SongsPlugin.EasyWorshipSongImport', invalid_dir_msg))
|
||||
return
|
||||
# get database handles
|
||||
songs_conn = sqlite3.connect(songs_db_path)
|
||||
words_conn = sqlite3.connect(song_words_db_path)
|
||||
if songs_conn is None or words_conn is None:
|
||||
self.log_error(self.import_source, translate('SongsPlugin.EasyWorshipSongImport',
|
||||
'This is not a valid Easy Worship 6 database.'))
|
||||
songs_conn.close()
|
||||
words_conn.close()
|
||||
return
|
||||
songs_db = songs_conn.cursor()
|
||||
words_db = words_conn.cursor()
|
||||
if songs_conn is None or words_conn is None:
|
||||
self.log_error(self.import_source, translate('SongsPlugin.EasyWorshipSongImport',
|
||||
'This is not a valid Easy Worship 6 database.'))
|
||||
songs_conn.close()
|
||||
words_conn.close()
|
||||
return
|
||||
# Take a stab at how text is encoded
|
||||
self.encoding = 'cp1252'
|
||||
self.encoding = retrieve_windows_encoding(self.encoding)
|
||||
if not self.encoding:
|
||||
log.debug('No encoding set.')
|
||||
return
|
||||
# import songs
|
||||
songs = songs_db.execute('SELECT rowid,title,author,copyright,vendor_id FROM song;')
|
||||
for song in songs:
|
||||
song_id = song[0]
|
||||
# keep extra copy of title for error message because error check clears it
|
||||
self.title = title = song[1]
|
||||
self.author = song[2]
|
||||
self.copyright = song[3]
|
||||
self.ccli_number = song[4]
|
||||
words = words_db.execute('SELECT words FROM word WHERE song_id = ?;', (song_id,))
|
||||
self.set_song_import_object(self.author, words.fetchone()[0].encode())
|
||||
if not self.finish():
|
||||
self.log_error(self.import_source,
|
||||
translate('SongsPlugin.EasyWorshipSongImport',
|
||||
'"{title}" could not be imported. {entry}').
|
||||
format(title=title, entry=self.entry_error_log))
|
||||
# close database handles
|
||||
songs_conn.close()
|
||||
words_conn.close()
|
||||
return
|
||||
|
||||
def set_song_import_object(self, authors, words):
|
||||
"""
|
||||
Set the SongImport object members.
|
||||
|
@ -409,7 +488,7 @@ class EasyWorshipSongImport(SongImport):
|
|||
self.comments += str(translate('SongsPlugin.EasyWorshipSongImport',
|
||||
'\n[above are Song Tags with notes imported from EasyWorship]'))
|
||||
|
||||
def find_field(self, field_name):
|
||||
def db_find_field(self, field_name):
|
||||
"""
|
||||
Find a field in the descriptions
|
||||
|
||||
|
@ -417,7 +496,7 @@ class EasyWorshipSongImport(SongImport):
|
|||
"""
|
||||
return [i for i, x in enumerate(self.field_descriptions) if x.name == field_name][0]
|
||||
|
||||
def set_record_struct(self, field_descriptions):
|
||||
def db_set_record_struct(self, field_descriptions):
|
||||
"""
|
||||
Save the record structure
|
||||
|
||||
|
@ -445,7 +524,7 @@ class EasyWorshipSongImport(SongImport):
|
|||
self.record_structure = struct.Struct(''.join(fsl))
|
||||
self.field_descriptions = field_descriptions
|
||||
|
||||
def get_field(self, field_desc_index):
|
||||
def db_get_field(self, field_desc_index):
|
||||
"""
|
||||
Extract the field
|
||||
|
||||
|
@ -489,7 +568,7 @@ class EasyWorshipSongImport(SongImport):
|
|||
else:
|
||||
return 0
|
||||
|
||||
def get_bytes(self, pos, length):
|
||||
def ews_get_bytes(self, pos, length):
|
||||
"""
|
||||
Get bytes from ews_file
|
||||
|
||||
|
@ -500,7 +579,7 @@ class EasyWorshipSongImport(SongImport):
|
|||
self.ews_file.seek(pos)
|
||||
return self.ews_file.read(length)
|
||||
|
||||
def get_string(self, pos, length):
|
||||
def ews_get_string(self, pos, length):
|
||||
"""
|
||||
Get string from ews_file
|
||||
|
||||
|
@ -508,12 +587,12 @@ class EasyWorshipSongImport(SongImport):
|
|||
:param length: Characters to read
|
||||
:return: String read
|
||||
"""
|
||||
bytes = self.get_bytes(pos, length)
|
||||
bytes = self.ews_get_bytes(pos, length)
|
||||
mask = '<' + str(length) + 's'
|
||||
byte_str, = struct.unpack(mask, bytes)
|
||||
return byte_str.decode(self.encoding).replace('\0', '').strip()
|
||||
|
||||
def get_i16(self, pos):
|
||||
def ews_get_i16(self, pos):
|
||||
"""
|
||||
Get short int from ews_file
|
||||
|
||||
|
@ -521,19 +600,19 @@ class EasyWorshipSongImport(SongImport):
|
|||
:return: Short integer read
|
||||
"""
|
||||
|
||||
bytes = self.get_bytes(pos, 2)
|
||||
bytes = self.ews_get_bytes(pos, 2)
|
||||
mask = '<h'
|
||||
number, = struct.unpack(mask, bytes)
|
||||
return number
|
||||
|
||||
def get_i32(self, pos):
|
||||
def ews_get_i32(self, pos):
|
||||
"""
|
||||
Get long int from ews_file
|
||||
|
||||
:param pos: Position to read from
|
||||
:return: Long integer read
|
||||
"""
|
||||
bytes = self.get_bytes(pos, 4)
|
||||
bytes = self.ews_get_bytes(pos, 4)
|
||||
mask = '<i'
|
||||
number, = struct.unpack(mask, bytes)
|
||||
return number
|
||||
|
|
|
@ -150,7 +150,12 @@ class OpenLPSongImport(SongImport):
|
|||
class_mapper(OldSongBookEntry)
|
||||
except UnmappedClassError:
|
||||
mapper(OldSongBookEntry, source_songs_songbooks_table, properties={'songbook': relation(OldBook)})
|
||||
if has_authors_songs and 'author_type' in source_authors_songs_table.c.values():
|
||||
if has_authors_songs:
|
||||
try:
|
||||
class_mapper(OldAuthorSong)
|
||||
except UnmappedClassError:
|
||||
mapper(OldAuthorSong, source_authors_songs_table)
|
||||
if has_authors_songs and 'author_type' in source_authors_songs_table.c.keys():
|
||||
has_author_type = True
|
||||
else:
|
||||
has_author_type = False
|
||||
|
@ -191,11 +196,6 @@ class OpenLPSongImport(SongImport):
|
|||
class_mapper(OldTopic)
|
||||
except UnmappedClassError:
|
||||
mapper(OldTopic, source_topics_table)
|
||||
if has_authors_songs:
|
||||
try:
|
||||
class_mapper(OldTopic)
|
||||
except UnmappedClassError:
|
||||
mapper(OldTopic, source_topics_table)
|
||||
|
||||
source_songs = self.source_session.query(OldSong).all()
|
||||
if self.import_wizard:
|
||||
|
|
|
@ -26,7 +26,7 @@ import re
|
|||
from lxml import objectify
|
||||
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.importers.songimport import SongImport
|
||||
from openlp.plugins.songs.lib.ui import SongStrings
|
||||
|
@ -87,7 +87,7 @@ class OpenSongImport(SongImport):
|
|||
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
|
||||
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
|
||||
1 Some____ Words
|
||||
|
@ -195,14 +195,34 @@ class OpenSongImport(SongImport):
|
|||
lyrics = str(root.lyrics)
|
||||
else:
|
||||
lyrics = ''
|
||||
chords = []
|
||||
for this_line in lyrics.split('\n'):
|
||||
if not this_line.strip():
|
||||
continue
|
||||
# skip this line if it is a comment
|
||||
if this_line.startswith(';'):
|
||||
continue
|
||||
# skip guitar chords and page and column breaks
|
||||
if this_line.startswith('.') or this_line.startswith('---') or this_line.startswith('-!!'):
|
||||
# skip page and column breaks
|
||||
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
|
||||
# verse/chorus/etc. marker
|
||||
if this_line.startswith('['):
|
||||
|
@ -228,12 +248,20 @@ class OpenSongImport(SongImport):
|
|||
# number at start of line.. it's verse number
|
||||
if this_line[0].isdigit():
|
||||
verse_num = this_line[0]
|
||||
this_line = this_line[1:].strip()
|
||||
this_line = this_line[1:]
|
||||
verses.setdefault(verse_tag, {})
|
||||
verses[verse_tag].setdefault(verse_num, {})
|
||||
if inst not in verses[verse_tag][verse_num]:
|
||||
verses[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
|
||||
this_line = self.tidy_text(this_line)
|
||||
this_line = this_line.replace('_', '')
|
||||
|
|
|
@ -25,10 +25,12 @@ The :mod:`songbeamer` module provides the functionality for importing SongBeamer
|
|||
import logging
|
||||
import os
|
||||
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.importers.songimport import SongImport
|
||||
from openlp.core.common import Settings, is_win, is_macosx, get_file_encoding
|
||||
|
||||
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):
|
||||
"""
|
||||
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.current_verse = ''
|
||||
self.current_verse_type = VerseType.tags[VerseType.Verse]
|
||||
read_verses = False
|
||||
self.chord_table = None
|
||||
file_name = os.path.split(import_file)[1]
|
||||
if os.path.isfile(import_file):
|
||||
# Detect the encoding
|
||||
|
@ -125,33 +134,103 @@ class SongBeamerImport(SongImport):
|
|||
continue
|
||||
self.title = file_name.split('.sng')[0]
|
||||
read_verses = False
|
||||
for line in song_data:
|
||||
# Just make sure that the line is of the type 'Unicode'.
|
||||
line = str(line).strip()
|
||||
# The first verse separator doesn't count, but the others does, so line count starts at -1
|
||||
line_number = -1
|
||||
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:
|
||||
self.parseTags(line)
|
||||
elif line.startswith('--'):
|
||||
# --- and -- allowed for page-breaks (difference in Songbeamer only in printout)
|
||||
self.parse_tags(line)
|
||||
elif stripped_line.startswith('---'):
|
||||
# '---' is a verse breaker
|
||||
if self.current_verse:
|
||||
self.replace_html_tags()
|
||||
self.add_verse(self.current_verse, self.current_verse_type)
|
||||
self.current_verse = ''
|
||||
self.current_verse_type = VerseType.tags[VerseType.Verse]
|
||||
first_verse = False
|
||||
read_verses = 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:
|
||||
if verse_start:
|
||||
verse_start = False
|
||||
if not self.check_verse_marks(line):
|
||||
self.current_verse = line + '\n'
|
||||
verse_mark = self.check_verse_marks(line)
|
||||
# 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:
|
||||
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:
|
||||
self.replace_html_tags()
|
||||
self.add_verse(self.current_verse, self.current_verse_type)
|
||||
if not self.finish():
|
||||
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):
|
||||
"""
|
||||
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:
|
||||
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.
|
||||
|
||||
|
@ -176,8 +255,10 @@ class SongBeamerImport(SongImport):
|
|||
self.add_copyright(tag_val[1])
|
||||
elif tag_val[0] == '#AddCopyrightInfo':
|
||||
pass
|
||||
elif tag_val[0] == '#AudioFile':
|
||||
self.parse_audio_file(tag_val[1])
|
||||
elif tag_val[0] == '#Author':
|
||||
self.parse_author(tag_val[1])
|
||||
self.parse_author(tag_val[1], 'words')
|
||||
elif tag_val[0] == '#BackgroundImage':
|
||||
pass
|
||||
elif tag_val[0] == '#Bible':
|
||||
|
@ -187,13 +268,16 @@ class SongBeamerImport(SongImport):
|
|||
elif tag_val[0] == '#CCLI':
|
||||
self.ccli_number = tag_val[1]
|
||||
elif tag_val[0] == '#Chords':
|
||||
pass
|
||||
self.chord_table = self.parse_chords(tag_val[1])
|
||||
elif tag_val[0] == '#ChurchSongID':
|
||||
pass
|
||||
elif tag_val[0] == '#ColorChords':
|
||||
pass
|
||||
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':
|
||||
pass
|
||||
elif tag_val[0] == '#Font':
|
||||
|
@ -217,7 +301,7 @@ class SongBeamerImport(SongImport):
|
|||
elif tag_val[0] == '#LangCount':
|
||||
pass
|
||||
elif tag_val[0] == '#Melody':
|
||||
self.parse_author(tag_val[1])
|
||||
self.parse_author(tag_val[1], 'music')
|
||||
elif tag_val[0] == '#NatCopyright':
|
||||
pass
|
||||
elif tag_val[0] == '#OTitle':
|
||||
|
@ -243,7 +327,7 @@ class SongBeamerImport(SongImport):
|
|||
elif tag_val[0] == '#TextAlign':
|
||||
pass
|
||||
elif tag_val[0] == '#Title':
|
||||
self.title = str(tag_val[1]).strip()
|
||||
self.title = tag_val[1].strip()
|
||||
elif tag_val[0] == '#TitleAlign':
|
||||
pass
|
||||
elif tag_val[0] == '#TitleFontSize':
|
||||
|
@ -263,25 +347,80 @@ class SongBeamerImport(SongImport):
|
|||
elif tag_val[0] == '#Version':
|
||||
pass
|
||||
elif tag_val[0] == '#VerseOrder':
|
||||
# TODO: add the verse order.
|
||||
pass
|
||||
verse_order = tag_val[1].strip()
|
||||
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):
|
||||
"""
|
||||
Check and add the verse's MarkType. Returns ``True`` if the given line contains a correct verse mark otherwise
|
||||
``False``.
|
||||
|
||||
:param line: The line to check for marks (unicode).
|
||||
:param line: The line to check for marks.
|
||||
"""
|
||||
marks = line.split(' ')
|
||||
if len(marks) <= 2 and marks[0].lower() in SongBeamerTypes.MarkTypes:
|
||||
self.current_verse_type = SongBeamerTypes.MarkTypes[marks[0].lower()]
|
||||
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=']
|
||||
new_verse_mark = self.convert_verse_marks(line)
|
||||
if new_verse_mark:
|
||||
self.current_verse_type = new_verse_mark
|
||||
return True
|
||||
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 += 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
|
||||
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('.'):
|
||||
author2 = author2[:-1]
|
||||
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):
|
||||
"""
|
||||
|
@ -304,12 +307,23 @@ class SongImport(QtCore.QObject):
|
|||
if verse_def not in self.verse_order_list_generated:
|
||||
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:
|
||||
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
|
||||
|
||||
def check_complete(self):
|
||||
|
|
|
@ -26,8 +26,9 @@ exproted from Lyrix."""
|
|||
import logging
|
||||
import json
|
||||
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.db import AuthorType
|
||||
|
||||
|
@ -123,7 +124,11 @@ class VideoPsalmImport(SongImport):
|
|||
for verse in song['Verses']:
|
||||
if 'Text' not in verse:
|
||||
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():
|
||||
self.log_error('Could not import {title}'.format(title=self.title))
|
||||
except Exception as e:
|
||||
|
|
|
@ -61,7 +61,7 @@ import re
|
|||
|
||||
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.lib import FormattingTags
|
||||
from openlp.plugins.songs.lib import VerseType, clean_song
|
||||
|
@ -154,7 +154,7 @@ class OpenLyrics(object):
|
|||
OpenLP does not support the attribute *lang*.
|
||||
|
||||
``<chord>``
|
||||
This property is not supported.
|
||||
This property is fully supported.
|
||||
|
||||
``<comments>``
|
||||
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.
|
||||
if index < len(optional_verses) - 1:
|
||||
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):
|
||||
"""
|
||||
|
@ -595,8 +607,7 @@ class OpenLyrics(object):
|
|||
|
||||
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
|
||||
converted.
|
||||
Converts the xml text with mixed content to OpenLP representation. Chords and formatting tags are converted.
|
||||
|
||||
: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
|
||||
|
@ -608,12 +619,14 @@ class OpenLyrics(object):
|
|||
# TODO: Verify format() with template variables
|
||||
if element.tag == NSMAP % 'comment':
|
||||
if element.tail:
|
||||
# Append tail text at chord element.
|
||||
# Append tail text at comment element.
|
||||
text += element.tail
|
||||
return text
|
||||
# Skip <chord> element - not yet supported.
|
||||
# Convert chords to ChordPro format which OpenLP uses internally
|
||||
# TODO: Verify format() with template variables
|
||||
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:
|
||||
# Append tail text at chord element.
|
||||
text += element.tail
|
||||
|
@ -666,7 +679,7 @@ class OpenLyrics(object):
|
|||
text = self._process_lines_mixed_content(element)
|
||||
# OpenLyrics version <= 0.7 contains <line> elements to represent lines. First child element is tested.
|
||||
else:
|
||||
# Loop over the "line" elements removing comments and chords.
|
||||
# Loop over the "line" elements removing comments
|
||||
for line in element:
|
||||
# Skip comment lines.
|
||||
# TODO: Verify format() with template variables
|
||||
|
|
|
@ -60,6 +60,35 @@ class SongsTab(SettingsTab):
|
|||
self.display_copyright_check_box.setObjectName('copyright_check_box')
|
||||
self.mode_layout.addWidget(self.display_copyright_check_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.right_layout.addStretch()
|
||||
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_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.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):
|
||||
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',
|
||||
'Display "{symbol}" symbol before copyright '
|
||||
'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):
|
||||
self.song_search = (check_state == QtCore.Qt.Checked)
|
||||
|
@ -104,6 +149,21 @@ class SongsTab(SettingsTab):
|
|||
def on_copyright_check_box_changed(self, check_state):
|
||||
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):
|
||||
settings = Settings()
|
||||
settings.beginGroup(self.settings_section)
|
||||
|
@ -113,12 +173,25 @@ class SongsTab(SettingsTab):
|
|||
self.display_songbook = settings.value('display songbook')
|
||||
self.display_written_by = settings.value('display written by')
|
||||
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.update_on_edit_check_box.setChecked(self.update_edit)
|
||||
self.add_from_service_check_box.setChecked(self.update_load)
|
||||
self.display_songbook_check_box.setChecked(self.display_songbook)
|
||||
self.display_written_by_check_box.setChecked(self.display_written_by)
|
||||
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()
|
||||
|
||||
def save(self):
|
||||
|
@ -130,6 +203,10 @@ class SongsTab(SettingsTab):
|
|||
settings.setValue('display songbook', self.display_songbook)
|
||||
settings.setValue('display written by', self.display_written_by)
|
||||
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()
|
||||
if self.tab_visited:
|
||||
self.settings_form.register_post_process('songs_config_updated')
|
||||
|
|
|
@ -66,7 +66,11 @@ __default_settings__ = {
|
|||
'songs/last directory export': '',
|
||||
'songs/songselect username': '',
|
||||
'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,
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@ from PyQt5 import QtCore, QtWidgets
|
|||
from openlp.core.common import translate
|
||||
from openlp.core.lib import build_icon
|
||||
from openlp.core.lib.ui import create_button_box
|
||||
from openlp.core.ui.lib import PathEdit, PathType
|
||||
|
||||
|
||||
class Ui_SongUsageDetailDialog(object):
|
||||
|
@ -68,20 +69,13 @@ class Ui_SongUsageDetailDialog(object):
|
|||
self.file_horizontal_layout.setSpacing(8)
|
||||
self.file_horizontal_layout.setContentsMargins(8, 8, 8, 8)
|
||||
self.file_horizontal_layout.setObjectName('file_horizontal_layout')
|
||||
self.file_line_edit = QtWidgets.QLineEdit(self.file_group_box)
|
||||
self.file_line_edit.setObjectName('file_line_edit')
|
||||
self.file_line_edit.setReadOnly(True)
|
||||
self.file_horizontal_layout.addWidget(self.file_line_edit)
|
||||
self.save_file_push_button = QtWidgets.QPushButton(self.file_group_box)
|
||||
self.save_file_push_button.setMaximumWidth(self.save_file_push_button.size().height())
|
||||
self.save_file_push_button.setIcon(build_icon(':/general/general_open.png'))
|
||||
self.save_file_push_button.setObjectName('save_file_push_button')
|
||||
self.file_horizontal_layout.addWidget(self.save_file_push_button)
|
||||
self.report_path_edit = PathEdit(self.file_group_box, path_type = PathType.Directories, show_revert=False)
|
||||
self.file_horizontal_layout.addWidget(self.report_path_edit)
|
||||
self.vertical_layout.addWidget(self.file_group_box)
|
||||
self.button_box = create_button_box(song_usage_detail_dialog, 'button_box', ['cancel', 'ok'])
|
||||
self.vertical_layout.addWidget(self.button_box)
|
||||
self.retranslateUi(song_usage_detail_dialog)
|
||||
self.save_file_push_button.clicked.connect(song_usage_detail_dialog.define_output_location)
|
||||
self.report_path_edit.pathChanged.connect(song_usage_detail_dialog.on_report_path_edit_path_changed)
|
||||
|
||||
def retranslateUi(self, song_usage_detail_dialog):
|
||||
"""
|
||||
|
|
|
@ -54,25 +54,20 @@ class SongUsageDetailForm(QtWidgets.QDialog, Ui_SongUsageDetailDialog, RegistryP
|
|||
"""
|
||||
self.from_date_calendar.setSelectedDate(Settings().value(self.plugin.settings_section + '/from date'))
|
||||
self.to_date_calendar.setSelectedDate(Settings().value(self.plugin.settings_section + '/to date'))
|
||||
self.file_line_edit.setText(Settings().value(self.plugin.settings_section + '/last directory export'))
|
||||
self.report_path_edit.path = Settings().value(self.plugin.settings_section + '/last directory export')
|
||||
|
||||
def define_output_location(self):
|
||||
def on_report_path_edit_path_changed(self, file_path):
|
||||
"""
|
||||
Triggered when the Directory selection button is clicked
|
||||
"""
|
||||
path = QtWidgets.QFileDialog.getExistingDirectory(
|
||||
self, translate('SongUsagePlugin.SongUsageDetailForm', 'Output File Location'),
|
||||
Settings().value(self.plugin.settings_section + '/last directory export'))
|
||||
if path:
|
||||
Settings().setValue(self.plugin.settings_section + '/last directory export', path)
|
||||
self.file_line_edit.setText(path)
|
||||
Settings().setValue(self.plugin.settings_section + '/last directory export', file_path)
|
||||
|
||||
def accept(self):
|
||||
"""
|
||||
Ok was triggered so lets save the data and run the report
|
||||
"""
|
||||
log.debug('accept')
|
||||
path = self.file_line_edit.text()
|
||||
path = self.report_path_edit.path
|
||||
if not path:
|
||||
self.main_window.error_message(
|
||||
translate('SongUsagePlugin.SongUsageDetailForm', 'Output Path Not Selected'),
|
||||
|
|
|
@ -35,8 +35,9 @@ import sys
|
|||
import time
|
||||
from subprocess import Popen, PIPE
|
||||
|
||||
appveyor_build_url = 'https://ci.appveyor.com/project/TomasGroth/openlp/build'
|
||||
appveyor_api_url = 'https://ci.appveyor.com/api/projects/TomasGroth/openlp'
|
||||
appveyor_build_url = 'https://ci.appveyor.com/project/OpenLP/{project}/build'
|
||||
appveyor_api_url = 'https://ci.appveyor.com/api/projects/OpenLP/{project}'
|
||||
appveyor_log_url = 'https://ci.appveyor.com/api/buildjobs/{buildid}/log'
|
||||
|
||||
webhook_element = \
|
||||
{
|
||||
|
@ -84,7 +85,7 @@ def get_version():
|
|||
return version_string, version
|
||||
|
||||
|
||||
def get_yml(branch):
|
||||
def get_yml(branch, build_type):
|
||||
"""
|
||||
Returns the content of appveyor.yml and inserts the branch to be build
|
||||
"""
|
||||
|
@ -92,7 +93,7 @@ def get_yml(branch):
|
|||
yml_text = f.read()
|
||||
f.close()
|
||||
yml_text = yml_text.replace('BRANCHNAME', branch)
|
||||
if 'openlp-core/openlp/trunk' in branch:
|
||||
if build_type in ['openlp', 'trunk']:
|
||||
yml_text = yml_text.replace('BUILD_DOCS', '$TRUE')
|
||||
else:
|
||||
yml_text = yml_text.replace('BUILD_DOCS', '$FALSE')
|
||||
|
@ -115,23 +116,28 @@ def hook(webhook_url, yml):
|
|||
print(responce.read().decode('utf-8'))
|
||||
|
||||
|
||||
def get_appveyor_build_url(branch):
|
||||
def get_appveyor_build_url(build_type):
|
||||
"""
|
||||
Get the url of the build.
|
||||
"""
|
||||
responce = urllib.request.urlopen(appveyor_api_url)
|
||||
responce = urllib.request.urlopen(appveyor_api_url.format(project=build_type))
|
||||
json_str = responce.read().decode('utf-8')
|
||||
build_json = json.loads(json_str)
|
||||
build_url = '%s/%s' % (appveyor_build_url, build_json['build']['version'])
|
||||
print('Check this URL for build status: %s' % build_url)
|
||||
build_url = '%s/%s' % (appveyor_build_url.format(project=build_type), build_json['build']['version'])
|
||||
print(build_url.format(project=build_type))
|
||||
print(appveyor_log_url.format(buildid=build_json['build']['jobs'][0]['jobId']))
|
||||
|
||||
|
||||
if len(sys.argv) != 3:
|
||||
print('Usage: %s <webhook-url> <branch>' % sys.argv[0])
|
||||
if len(sys.argv) != 4:
|
||||
print('Invalid number of arguments\nUsage: %s <webhook-url> <branch> <dev|trunk|openlp>' % sys.argv[0])
|
||||
else:
|
||||
webhook_url = sys.argv[1]
|
||||
branch = sys.argv[2]
|
||||
hook(webhook_url, get_yml(branch))
|
||||
build_type = sys.argv[3]
|
||||
if build_type not in ['dev', 'trunk', 'openlp']:
|
||||
print('Invalid build type\nUsage: %s <webhook-url> <branch> <dev|trunk|openlp>' % sys.argv[0])
|
||||
exit()
|
||||
hook(webhook_url, get_yml(branch, build_type))
|
||||
# Wait 5 seconds to make sure the hook has been triggered
|
||||
time.sleep(5)
|
||||
get_appveyor_build_url(branch)
|
||||
get_appveyor_build_url(build_type)
|
||||
|
|
|
@ -12,21 +12,21 @@ environment:
|
|||
|
||||
install:
|
||||
# Install dependencies from pypi
|
||||
- "%PYTHON%\\python.exe -m pip install sqlalchemy alembic chardet beautifulsoup4 Mako nose mock pyodbc psycopg2 pypiwin32 pyenchant"
|
||||
- "%PYTHON%\\python.exe -m pip install sqlalchemy alembic chardet beautifulsoup4 Mako nose mock pyodbc==4.0.8 psycopg2 pypiwin32 pyenchant"
|
||||
# Install mysql dependency
|
||||
- "%PYTHON%\\python.exe -m pip install http://cdn.mysql.com/Downloads/Connector-Python/mysql-connector-python-2.0.4.zip#md5=3df394d89300db95163f17c843ef49df"
|
||||
# Download and install lxml and pyicu (originally from http://www.lfd.uci.edu/~gohlke/pythonlibs/)
|
||||
- "%PYTHON%\\python.exe -m pip install https://get.openlp.org/win-sdk/lxml-3.6.4-cp34-cp34m-win32.whl"
|
||||
- "%PYTHON%\\python.exe -m pip install https://get.openlp.org/win-sdk/PyICU-1.9.5-cp34-cp34m-win32.whl"
|
||||
# Download and install PyQt5
|
||||
- curl -L -O http://downloads.sourceforge.net/project/pyqt/PyQt5/PyQt-5.5.1/PyQt5-5.5.1-gpl-Py3.4-Qt5.5.1-x32.exe
|
||||
- appveyor DownloadFile http://downloads.sourceforge.net/project/pyqt/PyQt5/PyQt-5.5.1/PyQt5-5.5.1-gpl-Py3.4-Qt5.5.1-x32.exe
|
||||
- PyQt5-5.5.1-gpl-Py3.4-Qt5.5.1-x32.exe /S
|
||||
# Download and unpack mupdf
|
||||
- curl -O http://mupdf.com/downloads/archive/mupdf-1.9a-windows.zip
|
||||
- appveyor DownloadFile http://mupdf.com/downloads/archive/mupdf-1.9a-windows.zip
|
||||
- 7z x mupdf-1.9a-windows.zip
|
||||
- cp mupdf-1.9a-windows/mupdf.exe openlp-branch/mupdf.exe
|
||||
# Download and unpack mediainfo
|
||||
- curl -O https://mediaarea.net/download/binary/mediainfo/0.7.90/MediaInfo_CLI_0.7.90_Windows_i386.zip
|
||||
- appveyor DownloadFile https://mediaarea.net/download/binary/mediainfo/0.7.90/MediaInfo_CLI_0.7.90_Windows_i386.zip
|
||||
- mkdir MediaInfo
|
||||
- 7z x -oMediaInfo MediaInfo_CLI_0.7.90_Windows_i386.zip
|
||||
- cp MediaInfo\\MediaInfo.exe openlp-branch\\MediaInfo.exe
|
||||
|
@ -43,15 +43,15 @@ test_script:
|
|||
after_test:
|
||||
# This is where we create a package using PyInstaller
|
||||
# First get PyInstaller
|
||||
- curl -L -O https://github.com/pyinstaller/pyinstaller/releases/download/v3.2/PyInstaller-3.2.zip
|
||||
- appveyor DownloadFile https://github.com/pyinstaller/pyinstaller/releases/download/v3.2/PyInstaller-3.2.zip
|
||||
- 7z x PyInstaller-3.2.zip
|
||||
# Install PyInstaller dependencies
|
||||
- "%PYTHON%\\python.exe -m pip install future pefile"
|
||||
# Download and install Inno Setup - used for packaging
|
||||
- curl -L -O http://www.jrsoftware.org/download.php/is-unicode.exe
|
||||
- appveyor DownloadFile http://www.jrsoftware.org/download.php/is-unicode.exe
|
||||
- is-unicode.exe /VERYSILENT /SUPPRESSMSGBOXES /SP-
|
||||
# Download and unpack portable-bundle
|
||||
- curl -O https://get.openlp.org/win-sdk/portable-setup.7z
|
||||
- appveyor DownloadFile https://get.openlp.org/win-sdk/portable-setup.7z
|
||||
- 7z x portable-setup.7z
|
||||
# Disabled portable installers - can't figure out how to make them silent
|
||||
# - curl -L -O http://downloads.sourceforge.net/project/portableapps/PortableApps.com%20Installer/PortableApps.comInstaller_3.4.4.paf.exe
|
||||
|
@ -61,7 +61,7 @@ after_test:
|
|||
# - curl -L -O http://downloads.sourceforge.net/project/portableapps/NSIS%20Portable/NSISPortable_3.0_English.paf.exe
|
||||
# - NSISPortable_3.0_English.paf.exe /S
|
||||
# Get the packaging code
|
||||
- curl -L http://bazaar.launchpad.net/~openlp-core/openlp/packaging/tarball -o packaging.tar.gz
|
||||
- appveyor DownloadFile http://bazaar.launchpad.net/~openlp-core/openlp/packaging/tarball -FileName packaging.tar.gz
|
||||
- 7z e packaging.tar.gz
|
||||
- 7z x packaging.tar
|
||||
- mv ~openlp-core/openlp/packaging packaging
|
||||
|
@ -74,7 +74,7 @@ after_test:
|
|||
7z x documentation.tar
|
||||
mv ~openlp-core/openlp/documentation documentation
|
||||
cd packaging
|
||||
&"$env:PYTHON\python.exe" builders/windows-builder.py --skip-update --skip-translations -c windows/config-appveyor.ini -b ../openlp-branch -d ../documentation --portable
|
||||
&"$env:PYTHON\python.exe" builders/windows-builder.py --skip-update -c windows/config-appveyor.ini -b ../openlp-branch -d ../documentation --portable
|
||||
} else {
|
||||
cd packaging
|
||||
&"$env:PYTHON\python.exe" builders/windows-builder.py --skip-update --skip-translations -c windows/config-appveyor.ini -b ../openlp-branch --portable
|
||||
|
|
|
@ -250,5 +250,6 @@ def main():
|
|||
print_qt_image_formats()
|
||||
print_enchant_backends_and_languages()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
|
@ -59,14 +59,13 @@ class OpenLPJobs(object):
|
|||
Branch_Pull = 'Branch-01-Pull'
|
||||
Branch_Functional = 'Branch-02-Functional-Tests'
|
||||
Branch_Interface = 'Branch-03-Interface-Tests'
|
||||
Branch_Windows_Functional = 'Branch-04a-Windows_Functional_Tests'
|
||||
Branch_Windows_Interface = 'Branch-04b-Windows_Interface_Tests'
|
||||
Branch_PEP = 'Branch-05a-Code_Analysis'
|
||||
Branch_Coverage = 'Branch-05b-Test_Coverage'
|
||||
Branch_Pylint = 'Branch-05c-Code_Analysis2'
|
||||
Branch_PEP = 'Branch-04a-Code_Analysis'
|
||||
Branch_Coverage = 'Branch-04b-Test_Coverage'
|
||||
Branch_Pylint = 'Branch-04c-Code_Analysis2'
|
||||
Branch_AppVeyor = 'Branch-05-AppVeyor-Tests'
|
||||
|
||||
Jobs = [Branch_Pull, Branch_Functional, Branch_Interface, Branch_Windows_Functional, Branch_Windows_Interface,
|
||||
Branch_PEP, Branch_Coverage, Branch_Pylint]
|
||||
Jobs = [Branch_Pull, Branch_Functional, Branch_Interface, Branch_PEP, Branch_Coverage, Branch_Pylint,
|
||||
Branch_AppVeyor]
|
||||
|
||||
|
||||
class Colour(object):
|
||||
|
@ -218,5 +217,6 @@ def main():
|
|||
else:
|
||||
parser.print_help()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
[pep8]
|
||||
exclude=resources.py,vlc.py
|
||||
max-line-length = 120
|
||||
ignore = E402
|
||||
ignore = E402,E722
|
||||
|
|
|
@ -25,13 +25,8 @@ Base directory for tests
|
|||
import sys
|
||||
from PyQt5 import QtWidgets
|
||||
|
||||
if sys.version_info[1] >= 3:
|
||||
from unittest.mock import ANY, MagicMock, patch, mock_open, call, PropertyMock
|
||||
else:
|
||||
from mock import ANY, MagicMock, patch, mock_open, call, PropertyMock
|
||||
|
||||
# Only one QApplication can be created. Use QtWidgets.QApplication.instance() when you need to "create" a QApplication.
|
||||
application = QtWidgets.QApplication([])
|
||||
application.setApplicationName('OpenLP')
|
||||
|
||||
__all__ = ['ANY', 'MagicMock', 'patch', 'mock_open', 'call', 'application', 'PropertyMock']
|
||||
__all__ = ['application']
|
||||
|
|
|
@ -19,11 +19,12 @@
|
|||
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
|
||||
import sys
|
||||
from unittest import TestCase, skip
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from PyQt5 import QtWidgets
|
||||
|
||||
from openlp.core import OpenLP, parse_options
|
||||
|
||||
|
||||
|
@ -131,11 +132,11 @@ class TestInitFunctions(TestCase):
|
|||
self.assertEquals(args.rargs, 'dummy_temp', 'The service file should not be blank')
|
||||
|
||||
|
||||
@skip('Figure out why this is causing a segfault')
|
||||
class TestOpenLP(TestCase):
|
||||
"""
|
||||
Test the OpenLP app class
|
||||
"""
|
||||
@skip('Figure out why this is causing a segfault')
|
||||
@patch('openlp.core.QtWidgets.QApplication.exec')
|
||||
def test_exec(self, mocked_exec):
|
||||
"""
|
||||
|
@ -155,4 +156,72 @@ class TestOpenLP(TestCase):
|
|||
app.shared_memory.detach.assert_called_once_with()
|
||||
assert result is False
|
||||
|
||||
del app
|
||||
@patch('openlp.core.QtCore.QSharedMemory')
|
||||
def test_is_already_running_not_running(self, MockedSharedMemory):
|
||||
"""
|
||||
Test the is_already_running() method when OpenLP is NOT running
|
||||
"""
|
||||
# GIVEN: An OpenLP app and some mocks
|
||||
mocked_shared_memory = MagicMock()
|
||||
mocked_shared_memory.attach.return_value = False
|
||||
MockedSharedMemory.return_value = mocked_shared_memory
|
||||
app = OpenLP([])
|
||||
|
||||
# WHEN: is_already_running() is called
|
||||
result = app.is_already_running()
|
||||
|
||||
# THEN: The result should be false
|
||||
MockedSharedMemory.assert_called_once_with('OpenLP')
|
||||
mocked_shared_memory.attach.assert_called_once_with()
|
||||
mocked_shared_memory.create.assert_called_once_with(1)
|
||||
assert result is False
|
||||
|
||||
@patch('openlp.core.QtWidgets.QMessageBox.critical')
|
||||
@patch('openlp.core.QtWidgets.QMessageBox.StandardButtons')
|
||||
@patch('openlp.core.QtCore.QSharedMemory')
|
||||
def test_is_already_running_is_running_continue(self, MockedSharedMemory, MockedStandardButtons, mocked_critical):
|
||||
"""
|
||||
Test the is_already_running() method when OpenLP IS running and the user chooses to continue
|
||||
"""
|
||||
# GIVEN: An OpenLP app and some mocks
|
||||
mocked_shared_memory = MagicMock()
|
||||
mocked_shared_memory.attach.return_value = True
|
||||
MockedSharedMemory.return_value = mocked_shared_memory
|
||||
MockedStandardButtons.return_value = 0
|
||||
mocked_critical.return_value = QtWidgets.QMessageBox.Yes
|
||||
app = OpenLP([])
|
||||
|
||||
# WHEN: is_already_running() is called
|
||||
result = app.is_already_running()
|
||||
|
||||
# THEN: The result should be false
|
||||
MockedSharedMemory.assert_called_once_with('OpenLP')
|
||||
mocked_shared_memory.attach.assert_called_once_with()
|
||||
MockedStandardButtons.assert_called_once_with(QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)
|
||||
mocked_critical.assert_called_once_with(None, 'Error', 'OpenLP is already running. Do you wish to continue?', 0)
|
||||
assert result is False
|
||||
|
||||
@patch('openlp.core.QtWidgets.QMessageBox.critical')
|
||||
@patch('openlp.core.QtWidgets.QMessageBox.StandardButtons')
|
||||
@patch('openlp.core.QtCore.QSharedMemory')
|
||||
def test_is_already_running_is_running_stop(self, MockedSharedMemory, MockedStandardButtons, mocked_critical):
|
||||
"""
|
||||
Test the is_already_running() method when OpenLP IS running and the user chooses to stop
|
||||
"""
|
||||
# GIVEN: An OpenLP app and some mocks
|
||||
mocked_shared_memory = MagicMock()
|
||||
mocked_shared_memory.attach.return_value = True
|
||||
MockedSharedMemory.return_value = mocked_shared_memory
|
||||
MockedStandardButtons.return_value = 0
|
||||
mocked_critical.return_value = QtWidgets.QMessageBox.No
|
||||
app = OpenLP([])
|
||||
|
||||
# WHEN: is_already_running() is called
|
||||
result = app.is_already_running()
|
||||
|
||||
# THEN: The result should be false
|
||||
MockedSharedMemory.assert_called_once_with('OpenLP')
|
||||
mocked_shared_memory.attach.assert_called_once_with()
|
||||
MockedStandardButtons.assert_called_once_with(QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)
|
||||
mocked_critical.assert_called_once_with(None, 'Error', 'OpenLP is already running. Do you wish to continue?', 0)
|
||||
assert result is True
|
||||
|
|
|
@ -23,12 +23,13 @@
|
|||
Package to test the openlp.core.common.actions package.
|
||||
"""
|
||||
from unittest import TestCase
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from PyQt5 import QtGui, QtCore, QtWidgets
|
||||
|
||||
from openlp.core.common import Settings
|
||||
from openlp.core.common.actions import CategoryActionList, ActionList
|
||||
from tests.functional import MagicMock
|
||||
|
||||
from tests.helpers.testmixin import TestMixin
|
||||
|
||||
|
||||
|
@ -120,11 +121,11 @@ class TestCategoryActionList(TestCase):
|
|||
self.list.add(self.action2)
|
||||
|
||||
# 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
|
||||
self.assertEquals(len(self.list), 2)
|
||||
self.assertIs(l[0], self.action1)
|
||||
self.assertIs(l[1], self.action2)
|
||||
self.assertIs(list[0], self.action1)
|
||||
self.assertIs(list[1], self.action2)
|
||||
|
||||
def test_remove(self):
|
||||
"""
|
||||
|
|
|
@ -25,9 +25,9 @@ Functional tests to test the AppLocation class and related methods.
|
|||
import copy
|
||||
import os
|
||||
from unittest import TestCase
|
||||
from unittest.mock import patch
|
||||
|
||||
from openlp.core.common import AppLocation, get_frozen_path
|
||||
from tests.functional import patch
|
||||
|
||||
FILE_LIST = ['file1', 'file2', 'file3.txt', 'file4.txt', 'file5.mp3', 'file6.mp3']
|
||||
|
||||
|
|
|
@ -22,12 +22,13 @@
|
|||
"""
|
||||
Functional tests to test the AppLocation class and related methods.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from unittest import TestCase
|
||||
from unittest.mock import MagicMock, call, patch
|
||||
|
||||
from openlp.core.common import check_directory_exists, de_hump, trace_error_handler, translate, is_win, is_macosx, \
|
||||
is_linux, clean_button_text
|
||||
from tests.functional import MagicMock, patch
|
||||
from openlp.core import common
|
||||
from openlp.core.common import check_directory_exists, clean_button_text, de_hump, extension_loader, is_macosx, \
|
||||
is_linux, is_win, path_to_module, trace_error_handler, translate
|
||||
|
||||
|
||||
class TestCommonFunctions(TestCase):
|
||||
|
@ -73,6 +74,72 @@ class TestCommonFunctions(TestCase):
|
|||
mocked_exists.assert_called_with(directory_to_check)
|
||||
self.assertRaises(ValueError, check_directory_exists, directory_to_check)
|
||||
|
||||
def test_extension_loader_no_files_found(self):
|
||||
"""
|
||||
Test the `extension_loader` function when no files are found
|
||||
"""
|
||||
# GIVEN: A mocked `Path.glob` method which does not match any files
|
||||
with patch('openlp.core.common.AppLocation.get_directory', return_value='/app/dir/openlp'), \
|
||||
patch.object(common.Path, 'glob', return_value=[]), \
|
||||
patch('openlp.core.common.importlib.import_module') as mocked_import_module:
|
||||
|
||||
# WHEN: Calling `extension_loader`
|
||||
extension_loader('glob', ['file2.py', 'file3.py'])
|
||||
|
||||
# THEN: `extension_loader` should not try to import any files
|
||||
self.assertFalse(mocked_import_module.called)
|
||||
|
||||
def test_extension_loader_files_found(self):
|
||||
"""
|
||||
Test the `extension_loader` function when it successfully finds and loads some files
|
||||
"""
|
||||
# GIVEN: A mocked `Path.glob` method which returns a list of files
|
||||
with patch('openlp.core.common.AppLocation.get_directory', return_value='/app/dir/openlp'), \
|
||||
patch.object(common.Path, 'glob', return_value=[Path('/app/dir/openlp/import_dir/file1.py'),
|
||||
Path('/app/dir/openlp/import_dir/file2.py'),
|
||||
Path('/app/dir/openlp/import_dir/file3.py'),
|
||||
Path('/app/dir/openlp/import_dir/file4.py')]), \
|
||||
patch('openlp.core.common.importlib.import_module') as mocked_import_module:
|
||||
|
||||
# WHEN: Calling `extension_loader` with a list of files to exclude
|
||||
extension_loader('glob', ['file2.py', 'file3.py'])
|
||||
|
||||
# THEN: `extension_loader` should only try to import the files that are matched by the blob, excluding the
|
||||
# files listed in the `excluded_files` argument
|
||||
mocked_import_module.assert_has_calls([call('openlp.import_dir.file1'), call('openlp.import_dir.file4')])
|
||||
|
||||
def test_extension_loader_import_error(self):
|
||||
"""
|
||||
Test the `extension_loader` function when `SourceFileLoader` raises a `ImportError`
|
||||
"""
|
||||
# GIVEN: A mocked `import_module` which raises an `ImportError`
|
||||
with patch('openlp.core.common.AppLocation.get_directory', return_value='/app/dir/openlp'), \
|
||||
patch.object(common.Path, 'glob', return_value=[Path('/app/dir/openlp/import_dir/file1.py')]), \
|
||||
patch('openlp.core.common.importlib.import_module', side_effect=ImportError()), \
|
||||
patch('openlp.core.common.log') as mocked_logger:
|
||||
|
||||
# WHEN: Calling `extension_loader`
|
||||
extension_loader('glob')
|
||||
|
||||
# THEN: The `ImportError` should be caught and logged
|
||||
self.assertTrue(mocked_logger.warning.called)
|
||||
|
||||
def test_extension_loader_os_error(self):
|
||||
"""
|
||||
Test the `extension_loader` function when `import_module` raises a `ImportError`
|
||||
"""
|
||||
# GIVEN: A mocked `SourceFileLoader` which raises an `OSError`
|
||||
with patch('openlp.core.common.AppLocation.get_directory', return_value='/app/dir/openlp'), \
|
||||
patch.object(common.Path, 'glob', return_value=[Path('/app/dir/openlp/import_dir/file1.py')]), \
|
||||
patch('openlp.core.common.importlib.import_module', side_effect=OSError()), \
|
||||
patch('openlp.core.common.log') as mocked_logger:
|
||||
|
||||
# WHEN: Calling `extension_loader`
|
||||
extension_loader('glob')
|
||||
|
||||
# THEN: The `OSError` should be caught and logged
|
||||
self.assertTrue(mocked_logger.warning.called)
|
||||
|
||||
def test_de_hump_conversion(self):
|
||||
"""
|
||||
Test the de_hump function with a class name
|
||||
|
@ -84,7 +151,7 @@ class TestCommonFunctions(TestCase):
|
|||
new_string = de_hump(string)
|
||||
|
||||
# THEN: the new string should be converted to python format
|
||||
self.assertTrue(new_string == "my_class", 'The class name should have been converted')
|
||||
self.assertEqual(new_string, "my_class", 'The class name should have been converted')
|
||||
|
||||
def test_de_hump_static(self):
|
||||
"""
|
||||
|
@ -97,7 +164,20 @@ class TestCommonFunctions(TestCase):
|
|||
new_string = de_hump(string)
|
||||
|
||||
# THEN: the new string should be converted to python format
|
||||
self.assertTrue(new_string == "my_class", 'The class name should have been preserved')
|
||||
self.assertEqual(new_string, "my_class", 'The class name should have been preserved')
|
||||
|
||||
def test_path_to_module(self):
|
||||
"""
|
||||
Test `path_to_module` when supplied with a `Path` object
|
||||
"""
|
||||
# GIVEN: A `Path` object
|
||||
path = Path('openlp/core/ui/media/webkitplayer.py')
|
||||
|
||||
# WHEN: Calling path_to_module with the `Path` object
|
||||
result = path_to_module(path)
|
||||
|
||||
# THEN: path_to_module should return the module name
|
||||
self.assertEqual(result, 'openlp.core.ui.media.webkitplayer')
|
||||
|
||||
def test_trace_error_handler(self):
|
||||
"""
|
||||
|
|
|
@ -26,10 +26,10 @@ import os
|
|||
import tempfile
|
||||
import socket
|
||||
from unittest import TestCase
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from openlp.core.common.httputils import get_user_agent, get_web_page, get_url_file_size, url_get_file
|
||||
|
||||
from tests.functional import MagicMock, patch
|
||||
from tests.helpers.testmixin import TestMixin
|
||||
|
||||
|
||||
|
|
|
@ -25,10 +25,11 @@ Functional tests to test the AppLocation class and related methods.
|
|||
import os
|
||||
from io import BytesIO
|
||||
from unittest import TestCase
|
||||
from unittest.mock import MagicMock, PropertyMock, call, patch
|
||||
|
||||
from openlp.core.common import add_actions, clean_filename, delete_file, get_file_encoding, get_filesystem_encoding, \
|
||||
get_uno_command, get_uno_instance, split_filename
|
||||
from tests.functional import MagicMock, PropertyMock, call, patch
|
||||
|
||||
from tests.helpers.testmixin import TestMixin
|
||||
|
||||
|
||||
|
|
|
@ -23,8 +23,8 @@
|
|||
Functional tests to test the AppLocation class and related methods.
|
||||
"""
|
||||
from unittest import TestCase
|
||||
from unittest.mock import patch
|
||||
|
||||
from tests.functional import patch
|
||||
from openlp.core.common.languagemanager import get_locale_key, get_natural_key
|
||||
|
||||
|
||||
|
|
|
@ -24,9 +24,9 @@ Package to test the openlp.core.lib package.
|
|||
"""
|
||||
import os
|
||||
from unittest import TestCase
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from openlp.core.common import Registry
|
||||
from tests.functional import MagicMock
|
||||
|
||||
TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '../', '..', 'resources'))
|
||||
|
||||
|
|
|
@ -23,11 +23,10 @@
|
|||
Test the registry properties
|
||||
"""
|
||||
from unittest import TestCase
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from openlp.core.common import Registry, RegistryProperties
|
||||
|
||||
from tests.functional import MagicMock, patch
|
||||
|
||||
|
||||
class TestRegistryProperties(TestCase, RegistryProperties):
|
||||
"""
|
||||
|
|
|
@ -23,10 +23,11 @@
|
|||
Package to test the openlp.core.lib.settings package.
|
||||
"""
|
||||
from unittest import TestCase
|
||||
from unittest.mock import patch
|
||||
|
||||
from openlp.core.common import Settings
|
||||
from openlp.core.common.settings import recent_files_conv
|
||||
from tests.functional import patch
|
||||
|
||||
from tests.helpers.testmixin import TestMixin
|
||||
|
||||
|
||||
|
|
|
@ -23,10 +23,11 @@
|
|||
Package to test the openlp.core.common.versionchecker package.
|
||||
"""
|
||||
from unittest import TestCase
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from openlp.core.common.settings import Settings
|
||||
from openlp.core.common.versionchecker import VersionThread
|
||||
from tests.functional import MagicMock, patch
|
||||
|
||||
from tests.helpers.testmixin import TestMixin
|
||||
|
||||
|
||||
|
|
|
@ -24,13 +24,13 @@ Package to test the openlp.core.lib package.
|
|||
"""
|
||||
import os
|
||||
from unittest import TestCase
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
from sqlalchemy.pool import NullPool
|
||||
from sqlalchemy.orm.scoping import ScopedSession
|
||||
from sqlalchemy import MetaData
|
||||
|
||||
from openlp.core.lib.db import init_db, get_upgrade_op, delete_database
|
||||
from tests.functional import patch, MagicMock
|
||||
|
||||
|
||||
class TestDB(TestCase):
|
||||
|
|
|
@ -1,11 +1,31 @@
|
|||
# -*- 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 #
|
||||
###############################################################################
|
||||
"""
|
||||
Package to test the openlp.core.lib.filedialog package.
|
||||
"""
|
||||
from unittest import TestCase
|
||||
from unittest.mock import MagicMock, call, patch
|
||||
|
||||
from openlp.core.common import UiStrings
|
||||
from openlp.core.lib.filedialog import FileDialog
|
||||
from tests.functional import MagicMock, call, patch
|
||||
|
||||
|
||||
class TestFileDialog(TestCase):
|
||||
|
|
|
@ -24,10 +24,9 @@ Package to test the openlp.core.lib.formattingtags package.
|
|||
"""
|
||||
import copy
|
||||
from unittest import TestCase
|
||||
from unittest.mock import patch
|
||||
|
||||
from openlp.core.lib import FormattingTags
|
||||
from tests.functional import patch
|
||||
|
||||
|
||||
TAG = {
|
||||
'end tag': '{/aa}',
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
"""
|
||||
Package to test the openlp.core.lib.htmlbuilder module.
|
||||
"""
|
||||
|
||||
from unittest import TestCase
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from PyQt5 import QtCore, QtWebKit
|
||||
|
||||
from openlp.core.common import Settings
|
||||
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 tests.functional import MagicMock, patch
|
||||
|
||||
from tests.helpers.testmixin import TestMixin
|
||||
|
||||
HTML = """
|
||||
|
@ -60,6 +60,29 @@ HTML = """
|
|||
position: relative;
|
||||
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>
|
||||
<script>
|
||||
var timer = null;
|
||||
|
@ -211,6 +234,34 @@ FOOTER_CSS_BASE = """
|
|||
FOOTER_CSS = FOOTER_CSS_BASE % ('nowrap')
|
||||
FOOTER_CSS_WRAP = FOOTER_CSS_BASE % ('normal')
|
||||
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):
|
||||
|
@ -222,6 +273,7 @@ class Htmbuilder(TestCase, TestMixin):
|
|||
Create the UI
|
||||
"""
|
||||
self.build_settings()
|
||||
Settings().extend_default_settings(__default_settings__)
|
||||
|
||||
def tearDown(self):
|
||||
"""
|
||||
|
@ -403,3 +455,17 @@ class Htmbuilder(TestCase, TestMixin):
|
|||
# WHEN: Retrieving the webkit version
|
||||
# THEN: Webkit versions should match
|
||||
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')
|
||||
|
|
|
@ -25,14 +25,15 @@ Package to test the openlp.core.ui package.
|
|||
import os
|
||||
import time
|
||||
from threading import Lock
|
||||
|
||||
from unittest import TestCase
|
||||
from unittest.mock import patch
|
||||
|
||||
from PyQt5 import QtGui
|
||||
|
||||
from openlp.core.common import Registry
|
||||
from openlp.core.lib import ImageManager, ScreenList
|
||||
from openlp.core.lib.imagemanager import Priority
|
||||
from tests.functional import patch
|
||||
|
||||
from tests.helpers.testmixin import TestMixin
|
||||
|
||||
TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'resources'))
|
||||
|
|
|
@ -23,15 +23,16 @@
|
|||
Package to test the openlp.core.lib package.
|
||||
"""
|
||||
import os
|
||||
|
||||
from unittest import TestCase
|
||||
from datetime import datetime, timedelta
|
||||
from unittest import TestCase
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
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, \
|
||||
expand_tags, get_text_file_string, image_to_byte, resize_image, str_to_bool, validate_thumb
|
||||
from tests.functional import MagicMock, patch
|
||||
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'))
|
||||
|
||||
|
@ -746,3 +747,116 @@ class TestLib(TestCase):
|
|||
# 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, '
|
||||
'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!')
|
||||
|
|
|
@ -23,10 +23,10 @@
|
|||
Package to test the openlp.core.lib.mediamanageritem package.
|
||||
"""
|
||||
from unittest import TestCase
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from openlp.core.lib import MediaManagerItem
|
||||
|
||||
from tests.functional import MagicMock, patch
|
||||
from tests.helpers.testmixin import TestMixin
|
||||
|
||||
|
||||
|
|
|
@ -23,11 +23,11 @@
|
|||
Package to test the openlp.core.lib.pluginmanager package.
|
||||
"""
|
||||
from unittest import TestCase
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from openlp.core.common import Registry, Settings
|
||||
from openlp.core.lib.pluginmanager import PluginManager
|
||||
from openlp.core.lib import PluginStatus
|
||||
from tests.functional import MagicMock
|
||||
|
||||
|
||||
class TestPluginManager(TestCase):
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||
|
||||
###############################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2015 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 #
|
||||
###############################################################################
|
||||
"""
|
||||
Package to test the openlp.core.lib.projector.constants package.
|
||||
"""
|
||||
from unittest import TestCase, skip
|
||||
|
||||
|
||||
class TestProjectorConstants(TestCase):
|
||||
"""
|
||||
Test specific functions in the projector constants module.
|
||||
"""
|
||||
def build_pjlink_video_label_test(self):
|
||||
"""
|
||||
Test building PJLINK_DEFAULT_CODES dictionary
|
||||
"""
|
||||
# GIVEN: Test data
|
||||
from tests.resources.projector.data import TEST_VIDEO_CODES
|
||||
|
||||
# WHEN: Import projector PJLINK_DEFAULT_CODES
|
||||
from openlp.core.lib.projector.constants import PJLINK_DEFAULT_CODES
|
||||
|
||||
# THEN: Verify dictionary was build correctly
|
||||
self.assertEquals(PJLINK_DEFAULT_CODES, TEST_VIDEO_CODES, 'PJLink video strings should match')
|
|
@ -22,17 +22,16 @@
|
|||
"""
|
||||
Package to test the openlp.core.lib.projector.pjlink1 package.
|
||||
"""
|
||||
|
||||
from unittest import TestCase
|
||||
from unittest.mock import call, patch, MagicMock
|
||||
|
||||
from openlp.core.lib.projector.pjlink1 import PJLink1
|
||||
from openlp.core.lib.projector.constants import E_PARAMETER, ERROR_STRING, S_OFF, S_STANDBY, S_WARMUP, S_ON, \
|
||||
S_COOLDOWN, PJLINK_POWR_STATUS
|
||||
from openlp.core.lib.projector.pjlink1 import PJLink
|
||||
from openlp.core.lib.projector.constants import E_PARAMETER, ERROR_STRING, S_OFF, S_STANDBY, S_ON, \
|
||||
PJLINK_POWR_STATUS, S_CONNECTED
|
||||
|
||||
from tests.functional import patch, MagicMock
|
||||
from tests.resources.projector.data import TEST_PIN, TEST_SALT, TEST_CONNECT_AUTHENTICATE, TEST_HASH
|
||||
|
||||
pjlink_test = PJLink1(name='test', ip='127.0.0.1', pin=TEST_PIN, no_poll=True)
|
||||
pjlink_test = PJLink(name='test', ip='127.0.0.1', pin=TEST_PIN, no_poll=True)
|
||||
|
||||
|
||||
class TestPJLink(TestCase):
|
||||
|
@ -165,7 +164,13 @@ class TestPJLink(TestCase):
|
|||
'Lamp 3 hours should have been set to 33333')
|
||||
|
||||
@patch.object(pjlink_test, 'projectorReceivedData')
|
||||
def test_projector_process_power_on(self, mock_projectorReceivedData):
|
||||
@patch.object(pjlink_test, 'projectorUpdateIcons')
|
||||
@patch.object(pjlink_test, 'send_command')
|
||||
@patch.object(pjlink_test, 'change_status')
|
||||
def test_projector_process_power_on(self, mock_change_status,
|
||||
mock_send_command,
|
||||
mock_UpdateIcons,
|
||||
mock_ReceivedData):
|
||||
"""
|
||||
Test status power to ON
|
||||
"""
|
||||
|
@ -178,9 +183,17 @@ class TestPJLink(TestCase):
|
|||
|
||||
# THEN: Power should be set to ON
|
||||
self.assertEquals(pjlink.power, S_ON, 'Power should have been set to ON')
|
||||
mock_send_command.assert_called_once_with('INST')
|
||||
self.assertEquals(mock_UpdateIcons.emit.called, True, 'projectorUpdateIcons should have been called')
|
||||
|
||||
@patch.object(pjlink_test, 'projectorReceivedData')
|
||||
def test_projector_process_power_off(self, mock_projectorReceivedData):
|
||||
@patch.object(pjlink_test, 'projectorUpdateIcons')
|
||||
@patch.object(pjlink_test, 'send_command')
|
||||
@patch.object(pjlink_test, 'change_status')
|
||||
def test_projector_process_power_off(self, mock_change_status,
|
||||
mock_send_command,
|
||||
mock_UpdateIcons,
|
||||
mock_ReceivedData):
|
||||
"""
|
||||
Test status power to STANDBY
|
||||
"""
|
||||
|
@ -193,6 +206,8 @@ class TestPJLink(TestCase):
|
|||
|
||||
# THEN: Power should be set to STANDBY
|
||||
self.assertEquals(pjlink.power, S_STANDBY, 'Power should have been set to STANDBY')
|
||||
self.assertEquals(mock_send_command.called, False, 'send_command should not have been called')
|
||||
self.assertEquals(mock_UpdateIcons.emit.called, True, 'projectorUpdateIcons should have been called')
|
||||
|
||||
@patch.object(pjlink_test, 'projectorUpdateIcons')
|
||||
def test_projector_process_avmt_closed_unmuted(self, mock_projectorReceivedData):
|
||||
|
@ -368,3 +383,95 @@ class TestPJLink(TestCase):
|
|||
# THEN: send_command should have the proper authentication
|
||||
self.assertEquals("{test}".format(test=mock_send_command.call_args),
|
||||
"call(data='{hash}%1CLSS ?\\r')".format(hash=TEST_HASH))
|
||||
|
||||
@patch.object(pjlink_test, '_not_implemented')
|
||||
def not_implemented_test(self, mock_not_implemented):
|
||||
"""
|
||||
Test PJLink._not_implemented method being called
|
||||
"""
|
||||
# GIVEN: test object
|
||||
pjlink = pjlink_test
|
||||
test_cmd = 'TESTMEONLY'
|
||||
|
||||
# WHEN: A future command is called that is not implemented yet
|
||||
pjlink.process_command(test_cmd, "Garbage data for test only")
|
||||
|
||||
# THEN: PJLink.__not_implemented should have been called with test_cmd
|
||||
mock_not_implemented.assert_called_with(test_cmd)
|
||||
|
||||
@patch.object(pjlink_test, 'disconnect_from_host')
|
||||
def socket_abort_test(self, mock_disconnect):
|
||||
"""
|
||||
Test PJLink.socket_abort calls disconnect_from_host
|
||||
"""
|
||||
# GIVEN: Test object
|
||||
pjlink = pjlink_test
|
||||
|
||||
# WHEN: Calling socket_abort
|
||||
pjlink.socket_abort()
|
||||
|
||||
# THEN: disconnect_from_host should be called
|
||||
self.assertTrue(mock_disconnect.called, 'Should have called disconnect_from_host')
|
||||
|
||||
def poll_loop_not_connected_test(self):
|
||||
"""
|
||||
Test PJLink.poll_loop not connected return
|
||||
"""
|
||||
# GIVEN: Test object and mocks
|
||||
pjlink = pjlink_test
|
||||
pjlink.state = MagicMock()
|
||||
pjlink.timer = MagicMock()
|
||||
pjlink.state.return_value = False
|
||||
pjlink.ConnectedState = True
|
||||
|
||||
# WHEN: PJLink.poll_loop called
|
||||
pjlink.poll_loop()
|
||||
|
||||
# THEN: poll_loop should exit without calling any other method
|
||||
self.assertFalse(pjlink.timer.called, 'Should have returned without calling any other method')
|
||||
|
||||
@patch.object(pjlink_test, 'send_command')
|
||||
def poll_loop_start_test(self, mock_send_command):
|
||||
"""
|
||||
Test PJLink.poll_loop makes correct calls
|
||||
"""
|
||||
# GIVEN: test object and test data
|
||||
pjlink = pjlink_test
|
||||
pjlink.state = MagicMock()
|
||||
pjlink.timer = MagicMock()
|
||||
pjlink.timer.interval = MagicMock()
|
||||
pjlink.timer.setInterval = MagicMock()
|
||||
pjlink.timer.start = MagicMock()
|
||||
pjlink.poll_time = 20
|
||||
pjlink.power = S_ON
|
||||
pjlink.source_available = None
|
||||
pjlink.other_info = None
|
||||
pjlink.manufacturer = None
|
||||
pjlink.model = None
|
||||
pjlink.pjlink_name = None
|
||||
pjlink.ConnectedState = S_CONNECTED
|
||||
pjlink.timer.interval.return_value = 10
|
||||
pjlink.state.return_value = S_CONNECTED
|
||||
call_list = [
|
||||
call('POWR', queue=True),
|
||||
call('ERST', queue=True),
|
||||
call('LAMP', queue=True),
|
||||
call('AVMT', queue=True),
|
||||
call('INPT', queue=True),
|
||||
call('INST', queue=True),
|
||||
call('INFO', queue=True),
|
||||
call('INF1', queue=True),
|
||||
call('INF2', queue=True),
|
||||
call('NAME', queue=True),
|
||||
]
|
||||
|
||||
# WHEN: PJLink.poll_loop is called
|
||||
pjlink.poll_loop()
|
||||
|
||||
# THEN: proper calls were made to retrieve projector data
|
||||
# First, call to update the timer with the next interval
|
||||
self.assertTrue(pjlink.timer.setInterval.called, 'Should have updated the timer')
|
||||
# Next, should have called the timer to start
|
||||
self.assertTrue(pjlink.timer.start.called, 'Should have started the timer')
|
||||
# Finally, should have called send_command with a list of projetctor status checks
|
||||
mock_send_command.assert_has_calls(call_list, 'Should have queued projector queries')
|
||||
|
|
|
@ -26,13 +26,15 @@ record functions.
|
|||
PREREQUISITE: add_record() and get_all() functions validated.
|
||||
"""
|
||||
import os
|
||||
import shutil
|
||||
from unittest import TestCase
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from openlp.core.lib.projector.db import Manufacturer, Model, Projector, ProjectorDB, ProjectorSource, Source
|
||||
from openlp.core.lib.projector.constants import PJLINK_PORT
|
||||
|
||||
from tests.functional import MagicMock, patch
|
||||
from tests.resources.projector.data import TEST_DB, TEST1_DATA, TEST2_DATA, TEST3_DATA
|
||||
from tests.resources.projector.data import TEST_DB_PJLINK1, TEST_DB, TEST1_DATA, TEST2_DATA, TEST3_DATA
|
||||
from tests.utils.constants import TEST_RESOURCES_PATH
|
||||
|
||||
|
||||
def compare_data(one, two):
|
||||
|
@ -45,7 +47,11 @@ def compare_data(one, two):
|
|||
one.port == two.port and \
|
||||
one.name == two.name and \
|
||||
one.location == two.location and \
|
||||
one.notes == two.notes
|
||||
one.notes == two.notes and \
|
||||
one.sw_version == two.sw_version and \
|
||||
one.serial_no == two.serial_no and \
|
||||
one.model_filter == two.model_filter and \
|
||||
one.model_lamp == two.model_lamp
|
||||
|
||||
|
||||
def compare_source(one, two):
|
||||
|
@ -168,6 +174,10 @@ class TestProjectorDB(TestCase):
|
|||
record.name = TEST3_DATA['name']
|
||||
record.location = TEST3_DATA['location']
|
||||
record.notes = TEST3_DATA['notes']
|
||||
record.sw_version = TEST3_DATA['sw_version']
|
||||
record.serial_no = TEST3_DATA['serial_no']
|
||||
record.model_filter = TEST3_DATA['model_filter']
|
||||
record.model_lamp = TEST3_DATA['model_lamp']
|
||||
updated = self.projector.update_projector(record)
|
||||
self.assertTrue(updated, 'Save updated record should have returned True')
|
||||
record = self.projector.get_projector_by_ip(TEST3_DATA['ip'])
|
||||
|
@ -246,7 +256,8 @@ class TestProjectorDB(TestCase):
|
|||
projector = Projector()
|
||||
|
||||
# WHEN: projector() is populated
|
||||
# NOTE: projector.pin, projector.other, projector.sources should all return None
|
||||
# NOTE: projector.[pin, other, sources, sw_version, serial_no, sw_version, model_lamp, model_filter]
|
||||
# should all return None.
|
||||
# projector.source_list should return an empty list
|
||||
projector.id = 0
|
||||
projector.ip = '127.0.0.1'
|
||||
|
@ -262,8 +273,9 @@ class TestProjectorDB(TestCase):
|
|||
self.assertEqual(str(projector),
|
||||
'< Projector(id="0", ip="127.0.0.1", port="4352", pin="None", name="Test One", '
|
||||
'location="Somewhere over the rainbow", notes="Not again", pjlink_name="TEST", '
|
||||
'manufacturer="IN YOUR DREAMS", model="OpenLP", other="None", sources="None", '
|
||||
'source_list="[]") >',
|
||||
'manufacturer="IN YOUR DREAMS", model="OpenLP", serial_no="None", other="None", '
|
||||
'sources="None", source_list="[]", model_filter="None", model_lamp="None", '
|
||||
'sw_version="None") >',
|
||||
'Projector.__repr__() should have returned a proper representation string')
|
||||
|
||||
def test_projectorsource_repr(self):
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
Package to test the openlp.core.ui.renderer package.
|
||||
"""
|
||||
from unittest import TestCase
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from PyQt5 import QtCore
|
||||
|
||||
|
@ -31,7 +32,6 @@ from openlp.core.lib import Renderer, ScreenList, ServiceItem, FormattingTags
|
|||
from openlp.core.lib.renderer import words_split, get_start_tags
|
||||
from openlp.core.lib.theme import Theme
|
||||
|
||||
from tests.functional import MagicMock, patch
|
||||
|
||||
SCREEN = {
|
||||
'primary': False,
|
||||
|
|
|
@ -23,12 +23,12 @@
|
|||
Package to test the openlp.core.lib.screenlist package.
|
||||
"""
|
||||
from unittest import TestCase
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from PyQt5 import QtCore, QtWidgets
|
||||
|
||||
from openlp.core.common import Registry
|
||||
from openlp.core.lib import ScreenList
|
||||
from tests.functional import MagicMock
|
||||
|
||||
SCREEN = {
|
||||
'primary': False,
|
||||
|
|
|
@ -24,12 +24,12 @@ Package to test the openlp.core.lib package.
|
|||
"""
|
||||
import os
|
||||
from unittest import TestCase
|
||||
|
||||
from tests.functional import MagicMock, patch
|
||||
from tests.utils import assert_length, convert_file_service_item
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
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
|
||||
|
||||
VERSE = 'The Lord said to {r}Noah{/r}: \n'\
|
||||
'There\'s gonna be a {su}floody{/su}, {sb}floody{/sb}\n'\
|
||||
|
@ -38,6 +38,23 @@ VERSE = 'The Lord said to {r}Noah{/r}: \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{/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']
|
||||
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
|
||||
service_item = ServiceItem(None)
|
||||
service_item.add_icon = MagicMock()
|
||||
FormattingTags.load_tags()
|
||||
|
||||
# WHEN: We add a custom from a saved service
|
||||
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
|
||||
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')
|
||||
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')
|
||||
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')
|
||||
|
@ -300,6 +318,7 @@ class TestServiceItem(TestCase):
|
|||
# GIVEN: A new service item and a mocked add icon function
|
||||
service_item = ServiceItem(None)
|
||||
service_item.add_icon = MagicMock()
|
||||
FormattingTags.load_tags()
|
||||
|
||||
# WHEN: We add a custom from a saved service
|
||||
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
|
||||
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')
|
||||
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')
|
||||
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')
|
||||
|
|
|
@ -22,14 +22,15 @@
|
|||
"""
|
||||
Package to test the openlp.core.lib.ui package.
|
||||
"""
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
from unittest import TestCase
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
||||
from openlp.core.common import UiStrings, translate
|
||||
from openlp.core.lib.ui import add_welcome_page, create_button_box, create_horizontal_adjusting_combo_box, \
|
||||
create_button, create_action, create_valign_selection_widgets, find_and_set_in_combo_box, create_widget_action, \
|
||||
set_case_insensitive_completer
|
||||
from tests.functional import MagicMock, patch
|
||||
|
||||
|
||||
class TestUi(TestCase):
|
||||
|
|
|
@ -24,10 +24,10 @@ Package to test the openlp.core.ui.firsttimeform package.
|
|||
"""
|
||||
import datetime
|
||||
from unittest import TestCase
|
||||
from unittest.mock import patch
|
||||
|
||||
from openlp.core.ui.aboutform import AboutForm
|
||||
|
||||
from tests.functional import patch
|
||||
from tests.helpers.testmixin import TestMixin
|
||||
|
||||
|
||||
|
|
|
@ -26,15 +26,13 @@ Package to test the openlp.core.ui.exeptionform package.
|
|||
import os
|
||||
import tempfile
|
||||
from unittest import TestCase
|
||||
from unittest.mock import mock_open
|
||||
from unittest.mock import mock_open, patch
|
||||
|
||||
from openlp.core.common import Registry
|
||||
|
||||
from tests.functional import patch
|
||||
from tests.helpers.testmixin import TestMixin
|
||||
|
||||
from openlp.core.ui import exceptionform
|
||||
|
||||
from tests.helpers.testmixin import TestMixin
|
||||
|
||||
exceptionform.WEBKIT_VERSION = 'Webkit Test'
|
||||
exceptionform.MIGRATE_VERSION = 'Migrate Test'
|
||||
exceptionform.CHARDET_VERSION = 'CHARDET Test'
|
||||
|
|
|
@ -22,17 +22,16 @@
|
|||
"""
|
||||
Package to test the openlp.core.utils.__init__ package.
|
||||
"""
|
||||
|
||||
from unittest import TestCase
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
|
||||
from tests.functional import patch
|
||||
from tests.helpers.testmixin import TestMixin
|
||||
from unittest import TestCase
|
||||
from unittest.mock import patch
|
||||
|
||||
from openlp.core.common.httputils import CONNECTION_RETRIES, get_web_page
|
||||
|
||||
from tests.helpers.testmixin import TestMixin
|
||||
|
||||
|
||||
class TestFirstTimeWizard(TestMixin, TestCase):
|
||||
"""
|
||||
|
|
|
@ -26,11 +26,11 @@ import os
|
|||
import tempfile
|
||||
import urllib
|
||||
from unittest import TestCase
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from openlp.core.common import Registry
|
||||
from openlp.core.ui.firsttimeform import FirstTimeForm
|
||||
|
||||
from tests.functional import MagicMock, patch
|
||||
from tests.helpers.testmixin import TestMixin
|
||||
|
||||
FAKE_CONFIG = b"""
|
||||
|
|
|
@ -23,8 +23,7 @@
|
|||
Package to test the openlp.core.ui.formattingtagsform package.
|
||||
"""
|
||||
from unittest import TestCase
|
||||
|
||||
from tests.functional import MagicMock, patch, call
|
||||
from unittest.mock import MagicMock, patch, call
|
||||
|
||||
from openlp.core.ui.formattingtagform import FormattingTagForm
|
||||
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
Package to test the openlp.core.ui.slidecontroller package.
|
||||
"""
|
||||
from unittest import TestCase, skipUnless
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from PyQt5 import QtCore
|
||||
|
||||
|
@ -33,11 +34,9 @@ from openlp.core.ui.media import MediaController
|
|||
from openlp.core.ui.maindisplay import TRANSPARENT_STYLESHEET, OPAQUE_STYLESHEET
|
||||
|
||||
from tests.helpers.testmixin import TestMixin
|
||||
from tests.functional import MagicMock, patch
|
||||
|
||||
if is_macosx():
|
||||
from ctypes import pythonapi, c_void_p, c_char_p, py_object
|
||||
|
||||
from sip import voidptr
|
||||
from objc import objc_object
|
||||
from AppKit import NSMainMenuWindowLevel, NSWindowCollectionBehaviorManaged
|
||||
|
|
|
@ -23,8 +23,8 @@
|
|||
Package to test openlp.core.ui.mainwindow package.
|
||||
"""
|
||||
import os
|
||||
|
||||
from unittest import TestCase
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from PyQt5 import QtWidgets
|
||||
|
||||
|
@ -32,7 +32,6 @@ from openlp.core.ui.mainwindow import MainWindow
|
|||
from openlp.core.lib.ui import UiStrings
|
||||
from openlp.core.common.registry import Registry
|
||||
|
||||
from tests.functional import MagicMock, patch
|
||||
from tests.helpers.testmixin import TestMixin
|
||||
from tests.utils.constants import TEST_RESOURCES_PATH
|
||||
|
||||
|
|
|
@ -22,12 +22,13 @@
|
|||
"""
|
||||
Package to test the openlp.core.ui package.
|
||||
"""
|
||||
from PyQt5 import QtCore
|
||||
from unittest import TestCase
|
||||
from unittest.mock import patch
|
||||
|
||||
from PyQt5 import QtCore
|
||||
|
||||
from openlp.core.ui.media import get_media_players, parse_optical_path
|
||||
|
||||
from tests.functional import MagicMock, patch
|
||||
from tests.helpers.testmixin import TestMixin
|
||||
|
||||
|
||||
|
|
|
@ -24,15 +24,14 @@ Package to test the openlp.core.ui.slidecontroller package.
|
|||
"""
|
||||
import os
|
||||
from unittest import TestCase
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import PyQt5
|
||||
|
||||
from openlp.core.common import Registry, ThemeLevel
|
||||
from openlp.core.ui.lib.toolbar import OpenLPToolbar
|
||||
from openlp.core.lib import ServiceItem, ServiceItemType, ItemCapabilities
|
||||
from openlp.core.ui import ServiceManager
|
||||
|
||||
from tests.functional import MagicMock, patch
|
||||
from openlp.core.ui.lib.toolbar import OpenLPToolbar
|
||||
|
||||
|
||||
class TestServiceManager(TestCase):
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue