This commit is contained in:
Tim Bentley 2017-05-23 06:00:42 +01:00
commit 6fe9dfeb04
196 changed files with 4600 additions and 1393 deletions

View File

@ -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

View File

@ -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))

View File

@ -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))

View File

@ -252,4 +252,5 @@ def url_get_file(callback, url, f_path, sha256=None):
os.remove(f_path)
return True
__all__ = ['get_web_page']

View File

@ -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

View File

@ -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 += '&nbsp;'
else:
if not remainder:
for c in range(math.floor((chordlen - taillen) / 2)):
whitespaces += '_'
else:
for c in range(chordlen - taillen + 1):
whitespaces += '&nbsp;'
else:
if not tail and remainder and remainder[0] == ' ':
for c in range(chordlen):
whitespaces += '&nbsp;'
if whitespaces:
if '_' in whitespaces:
ws_length = len(whitespaces)
if ws_length == 1:
whitespaces = '&ndash;'
else:
wsl_mod = ws_length // 2
ws_right = ws_left = ' ' * wsl_mod
whitespaces = ws_left + '&ndash;' + 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 == '&nbsp;':
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 = '&nbsp;'
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 = '&nbsp;'
if lyric == '':
lyric = '&nbsp;'
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}&nbsp;{endtags}</td>'.format(
starttags=start_formatting_tags, lyrics=lyrics[i], endtags=end_formatting_tags)
else:
spacing = ''
if spacer > 0:
space = '&nbsp;' * 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">&nbsp;</td></tr><tr><td class="lyrics">' \
'{starttags}{lyrics}&nbsp;{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

View File

@ -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

View File

@ -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)

View File

@ -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:

View File

@ -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])

View File

@ -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):

View File

@ -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

View File

@ -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):

View File

@ -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('&amp;nbsp;', '&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:

View File

@ -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):
"""

View File

@ -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)

View File

@ -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.

View File

@ -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']

View File

@ -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.'))

205
openlp/core/ui/lib/pathedit.py Executable file
View File

@ -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)

View File

@ -143,6 +143,7 @@ def format_milliseconds(milliseconds):
seconds=seconds,
millis=millis)
from .mediacontroller import MediaController
from .playertab import PlayerTab

View File

@ -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))

View File

@ -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([

View File

@ -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')

View File

@ -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

View File

@ -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):
"""

View File

@ -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

View File

@ -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)

View File

@ -429,4 +429,5 @@ class BibleManager(OpenLPMixin, RegistryProperties):
for bible in self.db_cache:
self.db_cache[bible].finalise()
__all__ = ['BibleFormat']

View File

@ -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__)

View File

@ -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

View File

@ -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__)

View File

@ -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__)

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -0,0 +1,46 @@
<!DOCTYPE html>
<html>
<!--
###############################################################################
# OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- #
# Copyright (c) 2008-2017 OpenLP Developers #
# --------------------------------------------------------------------------- #
# This program is free software; you can redistribute it and/or modify it #
# under the terms of the GNU General Public License as published by the Free #
# Software Foundation; version 2 of the License. #
# #
# This program is distributed in the hope that it will be useful, but WITHOUT #
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
# more details. #
# #
# You should have received a copy of the GNU General Public License along #
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
-->
<head>
<meta charset="utf-8" />
<title>${chords_title}</title>
<link rel="stylesheet" href="/css/stage.css" />
<link rel="stylesheet" href="/css/chords.css" />
<link rel="shortcut icon" type="image/x-icon" href="/images/favicon.ico">
<script type="text/javascript" src="/assets/jquery.min.js"></script>
<script type="text/javascript" src="/js/chords.js"></script>
</head>
<body>
<input type="hidden" id="next-text" value="${next}" />
<div id="right">
<div id="clock"></div>
<div id="chords" class="button">Toggle Chords</div>
<div id="notes"></div>
</div>
<div id="header">
<div id="verseorder"></div>
<div id="transpose">Transpose:</div> <div class="button" id="transposedown">-</div> <div id="transposevalue">0</div> <div class="button" id="transposeup">+</div> <div id="capodisplay">(Capo)</div>
</div>
<div id="currentslide"></div>
<div id="nextslide"></div>
</body>
</html>

View File

@ -0,0 +1,96 @@
/******************************************************************************
* OpenLP - Open Source Lyrics Projection *
* --------------------------------------------------------------------------- *
* Copyright (c) 2008-2017 OpenLP Developers *
* --------------------------------------------------------------------------- *
* This program is free software; you can redistribute it and/or modify it *
* under the terms of the GNU General Public License as published by the Free *
* Software Foundation; version 2 of the License. *
* *
* This program is distributed in the hope that it will be useful, but WITHOUT *
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or *
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for *
* more details. *
* *
* You should have received a copy of the GNU General Public License along *
* with this program; if not, write to the Free Software Foundation, Inc., 59 *
* Temple Place, Suite 330, Boston, MA 02111-1307 USA *
******************************************************************************/
#header {
padding-bottom: 1em;
}
#transpose,
#transposevalue,
#capodisplay {
display: inline-block;
font-size: 30pt;
color: gray;
vertical-align: middle;
}
.button {
display: inline-block;
box-sizing: border-box;
border: 1px solid gray;
border-radius: .3em;
padding: 0 .2em;
min-width: 1.2em;
line-height: 1.2em;
font-size: 25pt;
font-weight: bold;
text-align: center;
text-decoration: none;
text-shadow: 0px 1px 0px white;
color: black;
background: linear-gradient(to bottom, white 5%, gray 100%);
background-color: gray;
cursor: pointer;
}
.button:hover {
background: linear-gradient(to bottom, white 10%, gray 150%);
color: darkslategray ;
background-color: gray;
}
.button:active {
position:relative;
top:1px;
}
/* Extending existing definition in stage.css */
#verseorder {
line-height: 1.5;
display: inline-block;
vertical-align: middle;
}
.chordline {
line-height: 2.0;
}
.chordline1 {
line-height: 1.0
}
.chordline span.chord span {
position: relative;
}
.chordline span.chord span strong {
position: absolute;
top: -0.8em;
left: 0;
font-size: 30pt;
font-weight: normal;
line-height: normal;
color: yellow;
}
.ws {
white-space: pre-wrap;
}
#nextslide .chordline span.chord span strong {
color: gray;
}

View File

@ -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 {

View File

@ -0,0 +1,331 @@
/******************************************************************************
* OpenLP - Open Source Lyrics Projection *
* --------------------------------------------------------------------------- *
* Copyright (c) 2008-2017 OpenLP Developers *
* --------------------------------------------------------------------------- *
* This program is free software; you can redistribute it and/or modify it *
* under the terms of the GNU General Public License as published by the Free *
* Software Foundation; version 2 of the License. *
* *
* This program is distributed in the hope that it will be useful, but WITHOUT *
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or *
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for *
* more details. *
* *
* You should have received a copy of the GNU General Public License along *
* with this program; if not, write to the Free Software Foundation, Inc., 59 *
* Temple Place, Suite 330, Boston, MA 02111-1307 USA *
******************************************************************************/
var lastChord;
var notesSharpNotation = {}
var notesFlatNotation = {}
// See https://en.wikipedia.org/wiki/Musical_note#12-tone_chromatic_scale
notesSharpNotation['german'] = ['C','C#','D','D#','E','F','F#','G','G#','A','A#','H'];
notesFlatNotation['german'] = ['C','Db','D','Eb','Fb','F','Gb','G','Ab','A','B','H'];
notesSharpNotation['english'] = ['C','C#','D','D#','E','F','F#','G','G#','A','A#','B'];
notesFlatNotation['english'] = ['C','Db','D','Eb','Fb','F','Gb','G','Ab','A','Bb','B'];
notesSharpNotation['neo-latin'] = ['Do','Do#','Re','Re#','Mi','Fa','Fa#','Sol','Sol#','La','La#','Si'];
notesFlatNotation['neo-latin'] = ['Do','Reb','Re','Mib','Fab','Fa','Solb','Sol','Lab','La','Sib','Si'];
function getTransposeValue(songId) {
if (localStorage.getItem(songId + '_transposeValue')) {return localStorage.getItem(songId + '_transposeValue');}
else {return 0;}
}
function storeTransposeValue(songId,transposeValueToSet) {
localStorage.setItem(songId + '_transposeValue', transposeValueToSet);
}
// NOTE: This function has a python equivalent in openlp/plugins/songs/lib/__init__.py - make sure to update both!
function transposeChord(chord, transposeValue, notation) {
var chordSplit = chord.replace('♭', 'b').split(/[\/]/);
var transposedChord = '', note, notenumber, rest, currentChord;
var notesSharp = notesSharpNotation[notation];
var notesFlat = notesFlatNotation[notation];
var notesPreferred = ['b','#','#','#','#','#','#','#','#','#','#','#'];
for (i = 0; i <= chordSplit.length - 1; i++) {
if (i > 0) {
transposedChord += '/';
}
currentchord = chordSplit[i];
if (currentchord.length > 0 && currentchord.charAt(0) === '(') {
transposedChord += '(';
if (currentchord.length > 1) {
currentchord = currentchord.substr(1);
} else {
currentchord = "";
}
}
if (currentchord.length > 0) {
if (currentchord.length > 1) {
if ('#b'.indexOf(currentchord.charAt(1)) === -1) {
note = currentchord.substr(0, 1);
rest = currentchord.substr(1);
} else {
note = currentchord.substr(0, 2);
rest = currentchord.substr(2);
}
} else {
note = currentchord;
rest = "";
}
notenumber = (notesSharp.indexOf(note) === -1 ? notesFlat.indexOf(note) : notesSharp.indexOf(note));
notenumber += parseInt(transposeValue);
while (notenumber > 11) {
notenumber -= 12;
}
while (notenumber < 0) {
notenumber += 12;
}
if (i === 0) {
currentChord = notesPreferred[notenumber] === '#' ? notesSharp[notenumber] : notesFlat[notenumber];
lastChord = currentChord;
} else {
currentChord = notesSharp.indexOf(lastChord) === -1 ? notesFlat[notenumber] : notesSharp[notenumber];
}
if (!(notesFlat.indexOf(note) === -1 && notesSharp.indexOf(note) === -1)) {
transposedChord += currentChord + rest;
} else {
transposedChord += note + rest;
}
}
}
return transposedChord;
}
var OpenLPChordOverflowFillCount = 0;
window.OpenLP = {
showchords:true,
loadService: function (event) {
$.getJSON(
"/api/service/list",
function (data, status) {
OpenLP.nextSong = "";
$("#notes").html("");
for (idx in data.results.items) {
idx = parseInt(idx, 10);
if (data.results.items[idx]["selected"]) {
$("#notes").html(data.results.items[idx]["notes"].replace(/\n/g, "<br />"));
if (data.results.items.length > idx + 1) {
OpenLP.nextSong = data.results.items[idx + 1]["title"];
}
break;
}
}
OpenLP.updateSlide();
}
);
},
loadSlides: function (event) {
$.getJSON(
"/api/controller/live/text",
function (data, status) {
OpenLP.currentSlides = data.results.slides;
$('#transposevalue').text(getTransposeValue(OpenLP.currentSlides[0].text.split("\n")[0]));
OpenLP.currentSlide = 0;
OpenLP.currentTags = Array();
var div = $("#verseorder");
div.html("");
var tag = "";
var tags = 0;
var lastChange = 0;
$.each(data.results.slides, function(idx, slide) {
var prevtag = tag;
tag = slide["tag"];
if (tag != prevtag) {
// If the tag has changed, add new one to the list
lastChange = idx;
tags = tags + 1;
div.append("&nbsp;<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("&nbsp;<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 += '&nbsp;';}
}
} 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 += '&nbsp;';}
}
};
} else {
if (!$tail && $remainder.charAt(0) == ' ') {for (c = 0; c < $chordlen; c++) {w += '&nbsp;';}}
}
if (w!='') {
if (w[0] == '_') {
ws_length = w.length;
if (ws_length==1) {
w = '&ndash;';
} else {
wsl_mod = Math.floor(ws_length / 2);
ws_right = ws_left = new Array(wsl_mod + 1).join(' ');
w = ws_left + '&ndash;' + 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();
});
});

View File

@ -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

View File

@ -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',

View File

@ -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'))

View File

@ -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()

View File

@ -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.

View File

@ -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

View File

@ -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,

View File

@ -0,0 +1,178 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
###############################################################################
# OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- #
# Copyright (c) 2008-2016 OpenLP Developers #
# --------------------------------------------------------------------------- #
# This program is free software; you can redistribute it and/or modify it #
# under the terms of the GNU General Public License as published by the Free #
# Software Foundation; version 2 of the License. #
# #
# This program is distributed in the hope that it will be useful, but WITHOUT #
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
# more details. #
# #
# You should have received a copy of the GNU General Public License along #
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
"""
The :mod:`chordpro` module provides the functionality for importing
ChordPro files into the current database.
"""
import logging
import re
from openlp.core.common import Settings
from .songimport import SongImport
log = logging.getLogger(__name__)
class ChordProImport(SongImport):
"""
The :class:`ChordProImport` class provides OpenLP with the
ability to import ChordPro files.
This importer is based on the information available on these webpages:
http://webchord.sourceforge.net/tech.html
http://www.vromans.org/johan/projects/Chordii/chordpro/
http://www.tenbyten.com/software/songsgen/help/HtmlHelp/files_reference.htm
http://linkesoft.com/songbook/chordproformat.html
"""
def do_import(self):
self.import_wizard.progress_bar.setMaximum(len(self.import_source))
for filename in self.import_source:
if self.stop_import_flag:
return
song_file = open(filename, 'rt')
self.do_import_file(song_file)
song_file.close()
def do_import_file(self, song_file):
"""
Imports the songs in the given file
:param song_file: The file object to be imported from.
"""
self.set_defaults()
# Loop over the lines of the file
file_content = song_file.read()
current_verse = ''
current_verse_type = 'v'
skip_block = False
for line in file_content.splitlines():
line = line.rstrip()
# Detect tags
if line.startswith('{'):
tag_name, tag_value = self.parse_tag(line)
# Detect which tag
if tag_name in ['title', 't']:
self.title = tag_value
elif tag_name in ['subtitle', 'su', 'st']:
self.alternate_title = tag_value
elif tag_name in ['comment', 'c', 'comment_italic', 'ci', 'comment_box', 'cb']:
# Detect if the comment is used as a chorus repeat marker
if tag_value.lower().startswith('chorus'):
if current_verse.strip():
# Add collected verse to the lyrics
# Strip out chords if set up to
if not Settings().value('songs/enable chords') or Settings().value(
'songs/disable chords import'):
current_verse = re.sub(r'\[.*?\]', '', current_verse)
self.add_verse(current_verse.rstrip(), current_verse_type)
current_verse_type = 'v'
current_verse = ''
self.repeat_verse('c1')
else:
self.add_comment(tag_value)
elif tag_name in ['start_of_chorus', 'soc']:
current_verse_type = 'c'
elif tag_name in ['end_of_chorus', 'eoc']:
# Add collected chorus to the lyrics
# Strip out chords if set up to
if not Settings().value('songs/enable chords') or Settings().value('songs/disable chords import'):
current_verse = re.sub(r'\[.*?\]', '', current_verse)
self.add_verse(current_verse.rstrip(), current_verse_type)
current_verse_type = 'v'
current_verse = ''
elif tag_name in ['start_of_tab', 'sot']:
if current_verse.strip():
# Add collected verse to the lyrics
# Strip out chords if set up to
if not Settings().value('songs/enable chords') or Settings().value(
'songs/disable chords import'):
current_verse = re.sub(r'\[.*?\]', '', current_verse)
self.add_verse(current_verse.rstrip(), current_verse_type)
current_verse_type = 'v'
current_verse = ''
skip_block = True
elif tag_name in ['end_of_tab', 'eot']:
skip_block = False
elif tag_name in ['new_song', 'ns']:
# A new song starts below this tag
if self.verses and self.title:
if current_verse.strip():
# Strip out chords if set up to
if not Settings().value('songs/enable chords') or Settings().value(
'songs/disable chords import'):
current_verse = re.sub(r'\[.*?\]', '', current_verse)
self.add_verse(current_verse.rstrip(), current_verse_type)
if not self.finish():
self.log_error(song_file.name)
self.set_defaults()
current_verse_type = 'v'
current_verse = ''
else:
# Unsupported tag
log.debug('unsupported tag: %s' % line)
elif line.startswith('#'):
# Found a comment line, which is ignored...
continue
elif line == "['|]":
# Found a vertical bar
continue
else:
if skip_block:
continue
elif line == '' and current_verse.strip() and current_verse_type != 'c':
# Add collected verse to the lyrics
# Strip out chords if set up to
if not Settings().value('songs/enable chords') or Settings().value('songs/disable chords import'):
current_verse = re.sub(r'\[.*?\]', '', current_verse)
self.add_verse(current_verse.rstrip(), current_verse_type)
current_verse_type = 'v'
current_verse = ''
else:
if current_verse.strip() == '':
current_verse = line + '\n'
else:
current_verse += line + '\n'
if current_verse.strip():
# Strip out chords if set up to
if not Settings().value('songs/enable chords') or Settings().value(
'songs/disable chords import'):
current_verse = re.sub(r'\[.*?\]', '', current_verse)
self.add_verse(current_verse.rstrip(), current_verse_type)
if not self.finish():
self.log_error(song_file.name)
def parse_tag(self, line):
"""
:param line: Line with the tag to be parsed
:return: A tuple with tag name and tag value (if any)
"""
# Strip the first '}'
line = line[1:].strip()
colon_idx = line.find(':')
# check if this is a tag without value
if colon_idx < 0:
# strip the final '}' and return the tag name
return line[:-1], None
tag_name = line[:colon_idx]
tag_value = line[colon_idx + 1:-1].strip()
return tag_name, tag_value

View File

@ -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

View File

@ -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:

View File

@ -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('_', '')

View File

@ -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)

View File

@ -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):

View File

@ -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:

View File

@ -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

View File

@ -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')

View File

@ -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,
}

View File

@ -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):
"""

View File

@ -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'),

View File

@ -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)

View File

@ -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

View File

@ -250,5 +250,6 @@ def main():
print_qt_image_formats()
print_enchant_backends_and_languages()
if __name__ == '__main__':
main()

View File

@ -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()

View File

@ -1,4 +1,4 @@
[pep8]
exclude=resources.py,vlc.py
max-line-length = 120
ignore = E402
ignore = E402,E722

View File

@ -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']

View File

@ -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

View File

@ -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):
"""

View File

@ -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']

View File

@ -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):
"""

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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'))

View File

@ -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):
"""

View File

@ -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

View File

@ -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

View File

@ -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):

View File

@ -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):

View File

@ -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}',

View File

@ -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')

View File

@ -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'))

View File

@ -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">' \
'&nbsp;&nbsp;</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>&#x27;M NOT MOVED BY WHAT I SEE HALLE<span class="chord"><span><strong>F</strong>' \
'</span></span>LUJA<span class="chord"><span><strong>C</strong></span></span>H</span>'
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">&nbsp;</td><td class="chord">D</td></tr><tr><td class="lyrics">{st}{/st}' \
'</td><td class="lyrics">{st}Amazing&nbsp;{/st}</td></tr></table><table class="segment" ' \
'cellpadding="0" cellspacing="0" border="0" align="left"><tr class="chordrow">' \
'<td class="chord">&nbsp;</td><td class="chord">D7</td></tr><tr><td class="lyrics">{st}{r}gr' \
'{/r}{/st}</td><td class="lyrics">{r}{st}ace{/r}&nbsp;{/st}</td></tr></table><table ' \
'class="segment" cellpadding="0" cellspacing="0" border="0" align="left"><tr class="chordrow">'\
'<td class="chord">&nbsp;</td></tr><tr><td class="lyrics">{st}&nbsp;{/st}</td></tr></table>' \
'<table class="segment" cellpadding="0" cellspacing="0" border="0" align="left"><tr ' \
'class="chordrow"><td class="chord">&nbsp;</td></tr><tr><td class="lyrics">{st}how&nbsp;{/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&nbsp;{/st}</td></tr></table><table class="segment" cellpadding="0" cellspacing="0" ' \
'border="0" align="left"><tr class="chordrow"><td class="chord">&nbsp;</td></tr><tr><td ' \
'class="lyrics">{st}the&nbsp;{/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&nbsp;{/st}</td></tr></table><table class="segment" ' \
'cellpadding="0" cellspacing="0" border="0" align="left"><tr class="chordrow"><td ' \
'class="chord">&nbsp;</td></tr><tr><td class="lyrics">{st}&nbsp;{/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}&nbsp;</td>' \
'</tr></table></td></tr></table>'
self.assertEqual(expected_html, text_with_expanded_chords, 'The expanded chords should look as expected!')

View File

@ -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

View File

@ -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):

View File

@ -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')

View File

@ -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')

View File

@ -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):

View File

@ -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,

View File

@ -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,

View File

@ -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&#x27;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&#x27;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')

View File

@ -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):

View File

@ -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

View File

@ -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'

View File

@ -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):
"""

View File

@ -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"""

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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