forked from openlp/openlp
head
This commit is contained in:
commit
78f38780ce
|
@ -246,7 +246,7 @@ class OpenLP(OpenLPMixin, QtWidgets.QApplication):
|
||||||
Settings().setValue('core/application version', openlp_version)
|
Settings().setValue('core/application version', openlp_version)
|
||||||
# If data_version is different from the current version ask if we should backup the data folder
|
# If data_version is different from the current version ask if we should backup the data folder
|
||||||
elif data_version != openlp_version:
|
elif data_version != openlp_version:
|
||||||
if self.splash.isVisible():
|
if can_show_splash and self.splash.isVisible():
|
||||||
self.splash.hide()
|
self.splash.hide()
|
||||||
if QtWidgets.QMessageBox.question(None, translate('OpenLP', 'Backup'),
|
if QtWidgets.QMessageBox.question(None, translate('OpenLP', 'Backup'),
|
||||||
translate('OpenLP', 'OpenLP has been upgraded, do you want to create\n'
|
translate('OpenLP', 'OpenLP has been upgraded, do you want to create\n'
|
||||||
|
|
|
@ -24,7 +24,7 @@ The :mod:`common` module contains most of the components and libraries that make
|
||||||
OpenLP work.
|
OpenLP work.
|
||||||
"""
|
"""
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import importlib
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
@ -32,6 +32,7 @@ import sys
|
||||||
import traceback
|
import traceback
|
||||||
from chardet.universaldetector import UniversalDetector
|
from chardet.universaldetector import UniversalDetector
|
||||||
from ipaddress import IPv4Address, IPv6Address, AddressValueError
|
from ipaddress import IPv4Address, IPv6Address, AddressValueError
|
||||||
|
from pathlib import Path
|
||||||
from shutil import which
|
from shutil import which
|
||||||
from subprocess import check_output, CalledProcessError, STDOUT
|
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')
|
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):
|
def get_frozen_path(frozen_option, non_frozen_option):
|
||||||
"""
|
"""
|
||||||
Return a path based on the system status.
|
Return a path based on the system status.
|
||||||
|
|
|
@ -252,4 +252,5 @@ def url_get_file(callback, url, f_path, sha256=None):
|
||||||
os.remove(f_path)
|
os.remove(f_path)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['get_web_page']
|
__all__ = ['get_web_page']
|
||||||
|
|
|
@ -23,10 +23,11 @@
|
||||||
The :mod:`lib` module contains most of the components and libraries that make
|
The :mod:`lib` module contains most of the components and libraries that make
|
||||||
OpenLP work.
|
OpenLP work.
|
||||||
"""
|
"""
|
||||||
|
import html
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from distutils.version import LooseVersion
|
import re
|
||||||
|
import math
|
||||||
|
|
||||||
from PyQt5 import QtCore, QtGui, Qt, QtWidgets
|
from PyQt5 import QtCore, QtGui, Qt, QtWidgets
|
||||||
|
|
||||||
|
@ -34,6 +35,8 @@ from openlp.core.common import translate
|
||||||
|
|
||||||
log = logging.getLogger(__name__ + '.__init__')
|
log = logging.getLogger(__name__ + '.__init__')
|
||||||
|
|
||||||
|
SLIMCHARS = 'fiíIÍjlĺľrtť.,;/ ()|"\'!:\\'
|
||||||
|
|
||||||
|
|
||||||
class ServiceItemContext(object):
|
class ServiceItemContext(object):
|
||||||
"""
|
"""
|
||||||
|
@ -281,11 +284,12 @@ def check_item_selected(list_widget, message):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def clean_tags(text):
|
def clean_tags(text, remove_chords=False):
|
||||||
"""
|
"""
|
||||||
Remove Tags from text for display
|
Remove Tags from text for display
|
||||||
|
|
||||||
:param text: Text to be cleaned
|
:param text: Text to be cleaned
|
||||||
|
:param remove_chords: Clean ChordPro tags
|
||||||
"""
|
"""
|
||||||
text = text.replace('<br>', '\n')
|
text = text.replace('<br>', '\n')
|
||||||
text = text.replace('{br}', '\n')
|
text = text.replace('{br}', '\n')
|
||||||
|
@ -293,21 +297,296 @@ def clean_tags(text):
|
||||||
for tag in FormattingTags.get_html_tags():
|
for tag in FormattingTags.get_html_tags():
|
||||||
text = text.replace(tag['start tag'], '')
|
text = text.replace(tag['start tag'], '')
|
||||||
text = text.replace(tag['end tag'], '')
|
text = text.replace(tag['end tag'], '')
|
||||||
|
# Remove ChordPro tags
|
||||||
|
if remove_chords:
|
||||||
|
text = re.sub(r'\[.+?\]', r'', text)
|
||||||
return text
|
return text
|
||||||
|
|
||||||
|
|
||||||
def expand_tags(text):
|
def expand_tags(text, expand_chord_tags=False, for_printing=False):
|
||||||
"""
|
"""
|
||||||
Expand tags HTML for display
|
Expand tags HTML for display
|
||||||
|
|
||||||
:param text: The text to be expanded.
|
:param text: The text to be expanded.
|
||||||
"""
|
"""
|
||||||
|
if expand_chord_tags:
|
||||||
|
if for_printing:
|
||||||
|
text = expand_chords_for_printing(text, '{br}')
|
||||||
|
else:
|
||||||
|
text = expand_chords(text)
|
||||||
for tag in FormattingTags.get_html_tags():
|
for tag in FormattingTags.get_html_tags():
|
||||||
text = text.replace(tag['start tag'], tag['start html'])
|
text = text.replace(tag['start tag'], tag['start html'])
|
||||||
text = text.replace(tag['end tag'], tag['end html'])
|
text = text.replace(tag['end tag'], tag['end html'])
|
||||||
return text
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def expand_and_align_chords_in_line(match):
|
||||||
|
"""
|
||||||
|
Expand the chords in the line and align them using whitespaces.
|
||||||
|
NOTE: There is equivalent javascript code in chords.js, in the updateSlide function. Make sure to update both!
|
||||||
|
|
||||||
|
:param match:
|
||||||
|
:return: The line with expanded html-chords
|
||||||
|
"""
|
||||||
|
whitespaces = ''
|
||||||
|
chordlen = 0
|
||||||
|
taillen = 0
|
||||||
|
# The match could be "[G]sweet the " from a line like "A[D]mazing [D7]grace! How [G]sweet the [D]sound!"
|
||||||
|
# The actual chord, would be "G" in match "[G]sweet the "
|
||||||
|
chord = match.group(1)
|
||||||
|
# The tailing word of the chord, would be "sweet" in match "[G]sweet the "
|
||||||
|
tail = match.group(2)
|
||||||
|
# The remainder of the line, until line end or next chord. Would be " the " in match "[G]sweet the "
|
||||||
|
remainder = match.group(3)
|
||||||
|
# Line end if found, else None
|
||||||
|
end = match.group(4)
|
||||||
|
# Based on char width calculate width of chord
|
||||||
|
for chord_char in chord:
|
||||||
|
if chord_char not in SLIMCHARS:
|
||||||
|
chordlen += 2
|
||||||
|
else:
|
||||||
|
chordlen += 1
|
||||||
|
# Based on char width calculate width of tail
|
||||||
|
for tail_char in tail:
|
||||||
|
if tail_char not in SLIMCHARS:
|
||||||
|
taillen += 2
|
||||||
|
else:
|
||||||
|
taillen += 1
|
||||||
|
# Based on char width calculate width of remainder
|
||||||
|
for remainder_char in remainder:
|
||||||
|
if remainder_char not in SLIMCHARS:
|
||||||
|
taillen += 2
|
||||||
|
else:
|
||||||
|
taillen += 1
|
||||||
|
# If the chord is wider than the tail+remainder and the line goes on, some padding is needed
|
||||||
|
if chordlen >= taillen and end is None:
|
||||||
|
# Decide if the padding should be "_" for drawing out words or spaces
|
||||||
|
if tail:
|
||||||
|
if not remainder:
|
||||||
|
for c in range(math.ceil((chordlen - taillen) / 2) + 2):
|
||||||
|
whitespaces += '_'
|
||||||
|
else:
|
||||||
|
for c in range(chordlen - taillen + 1):
|
||||||
|
whitespaces += ' '
|
||||||
|
else:
|
||||||
|
if not remainder:
|
||||||
|
for c in range(math.floor((chordlen - taillen) / 2)):
|
||||||
|
whitespaces += '_'
|
||||||
|
else:
|
||||||
|
for c in range(chordlen - taillen + 1):
|
||||||
|
whitespaces += ' '
|
||||||
|
else:
|
||||||
|
if not tail and remainder and remainder[0] == ' ':
|
||||||
|
for c in range(chordlen):
|
||||||
|
whitespaces += ' '
|
||||||
|
if whitespaces:
|
||||||
|
if '_' in whitespaces:
|
||||||
|
ws_length = len(whitespaces)
|
||||||
|
if ws_length == 1:
|
||||||
|
whitespaces = '–'
|
||||||
|
else:
|
||||||
|
wsl_mod = ws_length // 2
|
||||||
|
ws_right = ws_left = ' ' * wsl_mod
|
||||||
|
whitespaces = ws_left + '–' + ws_right
|
||||||
|
whitespaces = '<span class="ws">' + whitespaces + '</span>'
|
||||||
|
return '<span class="chord"><span><strong>' + html.escape(chord) + '</strong></span></span>' + html.escape(tail) + \
|
||||||
|
whitespaces + html.escape(remainder)
|
||||||
|
|
||||||
|
|
||||||
|
def expand_chords(text):
|
||||||
|
"""
|
||||||
|
Expand ChordPro tags
|
||||||
|
|
||||||
|
:param text:
|
||||||
|
"""
|
||||||
|
text_lines = text.split('{br}')
|
||||||
|
expanded_text_lines = []
|
||||||
|
chords_on_prev_line = False
|
||||||
|
for line in text_lines:
|
||||||
|
# If a ChordPro is detected in the line, replace it with a html-span tag and wrap the line in a span tag.
|
||||||
|
if '[' in line and ']' in line:
|
||||||
|
if chords_on_prev_line:
|
||||||
|
new_line = '<span class="chordline">'
|
||||||
|
else:
|
||||||
|
new_line = '<span class="chordline firstchordline">'
|
||||||
|
chords_on_prev_line = True
|
||||||
|
# Matches a chord, a tail, a remainder and a line end. See expand_and_align_chords_in_line() for more info.
|
||||||
|
new_line += re.sub(r'\[(.*?)\]([\u0080-\uFFFF,\w]*)'
|
||||||
|
'([\u0080-\uFFFF,\w,\s,\.,\,,\!,\?,\;,\:,\|,\",\',\-,\_]*)(\Z)?',
|
||||||
|
expand_and_align_chords_in_line, line)
|
||||||
|
new_line += '</span>'
|
||||||
|
expanded_text_lines.append(new_line)
|
||||||
|
else:
|
||||||
|
chords_on_prev_line = False
|
||||||
|
expanded_text_lines.append(html.escape(line))
|
||||||
|
return '{br}'.join(expanded_text_lines)
|
||||||
|
|
||||||
|
|
||||||
|
def compare_chord_lyric(chord, lyric):
|
||||||
|
"""
|
||||||
|
Compare the width of chord and lyrics. If chord width is greater than the lyric width the diff is returned.
|
||||||
|
|
||||||
|
:param chord:
|
||||||
|
:param lyric:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
chordlen = 0
|
||||||
|
if chord == ' ':
|
||||||
|
return 0
|
||||||
|
chord = re.sub(r'\{.*?\}', r'', chord)
|
||||||
|
lyric = re.sub(r'\{.*?\}', r'', lyric)
|
||||||
|
for chord_char in chord:
|
||||||
|
if chord_char not in SLIMCHARS:
|
||||||
|
chordlen += 2
|
||||||
|
else:
|
||||||
|
chordlen += 1
|
||||||
|
lyriclen = 0
|
||||||
|
for lyric_char in lyric:
|
||||||
|
if lyric_char not in SLIMCHARS:
|
||||||
|
lyriclen += 2
|
||||||
|
else:
|
||||||
|
lyriclen += 1
|
||||||
|
if chordlen > lyriclen:
|
||||||
|
return chordlen - lyriclen
|
||||||
|
else:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def find_formatting_tags(text, active_formatting_tags):
|
||||||
|
"""
|
||||||
|
Look for formatting tags in lyrics and adds/removes them to/from the given list. Returns the update list.
|
||||||
|
|
||||||
|
:param text:
|
||||||
|
:param active_formatting_tags:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if not re.search(r'\{.*?\}', text):
|
||||||
|
return active_formatting_tags
|
||||||
|
word_it = iter(text)
|
||||||
|
# Loop through lyrics to find any formatting tags
|
||||||
|
for char in word_it:
|
||||||
|
if char == '{':
|
||||||
|
tag = ''
|
||||||
|
char = next(word_it)
|
||||||
|
start_tag = True
|
||||||
|
if char == '/':
|
||||||
|
start_tag = False
|
||||||
|
char = next(word_it)
|
||||||
|
while char != '}':
|
||||||
|
tag += char
|
||||||
|
char = next(word_it)
|
||||||
|
# See if the found tag has an end tag
|
||||||
|
for formatting_tag in FormattingTags.get_html_tags():
|
||||||
|
if formatting_tag['start tag'] == '{' + tag + '}':
|
||||||
|
if formatting_tag['end tag']:
|
||||||
|
if start_tag:
|
||||||
|
# prepend the new tag to the list of active formatting tags
|
||||||
|
active_formatting_tags[:0] = [tag]
|
||||||
|
else:
|
||||||
|
# remove the tag from the list
|
||||||
|
active_formatting_tags.remove(tag)
|
||||||
|
# Break out of the loop matching the found tag against the tag list.
|
||||||
|
break
|
||||||
|
return active_formatting_tags
|
||||||
|
|
||||||
|
|
||||||
|
def expand_chords_for_printing(text, line_split):
|
||||||
|
"""
|
||||||
|
Expand ChordPro tags
|
||||||
|
|
||||||
|
:param text:
|
||||||
|
:param line_split:
|
||||||
|
"""
|
||||||
|
if not re.search(r'\[.*?\]', text):
|
||||||
|
return text
|
||||||
|
text_lines = text.split(line_split)
|
||||||
|
expanded_text_lines = []
|
||||||
|
for line in text_lines:
|
||||||
|
# If a ChordPro is detected in the line, build html tables.
|
||||||
|
new_line = '<table class="line" width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td>'
|
||||||
|
active_formatting_tags = []
|
||||||
|
if re.search(r'\[.*?\]', line):
|
||||||
|
words = line.split(' ')
|
||||||
|
in_chord = False
|
||||||
|
for word in words:
|
||||||
|
chords = []
|
||||||
|
lyrics = []
|
||||||
|
new_line += '<table class="segment" cellpadding="0" cellspacing="0" border="0" align="left">'
|
||||||
|
# If the word contains a chord, we need to handle it.
|
||||||
|
if re.search(r'\[.*?\]', word):
|
||||||
|
chord = ''
|
||||||
|
lyric = ''
|
||||||
|
# Loop over each character of the word
|
||||||
|
for char in word:
|
||||||
|
if char == '[':
|
||||||
|
in_chord = True
|
||||||
|
if lyric != '':
|
||||||
|
if chord == '':
|
||||||
|
chord = ' '
|
||||||
|
chords.append(chord)
|
||||||
|
lyrics.append(lyric)
|
||||||
|
chord = ''
|
||||||
|
lyric = ''
|
||||||
|
elif char == ']' and in_chord:
|
||||||
|
in_chord = False
|
||||||
|
elif in_chord:
|
||||||
|
chord += char
|
||||||
|
else:
|
||||||
|
lyric += char
|
||||||
|
if lyric != '' or chord != '':
|
||||||
|
if chord == '':
|
||||||
|
chord = ' '
|
||||||
|
if lyric == '':
|
||||||
|
lyric = ' '
|
||||||
|
chords.append(chord)
|
||||||
|
lyrics.append(lyric)
|
||||||
|
new_chord_line = '<tr class="chordrow">'
|
||||||
|
new_lyric_line = '</tr><tr>'
|
||||||
|
for i in range(len(lyrics)):
|
||||||
|
spacer = compare_chord_lyric(chords[i], lyrics[i])
|
||||||
|
# Handle formatting tags
|
||||||
|
start_formatting_tags = ''
|
||||||
|
if active_formatting_tags:
|
||||||
|
start_formatting_tags = '{' + '}{'.join(active_formatting_tags) + '}'
|
||||||
|
# Update list of active formatting tags
|
||||||
|
active_formatting_tags = find_formatting_tags(lyrics[i], active_formatting_tags)
|
||||||
|
end_formatting_tags = ''
|
||||||
|
if active_formatting_tags:
|
||||||
|
end_formatting_tags = '{/' + '}{/'.join(active_formatting_tags) + '}'
|
||||||
|
new_chord_line += '<td class="chord">%s</td>' % chords[i]
|
||||||
|
# Check if this is the last column, if so skip spacing calc and instead insert a single space
|
||||||
|
if i + 1 == len(lyrics):
|
||||||
|
new_lyric_line += '<td class="lyrics">{starttags}{lyrics} {endtags}</td>'.format(
|
||||||
|
starttags=start_formatting_tags, lyrics=lyrics[i], endtags=end_formatting_tags)
|
||||||
|
else:
|
||||||
|
spacing = ''
|
||||||
|
if spacer > 0:
|
||||||
|
space = ' ' * int(math.ceil(spacer / 2))
|
||||||
|
spacing = '<span class="chordspacing">%s-%s</span>' % (space, space)
|
||||||
|
new_lyric_line += '<td class="lyrics">{starttags}{lyrics}{spacing}{endtags}</td>'.format(
|
||||||
|
starttags=start_formatting_tags, lyrics=lyrics[i], spacing=spacing,
|
||||||
|
endtags=end_formatting_tags)
|
||||||
|
new_line += new_chord_line + new_lyric_line + '</tr>'
|
||||||
|
else:
|
||||||
|
start_formatting_tags = ''
|
||||||
|
if active_formatting_tags:
|
||||||
|
start_formatting_tags = '{' + '}{'.join(active_formatting_tags) + '}'
|
||||||
|
active_formatting_tags = find_formatting_tags(word, active_formatting_tags)
|
||||||
|
end_formatting_tags = ''
|
||||||
|
if active_formatting_tags:
|
||||||
|
end_formatting_tags = '{/' + '}{/'.join(active_formatting_tags) + '}'
|
||||||
|
new_line += '<tr class="chordrow"><td class="chord"> </td></tr><tr><td class="lyrics">' \
|
||||||
|
'{starttags}{lyrics} {endtags}</td></tr>'.format(
|
||||||
|
starttags=start_formatting_tags, lyrics=word, endtags=end_formatting_tags)
|
||||||
|
new_line += '</table>'
|
||||||
|
else:
|
||||||
|
new_line += line
|
||||||
|
new_line += '</td></tr></table>'
|
||||||
|
expanded_text_lines.append(new_line)
|
||||||
|
# the {br} tag used to split lines is not inserted again since the style of the line-tables makes them redundant.
|
||||||
|
return ''.join(expanded_text_lines)
|
||||||
|
|
||||||
|
|
||||||
def create_separated_list(string_list):
|
def create_separated_list(string_list):
|
||||||
"""
|
"""
|
||||||
Returns a string that represents a join of a list of strings with a localized separator.
|
Returns a string that represents a join of a list of strings with a localized separator.
|
||||||
|
@ -337,10 +616,10 @@ from .plugin import PluginStatus, StringContent, Plugin
|
||||||
from .pluginmanager import PluginManager
|
from .pluginmanager import PluginManager
|
||||||
from .settingstab import SettingsTab
|
from .settingstab import SettingsTab
|
||||||
from .serviceitem import ServiceItem, ServiceItemType, ItemCapabilities
|
from .serviceitem import ServiceItem, ServiceItemType, ItemCapabilities
|
||||||
from .htmlbuilder import build_html, build_lyrics_format_css, build_lyrics_outline_css
|
from .htmlbuilder import build_html, build_lyrics_format_css, build_lyrics_outline_css, build_chords_css
|
||||||
from .imagemanager import ImageManager
|
from .imagemanager import ImageManager
|
||||||
from .renderer import Renderer
|
from .renderer import Renderer
|
||||||
from .mediamanageritem import MediaManagerItem
|
from .mediamanageritem import MediaManagerItem
|
||||||
from .projector.db import ProjectorDB, Projector
|
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
|
from .projector.constants import PJLINK_PORT, ERROR_MSG, ERROR_STRING
|
||||||
|
|
|
@ -172,6 +172,7 @@ def upgrade_db(url, upgrade):
|
||||||
else:
|
else:
|
||||||
version = int(version_meta.value)
|
version = int(version_meta.value)
|
||||||
if version > upgrade.__version__:
|
if version > upgrade.__version__:
|
||||||
|
session.remove()
|
||||||
return version, upgrade.__version__
|
return version, upgrade.__version__
|
||||||
version += 1
|
version += 1
|
||||||
try:
|
try:
|
||||||
|
@ -194,7 +195,7 @@ def upgrade_db(url, upgrade):
|
||||||
session.commit()
|
session.commit()
|
||||||
upgrade_version = upgrade.__version__
|
upgrade_version = upgrade.__version__
|
||||||
version = int(version_meta.value)
|
version = int(version_meta.value)
|
||||||
session.close()
|
session.remove()
|
||||||
return version, upgrade_version
|
return version, upgrade_version
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -124,6 +124,25 @@ is the function which has to be called from outside. The generated and returned
|
||||||
position: relative;
|
position: relative;
|
||||||
top: -0.3em;
|
top: -0.3em;
|
||||||
}
|
}
|
||||||
|
/* Chords css */
|
||||||
|
.chordline {
|
||||||
|
line-height: 1.0em;
|
||||||
|
}
|
||||||
|
.chordline span.chord span {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.chordline span.chord span strong {
|
||||||
|
position: absolute;
|
||||||
|
top: -0.8em;
|
||||||
|
left: 0;
|
||||||
|
font-size: 75%;
|
||||||
|
font-weight: normal;
|
||||||
|
line-height: normal;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.firstchordline {
|
||||||
|
line-height: 1.0em;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<script>
|
<script>
|
||||||
var timer = null;
|
var timer = null;
|
||||||
|
@ -444,6 +463,7 @@ HTML_SRC = Template("""
|
||||||
position: relative;
|
position: relative;
|
||||||
top: -0.3em;
|
top: -0.3em;
|
||||||
}
|
}
|
||||||
|
/* Chords css */${chords_css}
|
||||||
</style>
|
</style>
|
||||||
<script>
|
<script>
|
||||||
var timer = null;
|
var timer = null;
|
||||||
|
@ -592,6 +612,30 @@ LYRICS_FORMAT_SRC = Template("""
|
||||||
height: ${height}px;${font_style}${font_weight}
|
height: ${height}px;${font_style}${font_weight}
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
CHORDS_FORMAT = Template("""
|
||||||
|
.chordline {
|
||||||
|
line-height: ${chord_line_height};
|
||||||
|
}
|
||||||
|
.chordline span.chord span {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.chordline span.chord span strong {
|
||||||
|
position: absolute;
|
||||||
|
top: -0.8em;
|
||||||
|
left: 0;
|
||||||
|
font-size: 75%;
|
||||||
|
font-weight: normal;
|
||||||
|
line-height: normal;
|
||||||
|
display: ${chords_display};
|
||||||
|
}
|
||||||
|
.firstchordline {
|
||||||
|
line-height: ${first_chord_line_height};
|
||||||
|
}
|
||||||
|
.ws {
|
||||||
|
display: ${chords_display};
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}""")
|
||||||
|
|
||||||
|
|
||||||
def build_html(item, screen, is_live, background, image=None, plugins=None):
|
def build_html(item, screen, is_live, background, image=None, plugins=None):
|
||||||
"""
|
"""
|
||||||
|
@ -636,7 +680,8 @@ def build_html(item, screen, is_live, background, image=None, plugins=None):
|
||||||
js_additions=js_additions,
|
js_additions=js_additions,
|
||||||
bg_image=bgimage_src,
|
bg_image=bgimage_src,
|
||||||
image=image_src,
|
image=image_src,
|
||||||
html_additions=html_additions)
|
html_additions=html_additions,
|
||||||
|
chords_css=build_chords_css())
|
||||||
|
|
||||||
|
|
||||||
def webkit_version():
|
def webkit_version():
|
||||||
|
@ -768,3 +813,16 @@ def build_footer_css(item, height):
|
||||||
return FOOTER_SRC.substitute(left=item.footer.x(), bottom=bottom, width=item.footer.width(),
|
return FOOTER_SRC.substitute(left=item.footer.x(), bottom=bottom, width=item.footer.width(),
|
||||||
family=theme.font_footer_name, size=theme.font_footer_size,
|
family=theme.font_footer_name, size=theme.font_footer_size,
|
||||||
color=theme.font_footer_color, space=whitespace)
|
color=theme.font_footer_color, space=whitespace)
|
||||||
|
|
||||||
|
|
||||||
|
def build_chords_css():
|
||||||
|
if Settings().value('songs/enable chords') and Settings().value('songs/mainview chords'):
|
||||||
|
chord_line_height = '2.0em'
|
||||||
|
chords_display = 'inline'
|
||||||
|
first_chord_line_height = '2.1em'
|
||||||
|
else:
|
||||||
|
chord_line_height = '1.0em'
|
||||||
|
chords_display = 'none'
|
||||||
|
first_chord_line_height = '1.0em'
|
||||||
|
return CHORDS_FORMAT.substitute(chord_line_height=chord_line_height, chords_display=chords_display,
|
||||||
|
first_chord_line_height=first_chord_line_height)
|
||||||
|
|
|
@ -23,10 +23,9 @@
|
||||||
Provide plugin management
|
Provide plugin management
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
import imp
|
|
||||||
|
|
||||||
from openlp.core.lib import Plugin, PluginStatus
|
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):
|
class PluginManager(RegistryMixin, OpenLPMixin, RegistryProperties):
|
||||||
|
@ -70,32 +69,8 @@ class PluginManager(RegistryMixin, OpenLPMixin, RegistryProperties):
|
||||||
"""
|
"""
|
||||||
Scan a directory for objects inheriting from the ``Plugin`` class.
|
Scan a directory for objects inheriting from the ``Plugin`` class.
|
||||||
"""
|
"""
|
||||||
start_depth = len(os.path.abspath(self.base_path).split(os.sep))
|
glob_pattern = os.path.join('openlp', 'plugins', '*', '*plugin.py')
|
||||||
present_plugin_dir = os.path.join(self.base_path, 'presentations')
|
extension_loader(glob_pattern)
|
||||||
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]))
|
|
||||||
plugin_classes = Plugin.__subclasses__()
|
plugin_classes = Plugin.__subclasses__()
|
||||||
plugin_objects = []
|
plugin_objects = []
|
||||||
for p in plugin_classes:
|
for p in plugin_classes:
|
||||||
|
|
|
@ -48,7 +48,8 @@ __all__ = ['S_OK', 'E_GENERAL', 'E_NOT_CONNECTED', 'E_FAN', 'E_LAMP', 'E_TEMP',
|
||||||
'S_INFO', 'S_NETWORK_SENDING', 'S_NETWORK_RECEIVED',
|
'S_INFO', 'S_NETWORK_SENDING', 'S_NETWORK_RECEIVED',
|
||||||
'ERROR_STRING', 'CR', 'LF', 'PJLINK_ERST_STATUS', 'PJLINK_POWR_STATUS',
|
'ERROR_STRING', 'CR', 'LF', 'PJLINK_ERST_STATUS', 'PJLINK_POWR_STATUS',
|
||||||
'PJLINK_PORT', 'PJLINK_MAX_PACKET', 'TIMEOUT', 'ERROR_MSG', 'PJLINK_ERRORS',
|
'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.
|
# Set common constants.
|
||||||
CR = chr(0x0D) # \r
|
CR = chr(0x0D) # \r
|
||||||
|
@ -56,20 +57,35 @@ LF = chr(0x0A) # \n
|
||||||
PJLINK_PORT = 4352
|
PJLINK_PORT = 4352
|
||||||
TIMEOUT = 30.0
|
TIMEOUT = 30.0
|
||||||
PJLINK_MAX_PACKET = 136
|
PJLINK_MAX_PACKET = 136
|
||||||
PJLINK_VALID_CMD = {'1': ['PJLINK', # Initial connection
|
# NOTE: Change format to account for some commands are both class 1 and 2
|
||||||
'POWR', # Power option
|
PJLINK_VALID_CMD = {
|
||||||
'INPT', # Video sources option
|
'ACKN': ['2', ], # UDP Reply to 'SRCH'
|
||||||
'AVMT', # Shutter option
|
'AVMT': ['1', ], # Shutter option
|
||||||
'ERST', # Error status option
|
'CLSS': ['1', ], # PJLink class support query
|
||||||
'LAMP', # Lamp(s) query (Includes fans)
|
'ERST': ['1', '2'], # Error status option
|
||||||
'INST', # Input sources available query
|
'FILT': ['2', ], # Get current filter usage time
|
||||||
'NAME', # Projector name query
|
'FREZ': ['2', ], # Set freeze/unfreeze picture being projected
|
||||||
'INF1', # Manufacturer name query
|
'INF1': ['1', ], # Manufacturer name query
|
||||||
'INF2', # Product name query
|
'INF2': ['1', ], # Product name query
|
||||||
'INFO', # Other information query
|
'INFO': ['1', ], # Other information query
|
||||||
'CLSS' # PJLink class support 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
|
# Error and status codes
|
||||||
S_OK = E_OK = 0 # E_OK included since I sometimes forget
|
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.
|
# 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'),
|
'2': translate('OpenLP.DB', 'Video'),
|
||||||
'3': translate('OpenLP.DB', 'Digital'),
|
'3': translate('OpenLP.DB', 'Digital'),
|
||||||
'4': translate('OpenLP.DB', 'Storage'),
|
'4': translate('OpenLP.DB', 'Storage'),
|
||||||
'5': translate('OpenLP.DB', 'Network')
|
'5': translate('OpenLP.DB', 'Network'),
|
||||||
|
'6': translate('OpenLP.DB', 'Internal')
|
||||||
}
|
}
|
||||||
|
|
||||||
PJLINK_DEFAULT_CODES = {
|
PJLINK_DEFAULT_ITEMS = {
|
||||||
'11': translate('OpenLP.DB', 'RGB 1'),
|
'1': translate('OpenLP.DB', '1'),
|
||||||
'12': translate('OpenLP.DB', 'RGB 2'),
|
'2': translate('OpenLP.DB', '2'),
|
||||||
'13': translate('OpenLP.DB', 'RGB 3'),
|
'3': translate('OpenLP.DB', '3'),
|
||||||
'14': translate('OpenLP.DB', 'RGB 4'),
|
'4': translate('OpenLP.DB', '4'),
|
||||||
'15': translate('OpenLP.DB', 'RGB 5'),
|
'5': translate('OpenLP.DB', '5'),
|
||||||
'16': translate('OpenLP.DB', 'RGB 6'),
|
'6': translate('OpenLP.DB', '6'),
|
||||||
'17': translate('OpenLP.DB', 'RGB 7'),
|
'7': translate('OpenLP.DB', '7'),
|
||||||
'18': translate('OpenLP.DB', 'RGB 8'),
|
'8': translate('OpenLP.DB', '8'),
|
||||||
'19': translate('OpenLP.DB', 'RGB 9'),
|
'9': translate('OpenLP.DB', '9'),
|
||||||
'21': translate('OpenLP.DB', 'Video 1'),
|
'A': translate('OpenLP.DB', 'A'),
|
||||||
'22': translate('OpenLP.DB', 'Video 2'),
|
'B': translate('OpenLP.DB', 'B'),
|
||||||
'23': translate('OpenLP.DB', 'Video 3'),
|
'C': translate('OpenLP.DB', 'C'),
|
||||||
'24': translate('OpenLP.DB', 'Video 4'),
|
'D': translate('OpenLP.DB', 'D'),
|
||||||
'25': translate('OpenLP.DB', 'Video 5'),
|
'E': translate('OpenLP.DB', 'E'),
|
||||||
'26': translate('OpenLP.DB', 'Video 6'),
|
'F': translate('OpenLP.DB', 'F'),
|
||||||
'27': translate('OpenLP.DB', 'Video 7'),
|
'G': translate('OpenLP.DB', 'G'),
|
||||||
'28': translate('OpenLP.DB', 'Video 8'),
|
'H': translate('OpenLP.DB', 'H'),
|
||||||
'29': translate('OpenLP.DB', 'Video 9'),
|
'I': translate('OpenLP.DB', 'I'),
|
||||||
'31': translate('OpenLP.DB', 'Digital 1'),
|
'J': translate('OpenLP.DB', 'J'),
|
||||||
'32': translate('OpenLP.DB', 'Digital 2'),
|
'K': translate('OpenLP.DB', 'K'),
|
||||||
'33': translate('OpenLP.DB', 'Digital 3'),
|
'L': translate('OpenLP.DB', 'L'),
|
||||||
'34': translate('OpenLP.DB', 'Digital 4'),
|
'M': translate('OpenLP.DB', 'M'),
|
||||||
'35': translate('OpenLP.DB', 'Digital 5'),
|
'N': translate('OpenLP.DB', 'N'),
|
||||||
'36': translate('OpenLP.DB', 'Digital 6'),
|
'O': translate('OpenLP.DB', 'O'),
|
||||||
'37': translate('OpenLP.DB', 'Digital 7'),
|
'P': translate('OpenLP.DB', 'P'),
|
||||||
'38': translate('OpenLP.DB', 'Digital 8'),
|
'Q': translate('OpenLP.DB', 'Q'),
|
||||||
'39': translate('OpenLP.DB', 'Digital 9'),
|
'R': translate('OpenLP.DB', 'R'),
|
||||||
'41': translate('OpenLP.DB', 'Storage 1'),
|
'S': translate('OpenLP.DB', 'S'),
|
||||||
'42': translate('OpenLP.DB', 'Storage 2'),
|
'T': translate('OpenLP.DB', 'T'),
|
||||||
'43': translate('OpenLP.DB', 'Storage 3'),
|
'U': translate('OpenLP.DB', 'U'),
|
||||||
'44': translate('OpenLP.DB', 'Storage 4'),
|
'V': translate('OpenLP.DB', 'V'),
|
||||||
'45': translate('OpenLP.DB', 'Storage 5'),
|
'W': translate('OpenLP.DB', 'W'),
|
||||||
'46': translate('OpenLP.DB', 'Storage 6'),
|
'X': translate('OpenLP.DB', 'X'),
|
||||||
'47': translate('OpenLP.DB', 'Storage 7'),
|
'Y': translate('OpenLP.DB', 'Y'),
|
||||||
'48': translate('OpenLP.DB', 'Storage 8'),
|
'Z': translate('OpenLP.DB', 'Z')
|
||||||
'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')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Due to the expanded nature of PJLink class 2 video sources,
|
||||||
|
# translate the individual types then build the video source
|
||||||
|
# dictionary from the translations.
|
||||||
|
PJLINK_DEFAULT_CODES = dict()
|
||||||
|
for source in PJLINK_DEFAULT_SOURCES:
|
||||||
|
for item in PJLINK_DEFAULT_ITEMS:
|
||||||
|
label = "{source}{item}".format(source=source, item=item)
|
||||||
|
PJLINK_DEFAULT_CODES[label] = "{source} {item}".format(source=PJLINK_DEFAULT_SOURCES[source],
|
||||||
|
item=PJLINK_DEFAULT_ITEMS[item])
|
||||||
|
|
|
@ -150,11 +150,15 @@ class Projector(CommonBase, Base):
|
||||||
name: Column(String(20))
|
name: Column(String(20))
|
||||||
location: Column(String(30))
|
location: Column(String(30))
|
||||||
notes: Column(String(200))
|
notes: Column(String(200))
|
||||||
pjlink_name: Column(String(128)) # From projector (future)
|
pjlink_name: Column(String(128)) # From projector
|
||||||
manufacturer: Column(String(128)) # From projector (future)
|
manufacturer: Column(String(128)) # From projector
|
||||||
model: Column(String(128)) # From projector (future)
|
model: Column(String(128)) # From projector
|
||||||
other: Column(String(128)) # From projector (future)
|
other: Column(String(128)) # From projector
|
||||||
sources: Column(String(128)) # From projector (future)
|
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
|
ProjectorSource relates
|
||||||
"""
|
"""
|
||||||
|
@ -164,8 +168,9 @@ class Projector(CommonBase, Base):
|
||||||
"""
|
"""
|
||||||
return '< Projector(id="{data}", ip="{ip}", port="{port}", pin="{pin}", name="{name}", ' \
|
return '< Projector(id="{data}", ip="{ip}", port="{port}", pin="{pin}", name="{name}", ' \
|
||||||
'location="{location}", notes="{notes}", pjlink_name="{pjlink_name}", ' \
|
'location="{location}", notes="{notes}", pjlink_name="{pjlink_name}", ' \
|
||||||
'manufacturer="{manufacturer}", model="{model}", other="{other}", ' \
|
'manufacturer="{manufacturer}", model="{model}", serial_no="{serial}", other="{other}", ' \
|
||||||
'sources="{sources}", source_list="{source_list}") >'.format(data=self.id,
|
'sources="{sources}", source_list="{source_list}", model_filter="{mfilter}", ' \
|
||||||
|
'model_lamp="{mlamp}", sw_version="{sw_ver}") >'.format(data=self.id,
|
||||||
ip=self.ip,
|
ip=self.ip,
|
||||||
port=self.port,
|
port=self.port,
|
||||||
pin=self.pin,
|
pin=self.pin,
|
||||||
|
@ -177,7 +182,11 @@ class Projector(CommonBase, Base):
|
||||||
model=self.model,
|
model=self.model,
|
||||||
other=self.other,
|
other=self.other,
|
||||||
sources=self.sources,
|
sources=self.sources,
|
||||||
source_list=self.source_list)
|
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))
|
ip = Column(String(100))
|
||||||
port = Column(String(8))
|
port = Column(String(8))
|
||||||
pin = Column(String(20))
|
pin = Column(String(20))
|
||||||
|
@ -189,6 +198,10 @@ class Projector(CommonBase, Base):
|
||||||
model = Column(String(128))
|
model = Column(String(128))
|
||||||
other = Column(String(128))
|
other = Column(String(128))
|
||||||
sources = 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',
|
source_list = relationship('ProjectorSource',
|
||||||
order_by='ProjectorSource.code',
|
order_by='ProjectorSource.code',
|
||||||
backref='projector',
|
backref='projector',
|
||||||
|
@ -359,6 +372,10 @@ class ProjectorDB(Manager):
|
||||||
old_projector.model = projector.model
|
old_projector.model = projector.model
|
||||||
old_projector.other = projector.other
|
old_projector.other = projector.other
|
||||||
old_projector.sources = projector.sources
|
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)
|
return self.save_object(old_projector)
|
||||||
|
|
||||||
def delete_projector(self, projector):
|
def delete_projector(self, projector):
|
||||||
|
|
|
@ -42,7 +42,7 @@ log = logging.getLogger(__name__)
|
||||||
|
|
||||||
log.debug('pjlink1 loaded')
|
log.debug('pjlink1 loaded')
|
||||||
|
|
||||||
__all__ = ['PJLink1']
|
__all__ = ['PJLink']
|
||||||
|
|
||||||
from codecs import decode
|
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_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, \
|
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, \
|
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, \
|
PJLINK_DEFAULT_CODES, STATUS_STRING, S_CONNECTED, S_CONNECTING, S_NETWORK_RECEIVED, S_NETWORK_SENDING, \
|
||||||
S_OFF, S_OK, S_ON, S_STATUS
|
S_NOT_CONNECTED, S_OFF, S_OK, S_ON, S_STATUS
|
||||||
|
|
||||||
# Shortcuts
|
# Shortcuts
|
||||||
SocketError = QtNetwork.QAbstractSocket.SocketError
|
SocketError = QtNetwork.QAbstractSocket.SocketError
|
||||||
SocketSTate = QtNetwork.QAbstractSocket.SocketState
|
SocketSTate = QtNetwork.QAbstractSocket.SocketState
|
||||||
|
|
||||||
PJLINK_PREFIX = '%'
|
PJLINK_PREFIX = '%'
|
||||||
PJLINK_CLASS = '1'
|
PJLINK_CLASS = '1' # Default to class 1 until we query the projector
|
||||||
PJLINK_HEADER = '{prefix}{linkclass}'.format(prefix=PJLINK_PREFIX, linkclass=PJLINK_CLASS)
|
# 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
|
PJLINK_SUFFIX = CR
|
||||||
|
|
||||||
|
|
||||||
class PJLink1(QtNetwork.QTcpSocket):
|
class PJLink(QtNetwork.QTcpSocket):
|
||||||
"""
|
"""
|
||||||
Socket service for connecting to a PJLink-capable projector.
|
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
|
projectorNoAuthentication = QtCore.pyqtSignal(str) # PIN set and no authentication needed
|
||||||
projectorReceivedData = QtCore.pyqtSignal() # Notify when received data finished processing
|
projectorReceivedData = QtCore.pyqtSignal() # Notify when received data finished processing
|
||||||
projectorUpdateIcons = QtCore.pyqtSignal() # Update the status icons on toolbar
|
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):
|
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.ip = ip
|
||||||
self.port = port
|
self.port = port
|
||||||
self.pin = pin
|
self.pin = pin
|
||||||
super(PJLink1, self).__init__()
|
super(PJLink, self).__init__()
|
||||||
self.dbid = None
|
self.dbid = None
|
||||||
self.location = None
|
self.location = None
|
||||||
self.notes = None
|
self.notes = None
|
||||||
|
@ -133,7 +162,7 @@ class PJLink1(QtNetwork.QTcpSocket):
|
||||||
# Socket timer for some possible brain-dead projectors or network cable pulled
|
# Socket timer for some possible brain-dead projectors or network cable pulled
|
||||||
self.socket_timer = None
|
self.socket_timer = None
|
||||||
# Map command to function
|
# Map command to function
|
||||||
self.pjlink1_functions = {
|
self.pjlink_functions = {
|
||||||
'AVMT': self.process_avmt,
|
'AVMT': self.process_avmt,
|
||||||
'CLSS': self.process_clss,
|
'CLSS': self.process_clss,
|
||||||
'ERST': self.process_erst,
|
'ERST': self.process_erst,
|
||||||
|
@ -244,8 +273,6 @@ class PJLink1(QtNetwork.QTcpSocket):
|
||||||
self.send_command('INF2', queue=True)
|
self.send_command('INF2', queue=True)
|
||||||
if self.pjlink_name is None:
|
if self.pjlink_name is None:
|
||||||
self.send_command('NAME', queue=True)
|
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):
|
def _get_status(self, status):
|
||||||
"""
|
"""
|
||||||
|
@ -259,7 +286,7 @@ class PJLink1(QtNetwork.QTcpSocket):
|
||||||
elif status in STATUS_STRING:
|
elif status in STATUS_STRING:
|
||||||
return STATUS_STRING[status], ERROR_MSG[status]
|
return STATUS_STRING[status], ERROR_MSG[status]
|
||||||
else:
|
else:
|
||||||
return status, translate('OpenLP.PJLink1', 'Unknown status')
|
return status, translate('OpenLP.PJLink', 'Unknown status')
|
||||||
|
|
||||||
def change_status(self, status, msg=None):
|
def change_status(self, status, msg=None):
|
||||||
"""
|
"""
|
||||||
|
@ -269,7 +296,7 @@ class PJLink1(QtNetwork.QTcpSocket):
|
||||||
:param status: Status code
|
:param status: Status code
|
||||||
:param msg: Optional message
|
: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)
|
(code, message) = self._get_status(status)
|
||||||
if msg is not None:
|
if msg is not None:
|
||||||
message = msg
|
message = msg
|
||||||
|
@ -322,7 +349,7 @@ class PJLink1(QtNetwork.QTcpSocket):
|
||||||
elif len(read) < 8:
|
elif len(read) < 8:
|
||||||
log.warning('({ip}) Not enough data read)'.format(ip=self.ip))
|
log.warning('({ip}) Not enough data read)'.format(ip=self.ip))
|
||||||
return
|
return
|
||||||
data = decode(read, 'ascii')
|
data = decode(read, 'utf-8')
|
||||||
# Possibility of extraneous data on input when reading.
|
# Possibility of extraneous data on input when reading.
|
||||||
# Clean out extraneous characters in buffer.
|
# Clean out extraneous characters in buffer.
|
||||||
dontcare = self.readLine(self.max_size)
|
dontcare = self.readLine(self.max_size)
|
||||||
|
@ -403,25 +430,24 @@ class PJLink1(QtNetwork.QTcpSocket):
|
||||||
return
|
return
|
||||||
self.socket_timer.stop()
|
self.socket_timer.stop()
|
||||||
self.projectorNetwork.emit(S_NETWORK_RECEIVED)
|
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()
|
data = data_in.strip()
|
||||||
if len(data) < 7:
|
if len(data) < 7:
|
||||||
# Not enough data for a packet
|
# Not enough data for a packet
|
||||||
log.debug('({ip}) get_data(): Packet length < 7: "{data}"'.format(ip=self.ip, data=data))
|
log.debug('({ip}) get_data(): Packet length < 7: "{data}"'.format(ip=self.ip, data=data))
|
||||||
self.send_busy = False
|
self.receive_data_signal()
|
||||||
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()
|
|
||||||
return
|
return
|
||||||
elif '=' not in data:
|
elif '=' not in data:
|
||||||
log.warning('({ip}) get_data(): Invalid packet received'.format(ip=self.ip))
|
log.warning('({ip}) get_data(): Invalid packet received'.format(ip=self.ip))
|
||||||
self.send_busy = False
|
self.receive_data_signal()
|
||||||
self.projectorReceivedData.emit()
|
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
|
return
|
||||||
data_split = data.split('=')
|
data_split = data.split('=')
|
||||||
try:
|
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(): 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()))
|
log.warning('({ip}) get_data(): Received data: "{data}"'.format(ip=self.ip, data=data_in.strip()))
|
||||||
self.change_status(E_INVALID_DATA)
|
self.change_status(E_INVALID_DATA)
|
||||||
self.send_busy = False
|
self.receive_data_signal()
|
||||||
self.projectorReceivedData.emit()
|
|
||||||
return
|
return
|
||||||
|
if not (cmd in PJLINK_VALID_CMD and class_ in PJLINK_VALID_CMD[cmd]):
|
||||||
if not (self.pjlink_class in PJLINK_VALID_CMD and cmd in PJLINK_VALID_CMD[self.pjlink_class]):
|
|
||||||
log.warning('({ip}) get_data(): Invalid packet - unknown command "{data}"'.format(ip=self.ip, data=cmd))
|
log.warning('({ip}) get_data(): Invalid packet - unknown command "{data}"'.format(ip=self.ip, data=cmd))
|
||||||
self.send_busy = False
|
self.receive_data_signal()
|
||||||
self.projectorReceivedData.emit()
|
|
||||||
return
|
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)
|
return self.process_command(cmd, data)
|
||||||
|
|
||||||
@QtCore.pyqtSlot(QtNetwork.QAbstractSocket.SocketError)
|
@QtCore.pyqtSlot(QtNetwork.QAbstractSocket.SocketError)
|
||||||
|
@ -487,8 +513,10 @@ class PJLink1(QtNetwork.QTcpSocket):
|
||||||
data=opts,
|
data=opts,
|
||||||
salt='' if salt is None
|
salt='' if salt is None
|
||||||
else ' with hash'))
|
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,
|
out = '{salt}{header}{command} {options}{suffix}'.format(salt="" if salt is None else salt,
|
||||||
header=PJLINK_HEADER,
|
header=header,
|
||||||
command=cmd,
|
command=cmd,
|
||||||
options=opts,
|
options=opts,
|
||||||
suffix=CR)
|
suffix=CR)
|
||||||
|
@ -510,11 +538,12 @@ class PJLink1(QtNetwork.QTcpSocket):
|
||||||
self._send_command()
|
self._send_command()
|
||||||
|
|
||||||
@QtCore.pyqtSlot()
|
@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.
|
Socket interface to send data. If data=None, then check queue.
|
||||||
|
|
||||||
:param data: Immediate data to send
|
: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()'.format(ip=self.ip))
|
||||||
log.debug('({ip}) _send_string(): Connection status: {data}'.format(ip=self.ip, data=self.state()))
|
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))
|
log.debug('({ip}) _send_string(): Queue = {data}'.format(ip=self.ip, data=self.send_queue))
|
||||||
self.socket_timer.start()
|
self.socket_timer.start()
|
||||||
self.projectorNetwork.emit(S_NETWORK_SENDING)
|
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
|
self.waitForBytesWritten(2000) # 2 seconds should be enough
|
||||||
if sent == -1:
|
if sent == -1:
|
||||||
# Network error?
|
# Network error?
|
||||||
self.change_status(E_NETWORK,
|
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):
|
def process_command(self, cmd, data):
|
||||||
"""
|
"""
|
||||||
|
@ -556,7 +585,13 @@ class PJLink1(QtNetwork.QTcpSocket):
|
||||||
:param cmd: Command to process
|
:param cmd: Command to process
|
||||||
:param data: Data being processed
|
: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:
|
if data in PJLINK_ERRORS:
|
||||||
# Oops - projector error
|
# Oops - projector error
|
||||||
log.error('({ip}) Projector returned error "{data}"'.format(ip=self.ip, data=data))
|
log.error('({ip}) Projector returned error "{data}"'.format(ip=self.ip, data=data))
|
||||||
|
@ -568,8 +603,7 @@ class PJLink1(QtNetwork.QTcpSocket):
|
||||||
self.projectorAuthentication.emit(self.name)
|
self.projectorAuthentication.emit(self.name)
|
||||||
elif data.upper() == 'ERR1':
|
elif data.upper() == 'ERR1':
|
||||||
# Undefined command
|
# Undefined command
|
||||||
self.change_status(E_UNDEFINED, '{error} "{data}"'.format(error=translate('OpenLP.PJLink1',
|
self.change_status(E_UNDEFINED, '{error}: "{data}"'.format(error=ERROR_MSG[E_UNDEFINED],
|
||||||
'Undefined command:'),
|
|
||||||
data=cmd))
|
data=cmd))
|
||||||
elif data.upper() == 'ERR2':
|
elif data.upper() == 'ERR2':
|
||||||
# Invalid parameter
|
# Invalid parameter
|
||||||
|
@ -591,8 +625,9 @@ class PJLink1(QtNetwork.QTcpSocket):
|
||||||
self.projectorReceivedData.emit()
|
self.projectorReceivedData.emit()
|
||||||
return
|
return
|
||||||
|
|
||||||
if cmd in self.pjlink1_functions:
|
if cmd in self.pjlink_functions:
|
||||||
self.pjlink1_functions[cmd](data)
|
log.debug('({ip}) Calling function for {cmd}'.format(ip=self.ip, cmd=cmd))
|
||||||
|
self.pjlink_functions[cmd](data)
|
||||||
else:
|
else:
|
||||||
log.warning('({ip}) Invalid command {data}'.format(ip=self.ip, data=cmd))
|
log.warning('({ip}) Invalid command {data}'.format(ip=self.ip, data=cmd))
|
||||||
self.send_busy = False
|
self.send_busy = False
|
||||||
|
@ -628,6 +663,7 @@ class PJLink1(QtNetwork.QTcpSocket):
|
||||||
|
|
||||||
:param data: Power status
|
:param data: Power status
|
||||||
"""
|
"""
|
||||||
|
log.debug('({ip}: Processing POWR command'.format(ip=self.ip))
|
||||||
if data in PJLINK_POWR_STATUS:
|
if data in PJLINK_POWR_STATUS:
|
||||||
power = PJLINK_POWR_STATUS[data]
|
power = PJLINK_POWR_STATUS[data]
|
||||||
update_icons = self.power != power
|
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))
|
log.debug('({ip}) Setting AVMT to "10" (shutter open)'.format(ip=self.ip))
|
||||||
self.send_command(cmd='AVMT', opts='10')
|
self.send_command(cmd='AVMT', opts='10')
|
||||||
self.poll_loop()
|
self.poll_loop()
|
||||||
|
|
||||||
|
def receive_data_signal(self):
|
||||||
|
"""
|
||||||
|
Clear any busy flags and send data received signal
|
||||||
|
"""
|
||||||
|
self.send_busy = False
|
||||||
|
self.projectorReceivedData.emit()
|
||||||
|
return
|
||||||
|
|
||||||
|
def _not_implemented(self, cmd):
|
||||||
|
"""
|
||||||
|
Log when a future PJLink command has not been implemented yet.
|
||||||
|
"""
|
||||||
|
log.warn("({ip}) Future command '{cmd}' has not been implemented yet".format(ip=self.ip,
|
||||||
|
cmd=cmd))
|
||||||
|
return
|
||||||
|
|
|
@ -27,7 +27,7 @@ from PyQt5 import QtGui, QtCore, QtWebKitWidgets
|
||||||
|
|
||||||
from openlp.core.common import Registry, RegistryProperties, OpenLPMixin, RegistryMixin, Settings
|
from openlp.core.common import Registry, RegistryProperties, OpenLPMixin, RegistryMixin, Settings
|
||||||
from openlp.core.lib import FormattingTags, ImageSource, ItemCapabilities, ScreenList, ServiceItem, expand_tags, \
|
from openlp.core.lib import FormattingTags, ImageSource, ItemCapabilities, ScreenList, ServiceItem, expand_tags, \
|
||||||
build_lyrics_format_css, build_lyrics_outline_css
|
build_lyrics_format_css, build_lyrics_outline_css, build_chords_css
|
||||||
from openlp.core.common import ThemeLevel
|
from openlp.core.common import ThemeLevel
|
||||||
from openlp.core.ui import MainDisplay
|
from openlp.core.ui import MainDisplay
|
||||||
|
|
||||||
|
@ -383,13 +383,14 @@ class Renderer(OpenLPMixin, RegistryMixin, RegistryProperties):
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
*{margin: 0; padding: 0; border: 0;}
|
*{margin: 0; padding: 0; border: 0;}
|
||||||
#main {position: absolute; top: 0px; ${format_css} ${outline_css}}
|
#main {position: absolute; top: 0px; ${format_css} ${outline_css}} ${chords_css}
|
||||||
</style></head>
|
</style></head>
|
||||||
<body><div id="main"></div></body></html>""")
|
<body><div id="main"></div></body></html>""")
|
||||||
self.web.setHtml(html.substitute(format_css=build_lyrics_format_css(theme_data,
|
self.web.setHtml(html.substitute(format_css=build_lyrics_format_css(theme_data,
|
||||||
self.page_width,
|
self.page_width,
|
||||||
self.page_height),
|
self.page_height),
|
||||||
outline_css=build_lyrics_outline_css(theme_data)))
|
outline_css=build_lyrics_outline_css(theme_data),
|
||||||
|
chords_css=build_chords_css()))
|
||||||
self.empty_height = self.web_frame.contentsSize().height()
|
self.empty_height = self.web_frame.contentsSize().height()
|
||||||
|
|
||||||
def _paginate_slide(self, lines, line_end):
|
def _paginate_slide(self, lines, line_end):
|
||||||
|
|
|
@ -34,7 +34,7 @@ import ntpath
|
||||||
from PyQt5 import QtGui
|
from PyQt5 import QtGui
|
||||||
|
|
||||||
from openlp.core.common import RegistryProperties, Settings, translate, AppLocation, md5_hash
|
from openlp.core.common import RegistryProperties, Settings, translate, AppLocation, md5_hash
|
||||||
from openlp.core.lib import ImageSource, build_icon, clean_tags, expand_tags
|
from openlp.core.lib import ImageSource, build_icon, clean_tags, expand_tags, expand_chords, create_thumb
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -117,7 +117,6 @@ class ItemCapabilities(object):
|
||||||
|
|
||||||
``HasThumbnails``
|
``HasThumbnails``
|
||||||
The item has related thumbnails available
|
The item has related thumbnails available
|
||||||
|
|
||||||
"""
|
"""
|
||||||
CanPreview = 1
|
CanPreview = 1
|
||||||
CanEdit = 2
|
CanEdit = 2
|
||||||
|
@ -247,6 +246,8 @@ class ServiceItem(RegistryProperties):
|
||||||
self.renderer.set_item_theme(self.theme)
|
self.renderer.set_item_theme(self.theme)
|
||||||
self.theme_data, self.main, self.footer = self.renderer.pre_render()
|
self.theme_data, self.main, self.footer = self.renderer.pre_render()
|
||||||
if self.service_item_type == ServiceItemType.Text:
|
if self.service_item_type == ServiceItemType.Text:
|
||||||
|
expand_chord_tags = hasattr(self, 'name') and self.name == 'songs' and Settings().value(
|
||||||
|
'songs/enable chords')
|
||||||
log.debug('Formatting slides: {title}'.format(title=self.title))
|
log.debug('Formatting slides: {title}'.format(title=self.title))
|
||||||
# Save rendered pages to this dict. In the case that a slide is used twice we can use the pages saved to
|
# Save rendered pages to this dict. In the case that a slide is used twice we can use the pages saved to
|
||||||
# the dict instead of rendering them again.
|
# the dict instead of rendering them again.
|
||||||
|
@ -260,13 +261,16 @@ class ServiceItem(RegistryProperties):
|
||||||
previous_pages[verse_tag] = (slide['raw_slide'], pages)
|
previous_pages[verse_tag] = (slide['raw_slide'], pages)
|
||||||
for page in pages:
|
for page in pages:
|
||||||
page = page.replace('<br>', '{br}')
|
page = page.replace('<br>', '{br}')
|
||||||
html_data = expand_tags(html.escape(page.rstrip()))
|
html_data = expand_tags(page.rstrip(), expand_chord_tags)
|
||||||
self._display_frames.append({
|
new_frame = {
|
||||||
'title': clean_tags(page),
|
'title': clean_tags(page),
|
||||||
'text': clean_tags(page.rstrip()),
|
'text': clean_tags(page.rstrip(), expand_chord_tags),
|
||||||
|
'chords_text': expand_chords(clean_tags(page.rstrip(), False)),
|
||||||
'html': html_data.replace('&nbsp;', ' '),
|
'html': html_data.replace('&nbsp;', ' '),
|
||||||
'verseTag': verse_tag
|
'printing_html': expand_tags(html.escape(page.rstrip()), expand_chord_tags, True),
|
||||||
})
|
'verseTag': verse_tag,
|
||||||
|
}
|
||||||
|
self._display_frames.append(new_frame)
|
||||||
elif self.service_item_type == ServiceItemType.Image or self.service_item_type == ServiceItemType.Command:
|
elif self.service_item_type == ServiceItemType.Image or self.service_item_type == ServiceItemType.Command:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -164,7 +164,7 @@ class ThemeXML(object):
|
||||||
jsn = get_text_file_string(json_file)
|
jsn = get_text_file_string(json_file)
|
||||||
jsn = json.loads(jsn)
|
jsn = json.loads(jsn)
|
||||||
self.expand_json(jsn)
|
self.expand_json(jsn)
|
||||||
self.background_filename = None
|
self.background_filename = ''
|
||||||
|
|
||||||
def expand_json(self, var, prev=None):
|
def expand_json(self, var, prev=None):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -25,13 +25,13 @@ The :mod:`advancedtab` provides an advanced settings facility.
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import sys
|
|
||||||
|
|
||||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||||
|
|
||||||
from openlp.core.common import AppLocation, Settings, SlideLimits, UiStrings, translate
|
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.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__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -153,32 +153,17 @@ class AdvancedTab(SettingsTab):
|
||||||
self.data_directory_group_box.setObjectName('data_directory_group_box')
|
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 = QtWidgets.QFormLayout(self.data_directory_group_box)
|
||||||
self.data_directory_layout.setObjectName('data_directory_layout')
|
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 = QtWidgets.QLabel(self.data_directory_group_box)
|
||||||
self.data_directory_new_label.setObjectName('data_directory_current_label')
|
self.data_directory_new_label.setObjectName('data_directory_current_label')
|
||||||
self.new_data_directory_edit = QtWidgets.QLineEdit(self.data_directory_group_box)
|
self.data_directory_path_edit = PathEdit(self.data_directory_group_box, path_type=PathType.Directories,
|
||||||
self.new_data_directory_edit.setObjectName('new_data_directory_edit')
|
default_path=AppLocation.get_directory(AppLocation.DataDir))
|
||||||
self.new_data_directory_edit.setReadOnly(True)
|
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 = 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.setObjectName('new_data_directory_has_files_label')
|
||||||
self.new_data_directory_has_files_label.setWordWrap(True)
|
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 = QtWidgets.QToolButton(self.data_directory_group_box)
|
||||||
self.data_directory_cancel_button.setObjectName('data_directory_cancel_button')
|
self.data_directory_cancel_button.setObjectName('data_directory_cancel_button')
|
||||||
self.data_directory_cancel_button.setIcon(build_icon(':/general/general_delete.png'))
|
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 = QtWidgets.QHBoxLayout()
|
||||||
self.data_directory_copy_check_layout.setObjectName('data_directory_copy_check_layout')
|
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)
|
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.addWidget(self.data_directory_copy_check_box)
|
||||||
self.data_directory_copy_check_layout.addStretch()
|
self.data_directory_copy_check_layout.addStretch()
|
||||||
self.data_directory_copy_check_layout.addWidget(self.data_directory_cancel_button)
|
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.data_directory_copy_check_layout)
|
||||||
self.data_directory_layout.addRow(self.new_data_directory_has_files_label)
|
self.data_directory_layout.addRow(self.new_data_directory_has_files_label)
|
||||||
self.left_layout.addWidget(self.data_directory_group_box)
|
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_edit.textChanged.connect(self.update_service_name_example)
|
||||||
self.service_name_revert_button.clicked.connect(self.on_service_name_revert_button_clicked)
|
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.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_path_edit.pathChanged.connect(self.on_data_directory_path_edit_path_changed)
|
||||||
self.data_directory_default_button.clicked.connect(self.on_data_directory_default_button_clicked)
|
|
||||||
self.data_directory_cancel_button.clicked.connect(self.on_data_directory_cancel_button_clicked)
|
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.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)
|
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.service_name_example_label.setText(translate('OpenLP.AdvancedTab', 'Example:'))
|
||||||
self.hide_mouse_group_box.setTitle(translate('OpenLP.AdvancedTab', 'Mouse Cursor'))
|
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.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', '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_cancel_button.setText(translate('OpenLP.AdvancedTab', 'Cancel'))
|
self.data_directory_cancel_button.setText(translate('OpenLP.AdvancedTab', 'Cancel'))
|
||||||
self.data_directory_cancel_button.setToolTip(
|
self.data_directory_cancel_button.setToolTip(
|
||||||
translate('OpenLP.AdvancedTab', 'Cancel OpenLP data directory location change.'))
|
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.new_data_directory_has_files_label.hide()
|
||||||
self.data_directory_cancel_button.hide()
|
self.data_directory_cancel_button.hide()
|
||||||
# Since data location can be changed, make sure the path is present.
|
# Since data location can be changed, make sure the path is present.
|
||||||
self.current_data_path = AppLocation.get_data_path()
|
self.data_directory_path_edit.path = AppLocation.get_data_path()
|
||||||
self.data_directory_label.setText(os.path.abspath(self.current_data_path))
|
|
||||||
# Don't allow data directory move if running portable.
|
# Don't allow data directory move if running portable.
|
||||||
if settings.value('advanced/is portable'):
|
if settings.value('advanced/is portable'):
|
||||||
self.data_directory_group_box.hide()
|
self.data_directory_group_box.hide()
|
||||||
|
@ -509,24 +485,10 @@ class AdvancedTab(SettingsTab):
|
||||||
self.service_name_edit.setText(UiStrings().DefaultServiceName)
|
self.service_name_edit.setText(UiStrings().DefaultServiceName)
|
||||||
self.service_name_edit.setFocus()
|
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.
|
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.
|
# Make sure they want to change the data.
|
||||||
answer = QtWidgets.QMessageBox.question(self, translate('OpenLP.AdvancedTab', 'Confirm Data Directory Change'),
|
answer = QtWidgets.QMessageBox.question(self, translate('OpenLP.AdvancedTab', 'Confirm Data Directory Change'),
|
||||||
translate('OpenLP.AdvancedTab', 'Are you sure you want to change the '
|
translate('OpenLP.AdvancedTab', 'Are you sure you want to change the '
|
||||||
|
@ -537,42 +499,14 @@ class AdvancedTab(SettingsTab):
|
||||||
QtWidgets.QMessageBox.No),
|
QtWidgets.QMessageBox.No),
|
||||||
QtWidgets.QMessageBox.No)
|
QtWidgets.QMessageBox.No)
|
||||||
if answer != QtWidgets.QMessageBox.Yes:
|
if answer != QtWidgets.QMessageBox.Yes:
|
||||||
|
self.data_directory_path_edit.path = AppLocation.get_data_path()
|
||||||
return
|
return
|
||||||
# Check if data already exists here.
|
# Check if data already exists here.
|
||||||
self.check_data_overwrite(new_data_path)
|
self.check_data_overwrite(new_data_path)
|
||||||
# Save the new location.
|
# Save the new location.
|
||||||
self.main_window.set_new_data_path(new_data_path)
|
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()
|
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):
|
def on_data_directory_copy_check_box_toggled(self):
|
||||||
"""
|
"""
|
||||||
Copy existing data when you change your data directory.
|
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.
|
Check if there's already data in the target directory.
|
||||||
"""
|
"""
|
||||||
test_path = os.path.join(data_path, 'songs')
|
test_path = os.path.join(data_path, 'songs')
|
||||||
self.data_directory_copy_check_box.show()
|
|
||||||
if os.path.exists(test_path):
|
if os.path.exists(test_path):
|
||||||
self.data_exists = True
|
self.data_exists = True
|
||||||
# Check is they want to replace existing data.
|
# Check is they want to replace existing data.
|
||||||
|
@ -603,6 +536,7 @@ class AdvancedTab(SettingsTab):
|
||||||
QtWidgets.QMessageBox.StandardButtons(QtWidgets.QMessageBox.Yes |
|
QtWidgets.QMessageBox.StandardButtons(QtWidgets.QMessageBox.Yes |
|
||||||
QtWidgets.QMessageBox.No),
|
QtWidgets.QMessageBox.No),
|
||||||
QtWidgets.QMessageBox.No)
|
QtWidgets.QMessageBox.No)
|
||||||
|
self.data_directory_copy_check_box.show()
|
||||||
if answer == QtWidgets.QMessageBox.Yes:
|
if answer == QtWidgets.QMessageBox.Yes:
|
||||||
self.data_directory_copy_check_box.setChecked(True)
|
self.data_directory_copy_check_box.setChecked(True)
|
||||||
self.new_data_directory_has_files_label.show()
|
self.new_data_directory_has_files_label.show()
|
||||||
|
@ -618,7 +552,7 @@ class AdvancedTab(SettingsTab):
|
||||||
"""
|
"""
|
||||||
Cancel the data directory location change
|
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.data_directory_copy_check_box.setChecked(False)
|
||||||
self.main_window.set_new_data_path(None)
|
self.main_window.set_new_data_path(None)
|
||||||
self.main_window.set_copy_data(False)
|
self.main_window.set_copy_data(False)
|
||||||
|
|
|
@ -27,8 +27,8 @@ import logging
|
||||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||||
|
|
||||||
from openlp.core.common import Registry, Settings, UiStrings, translate, get_images_filter
|
from openlp.core.common import Registry, Settings, UiStrings, translate, get_images_filter
|
||||||
from openlp.core.lib import SettingsTab, ScreenList, build_icon
|
from openlp.core.lib import SettingsTab, ScreenList
|
||||||
from openlp.core.ui.lib.colorbutton import ColorButton
|
from openlp.core.ui.lib import ColorButton, PathEdit
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -172,20 +172,8 @@ class GeneralTab(SettingsTab):
|
||||||
self.logo_layout.setObjectName('logo_layout')
|
self.logo_layout.setObjectName('logo_layout')
|
||||||
self.logo_file_label = QtWidgets.QLabel(self.logo_group_box)
|
self.logo_file_label = QtWidgets.QLabel(self.logo_group_box)
|
||||||
self.logo_file_label.setObjectName('logo_file_label')
|
self.logo_file_label.setObjectName('logo_file_label')
|
||||||
self.logo_file_edit = QtWidgets.QLineEdit(self.logo_group_box)
|
self.logo_file_path_edit = PathEdit(self.logo_group_box, default_path=':/graphics/openlp-splash-screen.png')
|
||||||
self.logo_file_edit.setObjectName('logo_file_edit')
|
self.logo_layout.addRow(self.logo_file_label, self.logo_file_path_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_color_label = QtWidgets.QLabel(self.logo_group_box)
|
self.logo_color_label = QtWidgets.QLabel(self.logo_group_box)
|
||||||
self.logo_color_label.setObjectName('logo_color_label')
|
self.logo_color_label.setObjectName('logo_color_label')
|
||||||
self.logo_color_button = ColorButton(self.logo_group_box)
|
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.logo_layout.addRow(self.logo_hide_on_startup_check_box)
|
||||||
self.right_layout.addWidget(self.logo_group_box)
|
self.right_layout.addWidget(self.logo_group_box)
|
||||||
self.logo_color_button.colorChanged.connect(self.on_logo_background_color_changed)
|
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
|
# Application Settings
|
||||||
self.settings_group_box = QtWidgets.QGroupBox(self.right_column)
|
self.settings_group_box = QtWidgets.QGroupBox(self.right_column)
|
||||||
self.settings_group_box.setObjectName('settings_group_box')
|
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_group_box.setTitle(translate('OpenLP.GeneralTab', 'Logo'))
|
||||||
self.logo_color_label.setText(UiStrings().BackgroundColorColon)
|
self.logo_color_label.setText(UiStrings().BackgroundColorColon)
|
||||||
self.logo_file_label.setText(translate('OpenLP.GeneralTab', 'Logo file:'))
|
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.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.check_for_updates_check_box.setText(translate('OpenLP.GeneralTab', 'Check for updates to OpenLP'))
|
||||||
self.settings_group_box.setTitle(translate('OpenLP.GeneralTab', 'Application Settings'))
|
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.audio_group_box.setTitle(translate('OpenLP.GeneralTab', 'Background Audio'))
|
||||||
self.start_paused_check_box.setText(translate('OpenLP.GeneralTab', 'Start background audio paused'))
|
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.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):
|
def load(self):
|
||||||
"""
|
"""
|
||||||
|
@ -304,7 +291,7 @@ class GeneralTab(SettingsTab):
|
||||||
self.auto_open_check_box.setChecked(settings.value('auto open'))
|
self.auto_open_check_box.setChecked(settings.value('auto open'))
|
||||||
self.show_splash_check_box.setChecked(settings.value('show splash'))
|
self.show_splash_check_box.setChecked(settings.value('show splash'))
|
||||||
self.logo_background_color = settings.value('logo background color')
|
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_hide_on_startup_check_box.setChecked(settings.value('logo hide on startup'))
|
||||||
self.logo_color_button.color = self.logo_background_color
|
self.logo_color_button.color = self.logo_background_color
|
||||||
self.check_for_updates_check_box.setChecked(settings.value('update check'))
|
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('auto open', self.auto_open_check_box.isChecked())
|
||||||
settings.setValue('show splash', self.show_splash_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 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('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('update check', self.check_for_updates_check_box.isChecked())
|
||||||
settings.setValue('save prompt', self.save_check_service_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
|
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):
|
def on_logo_background_color_changed(self, color):
|
||||||
"""
|
"""
|
||||||
Select the background color for logo.
|
Select the background color for logo.
|
||||||
|
|
|
@ -21,14 +21,16 @@
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
from .colorbutton import ColorButton
|
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 .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 .spelltextedit import SpellTextEdit
|
||||||
|
from .treewidgetwithdnd import TreeWidgetWithDnD
|
||||||
|
|
||||||
__all__ = ['ColorButton', 'ListPreviewWidget', 'ListWidgetWithDnD', 'OpenLPToolbar', 'OpenLPDockWidget',
|
__all__ = ['ColorButton', 'ListPreviewWidget', 'ListWidgetWithDnD', 'MediaDockManager', 'OpenLPDockWidget',
|
||||||
'OpenLPWizard', 'WizardStrings', 'MediaDockManager', 'ListPreviewWidget', 'SpellTextEdit']
|
'OpenLPToolbar', 'OpenLPWizard', 'PathEdit', 'PathType', 'SpellTextEdit', 'TreeWidgetWithDnD',
|
||||||
|
'WizardStrings']
|
||||||
|
|
|
@ -39,7 +39,7 @@ class ColorButton(QtWidgets.QPushButton):
|
||||||
"""
|
"""
|
||||||
Initialise the ColorButton
|
Initialise the ColorButton
|
||||||
"""
|
"""
|
||||||
super(ColorButton, self).__init__()
|
super().__init__(parent)
|
||||||
self.parent = parent
|
self.parent = parent
|
||||||
self.change_color('#ffffff')
|
self.change_color('#ffffff')
|
||||||
self.setToolTip(translate('OpenLP.ColorButton', 'Click to select a color.'))
|
self.setToolTip(translate('OpenLP.ColorButton', 'Click to select a color.'))
|
||||||
|
|
|
@ -0,0 +1,205 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# OpenLP - Open Source Lyrics Projection #
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Copyright (c) 2008-2017 OpenLP Developers #
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# This program is free software; you can redistribute it and/or modify it #
|
||||||
|
# under the terms of the GNU General Public License as published by the Free #
|
||||||
|
# Software Foundation; version 2 of the License. #
|
||||||
|
# #
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT #
|
||||||
|
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
|
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
|
||||||
|
# more details. #
|
||||||
|
# #
|
||||||
|
# You should have received a copy of the GNU General Public License along #
|
||||||
|
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||||
|
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||||
|
###############################################################################
|
||||||
|
from enum import Enum
|
||||||
|
import os.path
|
||||||
|
|
||||||
|
from PyQt5 import QtCore, QtWidgets
|
||||||
|
|
||||||
|
from openlp.core.common import UiStrings, translate
|
||||||
|
from openlp.core.lib import build_icon
|
||||||
|
|
||||||
|
|
||||||
|
class PathType(Enum):
|
||||||
|
Files = 1
|
||||||
|
Directories = 2
|
||||||
|
|
||||||
|
|
||||||
|
class PathEdit(QtWidgets.QWidget):
|
||||||
|
"""
|
||||||
|
The :class:`~openlp.core.ui.lib.pathedit.PathEdit` class subclasses QWidget to create a custom widget for use when
|
||||||
|
a file or directory needs to be selected.
|
||||||
|
"""
|
||||||
|
pathChanged = QtCore.pyqtSignal(str)
|
||||||
|
|
||||||
|
def __init__(self, parent=None, path_type=PathType.Files, default_path=None, dialog_caption=None, show_revert=True):
|
||||||
|
"""
|
||||||
|
Initalise the PathEdit widget
|
||||||
|
|
||||||
|
:param parent: The parent of the widget. This is just passed to the super method.
|
||||||
|
:type parent: QWidget or None
|
||||||
|
|
||||||
|
:param dialog_caption: Used to customise the caption in the QFileDialog.
|
||||||
|
:param dialog_caption: str
|
||||||
|
|
||||||
|
:param default_path: The default path. This is set as the path when the revert button is clicked
|
||||||
|
:type default_path: str
|
||||||
|
|
||||||
|
:param show_revert: Used to determin if the 'revert button' should be visible.
|
||||||
|
:type show_revert: bool
|
||||||
|
|
||||||
|
:return: None
|
||||||
|
:rtype: None
|
||||||
|
"""
|
||||||
|
super().__init__(parent)
|
||||||
|
self.default_path = default_path
|
||||||
|
self.dialog_caption = dialog_caption
|
||||||
|
self._path_type = path_type
|
||||||
|
self._path = None
|
||||||
|
self.filters = '{all_files} (*)'.format(all_files=UiStrings().AllFiles)
|
||||||
|
self._setup(show_revert)
|
||||||
|
|
||||||
|
def _setup(self, show_revert):
|
||||||
|
"""
|
||||||
|
Set up the widget
|
||||||
|
:param show_revert: Show or hide the revert button
|
||||||
|
:type show_revert: bool
|
||||||
|
|
||||||
|
:return: None
|
||||||
|
:rtype: None
|
||||||
|
"""
|
||||||
|
widget_layout = QtWidgets.QHBoxLayout()
|
||||||
|
widget_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.line_edit = QtWidgets.QLineEdit(self)
|
||||||
|
self.line_edit.setText(self._path)
|
||||||
|
widget_layout.addWidget(self.line_edit)
|
||||||
|
self.browse_button = QtWidgets.QToolButton(self)
|
||||||
|
self.browse_button.setIcon(build_icon(':/general/general_open.png'))
|
||||||
|
widget_layout.addWidget(self.browse_button)
|
||||||
|
self.revert_button = QtWidgets.QToolButton(self)
|
||||||
|
self.revert_button.setIcon(build_icon(':/general/general_revert.png'))
|
||||||
|
self.revert_button.setVisible(show_revert)
|
||||||
|
widget_layout.addWidget(self.revert_button)
|
||||||
|
self.setLayout(widget_layout)
|
||||||
|
# Signals and Slots
|
||||||
|
self.browse_button.clicked.connect(self.on_browse_button_clicked)
|
||||||
|
self.revert_button.clicked.connect(self.on_revert_button_clicked)
|
||||||
|
self.line_edit.editingFinished.connect(self.on_line_edit_editing_finished)
|
||||||
|
self.update_button_tool_tips()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path(self):
|
||||||
|
"""
|
||||||
|
A property getter method to return the selected path.
|
||||||
|
|
||||||
|
:return: The selected path
|
||||||
|
:rtype: str
|
||||||
|
"""
|
||||||
|
return self._path
|
||||||
|
|
||||||
|
@path.setter
|
||||||
|
def path(self, path):
|
||||||
|
"""
|
||||||
|
A Property setter method to set the selected path
|
||||||
|
|
||||||
|
:param path: The path to set the widget to
|
||||||
|
:type path: str
|
||||||
|
"""
|
||||||
|
self._path = path
|
||||||
|
self.line_edit.setText(path)
|
||||||
|
self.line_edit.setToolTip(path)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path_type(self):
|
||||||
|
"""
|
||||||
|
A property getter method to return the path_type. Path type allows you to sepecify if the user is restricted to
|
||||||
|
selecting a file or directory.
|
||||||
|
|
||||||
|
:return: The type selected
|
||||||
|
:rtype: Enum of PathEdit
|
||||||
|
"""
|
||||||
|
return self._path_type
|
||||||
|
|
||||||
|
@path_type.setter
|
||||||
|
def path_type(self, path_type):
|
||||||
|
"""
|
||||||
|
A Property setter method to set the path type
|
||||||
|
|
||||||
|
:param path: The type of path to select
|
||||||
|
:type path: Enum of PathEdit
|
||||||
|
"""
|
||||||
|
self._path_type = path_type
|
||||||
|
self.update_button_tool_tips()
|
||||||
|
|
||||||
|
def update_button_tool_tips(self):
|
||||||
|
"""
|
||||||
|
Called to update the tooltips on the buttons. This is changing path types, and when the widget is initalised
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
if self._path_type == PathType.Directories:
|
||||||
|
self.browse_button.setToolTip(translate('OpenLP.PathEdit', 'Browse for directory.'))
|
||||||
|
self.revert_button.setToolTip(translate('OpenLP.PathEdit', 'Revert to default directory.'))
|
||||||
|
else:
|
||||||
|
self.browse_button.setToolTip(translate('OpenLP.PathEdit', 'Browse for file.'))
|
||||||
|
self.revert_button.setToolTip(translate('OpenLP.PathEdit', 'Revert to default file.'))
|
||||||
|
|
||||||
|
def on_browse_button_clicked(self):
|
||||||
|
"""
|
||||||
|
A handler to handle a click on the browse button.
|
||||||
|
|
||||||
|
Show the QFileDialog and process the input from the user
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
caption = self.dialog_caption
|
||||||
|
path = ''
|
||||||
|
if self._path_type == PathType.Directories:
|
||||||
|
if not caption:
|
||||||
|
caption = translate('OpenLP.PathEdit', 'Select Directory')
|
||||||
|
path = QtWidgets.QFileDialog.getExistingDirectory(self, caption,
|
||||||
|
self._path, QtWidgets.QFileDialog.ShowDirsOnly)
|
||||||
|
elif self._path_type == PathType.Files:
|
||||||
|
if not caption:
|
||||||
|
caption = self.dialog_caption = translate('OpenLP.PathEdit', 'Select File')
|
||||||
|
path, filter_used = QtWidgets.QFileDialog.getOpenFileName(self, caption, self._path, self.filters)
|
||||||
|
if path:
|
||||||
|
path = os.path.normpath(path)
|
||||||
|
self.on_new_path(path)
|
||||||
|
|
||||||
|
def on_revert_button_clicked(self):
|
||||||
|
"""
|
||||||
|
A handler to handle a click on the revert button.
|
||||||
|
|
||||||
|
Set the new path to the value of the default_path instance variable.
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
self.on_new_path(self.default_path)
|
||||||
|
|
||||||
|
def on_line_edit_editing_finished(self):
|
||||||
|
"""
|
||||||
|
A handler to handle when the line edit has finished being edited.
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
self.on_new_path(self.line_edit.text())
|
||||||
|
|
||||||
|
def on_new_path(self, path):
|
||||||
|
"""
|
||||||
|
A method called to validate and set a new path.
|
||||||
|
|
||||||
|
Emits the pathChanged Signal
|
||||||
|
|
||||||
|
:param path: The new path
|
||||||
|
:type path: str
|
||||||
|
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
if self._path != path:
|
||||||
|
self.path = path
|
||||||
|
self.pathChanged.emit(path)
|
|
@ -143,6 +143,7 @@ def format_milliseconds(milliseconds):
|
||||||
seconds=seconds,
|
seconds=seconds,
|
||||||
millis=millis)
|
millis=millis)
|
||||||
|
|
||||||
|
|
||||||
from .mediacontroller import MediaController
|
from .mediacontroller import MediaController
|
||||||
from .playertab import PlayerTab
|
from .playertab import PlayerTab
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,8 @@ import os
|
||||||
import datetime
|
import datetime
|
||||||
from PyQt5 import QtCore, QtWidgets
|
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 import ItemCapabilities
|
||||||
from openlp.core.lib.ui import critical_error_message_box
|
from openlp.core.lib.ui import critical_error_message_box
|
||||||
from openlp.core.common import AppLocation
|
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
|
parse_optical_path
|
||||||
from openlp.core.ui.lib.toolbar import OpenLPToolbar
|
from openlp.core.ui.lib.toolbar import OpenLPToolbar
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
TICK_TIME = 200
|
TICK_TIME = 200
|
||||||
|
@ -172,19 +174,9 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties):
|
||||||
Check to see if we have any media Player's available.
|
Check to see if we have any media Player's available.
|
||||||
"""
|
"""
|
||||||
log.debug('_check_available_media_players')
|
log.debug('_check_available_media_players')
|
||||||
controller_dir = os.path.join(AppLocation.get_directory(AppLocation.AppDir), 'core', 'ui', 'media')
|
controller_dir = os.path.join('openlp', 'core', 'ui', 'media')
|
||||||
for filename in os.listdir(controller_dir):
|
glob_pattern = os.path.join(controller_dir, '*player.py')
|
||||||
if filename.endswith('player.py') and filename != 'mediaplayer.py':
|
extension_loader(glob_pattern, ['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)
|
|
||||||
player_classes = MediaPlayer.__subclasses__()
|
player_classes = MediaPlayer.__subclasses__()
|
||||||
for player_class in player_classes:
|
for player_class in player_classes:
|
||||||
self.register_players(player_class(self))
|
self.register_players(player_class(self))
|
||||||
|
|
|
@ -95,7 +95,7 @@ class Ui_PrintServiceDialog(object):
|
||||||
self.main_layout.addWidget(self.preview_widget)
|
self.main_layout.addWidget(self.preview_widget)
|
||||||
self.options_widget = QtWidgets.QWidget(print_service_dialog)
|
self.options_widget = QtWidgets.QWidget(print_service_dialog)
|
||||||
self.options_widget.hide()
|
self.options_widget.hide()
|
||||||
self.options_widget.resize(400, 300)
|
self.options_widget.resize(400, 350)
|
||||||
self.options_widget.setAutoFillBackground(True)
|
self.options_widget.setAutoFillBackground(True)
|
||||||
self.options_layout = QtWidgets.QVBoxLayout(self.options_widget)
|
self.options_layout = QtWidgets.QVBoxLayout(self.options_widget)
|
||||||
self.options_layout.setContentsMargins(8, 8, 8, 8)
|
self.options_layout.setContentsMargins(8, 8, 8, 8)
|
||||||
|
@ -121,6 +121,8 @@ class Ui_PrintServiceDialog(object):
|
||||||
self.group_layout.addWidget(self.notes_check_box)
|
self.group_layout.addWidget(self.notes_check_box)
|
||||||
self.meta_data_check_box = QtWidgets.QCheckBox()
|
self.meta_data_check_box = QtWidgets.QCheckBox()
|
||||||
self.group_layout.addWidget(self.meta_data_check_box)
|
self.group_layout.addWidget(self.meta_data_check_box)
|
||||||
|
self.show_chords_check_box = QtWidgets.QCheckBox()
|
||||||
|
self.group_layout.addWidget(self.show_chords_check_box)
|
||||||
self.group_layout.addStretch(1)
|
self.group_layout.addStretch(1)
|
||||||
self.options_group_box.setLayout(self.group_layout)
|
self.options_group_box.setLayout(self.group_layout)
|
||||||
self.options_layout.addWidget(self.options_group_box)
|
self.options_layout.addWidget(self.options_group_box)
|
||||||
|
@ -144,6 +146,7 @@ class Ui_PrintServiceDialog(object):
|
||||||
self.page_break_after_text.setText(translate('OpenLP.PrintServiceForm', 'Add page break before each text item'))
|
self.page_break_after_text.setText(translate('OpenLP.PrintServiceForm', 'Add page break before each text item'))
|
||||||
self.notes_check_box.setText(translate('OpenLP.PrintServiceForm', 'Include service item notes'))
|
self.notes_check_box.setText(translate('OpenLP.PrintServiceForm', 'Include service item notes'))
|
||||||
self.meta_data_check_box.setText(translate('OpenLP.PrintServiceForm', 'Include play length of media items'))
|
self.meta_data_check_box.setText(translate('OpenLP.PrintServiceForm', 'Include play length of media items'))
|
||||||
|
self.show_chords_check_box.setText(translate('OpenLP.PrintServiceForm', 'Show chords'))
|
||||||
self.title_line_edit.setText(translate('OpenLP.PrintServiceForm', 'Service Sheet'))
|
self.title_line_edit.setText(translate('OpenLP.PrintServiceForm', 'Service Sheet'))
|
||||||
# Do not change the order.
|
# Do not change the order.
|
||||||
self.zoom_combo_box.addItems([
|
self.zoom_combo_box.addItems([
|
||||||
|
|
|
@ -37,7 +37,7 @@ from openlp.core.common import AppLocation
|
||||||
DEFAULT_CSS = """/*
|
DEFAULT_CSS = """/*
|
||||||
Edit this file to customize the service order print. Note, that not all CSS
|
Edit this file to customize the service order print. Note, that not all CSS
|
||||||
properties are supported. See:
|
properties are supported. See:
|
||||||
http://doc.trolltech.com/4.7/richtext-html-subset.html#css-properties
|
https://doc.qt.io/qt-5/richtext-html-subset.html#css-properties
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.serviceTitle {
|
.serviceTitle {
|
||||||
|
@ -101,6 +101,19 @@ http://doc.trolltech.com/4.7/richtext-html-subset.html#css-properties
|
||||||
.newPage {
|
.newPage {
|
||||||
page-break-before: always;
|
page-break-before: always;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table.line {}
|
||||||
|
|
||||||
|
table.segment {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.chord {
|
||||||
|
font-size: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.lyrics {
|
||||||
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -172,6 +185,12 @@ class PrintServiceForm(QtWidgets.QDialog, Ui_PrintServiceDialog, RegistryPropert
|
||||||
self._add_element('h1', html.escape(self.title_line_edit.text()), html_data.body, classId='serviceTitle')
|
self._add_element('h1', html.escape(self.title_line_edit.text()), html_data.body, classId='serviceTitle')
|
||||||
for index, item in enumerate(self.service_manager.service_items):
|
for index, item in enumerate(self.service_manager.service_items):
|
||||||
self._add_preview_item(html_data.body, item['service_item'], index)
|
self._add_preview_item(html_data.body, item['service_item'], index)
|
||||||
|
if not self.show_chords_check_box.isChecked():
|
||||||
|
# Remove chord row and spacing span elements when not printing chords
|
||||||
|
for chord_row in html_data.find_class('chordrow'):
|
||||||
|
chord_row.drop_tree()
|
||||||
|
for spacing_span in html_data.find_class('chordspacing'):
|
||||||
|
spacing_span.drop_tree()
|
||||||
# Add the custom service notes:
|
# Add the custom service notes:
|
||||||
if self.footer_text_edit.toPlainText():
|
if self.footer_text_edit.toPlainText():
|
||||||
div = self._add_element('div', parent=html_data.body, classId='customNotes')
|
div = self._add_element('div', parent=html_data.body, classId='customNotes')
|
||||||
|
@ -196,13 +215,13 @@ class PrintServiceForm(QtWidgets.QDialog, Ui_PrintServiceDialog, RegistryPropert
|
||||||
verse_def = None
|
verse_def = None
|
||||||
verse_html = None
|
verse_html = None
|
||||||
for slide in item.get_frames():
|
for slide in item.get_frames():
|
||||||
if not verse_def or verse_def != slide['verseTag'] or verse_html == slide['html']:
|
if not verse_def or verse_def != slide['verseTag'] or verse_html == slide['printing_html']:
|
||||||
text_div = self._add_element('div', parent=div, classId='itemText')
|
text_div = self._add_element('div', parent=div, classId='itemText')
|
||||||
else:
|
elif 'chordspacing' not in slide['printing_html']:
|
||||||
self._add_element('br', parent=text_div)
|
self._add_element('br', parent=text_div)
|
||||||
self._add_element('span', slide['html'], text_div)
|
self._add_element('span', slide['printing_html'], text_div)
|
||||||
verse_def = slide['verseTag']
|
verse_def = slide['verseTag']
|
||||||
verse_html = slide['html']
|
verse_html = slide['printing_html']
|
||||||
# Break the page before the div element.
|
# Break the page before the div element.
|
||||||
if index != 0 and self.page_break_after_text.isChecked():
|
if index != 0 and self.page_break_after_text.isChecked():
|
||||||
div.set('class', 'item newPage')
|
div.set('class', 'item newPage')
|
||||||
|
|
|
@ -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, \
|
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
|
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.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.editform import ProjectorEditForm
|
||||||
from openlp.core.ui.projector.sourceselectform import SourceSelectTabs, SourceSelectSingle
|
from openlp.core.ui.projector.sourceselectform import SourceSelectTabs, SourceSelectSingle
|
||||||
|
|
||||||
|
@ -690,10 +690,10 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, UiProjecto
|
||||||
Helper app to build a projector instance
|
Helper app to build a projector instance
|
||||||
|
|
||||||
:param projector: Dict of projector database information
|
:param projector: Dict of projector database information
|
||||||
:returns: PJLink1() instance
|
:returns: PJLink() instance
|
||||||
"""
|
"""
|
||||||
log.debug('_add_projector()')
|
log.debug('_add_projector()')
|
||||||
return PJLink1(dbid=projector.id,
|
return PJLink(dbid=projector.id,
|
||||||
ip=projector.ip,
|
ip=projector.ip,
|
||||||
port=int(projector.port),
|
port=int(projector.port),
|
||||||
name=projector.name,
|
name=projector.name,
|
||||||
|
@ -961,7 +961,7 @@ class ProjectorItem(QtCore.QObject):
|
||||||
"""
|
"""
|
||||||
Initialization for ProjectorItem instance
|
Initialization for ProjectorItem instance
|
||||||
|
|
||||||
:param link: PJLink1 instance for QListWidgetItem
|
:param link: PJLink instance for QListWidgetItem
|
||||||
"""
|
"""
|
||||||
self.link = link
|
self.link = link
|
||||||
self.thread = None
|
self.thread = None
|
||||||
|
|
|
@ -69,10 +69,16 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties):
|
||||||
self.video_color_button.colorChanged.connect(self.on_video_color_changed)
|
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_start_button.colorChanged.connect(self.on_gradient_start_color_changed)
|
||||||
self.gradient_end_button.colorChanged.connect(self.on_gradient_end_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_path_edit.filters = \
|
||||||
self.image_file_edit.editingFinished.connect(self.on_image_file_edit_editing_finished)
|
'{name};;{text} (*)'.format(name=get_images_filter(), text=UiStrings().AllFiles)
|
||||||
self.video_browse_button.clicked.connect(self.on_video_browse_button_clicked)
|
self.image_path_edit.pathChanged.connect(self.on_image_path_edit_path_changed)
|
||||||
self.video_file_edit.editingFinished.connect(self.on_video_file_edit_editing_finished)
|
# 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.main_color_button.colorChanged.connect(self.on_main_color_changed)
|
||||||
self.outline_color_button.colorChanged.connect(self.on_outline_color_changed)
|
self.outline_color_button.colorChanged.connect(self.on_outline_color_changed)
|
||||||
self.shadow_color_button.colorChanged.connect(self.on_shadow_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('color', self.color_button)
|
||||||
self.background_page.registerField('gradient_start', self.gradient_start_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('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.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_color_button', self.main_color_button)
|
||||||
self.main_area_page.registerField('main_size_spin_box', self.main_size_spin_box)
|
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)
|
self.setField('background_type', 1)
|
||||||
elif self.theme.background_type == BackgroundType.to_string(BackgroundType.Image):
|
elif self.theme.background_type == BackgroundType.to_string(BackgroundType.Image):
|
||||||
self.image_color_button.color = self.theme.background_border_color
|
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)
|
self.setField('background_type', 2)
|
||||||
elif self.theme.background_type == BackgroundType.to_string(BackgroundType.Video):
|
elif self.theme.background_type == BackgroundType.to_string(BackgroundType.Video):
|
||||||
self.video_color_button.color = self.theme.background_border_color
|
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)
|
self.setField('background_type', 4)
|
||||||
elif self.theme.background_type == BackgroundType.to_string(BackgroundType.Transparent):
|
elif self.theme.background_type == BackgroundType.to_string(BackgroundType.Transparent):
|
||||||
self.setField('background_type', 3)
|
self.setField('background_type', 3)
|
||||||
|
@ -441,48 +448,20 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties):
|
||||||
"""
|
"""
|
||||||
self.theme.background_end_color = color
|
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.
|
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()
|
self.set_background_page_values()
|
||||||
|
|
||||||
def on_image_file_edit_editing_finished(self):
|
def on_video_path_edit_path_changed(self, filename):
|
||||||
"""
|
|
||||||
Background image path edited
|
|
||||||
"""
|
|
||||||
self.theme.background_filename = str(self.image_file_edit.text())
|
|
||||||
|
|
||||||
def on_video_browse_button_clicked(self):
|
|
||||||
"""
|
"""
|
||||||
Background video button pushed.
|
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()
|
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):
|
def on_main_color_changed(self, color):
|
||||||
"""
|
"""
|
||||||
Set the main colour value
|
Set the main colour value
|
||||||
|
|
|
@ -28,7 +28,7 @@ from openlp.core.common import UiStrings, translate, is_macosx
|
||||||
from openlp.core.lib import build_icon
|
from openlp.core.lib import build_icon
|
||||||
from openlp.core.lib.theme import HorizontalType, BackgroundType, BackgroundGradientType
|
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.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):
|
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_layout.addRow(self.image_color_label, self.image_color_button)
|
||||||
self.image_label = QtWidgets.QLabel(self.image_widget)
|
self.image_label = QtWidgets.QLabel(self.image_widget)
|
||||||
self.image_label.setObjectName('image_label')
|
self.image_label.setObjectName('image_label')
|
||||||
self.image_file_layout = QtWidgets.QHBoxLayout()
|
self.image_path_edit = PathEdit(self.image_widget,
|
||||||
self.image_file_layout.setObjectName('image_file_layout')
|
dialog_caption=translate('OpenLP.ThemeWizard', 'Select Image'),
|
||||||
self.image_file_edit = QtWidgets.QLineEdit(self.image_widget)
|
show_revert=False)
|
||||||
self.image_file_edit.setObjectName('image_file_edit')
|
self.image_layout.addRow(self.image_label, self.image_path_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_layout.setItem(2, QtWidgets.QFormLayout.LabelRole, self.spacer)
|
self.image_layout.setItem(2, QtWidgets.QFormLayout.LabelRole, self.spacer)
|
||||||
self.background_stack.addWidget(self.image_widget)
|
self.background_stack.addWidget(self.image_widget)
|
||||||
self.transparent_widget = QtWidgets.QWidget(self.background_page)
|
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_layout.addRow(self.video_color_label, self.video_color_button)
|
||||||
self.video_label = QtWidgets.QLabel(self.video_widget)
|
self.video_label = QtWidgets.QLabel(self.video_widget)
|
||||||
self.video_label.setObjectName('video_label')
|
self.video_label.setObjectName('video_label')
|
||||||
self.video_file_layout = QtWidgets.QHBoxLayout()
|
self.video_path_edit = PathEdit(self.video_widget,
|
||||||
self.video_file_layout.setObjectName('video_file_layout')
|
dialog_caption=translate('OpenLP.ThemeWizard', 'Select Video'),
|
||||||
self.video_file_edit = QtWidgets.QLineEdit(self.video_widget)
|
show_revert=False)
|
||||||
self.video_file_edit.setObjectName('video_file_edit')
|
self.video_layout.addRow(self.video_label, self.video_path_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_layout.setItem(2, QtWidgets.QFormLayout.LabelRole, self.spacer)
|
self.video_layout.setItem(2, QtWidgets.QFormLayout.LabelRole, self.spacer)
|
||||||
self.background_stack.addWidget(self.video_widget)
|
self.background_stack.addWidget(self.video_widget)
|
||||||
theme_wizard.addPage(self.background_page)
|
theme_wizard.addPage(self.background_page)
|
||||||
|
|
|
@ -135,7 +135,6 @@ class BibleImportForm(OpenLPWizard):
|
||||||
Add the bible import specific wizard pages.
|
Add the bible import specific wizard pages.
|
||||||
"""
|
"""
|
||||||
# Select Page
|
# Select Page
|
||||||
self.spacers = []
|
|
||||||
self.select_page = QtWidgets.QWizardPage()
|
self.select_page = QtWidgets.QWizardPage()
|
||||||
self.select_page.setObjectName('SelectPage')
|
self.select_page.setObjectName('SelectPage')
|
||||||
self.select_page_layout = QtWidgets.QVBoxLayout(self.select_page)
|
self.select_page_layout = QtWidgets.QVBoxLayout(self.select_page)
|
||||||
|
@ -148,8 +147,8 @@ class BibleImportForm(OpenLPWizard):
|
||||||
self.format_combo_box.addItems(['', '', '', '', '', '', ''])
|
self.format_combo_box.addItems(['', '', '', '', '', '', ''])
|
||||||
self.format_combo_box.setObjectName('FormatComboBox')
|
self.format_combo_box.setObjectName('FormatComboBox')
|
||||||
self.format_layout.addRow(self.format_label, self.format_combo_box)
|
self.format_layout.addRow(self.format_label, self.format_combo_box)
|
||||||
self.spacers.append(QtWidgets.QSpacerItem(10, 0, QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Minimum))
|
self.spacer = QtWidgets.QSpacerItem(10, 0, QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Minimum)
|
||||||
self.format_layout.setItem(1, QtWidgets.QFormLayout.LabelRole, self.spacers[-1])
|
self.format_layout.setItem(1, QtWidgets.QFormLayout.LabelRole, self.spacer)
|
||||||
self.select_page_layout.addLayout(self.format_layout)
|
self.select_page_layout.addLayout(self.format_layout)
|
||||||
self.select_stack = QtWidgets.QStackedLayout()
|
self.select_stack = QtWidgets.QStackedLayout()
|
||||||
self.select_stack.setObjectName('SelectStack')
|
self.select_stack.setObjectName('SelectStack')
|
||||||
|
@ -171,8 +170,7 @@ class BibleImportForm(OpenLPWizard):
|
||||||
self.osis_browse_button.setObjectName('OsisBrowseButton')
|
self.osis_browse_button.setObjectName('OsisBrowseButton')
|
||||||
self.osis_file_layout.addWidget(self.osis_browse_button)
|
self.osis_file_layout.addWidget(self.osis_browse_button)
|
||||||
self.osis_layout.addRow(self.osis_file_label, self.osis_file_layout)
|
self.osis_layout.addRow(self.osis_file_label, self.osis_file_layout)
|
||||||
self.spacers.append(QtWidgets.QSpacerItem(10, 0, QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Minimum))
|
self.osis_layout.setItem(1, QtWidgets.QFormLayout.LabelRole, self.spacer)
|
||||||
self.osis_layout.setItem(1, QtWidgets.QFormLayout.LabelRole, self.spacers[-1])
|
|
||||||
self.select_stack.addWidget(self.osis_widget)
|
self.select_stack.addWidget(self.osis_widget)
|
||||||
self.csv_widget = QtWidgets.QWidget(self.select_page)
|
self.csv_widget = QtWidgets.QWidget(self.select_page)
|
||||||
self.csv_widget.setObjectName('CsvWidget')
|
self.csv_widget.setObjectName('CsvWidget')
|
||||||
|
@ -205,8 +203,7 @@ class BibleImportForm(OpenLPWizard):
|
||||||
self.csv_verses_button.setObjectName('CsvVersesButton')
|
self.csv_verses_button.setObjectName('CsvVersesButton')
|
||||||
self.csv_verses_layout.addWidget(self.csv_verses_button)
|
self.csv_verses_layout.addWidget(self.csv_verses_button)
|
||||||
self.csv_layout.addRow(self.csv_verses_label, self.csv_verses_layout)
|
self.csv_layout.addRow(self.csv_verses_label, self.csv_verses_layout)
|
||||||
self.spacers.append(QtWidgets.QSpacerItem(10, 0, QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Minimum))
|
self.csv_layout.setItem(3, QtWidgets.QFormLayout.LabelRole, self.spacer)
|
||||||
self.csv_layout.setItem(2, QtWidgets.QFormLayout.LabelRole, self.spacers[-1])
|
|
||||||
self.select_stack.addWidget(self.csv_widget)
|
self.select_stack.addWidget(self.csv_widget)
|
||||||
self.open_song_widget = QtWidgets.QWidget(self.select_page)
|
self.open_song_widget = QtWidgets.QWidget(self.select_page)
|
||||||
self.open_song_widget.setObjectName('OpenSongWidget')
|
self.open_song_widget.setObjectName('OpenSongWidget')
|
||||||
|
@ -226,8 +223,7 @@ class BibleImportForm(OpenLPWizard):
|
||||||
self.open_song_browse_button.setObjectName('OpenSongBrowseButton')
|
self.open_song_browse_button.setObjectName('OpenSongBrowseButton')
|
||||||
self.open_song_file_layout.addWidget(self.open_song_browse_button)
|
self.open_song_file_layout.addWidget(self.open_song_browse_button)
|
||||||
self.open_song_layout.addRow(self.open_song_file_label, self.open_song_file_layout)
|
self.open_song_layout.addRow(self.open_song_file_label, self.open_song_file_layout)
|
||||||
self.spacers.append(QtWidgets.QSpacerItem(10, 0, QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Minimum))
|
self.open_song_layout.setItem(1, QtWidgets.QFormLayout.LabelRole, self.spacer)
|
||||||
self.open_song_layout.setItem(1, QtWidgets.QFormLayout.LabelRole, self.spacers[-1])
|
|
||||||
self.select_stack.addWidget(self.open_song_widget)
|
self.select_stack.addWidget(self.open_song_widget)
|
||||||
self.web_tab_widget = QtWidgets.QTabWidget(self.select_page)
|
self.web_tab_widget = QtWidgets.QTabWidget(self.select_page)
|
||||||
self.web_tab_widget.setObjectName('WebTabWidget')
|
self.web_tab_widget.setObjectName('WebTabWidget')
|
||||||
|
@ -304,8 +300,7 @@ class BibleImportForm(OpenLPWizard):
|
||||||
self.zefania_browse_button.setObjectName('ZefaniaBrowseButton')
|
self.zefania_browse_button.setObjectName('ZefaniaBrowseButton')
|
||||||
self.zefania_file_layout.addWidget(self.zefania_browse_button)
|
self.zefania_file_layout.addWidget(self.zefania_browse_button)
|
||||||
self.zefania_layout.addRow(self.zefania_file_label, self.zefania_file_layout)
|
self.zefania_layout.addRow(self.zefania_file_label, self.zefania_file_layout)
|
||||||
self.spacers.append(QtWidgets.QSpacerItem(10, 0, QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Minimum))
|
self.zefania_layout.setItem(5, QtWidgets.QFormLayout.LabelRole, self.spacer)
|
||||||
self.zefania_layout.setItem(1, QtWidgets.QFormLayout.LabelRole, self.spacers[-1])
|
|
||||||
self.select_stack.addWidget(self.zefania_widget)
|
self.select_stack.addWidget(self.zefania_widget)
|
||||||
self.sword_widget = QtWidgets.QWidget(self.select_page)
|
self.sword_widget = QtWidgets.QWidget(self.select_page)
|
||||||
self.sword_widget.setObjectName('SwordWidget')
|
self.sword_widget.setObjectName('SwordWidget')
|
||||||
|
@ -386,8 +381,7 @@ class BibleImportForm(OpenLPWizard):
|
||||||
self.wordproject_browse_button.setObjectName('WordProjectBrowseButton')
|
self.wordproject_browse_button.setObjectName('WordProjectBrowseButton')
|
||||||
self.wordproject_file_layout.addWidget(self.wordproject_browse_button)
|
self.wordproject_file_layout.addWidget(self.wordproject_browse_button)
|
||||||
self.wordproject_layout.addRow(self.wordproject_file_label, self.wordproject_file_layout)
|
self.wordproject_layout.addRow(self.wordproject_file_label, self.wordproject_file_layout)
|
||||||
self.spacers.append(QtWidgets.QSpacerItem(10, 0, QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Minimum))
|
self.wordproject_layout.setItem(5, QtWidgets.QFormLayout.LabelRole, self.spacer)
|
||||||
self.wordproject_layout.setItem(1, QtWidgets.QFormLayout.LabelRole, self.spacers[-1])
|
|
||||||
self.select_stack.addWidget(self.wordproject_widget)
|
self.select_stack.addWidget(self.wordproject_widget)
|
||||||
self.select_page_layout.addLayout(self.select_stack)
|
self.select_page_layout.addLayout(self.select_stack)
|
||||||
self.addPage(self.select_page)
|
self.addPage(self.select_page)
|
||||||
|
@ -505,8 +499,7 @@ class BibleImportForm(OpenLPWizard):
|
||||||
self.csv_verses_label.minimumSizeHint().width(),
|
self.csv_verses_label.minimumSizeHint().width(),
|
||||||
self.open_song_file_label.minimumSizeHint().width(),
|
self.open_song_file_label.minimumSizeHint().width(),
|
||||||
self.zefania_file_label.minimumSizeHint().width())
|
self.zefania_file_label.minimumSizeHint().width())
|
||||||
for spacer in self.spacers:
|
self.spacer.changeSize(label_width, 0, QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
|
||||||
spacer.changeSize(label_width, 0, QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
|
|
||||||
|
|
||||||
def validateCurrentPage(self):
|
def validateCurrentPage(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -401,4 +401,5 @@ class BibleManager(OpenLPMixin, RegistryProperties):
|
||||||
for bible in self.db_cache:
|
for bible in self.db_cache:
|
||||||
self.db_cache[bible].finalise()
|
self.db_cache[bible].finalise()
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['BibleFormat']
|
__all__ = ['BibleFormat']
|
||||||
|
|
|
@ -58,7 +58,8 @@ from PyQt5 import QtCore
|
||||||
|
|
||||||
from openlp.core.lib import ScreenList
|
from openlp.core.lib import ScreenList
|
||||||
from openlp.core.common import get_uno_command, get_uno_instance
|
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__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
|
@ -29,7 +29,7 @@ from subprocess import check_output, CalledProcessError
|
||||||
from openlp.core.common import AppLocation, check_binary_exists
|
from openlp.core.common import AppLocation, check_binary_exists
|
||||||
from openlp.core.common import Settings, is_win
|
from openlp.core.common import Settings, is_win
|
||||||
from openlp.core.lib import ScreenList
|
from openlp.core.lib import ScreenList
|
||||||
from .presentationcontroller import PresentationController, PresentationDocument
|
from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument
|
||||||
|
|
||||||
if is_win():
|
if is_win():
|
||||||
from subprocess import STARTUPINFO, STARTF_USESHOWWINDOW
|
from subprocess import STARTUPINFO, STARTF_USESHOWWINDOW
|
||||||
|
|
|
@ -43,7 +43,7 @@ if is_win():
|
||||||
from openlp.core.lib import ScreenList
|
from openlp.core.lib import ScreenList
|
||||||
from openlp.core.lib.ui import UiStrings, critical_error_message_box, translate
|
from openlp.core.lib.ui import UiStrings, critical_error_message_box, translate
|
||||||
from openlp.core.common import trace_error_handler, Registry
|
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__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
|
@ -35,7 +35,7 @@ if is_win():
|
||||||
|
|
||||||
from openlp.core.common import AppLocation
|
from openlp.core.common import AppLocation
|
||||||
from openlp.core.lib import ScreenList
|
from openlp.core.lib import ScreenList
|
||||||
from .presentationcontroller import PresentationController, PresentationDocument
|
from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
|
@ -197,6 +197,7 @@ class PPTViewer(QtWidgets.QWidget):
|
||||||
def openDialog(self):
|
def openDialog(self):
|
||||||
self.pptEdit.setText(QtWidgets.QFileDialog.getOpenFileName(self, 'Open file')[0])
|
self.pptEdit.setText(QtWidgets.QFileDialog.getOpenFileName(self, 'Open file')[0])
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
pptdll = cdll.LoadLibrary(r'pptviewlib.dll')
|
pptdll = cdll.LoadLibrary(r'pptviewlib.dll')
|
||||||
pptdll.SetDebug(1)
|
pptdll.SetDebug(1)
|
||||||
|
|
|
@ -25,7 +25,8 @@ from PyQt5 import QtGui, QtWidgets
|
||||||
from openlp.core.common import Settings, UiStrings, translate
|
from openlp.core.common import Settings, UiStrings, translate
|
||||||
from openlp.core.lib import SettingsTab, build_icon
|
from openlp.core.lib import SettingsTab, build_icon
|
||||||
from openlp.core.lib.ui import critical_error_message_box
|
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):
|
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 = QtWidgets.QCheckBox(self.pdf_group_box)
|
||||||
self.pdf_program_check_box.setObjectName('pdf_program_check_box')
|
self.pdf_program_check_box.setObjectName('pdf_program_check_box')
|
||||||
self.pdf_layout.addRow(self.pdf_program_check_box)
|
self.pdf_layout.addRow(self.pdf_program_check_box)
|
||||||
self.pdf_program_path_layout = QtWidgets.QHBoxLayout()
|
self.program_path_edit = PathEdit(self.pdf_group_box)
|
||||||
self.pdf_program_path_layout.setObjectName('pdf_program_path_layout')
|
self.pdf_layout.addRow(self.program_path_edit)
|
||||||
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.left_layout.addWidget(self.pdf_group_box)
|
self.left_layout.addWidget(self.pdf_group_box)
|
||||||
self.left_layout.addStretch()
|
self.left_layout.addStretch()
|
||||||
self.right_column.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred)
|
self.right_column.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred)
|
||||||
self.right_layout.addStretch()
|
self.right_layout.addStretch()
|
||||||
# Signals and slots
|
# Signals and slots
|
||||||
self.pdf_program_browse_button.clicked.connect(self.on_pdf_program_browse_button_clicked)
|
self.program_path_edit.pathChanged.connect(self.on_program_path_edit_path_changed)
|
||||||
self.pdf_program_check_box.clicked.connect(self.on_pdf_program_check_box_clicked)
|
self.pdf_program_check_box.clicked.connect(self.program_path_edit.setEnabled)
|
||||||
|
|
||||||
def retranslateUi(self):
|
def retranslateUi(self):
|
||||||
"""
|
"""
|
||||||
|
@ -132,6 +122,8 @@ class PresentationTab(SettingsTab):
|
||||||
'(This may fix PowerPoint scaling issues in Windows 8 and 10)'))
|
'(This may fix PowerPoint scaling issues in Windows 8 and 10)'))
|
||||||
self.pdf_program_check_box.setText(
|
self.pdf_program_check_box.setText(
|
||||||
translate('PresentationPlugin.PresentationTab', 'Use given full path for mudraw or ghostscript binary:'))
|
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):
|
def set_controller_text(self, checkbox, controller):
|
||||||
if checkbox.isEnabled():
|
if checkbox.isEnabled():
|
||||||
|
@ -161,11 +153,10 @@ class PresentationTab(SettingsTab):
|
||||||
# load pdf-program settings
|
# load pdf-program settings
|
||||||
enable_pdf_program = Settings().value(self.settings_section + '/enable_pdf_program')
|
enable_pdf_program = Settings().value(self.settings_section + '/enable_pdf_program')
|
||||||
self.pdf_program_check_box.setChecked(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.program_path_edit.setEnabled(enable_pdf_program)
|
||||||
self.pdf_program_browse_button.setEnabled(enable_pdf_program)
|
|
||||||
pdf_program = Settings().value(self.settings_section + '/pdf_program')
|
pdf_program = Settings().value(self.settings_section + '/pdf_program')
|
||||||
if pdf_program:
|
if pdf_program:
|
||||||
self.pdf_program_path.setText(pdf_program)
|
self.program_path_edit.path = pdf_program
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
"""
|
"""
|
||||||
|
@ -201,7 +192,7 @@ class PresentationTab(SettingsTab):
|
||||||
Settings().setValue(setting_key, self.ppt_window_check_box.checkState())
|
Settings().setValue(setting_key, self.ppt_window_check_box.checkState())
|
||||||
changed = True
|
changed = True
|
||||||
# Save pdf-settings
|
# 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()
|
enable_pdf_program = self.pdf_program_check_box.checkState()
|
||||||
# If the given program is blank disable using the program
|
# If the given program is blank disable using the program
|
||||||
if pdf_program == '':
|
if pdf_program == '':
|
||||||
|
@ -228,42 +219,12 @@ class PresentationTab(SettingsTab):
|
||||||
checkbox.setEnabled(controller.is_available())
|
checkbox.setEnabled(controller.is_available())
|
||||||
self.set_controller_text(checkbox, controller)
|
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.
|
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:
|
if filename:
|
||||||
program_type = PdfController.process_check_binary(filename)
|
if not PdfController.process_check_binary(filename):
|
||||||
if not program_type:
|
|
||||||
critical_error_message_box(UiStrings().Error,
|
critical_error_message_box(UiStrings().Error,
|
||||||
translate('PresentationPlugin.PresentationTab',
|
translate('PresentationPlugin.PresentationTab',
|
||||||
'The program is not ghostscript or mudraw which is required.'))
|
'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
|
|
||||||
|
|
|
@ -20,19 +20,18 @@
|
||||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
# 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
|
The :mod:`openlp.plugins.presentations.presentationplugin` module provides the ability for OpenLP to display
|
||||||
formats.
|
presentations from a variety of document formats.
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from PyQt5 import QtCore
|
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.core.lib import Plugin, StringContent, build_icon
|
||||||
from openlp.plugins.presentations.lib import PresentationController, PresentationMediaItem, PresentationTab
|
from openlp.plugins.presentations.lib import PresentationController, PresentationMediaItem, PresentationTab
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
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.
|
Check to see if we have any presentation software available. If not do not install the plugin.
|
||||||
"""
|
"""
|
||||||
log.debug('check_pre_conditions')
|
log.debug('check_pre_conditions')
|
||||||
controller_dir = os.path.join(AppLocation.get_directory(AppLocation.PluginsDir), 'presentations', 'lib')
|
controller_dir = os.path.join('openlp', 'plugins', 'presentations', 'lib')
|
||||||
for filename in os.listdir(controller_dir):
|
glob_pattern = os.path.join(controller_dir, '*controller.py')
|
||||||
if filename.endswith('controller.py') and filename != 'presentationcontroller.py':
|
extension_loader(glob_pattern, ['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_classes = PresentationController.__subclasses__()
|
controller_classes = PresentationController.__subclasses__()
|
||||||
for controller_class in controller_classes:
|
for controller_class in controller_classes:
|
||||||
controller = controller_class(self)
|
controller = controller_class(self)
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<!--
|
||||||
|
###############################################################################
|
||||||
|
# OpenLP - Open Source Lyrics Projection #
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Copyright (c) 2008-2017 OpenLP Developers #
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# This program is free software; you can redistribute it and/or modify it #
|
||||||
|
# under the terms of the GNU General Public License as published by the Free #
|
||||||
|
# Software Foundation; version 2 of the License. #
|
||||||
|
# #
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT #
|
||||||
|
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
|
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
|
||||||
|
# more details. #
|
||||||
|
# #
|
||||||
|
# You should have received a copy of the GNU General Public License along #
|
||||||
|
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||||
|
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||||
|
###############################################################################
|
||||||
|
-->
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>${chords_title}</title>
|
||||||
|
<link rel="stylesheet" href="/css/stage.css" />
|
||||||
|
<link rel="stylesheet" href="/css/chords.css" />
|
||||||
|
<link rel="shortcut icon" type="image/x-icon" href="/images/favicon.ico">
|
||||||
|
<script type="text/javascript" src="/assets/jquery.min.js"></script>
|
||||||
|
<script type="text/javascript" src="/js/chords.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<input type="hidden" id="next-text" value="${next}" />
|
||||||
|
<div id="right">
|
||||||
|
<div id="clock"></div>
|
||||||
|
<div id="chords" class="button">Toggle Chords</div>
|
||||||
|
<div id="notes"></div>
|
||||||
|
</div>
|
||||||
|
<div id="header">
|
||||||
|
<div id="verseorder"></div>
|
||||||
|
<div id="transpose">Transpose:</div> <div class="button" id="transposedown">-</div> <div id="transposevalue">0</div> <div class="button" id="transposeup">+</div> <div id="capodisplay">(Capo)</div>
|
||||||
|
</div>
|
||||||
|
<div id="currentslide"></div>
|
||||||
|
<div id="nextslide"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,96 @@
|
||||||
|
/******************************************************************************
|
||||||
|
* OpenLP - Open Source Lyrics Projection *
|
||||||
|
* --------------------------------------------------------------------------- *
|
||||||
|
* Copyright (c) 2008-2017 OpenLP Developers *
|
||||||
|
* --------------------------------------------------------------------------- *
|
||||||
|
* This program is free software; you can redistribute it and/or modify it *
|
||||||
|
* under the terms of the GNU General Public License as published by the Free *
|
||||||
|
* Software Foundation; version 2 of the License. *
|
||||||
|
* *
|
||||||
|
* This program is distributed in the hope that it will be useful, but WITHOUT *
|
||||||
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or *
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for *
|
||||||
|
* more details. *
|
||||||
|
* *
|
||||||
|
* You should have received a copy of the GNU General Public License along *
|
||||||
|
* with this program; if not, write to the Free Software Foundation, Inc., 59 *
|
||||||
|
* Temple Place, Suite 330, Boston, MA 02111-1307 USA *
|
||||||
|
******************************************************************************/
|
||||||
|
|
||||||
|
#header {
|
||||||
|
padding-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#transpose,
|
||||||
|
#transposevalue,
|
||||||
|
#capodisplay {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 30pt;
|
||||||
|
color: gray;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
display: inline-block;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 1px solid gray;
|
||||||
|
border-radius: .3em;
|
||||||
|
padding: 0 .2em;
|
||||||
|
min-width: 1.2em;
|
||||||
|
line-height: 1.2em;
|
||||||
|
font-size: 25pt;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
text-shadow: 0px 1px 0px white;
|
||||||
|
color: black;
|
||||||
|
background: linear-gradient(to bottom, white 5%, gray 100%);
|
||||||
|
background-color: gray;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.button:hover {
|
||||||
|
background: linear-gradient(to bottom, white 10%, gray 150%);
|
||||||
|
color: darkslategray ;
|
||||||
|
background-color: gray;
|
||||||
|
}
|
||||||
|
.button:active {
|
||||||
|
position:relative;
|
||||||
|
top:1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Extending existing definition in stage.css */
|
||||||
|
#verseorder {
|
||||||
|
line-height: 1.5;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chordline {
|
||||||
|
line-height: 2.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chordline1 {
|
||||||
|
line-height: 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
.chordline span.chord span {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chordline span.chord span strong {
|
||||||
|
position: absolute;
|
||||||
|
top: -0.8em;
|
||||||
|
left: 0;
|
||||||
|
font-size: 30pt;
|
||||||
|
font-weight: normal;
|
||||||
|
line-height: normal;
|
||||||
|
color: yellow;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nextslide .chordline span.chord span strong {
|
||||||
|
color: gray;
|
||||||
|
}
|
|
@ -21,6 +21,10 @@ body {
|
||||||
background-color: black;
|
background-color: black;
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
-webkit-user-select: none; /* Chrome/Safari */
|
||||||
|
-moz-user-select: none; /* Firefox */
|
||||||
|
-ms-user-select: none; /* IE 10+ */
|
||||||
|
user-select: none; /* Future */
|
||||||
}
|
}
|
||||||
|
|
||||||
#currentslide {
|
#currentslide {
|
||||||
|
|
|
@ -0,0 +1,331 @@
|
||||||
|
/******************************************************************************
|
||||||
|
* OpenLP - Open Source Lyrics Projection *
|
||||||
|
* --------------------------------------------------------------------------- *
|
||||||
|
* Copyright (c) 2008-2017 OpenLP Developers *
|
||||||
|
* --------------------------------------------------------------------------- *
|
||||||
|
* This program is free software; you can redistribute it and/or modify it *
|
||||||
|
* under the terms of the GNU General Public License as published by the Free *
|
||||||
|
* Software Foundation; version 2 of the License. *
|
||||||
|
* *
|
||||||
|
* This program is distributed in the hope that it will be useful, but WITHOUT *
|
||||||
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or *
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for *
|
||||||
|
* more details. *
|
||||||
|
* *
|
||||||
|
* You should have received a copy of the GNU General Public License along *
|
||||||
|
* with this program; if not, write to the Free Software Foundation, Inc., 59 *
|
||||||
|
* Temple Place, Suite 330, Boston, MA 02111-1307 USA *
|
||||||
|
******************************************************************************/
|
||||||
|
var lastChord;
|
||||||
|
|
||||||
|
var notesSharpNotation = {}
|
||||||
|
var notesFlatNotation = {}
|
||||||
|
|
||||||
|
// See https://en.wikipedia.org/wiki/Musical_note#12-tone_chromatic_scale
|
||||||
|
notesSharpNotation['german'] = ['C','C#','D','D#','E','F','F#','G','G#','A','A#','H'];
|
||||||
|
notesFlatNotation['german'] = ['C','Db','D','Eb','Fb','F','Gb','G','Ab','A','B','H'];
|
||||||
|
notesSharpNotation['english'] = ['C','C#','D','D#','E','F','F#','G','G#','A','A#','B'];
|
||||||
|
notesFlatNotation['english'] = ['C','Db','D','Eb','Fb','F','Gb','G','Ab','A','Bb','B'];
|
||||||
|
notesSharpNotation['neo-latin'] = ['Do','Do#','Re','Re#','Mi','Fa','Fa#','Sol','Sol#','La','La#','Si'];
|
||||||
|
notesFlatNotation['neo-latin'] = ['Do','Reb','Re','Mib','Fab','Fa','Solb','Sol','Lab','La','Sib','Si'];
|
||||||
|
|
||||||
|
function getTransposeValue(songId) {
|
||||||
|
if (localStorage.getItem(songId + '_transposeValue')) {return localStorage.getItem(songId + '_transposeValue');}
|
||||||
|
else {return 0;}
|
||||||
|
}
|
||||||
|
|
||||||
|
function storeTransposeValue(songId,transposeValueToSet) {
|
||||||
|
localStorage.setItem(songId + '_transposeValue', transposeValueToSet);
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: This function has a python equivalent in openlp/plugins/songs/lib/__init__.py - make sure to update both!
|
||||||
|
function transposeChord(chord, transposeValue, notation) {
|
||||||
|
var chordSplit = chord.replace('♭', 'b').split(/[\/]/);
|
||||||
|
var transposedChord = '', note, notenumber, rest, currentChord;
|
||||||
|
var notesSharp = notesSharpNotation[notation];
|
||||||
|
var notesFlat = notesFlatNotation[notation];
|
||||||
|
var notesPreferred = ['b','#','#','#','#','#','#','#','#','#','#','#'];
|
||||||
|
for (i = 0; i <= chordSplit.length - 1; i++) {
|
||||||
|
if (i > 0) {
|
||||||
|
transposedChord += '/';
|
||||||
|
}
|
||||||
|
currentchord = chordSplit[i];
|
||||||
|
if (currentchord.length > 0 && currentchord.charAt(0) === '(') {
|
||||||
|
transposedChord += '(';
|
||||||
|
if (currentchord.length > 1) {
|
||||||
|
currentchord = currentchord.substr(1);
|
||||||
|
} else {
|
||||||
|
currentchord = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (currentchord.length > 0) {
|
||||||
|
if (currentchord.length > 1) {
|
||||||
|
if ('#b'.indexOf(currentchord.charAt(1)) === -1) {
|
||||||
|
note = currentchord.substr(0, 1);
|
||||||
|
rest = currentchord.substr(1);
|
||||||
|
} else {
|
||||||
|
note = currentchord.substr(0, 2);
|
||||||
|
rest = currentchord.substr(2);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
note = currentchord;
|
||||||
|
rest = "";
|
||||||
|
}
|
||||||
|
notenumber = (notesSharp.indexOf(note) === -1 ? notesFlat.indexOf(note) : notesSharp.indexOf(note));
|
||||||
|
notenumber += parseInt(transposeValue);
|
||||||
|
while (notenumber > 11) {
|
||||||
|
notenumber -= 12;
|
||||||
|
}
|
||||||
|
while (notenumber < 0) {
|
||||||
|
notenumber += 12;
|
||||||
|
}
|
||||||
|
if (i === 0) {
|
||||||
|
currentChord = notesPreferred[notenumber] === '#' ? notesSharp[notenumber] : notesFlat[notenumber];
|
||||||
|
lastChord = currentChord;
|
||||||
|
} else {
|
||||||
|
currentChord = notesSharp.indexOf(lastChord) === -1 ? notesFlat[notenumber] : notesSharp[notenumber];
|
||||||
|
}
|
||||||
|
if (!(notesFlat.indexOf(note) === -1 && notesSharp.indexOf(note) === -1)) {
|
||||||
|
transposedChord += currentChord + rest;
|
||||||
|
} else {
|
||||||
|
transposedChord += note + rest;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return transposedChord;
|
||||||
|
}
|
||||||
|
|
||||||
|
var OpenLPChordOverflowFillCount = 0;
|
||||||
|
window.OpenLP = {
|
||||||
|
showchords:true,
|
||||||
|
loadService: function (event) {
|
||||||
|
$.getJSON(
|
||||||
|
"/api/service/list",
|
||||||
|
function (data, status) {
|
||||||
|
OpenLP.nextSong = "";
|
||||||
|
$("#notes").html("");
|
||||||
|
for (idx in data.results.items) {
|
||||||
|
idx = parseInt(idx, 10);
|
||||||
|
if (data.results.items[idx]["selected"]) {
|
||||||
|
$("#notes").html(data.results.items[idx]["notes"].replace(/\n/g, "<br />"));
|
||||||
|
if (data.results.items.length > idx + 1) {
|
||||||
|
OpenLP.nextSong = data.results.items[idx + 1]["title"];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OpenLP.updateSlide();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
loadSlides: function (event) {
|
||||||
|
$.getJSON(
|
||||||
|
"/api/controller/live/text",
|
||||||
|
function (data, status) {
|
||||||
|
OpenLP.currentSlides = data.results.slides;
|
||||||
|
$('#transposevalue').text(getTransposeValue(OpenLP.currentSlides[0].text.split("\n")[0]));
|
||||||
|
OpenLP.currentSlide = 0;
|
||||||
|
OpenLP.currentTags = Array();
|
||||||
|
var div = $("#verseorder");
|
||||||
|
div.html("");
|
||||||
|
var tag = "";
|
||||||
|
var tags = 0;
|
||||||
|
var lastChange = 0;
|
||||||
|
$.each(data.results.slides, function(idx, slide) {
|
||||||
|
var prevtag = tag;
|
||||||
|
tag = slide["tag"];
|
||||||
|
if (tag != prevtag) {
|
||||||
|
// If the tag has changed, add new one to the list
|
||||||
|
lastChange = idx;
|
||||||
|
tags = tags + 1;
|
||||||
|
div.append(" <span>");
|
||||||
|
$("#verseorder span").last().attr("id", "tag" + tags).text(tag);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if ((slide["chords_text"] == data.results.slides[lastChange]["chords_text"]) &&
|
||||||
|
(data.results.slides.length > idx + (idx - lastChange))) {
|
||||||
|
// If the tag hasn't changed, check to see if the same verse
|
||||||
|
// has been repeated consecutively. Note the verse may have been
|
||||||
|
// split over several slides, so search through. If so, repeat the tag.
|
||||||
|
var match = true;
|
||||||
|
for (var idx2 = 0; idx2 < idx - lastChange; idx2++) {
|
||||||
|
if(data.results.slides[lastChange + idx2]["chords_text"] != data.results.slides[idx + idx2]["chords_text"]) {
|
||||||
|
match = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (match) {
|
||||||
|
lastChange = idx;
|
||||||
|
tags = tags + 1;
|
||||||
|
div.append(" <span>");
|
||||||
|
$("#verseorder span").last().attr("id", "tag" + tags).text(tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OpenLP.currentTags[idx] = tags;
|
||||||
|
if (slide["selected"])
|
||||||
|
OpenLP.currentSlide = idx;
|
||||||
|
})
|
||||||
|
OpenLP.loadService();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
updateSlide: function() {
|
||||||
|
// Show the current slide on top. Any trailing slides for the same verse
|
||||||
|
// are shown too underneath in grey.
|
||||||
|
// Then leave a blank line between following verses
|
||||||
|
var transposeValue = getTransposeValue(OpenLP.currentSlides[0].text.split("\n")[0]);
|
||||||
|
var chordclass=/class="[a-z\s]*chord[a-z\s]*"\s*style="display:\s?none"/g;
|
||||||
|
var chordclassshow='class="chord"';
|
||||||
|
var regchord=/<span class="chord"><span><strong>([\(\w#b♭\+\*\d/\)-]+)<\/strong><\/span><\/span>([\u0080-\uFFFF,\w]*)(<span class="ws">.+?<\/span>)?([\u0080-\uFFFF,\w,\s,\.,\,,\!,\?,\;,\:,\|,\",\',\-,\_]*)(<br>)?/g;
|
||||||
|
// NOTE: There is equivalent python code in openlp/core/lib/__init__.py, in the expand_and_align_chords_in_line function. Make sure to update both!
|
||||||
|
var replaceChords=function(mstr,$chord,$tail,$skips,$remainder,$end) {
|
||||||
|
var w='';
|
||||||
|
var $chordlen = 0;
|
||||||
|
var $taillen = 0;
|
||||||
|
var slimchars='fiíIÍjlĺľrtť.,;/ ()|"\'!:\\';
|
||||||
|
// Transpose chord as dictated by the transpose value in local storage
|
||||||
|
if (transposeValue != 0) {
|
||||||
|
$chord = transposeChord($chord, transposeValue, OpenLP.chordNotation);
|
||||||
|
}
|
||||||
|
for (var i = 0; i < $chord.length; i++) if (slimchars.indexOf($chord.charAt(i)) === -1) {$chordlen += 2;} else {$chordlen += 1;}
|
||||||
|
for (var i = 0; i < $tail.length; i++) if (slimchars.indexOf($tail.charAt(i)) === -1) {$taillen += 2;} else {$taillen += 1;}
|
||||||
|
for (var i = 0; i < $remainder.length; i++) if (slimchars.indexOf($tail.charAt(i)) === -1) {$taillen += 2;} else {$taillen += 1;}
|
||||||
|
if ($chordlen >= $taillen && !$end) {
|
||||||
|
if ($tail.length){
|
||||||
|
if (!$remainder.length) {
|
||||||
|
for (c = 0; c < Math.ceil(($chordlen - $taillen) / 2) + 1; c++) {w += '_';}
|
||||||
|
} else {
|
||||||
|
for (c = 0; c < $chordlen - $taillen + 2; c++) {w += ' ';}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!$remainder.length) {
|
||||||
|
for (c = 0; c < Math.floor(($chordlen - $taillen) / 2) + 1; c++) {w += '_';}
|
||||||
|
} else {
|
||||||
|
for (c = 0; c < $chordlen - $taillen + 1; c++) {w += ' ';}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
if (!$tail && $remainder.charAt(0) == ' ') {for (c = 0; c < $chordlen; c++) {w += ' ';}}
|
||||||
|
}
|
||||||
|
if (w!='') {
|
||||||
|
if (w[0] == '_') {
|
||||||
|
ws_length = w.length;
|
||||||
|
if (ws_length==1) {
|
||||||
|
w = '–';
|
||||||
|
} else {
|
||||||
|
wsl_mod = Math.floor(ws_length / 2);
|
||||||
|
ws_right = ws_left = new Array(wsl_mod + 1).join(' ');
|
||||||
|
w = ws_left + '–' + ws_right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w='<span class="ws">' + w + '</span>';
|
||||||
|
}
|
||||||
|
return $.grep(['<span class="chord"><span><strong>', $chord, '</strong></span></span>', $tail, w, $remainder, $end], Boolean).join('');
|
||||||
|
};
|
||||||
|
$("#verseorder span").removeClass("currenttag");
|
||||||
|
$("#tag" + OpenLP.currentTags[OpenLP.currentSlide]).addClass("currenttag");
|
||||||
|
var slide = OpenLP.currentSlides[OpenLP.currentSlide];
|
||||||
|
var text = "";
|
||||||
|
// use title if available
|
||||||
|
if (slide["title"]) {
|
||||||
|
text = slide["title"];
|
||||||
|
} else {
|
||||||
|
text = slide["chords_text"];
|
||||||
|
if(OpenLP.showchords) {
|
||||||
|
text = text.replace(chordclass,chordclassshow);
|
||||||
|
text = text.replace(regchord, replaceChords);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// use thumbnail if available
|
||||||
|
if (slide["img"]) {
|
||||||
|
text += "<br /><img src='" + slide["img"].replace("/thumbnails/", "/thumbnails320x240/") + "'><br />";
|
||||||
|
}
|
||||||
|
// use notes if available
|
||||||
|
if (slide["slide_notes"]) {
|
||||||
|
text += '<br />' + slide["slide_notes"];
|
||||||
|
}
|
||||||
|
text = text.replace(/\n/g, "<br />");
|
||||||
|
$("#currentslide").html(text);
|
||||||
|
text = "";
|
||||||
|
if (OpenLP.currentSlide < OpenLP.currentSlides.length - 1) {
|
||||||
|
for (var idx = OpenLP.currentSlide + 1; idx < OpenLP.currentSlides.length; idx++) {
|
||||||
|
if (OpenLP.currentTags[idx] != OpenLP.currentTags[idx - 1])
|
||||||
|
text = text + "<p class=\"nextslide\">";
|
||||||
|
if (OpenLP.currentSlides[idx]["title"]) {
|
||||||
|
text = text + OpenLP.currentSlides[idx]["title"];
|
||||||
|
} else {
|
||||||
|
text = text + OpenLP.currentSlides[idx]["chords_text"];
|
||||||
|
if(OpenLP.showchords) {
|
||||||
|
text = text.replace(chordclass,chordclassshow);
|
||||||
|
text = text.replace(regchord, replaceChords);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (OpenLP.currentTags[idx] != OpenLP.currentTags[idx - 1])
|
||||||
|
text = text + "</p>";
|
||||||
|
else
|
||||||
|
text = text + "<br />";
|
||||||
|
}
|
||||||
|
text = text.replace(/\n/g, "<br />");
|
||||||
|
$("#nextslide").html(text);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
text = "<p class=\"nextslide\">" + $("#next-text").val() + ": " + OpenLP.nextSong + "</p>";
|
||||||
|
$("#nextslide").html(text);
|
||||||
|
}
|
||||||
|
if(!OpenLP.showchords) {
|
||||||
|
$(".chordline").toggleClass('chordline1');
|
||||||
|
$(".chord").toggle();
|
||||||
|
$(".ws").toggle();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateClock: function(data) {
|
||||||
|
var div = $("#clock");
|
||||||
|
var t = new Date();
|
||||||
|
var h = t.getHours();
|
||||||
|
if (data.results.twelve && h > 12)
|
||||||
|
h = h - 12;
|
||||||
|
if (h < 10) h = '0' + h + '';
|
||||||
|
var m = t.getMinutes();
|
||||||
|
if (m < 10)
|
||||||
|
m = '0' + m + '';
|
||||||
|
div.html(h + ":" + m);
|
||||||
|
},
|
||||||
|
pollServer: function () {
|
||||||
|
$.getJSON(
|
||||||
|
"/api/poll",
|
||||||
|
function (data, status) {
|
||||||
|
OpenLP.updateClock(data);
|
||||||
|
OpenLP.chordNotation = data.results.chordNotation;
|
||||||
|
if (OpenLP.currentItem != data.results.item || OpenLP.currentService != data.results.service) {
|
||||||
|
OpenLP.currentItem = data.results.item;
|
||||||
|
OpenLP.currentService = data.results.service;
|
||||||
|
OpenLP.loadSlides();
|
||||||
|
}
|
||||||
|
else if (OpenLP.currentSlide != data.results.slide) {
|
||||||
|
OpenLP.currentSlide = parseInt(data.results.slide, 10);
|
||||||
|
OpenLP.updateSlide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$.ajaxSetup({ cache: false });
|
||||||
|
setInterval("OpenLP.pollServer();", 500);
|
||||||
|
OpenLP.pollServer();
|
||||||
|
$(document).ready(function() {
|
||||||
|
$('#transposeup').click(function(e) {
|
||||||
|
$('#transposevalue').text(parseInt($('#transposevalue').text()) + 1);
|
||||||
|
storeTransposeValue(OpenLP.currentSlides[0].text.split("\n")[0], $('#transposevalue').text());
|
||||||
|
OpenLP.loadSlides();
|
||||||
|
});
|
||||||
|
$('#transposedown').click(function(e) {
|
||||||
|
$('#transposevalue').text(parseInt($('#transposevalue').text()) - 1);
|
||||||
|
storeTransposeValue(OpenLP.currentSlides[0].text.split("\n")[0], $('#transposevalue').text());
|
||||||
|
OpenLP.loadSlides();
|
||||||
|
});
|
||||||
|
$('#chords').click(function () {
|
||||||
|
OpenLP.showchords = OpenLP.showchords ? false : true;
|
||||||
|
OpenLP.loadSlides();
|
||||||
|
});
|
||||||
|
});
|
|
@ -152,6 +152,7 @@ class HttpRouter(RegistryProperties):
|
||||||
('^/$', {'function': self.serve_file, 'secure': False}),
|
('^/$', {'function': self.serve_file, 'secure': False}),
|
||||||
('^/(stage)$', {'function': self.serve_file, 'secure': False}),
|
('^/(stage)$', {'function': self.serve_file, 'secure': False}),
|
||||||
('^/(stage)/(.*)$', {'function': self.stages, 'secure': False}),
|
('^/(stage)/(.*)$', {'function': self.stages, 'secure': False}),
|
||||||
|
('^/(chords)$', {'function': self.serve_file, 'secure': False}),
|
||||||
('^/(main)$', {'function': self.serve_file, 'secure': False}),
|
('^/(main)$', {'function': self.serve_file, 'secure': False}),
|
||||||
(r'^/(\w+)/thumbnails([^/]+)?/(.*)$', {'function': self.serve_thumbnail, 'secure': False}),
|
(r'^/(\w+)/thumbnails([^/]+)?/(.*)$', {'function': self.serve_thumbnail, 'secure': False}),
|
||||||
(r'^/api/poll$', {'function': self.poll, 'secure': False}),
|
(r'^/api/poll$', {'function': self.poll, 'secure': False}),
|
||||||
|
@ -318,10 +319,12 @@ class HttpRouter(RegistryProperties):
|
||||||
"""
|
"""
|
||||||
remote = translate('RemotePlugin.Mobile', 'Remote')
|
remote = translate('RemotePlugin.Mobile', 'Remote')
|
||||||
stage = translate('RemotePlugin.Mobile', 'Stage View')
|
stage = translate('RemotePlugin.Mobile', 'Stage View')
|
||||||
|
chords = translate('RemotePlugin.Mobile', 'Chords View')
|
||||||
live = translate('RemotePlugin.Mobile', 'Live View')
|
live = translate('RemotePlugin.Mobile', 'Live View')
|
||||||
self.template_vars = {
|
self.template_vars = {
|
||||||
'app_title': "{main} {remote}".format(main=UiStrings().OLPV2x, remote=remote),
|
'app_title': "{main} {remote}".format(main=UiStrings().OLPV2x, remote=remote),
|
||||||
'stage_title': "{main} {stage}".format(main=UiStrings().OLPV2x, stage=stage),
|
'stage_title': "{main} {stage}".format(main=UiStrings().OLPV2x, stage=stage),
|
||||||
|
'chords_title': "{main} {chords}".format(main=UiStrings().OLPV2x, chords=chords),
|
||||||
'live_title': "{main} {live}".format(main=UiStrings().OLPV2x, live=live),
|
'live_title': "{main} {live}".format(main=UiStrings().OLPV2x, live=live),
|
||||||
'service_manager': translate('RemotePlugin.Mobile', 'Service Manager'),
|
'service_manager': translate('RemotePlugin.Mobile', 'Service Manager'),
|
||||||
'slide_controller': translate('RemotePlugin.Mobile', 'Slide Controller'),
|
'slide_controller': translate('RemotePlugin.Mobile', 'Slide Controller'),
|
||||||
|
@ -482,7 +485,8 @@ class HttpRouter(RegistryProperties):
|
||||||
'display': self.live_controller.desktop_screen.isChecked(),
|
'display': self.live_controller.desktop_screen.isChecked(),
|
||||||
'version': 2,
|
'version': 2,
|
||||||
'isSecure': Settings().value(self.settings_section + '/authentication enabled'),
|
'isSecure': Settings().value(self.settings_section + '/authentication enabled'),
|
||||||
'isAuthorised': self.authorised
|
'isAuthorised': self.authorised,
|
||||||
|
'chordNotation': Settings().value('songs/chord notation'),
|
||||||
}
|
}
|
||||||
self.do_json_header()
|
self.do_json_header()
|
||||||
return json.dumps({'results': result}).encode()
|
return json.dumps({'results': result}).encode()
|
||||||
|
@ -554,6 +558,7 @@ class HttpRouter(RegistryProperties):
|
||||||
item['tag'] = str(frame['verseTag'])
|
item['tag'] = str(frame['verseTag'])
|
||||||
else:
|
else:
|
||||||
item['tag'] = str(index + 1)
|
item['tag'] = str(index + 1)
|
||||||
|
item['chords_text'] = str(frame['chords_text'])
|
||||||
item['text'] = str(frame['text'])
|
item['text'] = str(frame['text'])
|
||||||
item['html'] = str(frame['html'])
|
item['html'] = str(frame['html'])
|
||||||
# Handle images, unless a custom thumbnail is given or if thumbnails is disabled
|
# Handle images, unless a custom thumbnail is given or if thumbnails is disabled
|
||||||
|
|
|
@ -81,6 +81,12 @@ class RemoteTab(SettingsTab):
|
||||||
self.stage_url.setObjectName('stage_url')
|
self.stage_url.setObjectName('stage_url')
|
||||||
self.stage_url.setOpenExternalLinks(True)
|
self.stage_url.setOpenExternalLinks(True)
|
||||||
self.http_setting_layout.addRow(self.stage_url_label, self.stage_url)
|
self.http_setting_layout.addRow(self.stage_url_label, self.stage_url)
|
||||||
|
self.chords_url_label = QtWidgets.QLabel(self.http_settings_group_box)
|
||||||
|
self.chords_url_label.setObjectName('chords_url_label')
|
||||||
|
self.chords_url = QtWidgets.QLabel(self.http_settings_group_box)
|
||||||
|
self.chords_url.setObjectName('chords_url')
|
||||||
|
self.chords_url.setOpenExternalLinks(True)
|
||||||
|
self.http_setting_layout.addRow(self.chords_url_label, self.chords_url)
|
||||||
self.live_url_label = QtWidgets.QLabel(self.http_settings_group_box)
|
self.live_url_label = QtWidgets.QLabel(self.http_settings_group_box)
|
||||||
self.live_url_label.setObjectName('live_url_label')
|
self.live_url_label.setObjectName('live_url_label')
|
||||||
self.live_url = QtWidgets.QLabel(self.http_settings_group_box)
|
self.live_url = QtWidgets.QLabel(self.http_settings_group_box)
|
||||||
|
@ -148,6 +154,7 @@ class RemoteTab(SettingsTab):
|
||||||
self.port_label.setText(translate('RemotePlugin.RemoteTab', 'Port number:'))
|
self.port_label.setText(translate('RemotePlugin.RemoteTab', 'Port number:'))
|
||||||
self.remote_url_label.setText(translate('RemotePlugin.RemoteTab', 'Remote URL:'))
|
self.remote_url_label.setText(translate('RemotePlugin.RemoteTab', 'Remote URL:'))
|
||||||
self.stage_url_label.setText(translate('RemotePlugin.RemoteTab', 'Stage view URL:'))
|
self.stage_url_label.setText(translate('RemotePlugin.RemoteTab', 'Stage view URL:'))
|
||||||
|
self.chords_url_label.setText(translate('RemotePlugin.RemoteTab', 'Chords view URL:'))
|
||||||
self.live_url_label.setText(translate('RemotePlugin.RemoteTab', 'Live view URL:'))
|
self.live_url_label.setText(translate('RemotePlugin.RemoteTab', 'Live view URL:'))
|
||||||
self.twelve_hour_check_box.setText(translate('RemotePlugin.RemoteTab', 'Display stage time in 12h format'))
|
self.twelve_hour_check_box.setText(translate('RemotePlugin.RemoteTab', 'Display stage time in 12h format'))
|
||||||
self.thumbnails_check_box.setText(translate('RemotePlugin.RemoteTab',
|
self.thumbnails_check_box.setText(translate('RemotePlugin.RemoteTab',
|
||||||
|
|
|
@ -25,6 +25,7 @@ from PyQt5 import QtWidgets
|
||||||
from openlp.core.ui.lib import SpellTextEdit
|
from openlp.core.ui.lib import SpellTextEdit
|
||||||
from openlp.core.lib import build_icon, translate
|
from openlp.core.lib import build_icon, translate
|
||||||
from openlp.core.lib.ui import UiStrings, create_button_box
|
from openlp.core.lib.ui import UiStrings, create_button_box
|
||||||
|
from openlp.core.common import Settings
|
||||||
from openlp.plugins.songs.lib import VerseType
|
from openlp.plugins.songs.lib import VerseType
|
||||||
|
|
||||||
|
|
||||||
|
@ -63,6 +64,21 @@ class Ui_EditVerseDialog(object):
|
||||||
self.verse_type_layout.addWidget(self.insert_button)
|
self.verse_type_layout.addWidget(self.insert_button)
|
||||||
self.verse_type_layout.addStretch()
|
self.verse_type_layout.addStretch()
|
||||||
self.dialog_layout.addLayout(self.verse_type_layout)
|
self.dialog_layout.addLayout(self.verse_type_layout)
|
||||||
|
if Settings().value('songs/enable chords'):
|
||||||
|
self.transpose_layout = QtWidgets.QHBoxLayout()
|
||||||
|
self.transpose_layout.setObjectName('transpose_layout')
|
||||||
|
self.transpose_label = QtWidgets.QLabel(edit_verse_dialog)
|
||||||
|
self.transpose_label.setObjectName('transpose_label')
|
||||||
|
self.transpose_layout.addWidget(self.transpose_label)
|
||||||
|
self.transpose_up_button = QtWidgets.QPushButton(edit_verse_dialog)
|
||||||
|
self.transpose_up_button.setIcon(build_icon(':/services/service_up.png'))
|
||||||
|
self.transpose_up_button.setObjectName('transpose_up')
|
||||||
|
self.transpose_layout.addWidget(self.transpose_up_button)
|
||||||
|
self.transpose_down_button = QtWidgets.QPushButton(edit_verse_dialog)
|
||||||
|
self.transpose_down_button.setIcon(build_icon(':/services/service_down.png'))
|
||||||
|
self.transpose_down_button.setObjectName('transpose_down')
|
||||||
|
self.transpose_layout.addWidget(self.transpose_down_button)
|
||||||
|
self.dialog_layout.addLayout(self.transpose_layout)
|
||||||
self.button_box = create_button_box(edit_verse_dialog, 'button_box', ['cancel', 'ok'])
|
self.button_box = create_button_box(edit_verse_dialog, 'button_box', ['cancel', 'ok'])
|
||||||
self.dialog_layout.addWidget(self.button_box)
|
self.dialog_layout.addWidget(self.button_box)
|
||||||
self.retranslateUi(edit_verse_dialog)
|
self.retranslateUi(edit_verse_dialog)
|
||||||
|
@ -82,3 +98,7 @@ class Ui_EditVerseDialog(object):
|
||||||
self.insert_button.setText(translate('SongsPlugin.EditVerseForm', '&Insert'))
|
self.insert_button.setText(translate('SongsPlugin.EditVerseForm', '&Insert'))
|
||||||
self.insert_button.setToolTip(translate('SongsPlugin.EditVerseForm',
|
self.insert_button.setToolTip(translate('SongsPlugin.EditVerseForm',
|
||||||
'Split a slide into two by inserting a verse splitter.'))
|
'Split a slide into two by inserting a verse splitter.'))
|
||||||
|
if Settings().value('songs/enable chords'):
|
||||||
|
self.transpose_label.setText(translate('SongsPlugin.EditVerseForm', 'Transpose:'))
|
||||||
|
self.transpose_up_button.setText(translate('SongsPlugin.EditVerseForm', 'Up'))
|
||||||
|
self.transpose_down_button.setText(translate('SongsPlugin.EditVerseForm', 'Down'))
|
||||||
|
|
|
@ -25,7 +25,9 @@ import logging
|
||||||
|
|
||||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||||
|
|
||||||
from openlp.plugins.songs.lib import VerseType
|
from openlp.plugins.songs.lib import VerseType, transpose_lyrics
|
||||||
|
from openlp.core.lib.ui import critical_error_message_box
|
||||||
|
from openlp.core.common import translate, Settings
|
||||||
from .editversedialog import Ui_EditVerseDialog
|
from .editversedialog import Ui_EditVerseDialog
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
@ -48,6 +50,9 @@ class EditVerseForm(QtWidgets.QDialog, Ui_EditVerseDialog):
|
||||||
self.split_button.clicked.connect(self.on_split_button_clicked)
|
self.split_button.clicked.connect(self.on_split_button_clicked)
|
||||||
self.verse_text_edit.cursorPositionChanged.connect(self.on_cursor_position_changed)
|
self.verse_text_edit.cursorPositionChanged.connect(self.on_cursor_position_changed)
|
||||||
self.verse_type_combo_box.currentIndexChanged.connect(self.on_verse_type_combo_box_changed)
|
self.verse_type_combo_box.currentIndexChanged.connect(self.on_verse_type_combo_box_changed)
|
||||||
|
if Settings().value('songs/enable chords'):
|
||||||
|
self.transpose_down_button.clicked.connect(self.on_transepose_down_button_clicked)
|
||||||
|
self.transpose_up_button.clicked.connect(self.on_transepose_up_button_clicked)
|
||||||
|
|
||||||
def insert_verse(self, verse_tag, verse_num=1):
|
def insert_verse(self, verse_tag, verse_num=1):
|
||||||
"""
|
"""
|
||||||
|
@ -95,6 +100,41 @@ class EditVerseForm(QtWidgets.QDialog, Ui_EditVerseDialog):
|
||||||
"""
|
"""
|
||||||
self.update_suggested_verse_number()
|
self.update_suggested_verse_number()
|
||||||
|
|
||||||
|
def on_transepose_up_button_clicked(self):
|
||||||
|
"""
|
||||||
|
The transpose up button clicked
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
transposed_lyrics = transpose_lyrics(self.verse_text_edit.toPlainText(), 1)
|
||||||
|
self.verse_text_edit.setPlainText(transposed_lyrics)
|
||||||
|
except ValueError as ve:
|
||||||
|
# Transposing failed
|
||||||
|
critical_error_message_box(title=translate('SongsPlugin.EditVerseForm', 'Transposing failed'),
|
||||||
|
message=translate('SongsPlugin.EditVerseForm',
|
||||||
|
'Transposing failed because of invalid chord:\n{err_msg}'
|
||||||
|
.format(err_msg=ve)))
|
||||||
|
return
|
||||||
|
self.verse_text_edit.setFocus()
|
||||||
|
self.verse_text_edit.moveCursor(QtGui.QTextCursor.End)
|
||||||
|
|
||||||
|
def on_transepose_down_button_clicked(self):
|
||||||
|
"""
|
||||||
|
The transpose down button clicked
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
transposed_lyrics = transpose_lyrics(self.verse_text_edit.toPlainText(), -1)
|
||||||
|
self.verse_text_edit.setPlainText(transposed_lyrics)
|
||||||
|
except ValueError as ve:
|
||||||
|
# Transposing failed
|
||||||
|
critical_error_message_box(title=translate('SongsPlugin.EditVerseForm', 'Transposing failed'),
|
||||||
|
message=translate('SongsPlugin.EditVerseForm',
|
||||||
|
'Transposing failed because of invalid chord:\n{err_msg}'
|
||||||
|
.format(err_msg=ve)))
|
||||||
|
return
|
||||||
|
self.verse_text_edit.setPlainText(transposed_lyrics)
|
||||||
|
self.verse_text_edit.setFocus()
|
||||||
|
self.verse_text_edit.moveCursor(QtGui.QTextCursor.End)
|
||||||
|
|
||||||
def update_suggested_verse_number(self):
|
def update_suggested_verse_number(self):
|
||||||
"""
|
"""
|
||||||
Adjusts the verse number SpinBox in regard to the selected verse type and the cursor's position.
|
Adjusts the verse number SpinBox in regard to the selected verse type and the cursor's position.
|
||||||
|
@ -169,3 +209,20 @@ class EditVerseForm(QtWidgets.QDialog, Ui_EditVerseDialog):
|
||||||
if not text.startswith('---['):
|
if not text.startswith('---['):
|
||||||
text = '---[{tag}:1]---\n{text}'.format(tag=VerseType.translated_names[VerseType.Verse], text=text)
|
text = '---[{tag}:1]---\n{text}'.format(tag=VerseType.translated_names[VerseType.Verse], text=text)
|
||||||
return text
|
return text
|
||||||
|
|
||||||
|
def accept(self):
|
||||||
|
"""
|
||||||
|
Test if any invalid chords has been entered before closing the verse editor
|
||||||
|
"""
|
||||||
|
if Settings().value('songs/enable chords'):
|
||||||
|
try:
|
||||||
|
transposed_lyrics = transpose_lyrics(self.verse_text_edit.toPlainText(), 1)
|
||||||
|
super(EditVerseForm, self).accept()
|
||||||
|
except ValueError as ve:
|
||||||
|
# Transposing failed
|
||||||
|
critical_error_message_box(title=translate('SongsPlugin.EditVerseForm', 'Invalid Chord'),
|
||||||
|
message=translate('SongsPlugin.EditVerseForm',
|
||||||
|
'An invalid chord was detected:\n{err_msg}'
|
||||||
|
.format(err_msg=ve)))
|
||||||
|
else:
|
||||||
|
super(EditVerseForm, self).accept()
|
||||||
|
|
|
@ -29,7 +29,7 @@ import re
|
||||||
|
|
||||||
from PyQt5 import QtWidgets
|
from PyQt5 import QtWidgets
|
||||||
|
|
||||||
from openlp.core.common import AppLocation, CONTROL_CHARS
|
from openlp.core.common import AppLocation, CONTROL_CHARS, Settings
|
||||||
from openlp.core.lib import translate, clean_tags
|
from openlp.core.lib import translate, clean_tags
|
||||||
from openlp.plugins.songs.lib.db import Author, MediaFile, Song, Topic
|
from openlp.plugins.songs.lib.db import Author, MediaFile, Song, Topic
|
||||||
from openlp.plugins.songs.lib.ui import SongStrings
|
from openlp.plugins.songs.lib.ui import SongStrings
|
||||||
|
@ -380,7 +380,7 @@ def clean_song(manager, song):
|
||||||
if isinstance(song.lyrics, bytes):
|
if isinstance(song.lyrics, bytes):
|
||||||
song.lyrics = str(song.lyrics, encoding='utf8')
|
song.lyrics = str(song.lyrics, encoding='utf8')
|
||||||
verses = SongXML().get_verses(song.lyrics)
|
verses = SongXML().get_verses(song.lyrics)
|
||||||
song.search_lyrics = ' '.join([clean_string(clean_tags(verse[1])) for verse in verses])
|
song.search_lyrics = ' '.join([clean_string(clean_tags(verse[1], True)) for verse in verses])
|
||||||
# The song does not have any author, add one.
|
# The song does not have any author, add one.
|
||||||
if not song.authors_songs:
|
if not song.authors_songs:
|
||||||
name = SongStrings.AuthorUnknown
|
name = SongStrings.AuthorUnknown
|
||||||
|
@ -512,10 +512,13 @@ def strip_rtf(text, default_encoding=None):
|
||||||
elif not ignorable:
|
elif not ignorable:
|
||||||
ebytes.append(int(hex_, 16))
|
ebytes.append(int(hex_, 16))
|
||||||
elif tchar:
|
elif tchar:
|
||||||
if curskip > 0:
|
if not ignorable:
|
||||||
curskip -= 1
|
|
||||||
elif not ignorable:
|
|
||||||
ebytes += tchar.encode()
|
ebytes += tchar.encode()
|
||||||
|
if len(ebytes) >= curskip:
|
||||||
|
ebytes = ebytes[curskip:]
|
||||||
|
else:
|
||||||
|
curskip -= len(ebytes)
|
||||||
|
ebytes = ""
|
||||||
text = ''.join(out)
|
text = ''.join(out)
|
||||||
return text, default_encoding
|
return text, default_encoding
|
||||||
|
|
||||||
|
@ -541,3 +544,123 @@ def delete_song(song_id, song_plugin):
|
||||||
except OSError:
|
except OSError:
|
||||||
log.exception('Could not remove directory: {path}'.format(path=save_path))
|
log.exception('Could not remove directory: {path}'.format(path=save_path))
|
||||||
song_plugin.manager.delete_object(Song, song_id)
|
song_plugin.manager.delete_object(Song, song_id)
|
||||||
|
|
||||||
|
|
||||||
|
def transpose_lyrics(lyrics, transepose_value):
|
||||||
|
"""
|
||||||
|
Transepose lyrics
|
||||||
|
|
||||||
|
:param lyrcs: The lyrics to be transposed
|
||||||
|
:param transepose_value: The value to transpose the lyrics with
|
||||||
|
:return: The transposed lyrics
|
||||||
|
"""
|
||||||
|
# Split text by verse delimiter - both normal and optional
|
||||||
|
verse_list = re.split('(---\[.+?:.+?\]---|\[---\])', lyrics)
|
||||||
|
transposed_lyrics = ''
|
||||||
|
notation = Settings().value('songs/chord notation')
|
||||||
|
for verse in verse_list:
|
||||||
|
if verse.startswith('---[') or verse == '[---]':
|
||||||
|
transposed_lyrics += verse
|
||||||
|
else:
|
||||||
|
transposed_lyrics += transpose_verse(verse, transepose_value, notation)
|
||||||
|
return transposed_lyrics
|
||||||
|
|
||||||
|
|
||||||
|
def transpose_verse(verse_text, transepose_value, notation):
|
||||||
|
"""
|
||||||
|
Transepose lyrics
|
||||||
|
|
||||||
|
:param lyrcs: The lyrics to be transposed
|
||||||
|
:param transepose_value: The value to transpose the lyrics with
|
||||||
|
:return: The transposed lyrics
|
||||||
|
"""
|
||||||
|
if '[' not in verse_text:
|
||||||
|
return verse_text
|
||||||
|
# Split the lyrics based on chord tags
|
||||||
|
lyric_list = re.split('(\[|\]|/)', verse_text)
|
||||||
|
transposed_lyrics = ''
|
||||||
|
in_tag = False
|
||||||
|
for word in lyric_list:
|
||||||
|
if not in_tag:
|
||||||
|
transposed_lyrics += word
|
||||||
|
if word == '[':
|
||||||
|
in_tag = True
|
||||||
|
else:
|
||||||
|
if word == ']':
|
||||||
|
in_tag = False
|
||||||
|
transposed_lyrics += word
|
||||||
|
elif word == '/':
|
||||||
|
transposed_lyrics += word
|
||||||
|
else:
|
||||||
|
# This MUST be a chord
|
||||||
|
transposed_lyrics += transpose_chord(word, transepose_value, notation)
|
||||||
|
# If still inside a chord tag something is wrong!
|
||||||
|
if in_tag:
|
||||||
|
return verse_text
|
||||||
|
else:
|
||||||
|
return transposed_lyrics
|
||||||
|
|
||||||
|
|
||||||
|
def transpose_chord(chord, transpose_value, notation):
|
||||||
|
"""
|
||||||
|
Transpose chord according to the notation used.
|
||||||
|
NOTE: This function has a javascript equivalent in chords.js - make sure to update both!
|
||||||
|
|
||||||
|
:param chord: The chord to transpose.
|
||||||
|
:param transpose_value: The value the chord should be transposed.
|
||||||
|
:param notation: The notation to use when transposing.
|
||||||
|
:return: The transposed chord.
|
||||||
|
"""
|
||||||
|
# See https://en.wikipedia.org/wiki/Musical_note#12-tone_chromatic_scale
|
||||||
|
notes_sharp_notation = {}
|
||||||
|
notes_flat_notation = {}
|
||||||
|
notes_sharp_notation['german'] = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'H']
|
||||||
|
notes_flat_notation['german'] = ['C', 'Db', 'D', 'Eb', 'Fb', 'F', 'Gb', 'G', 'Ab', 'A', 'B', 'H']
|
||||||
|
notes_sharp_notation['english'] = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']
|
||||||
|
notes_flat_notation['english'] = ['C', 'Db', 'D', 'Eb', 'Fb', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B']
|
||||||
|
notes_sharp_notation['neo-latin'] = ['Do', 'Do#', 'Re', 'Re#', 'Mi', 'Fa', 'Fa#', 'Sol', 'Sol#', 'La', 'La#', 'Si']
|
||||||
|
notes_flat_notation['neo-latin'] = ['Do', 'Reb', 'Re', 'Mib', 'Fab', 'Fa', 'Solb', 'Sol', 'Lab', 'La', 'Sib', 'Si']
|
||||||
|
chord_split = chord.replace('♭', 'b').split('/')
|
||||||
|
transposed_chord = ''
|
||||||
|
last_chord = ''
|
||||||
|
notes_sharp = notes_sharp_notation[notation]
|
||||||
|
notes_flat = notes_flat_notation[notation]
|
||||||
|
notes_preferred = ['b', '#', '#', '#', '#', '#', '#', '#', '#', '#', '#', '#']
|
||||||
|
for i in range(0, len(chord_split)):
|
||||||
|
if i > 0:
|
||||||
|
transposed_chord += '/'
|
||||||
|
currentchord = chord_split[i]
|
||||||
|
if currentchord and currentchord[0] == '(':
|
||||||
|
transposed_chord += '('
|
||||||
|
if len(currentchord) > 1:
|
||||||
|
currentchord = currentchord[1:]
|
||||||
|
else:
|
||||||
|
currentchord = ''
|
||||||
|
if len(currentchord) > 0:
|
||||||
|
if len(currentchord) > 1:
|
||||||
|
if '#b'.find(currentchord[1]) == -1:
|
||||||
|
note = currentchord[0:1]
|
||||||
|
rest = currentchord[1:]
|
||||||
|
else:
|
||||||
|
note = currentchord[0:2]
|
||||||
|
rest = currentchord[2:]
|
||||||
|
else:
|
||||||
|
note = currentchord
|
||||||
|
rest = ''
|
||||||
|
notenumber = notes_flat.index(note) if note not in notes_sharp else notes_sharp.index(note)
|
||||||
|
notenumber += transpose_value
|
||||||
|
while notenumber > 11:
|
||||||
|
notenumber -= 12
|
||||||
|
while notenumber < 0:
|
||||||
|
notenumber += 12
|
||||||
|
if i == 0:
|
||||||
|
current_chord = notes_sharp[notenumber] if notes_preferred[notenumber] == '#' else notes_flat[
|
||||||
|
notenumber]
|
||||||
|
last_chord = current_chord
|
||||||
|
else:
|
||||||
|
current_chord = notes_flat[notenumber] if last_chord not in notes_sharp else notes_sharp[notenumber]
|
||||||
|
if not (note not in notes_flat and note not in notes_sharp):
|
||||||
|
transposed_chord += current_chord + rest
|
||||||
|
else:
|
||||||
|
transposed_chord += note + rest
|
||||||
|
return transposed_chord
|
||||||
|
|
|
@ -48,6 +48,7 @@ from .importers.powerpraise import PowerPraiseImport
|
||||||
from .importers.presentationmanager import PresentationManagerImport
|
from .importers.presentationmanager import PresentationManagerImport
|
||||||
from .importers.lyrix import LyrixImport
|
from .importers.lyrix import LyrixImport
|
||||||
from .importers.videopsalm import VideoPsalmImport
|
from .importers.videopsalm import VideoPsalmImport
|
||||||
|
from .importers.chordpro import ChordProImport
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -155,29 +156,31 @@ class SongFormat(object):
|
||||||
OpenLP2 = 1
|
OpenLP2 = 1
|
||||||
Generic = 2
|
Generic = 2
|
||||||
CCLI = 3
|
CCLI = 3
|
||||||
DreamBeam = 4
|
ChordPro = 4
|
||||||
EasySlides = 5
|
DreamBeam = 5
|
||||||
EasyWorshipDB = 6
|
EasySlides = 6
|
||||||
EasyWorshipService = 7
|
EasyWorshipDB = 7
|
||||||
FoilPresenter = 8
|
EasyWorshipSqliteDB = 8
|
||||||
Lyrix = 9
|
EasyWorshipService = 9
|
||||||
MediaShout = 10
|
FoilPresenter = 10
|
||||||
OpenSong = 11
|
Lyrix = 11
|
||||||
OPSPro = 12
|
MediaShout = 12
|
||||||
PowerPraise = 13
|
OpenSong = 13
|
||||||
PowerSong = 14
|
OPSPro = 14
|
||||||
PresentationManager = 15
|
PowerPraise = 15
|
||||||
ProPresenter = 16
|
PowerSong = 16
|
||||||
SongBeamer = 17
|
PresentationManager = 17
|
||||||
SongPro = 18
|
ProPresenter = 18
|
||||||
SongShowPlus = 19
|
SongBeamer = 19
|
||||||
SongsOfFellowship = 20
|
SongPro = 20
|
||||||
SundayPlus = 21
|
SongShowPlus = 21
|
||||||
VideoPsalm = 22
|
SongsOfFellowship = 22
|
||||||
WordsOfWorship = 23
|
SundayPlus = 23
|
||||||
WorshipAssistant = 24
|
VideoPsalm = 24
|
||||||
WorshipCenterPro = 25
|
WordsOfWorship = 25
|
||||||
ZionWorx = 26
|
WorshipAssistant = 26
|
||||||
|
WorshipCenterPro = 27
|
||||||
|
ZionWorx = 28
|
||||||
|
|
||||||
# Set optional attribute defaults
|
# Set optional attribute defaults
|
||||||
__defaults__ = {
|
__defaults__ = {
|
||||||
|
@ -224,6 +227,13 @@ class SongFormat(object):
|
||||||
'filter': '{text} (*.usr *.txt *.bin)'.format(text=translate('SongsPlugin.ImportWizardForm',
|
'filter': '{text} (*.usr *.txt *.bin)'.format(text=translate('SongsPlugin.ImportWizardForm',
|
||||||
'CCLI SongSelect Files'))
|
'CCLI SongSelect Files'))
|
||||||
},
|
},
|
||||||
|
ChordPro: {
|
||||||
|
'class': ChordProImport,
|
||||||
|
'name': 'ChordPro',
|
||||||
|
'prefix': 'chordPro',
|
||||||
|
'filter': '{text} (*.cho *.crd *.chordpro *.chopro *.txt)'.format(
|
||||||
|
text=translate('SongsPlugin.ImportWizardForm', 'ChordPro Files'))
|
||||||
|
},
|
||||||
DreamBeam: {
|
DreamBeam: {
|
||||||
'class': DreamBeamImport,
|
'class': DreamBeamImport,
|
||||||
'name': 'DreamBeam',
|
'name': 'DreamBeam',
|
||||||
|
@ -242,9 +252,17 @@ class SongFormat(object):
|
||||||
'name': 'EasyWorship Song Database',
|
'name': 'EasyWorship Song Database',
|
||||||
'prefix': 'ew',
|
'prefix': 'ew',
|
||||||
'selectMode': SongFormatSelect.SingleFile,
|
'selectMode': SongFormatSelect.SingleFile,
|
||||||
'filter': '{text} (*.db)'.format(text=translate('SongsPlugin.ImportWizardForm',
|
'filter': '{text} (*.DB)'.format(text=translate('SongsPlugin.ImportWizardForm',
|
||||||
'EasyWorship Song Database'))
|
'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: {
|
EasyWorshipService: {
|
||||||
'class': EasyWorshipSongImport,
|
'class': EasyWorshipSongImport,
|
||||||
'name': 'EasyWorship Service File',
|
'name': 'EasyWorship Service File',
|
||||||
|
@ -427,9 +445,11 @@ class SongFormat(object):
|
||||||
SongFormat.OpenLP2,
|
SongFormat.OpenLP2,
|
||||||
SongFormat.Generic,
|
SongFormat.Generic,
|
||||||
SongFormat.CCLI,
|
SongFormat.CCLI,
|
||||||
|
SongFormat.ChordPro,
|
||||||
SongFormat.DreamBeam,
|
SongFormat.DreamBeam,
|
||||||
SongFormat.EasySlides,
|
SongFormat.EasySlides,
|
||||||
SongFormat.EasyWorshipDB,
|
SongFormat.EasyWorshipDB,
|
||||||
|
SongFormat.EasyWorshipSqliteDB,
|
||||||
SongFormat.EasyWorshipService,
|
SongFormat.EasyWorshipService,
|
||||||
SongFormat.FoilPresenter,
|
SongFormat.FoilPresenter,
|
||||||
SongFormat.Lyrix,
|
SongFormat.Lyrix,
|
||||||
|
|
|
@ -0,0 +1,178 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# OpenLP - Open Source Lyrics Projection #
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Copyright (c) 2008-2016 OpenLP Developers #
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# This program is free software; you can redistribute it and/or modify it #
|
||||||
|
# under the terms of the GNU General Public License as published by the Free #
|
||||||
|
# Software Foundation; version 2 of the License. #
|
||||||
|
# #
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT #
|
||||||
|
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
|
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
|
||||||
|
# more details. #
|
||||||
|
# #
|
||||||
|
# You should have received a copy of the GNU General Public License along #
|
||||||
|
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||||
|
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||||
|
###############################################################################
|
||||||
|
"""
|
||||||
|
The :mod:`chordpro` module provides the functionality for importing
|
||||||
|
ChordPro files into the current database.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
|
||||||
|
from openlp.core.common import Settings
|
||||||
|
|
||||||
|
from .songimport import SongImport
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ChordProImport(SongImport):
|
||||||
|
"""
|
||||||
|
The :class:`ChordProImport` class provides OpenLP with the
|
||||||
|
ability to import ChordPro files.
|
||||||
|
This importer is based on the information available on these webpages:
|
||||||
|
http://webchord.sourceforge.net/tech.html
|
||||||
|
http://www.vromans.org/johan/projects/Chordii/chordpro/
|
||||||
|
http://www.tenbyten.com/software/songsgen/help/HtmlHelp/files_reference.htm
|
||||||
|
http://linkesoft.com/songbook/chordproformat.html
|
||||||
|
"""
|
||||||
|
def do_import(self):
|
||||||
|
self.import_wizard.progress_bar.setMaximum(len(self.import_source))
|
||||||
|
for filename in self.import_source:
|
||||||
|
if self.stop_import_flag:
|
||||||
|
return
|
||||||
|
song_file = open(filename, 'rt')
|
||||||
|
self.do_import_file(song_file)
|
||||||
|
song_file.close()
|
||||||
|
|
||||||
|
def do_import_file(self, song_file):
|
||||||
|
"""
|
||||||
|
Imports the songs in the given file
|
||||||
|
:param song_file: The file object to be imported from.
|
||||||
|
"""
|
||||||
|
self.set_defaults()
|
||||||
|
# Loop over the lines of the file
|
||||||
|
file_content = song_file.read()
|
||||||
|
current_verse = ''
|
||||||
|
current_verse_type = 'v'
|
||||||
|
skip_block = False
|
||||||
|
for line in file_content.splitlines():
|
||||||
|
line = line.rstrip()
|
||||||
|
# Detect tags
|
||||||
|
if line.startswith('{'):
|
||||||
|
tag_name, tag_value = self.parse_tag(line)
|
||||||
|
# Detect which tag
|
||||||
|
if tag_name in ['title', 't']:
|
||||||
|
self.title = tag_value
|
||||||
|
elif tag_name in ['subtitle', 'su', 'st']:
|
||||||
|
self.alternate_title = tag_value
|
||||||
|
elif tag_name in ['comment', 'c', 'comment_italic', 'ci', 'comment_box', 'cb']:
|
||||||
|
# Detect if the comment is used as a chorus repeat marker
|
||||||
|
if tag_value.lower().startswith('chorus'):
|
||||||
|
if current_verse.strip():
|
||||||
|
# Add collected verse to the lyrics
|
||||||
|
# Strip out chords if set up to
|
||||||
|
if not Settings().value('songs/enable chords') or Settings().value(
|
||||||
|
'songs/disable chords import'):
|
||||||
|
current_verse = re.sub(r'\[.*?\]', '', current_verse)
|
||||||
|
self.add_verse(current_verse.rstrip(), current_verse_type)
|
||||||
|
current_verse_type = 'v'
|
||||||
|
current_verse = ''
|
||||||
|
self.repeat_verse('c1')
|
||||||
|
else:
|
||||||
|
self.add_comment(tag_value)
|
||||||
|
elif tag_name in ['start_of_chorus', 'soc']:
|
||||||
|
current_verse_type = 'c'
|
||||||
|
elif tag_name in ['end_of_chorus', 'eoc']:
|
||||||
|
# Add collected chorus to the lyrics
|
||||||
|
# Strip out chords if set up to
|
||||||
|
if not Settings().value('songs/enable chords') or Settings().value('songs/disable chords import'):
|
||||||
|
current_verse = re.sub(r'\[.*?\]', '', current_verse)
|
||||||
|
self.add_verse(current_verse.rstrip(), current_verse_type)
|
||||||
|
current_verse_type = 'v'
|
||||||
|
current_verse = ''
|
||||||
|
elif tag_name in ['start_of_tab', 'sot']:
|
||||||
|
if current_verse.strip():
|
||||||
|
# Add collected verse to the lyrics
|
||||||
|
# Strip out chords if set up to
|
||||||
|
if not Settings().value('songs/enable chords') or Settings().value(
|
||||||
|
'songs/disable chords import'):
|
||||||
|
current_verse = re.sub(r'\[.*?\]', '', current_verse)
|
||||||
|
self.add_verse(current_verse.rstrip(), current_verse_type)
|
||||||
|
current_verse_type = 'v'
|
||||||
|
current_verse = ''
|
||||||
|
skip_block = True
|
||||||
|
elif tag_name in ['end_of_tab', 'eot']:
|
||||||
|
skip_block = False
|
||||||
|
elif tag_name in ['new_song', 'ns']:
|
||||||
|
# A new song starts below this tag
|
||||||
|
if self.verses and self.title:
|
||||||
|
if current_verse.strip():
|
||||||
|
# Strip out chords if set up to
|
||||||
|
if not Settings().value('songs/enable chords') or Settings().value(
|
||||||
|
'songs/disable chords import'):
|
||||||
|
current_verse = re.sub(r'\[.*?\]', '', current_verse)
|
||||||
|
self.add_verse(current_verse.rstrip(), current_verse_type)
|
||||||
|
if not self.finish():
|
||||||
|
self.log_error(song_file.name)
|
||||||
|
self.set_defaults()
|
||||||
|
current_verse_type = 'v'
|
||||||
|
current_verse = ''
|
||||||
|
else:
|
||||||
|
# Unsupported tag
|
||||||
|
log.debug('unsupported tag: %s' % line)
|
||||||
|
elif line.startswith('#'):
|
||||||
|
# Found a comment line, which is ignored...
|
||||||
|
continue
|
||||||
|
elif line == "['|]":
|
||||||
|
# Found a vertical bar
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
if skip_block:
|
||||||
|
continue
|
||||||
|
elif line == '' and current_verse.strip() and current_verse_type != 'c':
|
||||||
|
# Add collected verse to the lyrics
|
||||||
|
# Strip out chords if set up to
|
||||||
|
if not Settings().value('songs/enable chords') or Settings().value('songs/disable chords import'):
|
||||||
|
current_verse = re.sub(r'\[.*?\]', '', current_verse)
|
||||||
|
self.add_verse(current_verse.rstrip(), current_verse_type)
|
||||||
|
current_verse_type = 'v'
|
||||||
|
current_verse = ''
|
||||||
|
else:
|
||||||
|
if current_verse.strip() == '':
|
||||||
|
current_verse = line + '\n'
|
||||||
|
else:
|
||||||
|
current_verse += line + '\n'
|
||||||
|
if current_verse.strip():
|
||||||
|
# Strip out chords if set up to
|
||||||
|
if not Settings().value('songs/enable chords') or Settings().value(
|
||||||
|
'songs/disable chords import'):
|
||||||
|
current_verse = re.sub(r'\[.*?\]', '', current_verse)
|
||||||
|
self.add_verse(current_verse.rstrip(), current_verse_type)
|
||||||
|
if not self.finish():
|
||||||
|
self.log_error(song_file.name)
|
||||||
|
|
||||||
|
def parse_tag(self, line):
|
||||||
|
"""
|
||||||
|
:param line: Line with the tag to be parsed
|
||||||
|
:return: A tuple with tag name and tag value (if any)
|
||||||
|
"""
|
||||||
|
# Strip the first '}'
|
||||||
|
line = line[1:].strip()
|
||||||
|
colon_idx = line.find(':')
|
||||||
|
# check if this is a tag without value
|
||||||
|
if colon_idx < 0:
|
||||||
|
# strip the final '}' and return the tag name
|
||||||
|
return line[:-1], None
|
||||||
|
tag_name = line[:colon_idx]
|
||||||
|
tag_value = line[colon_idx + 1:-1].strip()
|
||||||
|
return tag_name, tag_value
|
|
@ -28,6 +28,7 @@ import struct
|
||||||
import re
|
import re
|
||||||
import zlib
|
import zlib
|
||||||
import logging
|
import logging
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
from openlp.core.lib import translate
|
from openlp.core.lib import translate
|
||||||
from openlp.plugins.songs.lib import VerseType
|
from openlp.plugins.songs.lib import VerseType
|
||||||
|
@ -77,8 +78,10 @@ class EasyWorshipSongImport(SongImport):
|
||||||
"""
|
"""
|
||||||
if self.import_source.lower().endswith('ews'):
|
if self.import_source.lower().endswith('ews'):
|
||||||
self.import_ews()
|
self.import_ews()
|
||||||
else:
|
elif self.import_source.endswith('DB'):
|
||||||
self.import_db()
|
self.import_db()
|
||||||
|
else:
|
||||||
|
self.import_sqlite_db()
|
||||||
|
|
||||||
def import_ews(self):
|
def import_ews(self):
|
||||||
"""
|
"""
|
||||||
|
@ -125,8 +128,8 @@ class EasyWorshipSongImport(SongImport):
|
||||||
else:
|
else:
|
||||||
log.debug('Given ews file is of unknown version.')
|
log.debug('Given ews file is of unknown version.')
|
||||||
return
|
return
|
||||||
entry_count = self.get_i32(file_pos)
|
entry_count = self.ews_get_i32(file_pos)
|
||||||
entry_length = self.get_i16(file_pos + 4)
|
entry_length = self.ews_get_i16(file_pos + 4)
|
||||||
file_pos += 6
|
file_pos += 6
|
||||||
self.import_wizard.progress_bar.setMaximum(entry_count)
|
self.import_wizard.progress_bar.setMaximum(entry_count)
|
||||||
# Loop over songs
|
# Loop over songs
|
||||||
|
@ -144,13 +147,13 @@ class EasyWorshipSongImport(SongImport):
|
||||||
# 0x08 = Audio, 0x09 = Web
|
# 0x08 = Audio, 0x09 = Web
|
||||||
# 1410 Song number cstring 10
|
# 1410 Song number cstring 10
|
||||||
self.set_defaults()
|
self.set_defaults()
|
||||||
self.title = self.get_string(file_pos + 0, 50)
|
self.title = self.ews_get_string(file_pos + 0, 50)
|
||||||
authors = self.get_string(file_pos + 307, 50)
|
authors = self.ews_get_string(file_pos + 307, 50)
|
||||||
copyright = self.get_string(file_pos + 358, 100)
|
copyright = self.ews_get_string(file_pos + 358, 100)
|
||||||
admin = self.get_string(file_pos + 459, 50)
|
admin = self.ews_get_string(file_pos + 459, 50)
|
||||||
cont_ptr = self.get_i32(file_pos + 800)
|
cont_ptr = self.ews_get_i32(file_pos + 800)
|
||||||
cont_type = self.get_i32(file_pos + 820)
|
cont_type = self.ews_get_i32(file_pos + 820)
|
||||||
self.ccli_number = self.get_string(file_pos + 1410, 10)
|
self.ccli_number = self.ews_get_string(file_pos + 1410, 10)
|
||||||
# Only handle content type 1 (songs)
|
# Only handle content type 1 (songs)
|
||||||
if cont_type != 1:
|
if cont_type != 1:
|
||||||
file_pos += entry_length
|
file_pos += entry_length
|
||||||
|
@ -164,9 +167,9 @@ class EasyWorshipSongImport(SongImport):
|
||||||
# Checksum int32be 4 Alder-32 checksum.
|
# Checksum int32be 4 Alder-32 checksum.
|
||||||
# (unknown) 4 0x51 0x4b 0x03 0x04
|
# (unknown) 4 0x51 0x4b 0x03 0x04
|
||||||
# Content length int32le 4 Length of content after decompression
|
# Content length int32le 4 Length of content after decompression
|
||||||
content_length = self.get_i32(cont_ptr)
|
content_length = self.ews_get_i32(cont_ptr)
|
||||||
deflated_content = self.get_bytes(cont_ptr + 4, content_length - 10)
|
deflated_content = self.ews_get_bytes(cont_ptr + 4, content_length - 10)
|
||||||
deflated_length = self.get_i32(cont_ptr + 4 + content_length - 6)
|
deflated_length = self.ews_get_i32(cont_ptr + 4 + content_length - 6)
|
||||||
inflated_content = zlib.decompress(deflated_content, 15, deflated_length)
|
inflated_content = zlib.decompress(deflated_content, 15, deflated_length)
|
||||||
if copyright:
|
if copyright:
|
||||||
self.copyright = copyright
|
self.copyright = copyright
|
||||||
|
@ -196,7 +199,7 @@ class EasyWorshipSongImport(SongImport):
|
||||||
Import the songs from the database
|
Import the songs from the database
|
||||||
"""
|
"""
|
||||||
# Open the DB and MB files if they exist
|
# 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):
|
if not os.path.isfile(self.import_source):
|
||||||
self.log_error(self.import_source, translate('SongsPlugin.EasyWorshipSongImport',
|
self.log_error(self.import_source, translate('SongsPlugin.EasyWorshipSongImport',
|
||||||
'This file does not exist.'))
|
'This file does not exist.'))
|
||||||
|
@ -260,16 +263,16 @@ class EasyWorshipSongImport(SongImport):
|
||||||
for i, field_name in enumerate(field_names):
|
for i, field_name in enumerate(field_names):
|
||||||
field_type, field_size = struct.unpack_from('BB', field_info, i * 2)
|
field_type, field_size = struct.unpack_from('BB', field_info, i * 2)
|
||||||
field_descriptions.append(FieldDescEntry(field_name, field_type, field_size))
|
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
|
# Pick out the field description indexes we will need
|
||||||
try:
|
try:
|
||||||
success = True
|
success = True
|
||||||
fi_title = self.find_field(b'Title')
|
fi_title = self.db_find_field(b'Title')
|
||||||
fi_author = self.find_field(b'Author')
|
fi_author = self.db_find_field(b'Author')
|
||||||
fi_copy = self.find_field(b'Copyright')
|
fi_copy = self.db_find_field(b'Copyright')
|
||||||
fi_admin = self.find_field(b'Administrator')
|
fi_admin = self.db_find_field(b'Administrator')
|
||||||
fi_words = self.find_field(b'Words')
|
fi_words = self.db_find_field(b'Words')
|
||||||
fi_ccli = self.find_field(b'Song Number')
|
fi_ccli = self.db_find_field(b'Song Number')
|
||||||
except IndexError:
|
except IndexError:
|
||||||
# This is the wrong table
|
# This is the wrong table
|
||||||
success = False
|
success = False
|
||||||
|
@ -297,13 +300,13 @@ class EasyWorshipSongImport(SongImport):
|
||||||
raw_record = db_file.read(record_size)
|
raw_record = db_file.read(record_size)
|
||||||
self.fields = self.record_structure.unpack(raw_record)
|
self.fields = self.record_structure.unpack(raw_record)
|
||||||
self.set_defaults()
|
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.
|
# Get remaining fields.
|
||||||
copy = self.get_field(fi_copy)
|
copy = self.db_get_field(fi_copy)
|
||||||
admin = self.get_field(fi_admin)
|
admin = self.db_get_field(fi_admin)
|
||||||
ccli = self.get_field(fi_ccli)
|
ccli = self.db_get_field(fi_ccli)
|
||||||
authors = self.get_field(fi_author)
|
authors = self.db_get_field(fi_author)
|
||||||
words = self.get_field(fi_words)
|
words = self.db_get_field(fi_words)
|
||||||
if copy:
|
if copy:
|
||||||
self.copyright = copy.decode(self.encoding)
|
self.copyright = copy.decode(self.encoding)
|
||||||
if admin:
|
if admin:
|
||||||
|
@ -337,6 +340,82 @@ class EasyWorshipSongImport(SongImport):
|
||||||
db_file.close()
|
db_file.close()
|
||||||
self.memo_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):
|
def set_song_import_object(self, authors, words):
|
||||||
"""
|
"""
|
||||||
Set the SongImport object members.
|
Set the SongImport object members.
|
||||||
|
@ -409,7 +488,7 @@ class EasyWorshipSongImport(SongImport):
|
||||||
self.comments += str(translate('SongsPlugin.EasyWorshipSongImport',
|
self.comments += str(translate('SongsPlugin.EasyWorshipSongImport',
|
||||||
'\n[above are Song Tags with notes imported from EasyWorship]'))
|
'\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
|
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]
|
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
|
Save the record structure
|
||||||
|
|
||||||
|
@ -445,7 +524,7 @@ class EasyWorshipSongImport(SongImport):
|
||||||
self.record_structure = struct.Struct(''.join(fsl))
|
self.record_structure = struct.Struct(''.join(fsl))
|
||||||
self.field_descriptions = field_descriptions
|
self.field_descriptions = field_descriptions
|
||||||
|
|
||||||
def get_field(self, field_desc_index):
|
def db_get_field(self, field_desc_index):
|
||||||
"""
|
"""
|
||||||
Extract the field
|
Extract the field
|
||||||
|
|
||||||
|
@ -489,7 +568,7 @@ class EasyWorshipSongImport(SongImport):
|
||||||
else:
|
else:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
def get_bytes(self, pos, length):
|
def ews_get_bytes(self, pos, length):
|
||||||
"""
|
"""
|
||||||
Get bytes from ews_file
|
Get bytes from ews_file
|
||||||
|
|
||||||
|
@ -500,7 +579,7 @@ class EasyWorshipSongImport(SongImport):
|
||||||
self.ews_file.seek(pos)
|
self.ews_file.seek(pos)
|
||||||
return self.ews_file.read(length)
|
return self.ews_file.read(length)
|
||||||
|
|
||||||
def get_string(self, pos, length):
|
def ews_get_string(self, pos, length):
|
||||||
"""
|
"""
|
||||||
Get string from ews_file
|
Get string from ews_file
|
||||||
|
|
||||||
|
@ -508,12 +587,12 @@ class EasyWorshipSongImport(SongImport):
|
||||||
:param length: Characters to read
|
:param length: Characters to read
|
||||||
:return: String read
|
:return: String read
|
||||||
"""
|
"""
|
||||||
bytes = self.get_bytes(pos, length)
|
bytes = self.ews_get_bytes(pos, length)
|
||||||
mask = '<' + str(length) + 's'
|
mask = '<' + str(length) + 's'
|
||||||
byte_str, = struct.unpack(mask, bytes)
|
byte_str, = struct.unpack(mask, bytes)
|
||||||
return byte_str.decode(self.encoding).replace('\0', '').strip()
|
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
|
Get short int from ews_file
|
||||||
|
|
||||||
|
@ -521,19 +600,19 @@ class EasyWorshipSongImport(SongImport):
|
||||||
:return: Short integer read
|
:return: Short integer read
|
||||||
"""
|
"""
|
||||||
|
|
||||||
bytes = self.get_bytes(pos, 2)
|
bytes = self.ews_get_bytes(pos, 2)
|
||||||
mask = '<h'
|
mask = '<h'
|
||||||
number, = struct.unpack(mask, bytes)
|
number, = struct.unpack(mask, bytes)
|
||||||
return number
|
return number
|
||||||
|
|
||||||
def get_i32(self, pos):
|
def ews_get_i32(self, pos):
|
||||||
"""
|
"""
|
||||||
Get long int from ews_file
|
Get long int from ews_file
|
||||||
|
|
||||||
:param pos: Position to read from
|
:param pos: Position to read from
|
||||||
:return: Long integer read
|
:return: Long integer read
|
||||||
"""
|
"""
|
||||||
bytes = self.get_bytes(pos, 4)
|
bytes = self.ews_get_bytes(pos, 4)
|
||||||
mask = '<i'
|
mask = '<i'
|
||||||
number, = struct.unpack(mask, bytes)
|
number, = struct.unpack(mask, bytes)
|
||||||
return number
|
return number
|
||||||
|
|
|
@ -26,7 +26,7 @@ import re
|
||||||
from lxml import objectify
|
from lxml import objectify
|
||||||
from lxml.etree import Error, LxmlError
|
from lxml.etree import Error, LxmlError
|
||||||
|
|
||||||
from openlp.core.common import translate
|
from openlp.core.common import translate, Settings
|
||||||
from openlp.plugins.songs.lib import VerseType
|
from openlp.plugins.songs.lib import VerseType
|
||||||
from openlp.plugins.songs.lib.importers.songimport import SongImport
|
from openlp.plugins.songs.lib.importers.songimport import SongImport
|
||||||
from openlp.plugins.songs.lib.ui import SongStrings
|
from openlp.plugins.songs.lib.ui import SongStrings
|
||||||
|
@ -87,7 +87,7 @@ class OpenSongImport(SongImport):
|
||||||
All verses are imported and tagged appropriately.
|
All verses are imported and tagged appropriately.
|
||||||
|
|
||||||
Guitar chords can be provided "above" the lyrics (the line is preceded by a period "."), and one or more "_" can
|
Guitar chords can be provided "above" the lyrics (the line is preceded by a period "."), and one or more "_" can
|
||||||
be used to signify long-drawn-out words. Chords and "_" are removed by this importer. For example::
|
be used to signify long-drawn-out words. For example::
|
||||||
|
|
||||||
. A7 Bm
|
. A7 Bm
|
||||||
1 Some____ Words
|
1 Some____ Words
|
||||||
|
@ -195,14 +195,34 @@ class OpenSongImport(SongImport):
|
||||||
lyrics = str(root.lyrics)
|
lyrics = str(root.lyrics)
|
||||||
else:
|
else:
|
||||||
lyrics = ''
|
lyrics = ''
|
||||||
|
chords = []
|
||||||
for this_line in lyrics.split('\n'):
|
for this_line in lyrics.split('\n'):
|
||||||
if not this_line.strip():
|
if not this_line.strip():
|
||||||
continue
|
continue
|
||||||
# skip this line if it is a comment
|
# skip this line if it is a comment
|
||||||
if this_line.startswith(';'):
|
if this_line.startswith(';'):
|
||||||
continue
|
continue
|
||||||
# skip guitar chords and page and column breaks
|
# skip page and column breaks
|
||||||
if this_line.startswith('.') or this_line.startswith('---') or this_line.startswith('-!!'):
|
if this_line.startswith('---') or this_line.startswith('-!!'):
|
||||||
|
continue
|
||||||
|
# guitar chords marker
|
||||||
|
if this_line.startswith('.'):
|
||||||
|
# Find the position of the chords so they can be inserted in the lyrics
|
||||||
|
chords = []
|
||||||
|
this_line = this_line[1:]
|
||||||
|
chord = ''
|
||||||
|
i = 0
|
||||||
|
while i < len(this_line):
|
||||||
|
if this_line[i] != ' ':
|
||||||
|
chord_pos = i
|
||||||
|
chord += this_line[i]
|
||||||
|
i += 1
|
||||||
|
while i < len(this_line) and this_line[i] != ' ':
|
||||||
|
chord += this_line[i]
|
||||||
|
i += 1
|
||||||
|
chords.append((chord_pos, chord))
|
||||||
|
chord = ''
|
||||||
|
i += 1
|
||||||
continue
|
continue
|
||||||
# verse/chorus/etc. marker
|
# verse/chorus/etc. marker
|
||||||
if this_line.startswith('['):
|
if this_line.startswith('['):
|
||||||
|
@ -228,12 +248,20 @@ class OpenSongImport(SongImport):
|
||||||
# number at start of line.. it's verse number
|
# number at start of line.. it's verse number
|
||||||
if this_line[0].isdigit():
|
if this_line[0].isdigit():
|
||||||
verse_num = this_line[0]
|
verse_num = this_line[0]
|
||||||
this_line = this_line[1:].strip()
|
this_line = this_line[1:]
|
||||||
verses.setdefault(verse_tag, {})
|
verses.setdefault(verse_tag, {})
|
||||||
verses[verse_tag].setdefault(verse_num, {})
|
verses[verse_tag].setdefault(verse_num, {})
|
||||||
if inst not in verses[verse_tag][verse_num]:
|
if inst not in verses[verse_tag][verse_num]:
|
||||||
verses[verse_tag][verse_num][inst] = []
|
verses[verse_tag][verse_num][inst] = []
|
||||||
our_verse_order.append([verse_tag, verse_num, inst])
|
our_verse_order.append([verse_tag, verse_num, inst])
|
||||||
|
# If chords exists insert them
|
||||||
|
if chords and Settings().value('songs/enable chords') and not Settings().value(
|
||||||
|
'songs/disable chords import'):
|
||||||
|
offset = 0
|
||||||
|
for (column, chord) in chords:
|
||||||
|
this_line = '{pre}[{chord}]{post}'.format(pre=this_line[:offset + column], chord=chord,
|
||||||
|
post=this_line[offset + column:])
|
||||||
|
offset += len(chord) + 2
|
||||||
# Tidy text and remove the ____s from extended words
|
# Tidy text and remove the ____s from extended words
|
||||||
this_line = self.tidy_text(this_line)
|
this_line = self.tidy_text(this_line)
|
||||||
this_line = this_line.replace('_', '')
|
this_line = this_line.replace('_', '')
|
||||||
|
|
|
@ -25,10 +25,12 @@ The :mod:`songbeamer` module provides the functionality for importing SongBeamer
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import base64
|
||||||
|
import math
|
||||||
|
|
||||||
from openlp.core.common import get_file_encoding
|
|
||||||
from openlp.plugins.songs.lib import VerseType
|
from openlp.plugins.songs.lib import VerseType
|
||||||
from openlp.plugins.songs.lib.importers.songimport import SongImport
|
from openlp.plugins.songs.lib.importers.songimport import SongImport
|
||||||
|
from openlp.core.common import Settings, is_win, is_macosx, get_file_encoding
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -60,6 +62,13 @@ class SongBeamerTypes(object):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class VerseTagMode(object):
|
||||||
|
Unknown = 0
|
||||||
|
ContainsTags = 1
|
||||||
|
ContainsNoTags = 2
|
||||||
|
ContainsNoTagsRestart = 3
|
||||||
|
|
||||||
|
|
||||||
class SongBeamerImport(SongImport):
|
class SongBeamerImport(SongImport):
|
||||||
"""
|
"""
|
||||||
Import Song Beamer files(s). Song Beamer file format is text based in the beginning are one or more control tags
|
Import Song Beamer files(s). Song Beamer file format is text based in the beginning are one or more control tags
|
||||||
|
@ -109,7 +118,7 @@ class SongBeamerImport(SongImport):
|
||||||
self.set_defaults()
|
self.set_defaults()
|
||||||
self.current_verse = ''
|
self.current_verse = ''
|
||||||
self.current_verse_type = VerseType.tags[VerseType.Verse]
|
self.current_verse_type = VerseType.tags[VerseType.Verse]
|
||||||
read_verses = False
|
self.chord_table = None
|
||||||
file_name = os.path.split(import_file)[1]
|
file_name = os.path.split(import_file)[1]
|
||||||
if os.path.isfile(import_file):
|
if os.path.isfile(import_file):
|
||||||
# Detect the encoding
|
# Detect the encoding
|
||||||
|
@ -125,33 +134,103 @@ class SongBeamerImport(SongImport):
|
||||||
continue
|
continue
|
||||||
self.title = file_name.split('.sng')[0]
|
self.title = file_name.split('.sng')[0]
|
||||||
read_verses = False
|
read_verses = False
|
||||||
for line in song_data:
|
# The first verse separator doesn't count, but the others does, so line count starts at -1
|
||||||
# Just make sure that the line is of the type 'Unicode'.
|
line_number = -1
|
||||||
line = str(line).strip()
|
verse_tags_mode = VerseTagMode.Unknown
|
||||||
|
first_verse = True
|
||||||
|
idx = -1
|
||||||
|
while idx + 1 < len(song_data):
|
||||||
|
idx = idx + 1
|
||||||
|
line = song_data[idx].rstrip()
|
||||||
|
stripped_line = line.strip()
|
||||||
if line.startswith('#') and not read_verses:
|
if line.startswith('#') and not read_verses:
|
||||||
self.parseTags(line)
|
self.parse_tags(line)
|
||||||
elif line.startswith('--'):
|
elif stripped_line.startswith('---'):
|
||||||
# --- and -- allowed for page-breaks (difference in Songbeamer only in printout)
|
# '---' is a verse breaker
|
||||||
if self.current_verse:
|
if self.current_verse:
|
||||||
self.replace_html_tags()
|
self.replace_html_tags()
|
||||||
self.add_verse(self.current_verse, self.current_verse_type)
|
self.add_verse(self.current_verse, self.current_verse_type)
|
||||||
self.current_verse = ''
|
self.current_verse = ''
|
||||||
self.current_verse_type = VerseType.tags[VerseType.Verse]
|
self.current_verse_type = VerseType.tags[VerseType.Verse]
|
||||||
|
first_verse = False
|
||||||
read_verses = True
|
read_verses = True
|
||||||
verse_start = True
|
verse_start = True
|
||||||
|
# Songbeamer allows chord on line "-1", meaning the first line has only chords
|
||||||
|
if line_number == -1:
|
||||||
|
first_line = self.insert_chords(line_number, '')
|
||||||
|
if first_line:
|
||||||
|
self.current_verse = first_line.strip() + '\n'
|
||||||
|
line_number += 1
|
||||||
|
elif stripped_line.startswith('--'):
|
||||||
|
# '--' is a page breaker, we convert to optional page break
|
||||||
|
self.current_verse += '[---]\n'
|
||||||
|
line_number += 1
|
||||||
elif read_verses:
|
elif read_verses:
|
||||||
if verse_start:
|
if verse_start:
|
||||||
verse_start = False
|
verse_start = False
|
||||||
if not self.check_verse_marks(line):
|
verse_mark = self.check_verse_marks(line)
|
||||||
self.current_verse = line + '\n'
|
# To ensure that linenumbers are mapped correctly when inserting chords, we attempt to detect
|
||||||
|
# if verse tags are inserted manually or by SongBeamer. If they are inserted manually the lines
|
||||||
|
# should be counted, otherwise not. If all verses start with a tag we assume it is inserted by
|
||||||
|
# SongBeamer.
|
||||||
|
if first_verse and verse_tags_mode == VerseTagMode.Unknown:
|
||||||
|
if verse_mark:
|
||||||
|
verse_tags_mode = VerseTagMode.ContainsTags
|
||||||
else:
|
else:
|
||||||
self.current_verse += line + '\n'
|
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:
|
||||||
|
line = self.insert_chords(line_number, line)
|
||||||
|
self.current_verse += line.strip() + '\n'
|
||||||
|
line_number += 1
|
||||||
if self.current_verse:
|
if self.current_verse:
|
||||||
self.replace_html_tags()
|
self.replace_html_tags()
|
||||||
self.add_verse(self.current_verse, self.current_verse_type)
|
self.add_verse(self.current_verse, self.current_verse_type)
|
||||||
if not self.finish():
|
if not self.finish():
|
||||||
self.log_error(import_file)
|
self.log_error(import_file)
|
||||||
|
|
||||||
|
def insert_chords(self, line_number, line):
|
||||||
|
"""
|
||||||
|
Insert chords into text if any exists and chords import is enabled
|
||||||
|
|
||||||
|
:param linenumber: Number of the current line
|
||||||
|
:param line: The line of lyrics to insert chords
|
||||||
|
"""
|
||||||
|
if self.chord_table and Settings().value('songs/enable chords') and not Settings().value(
|
||||||
|
'songs/disable chords import') and line_number in self.chord_table:
|
||||||
|
line_idx = sorted(self.chord_table[line_number].keys(), reverse=True)
|
||||||
|
for idx in line_idx:
|
||||||
|
# In SongBeamer the column position of the chord can be a decimal, we just round it up.
|
||||||
|
int_idx = int(math.ceil(idx))
|
||||||
|
if int_idx < 0:
|
||||||
|
int_idx = 0
|
||||||
|
elif int_idx > len(line):
|
||||||
|
# If a chord is placed beyond the current end of the line, extend the line with spaces.
|
||||||
|
line += ' ' * (int_idx - len(line))
|
||||||
|
chord = self.chord_table[line_number][idx]
|
||||||
|
chord = chord.replace('<', '♭')
|
||||||
|
line = line[:int_idx] + '[' + chord + ']' + line[int_idx:]
|
||||||
|
return line
|
||||||
|
|
||||||
def replace_html_tags(self):
|
def replace_html_tags(self):
|
||||||
"""
|
"""
|
||||||
This can be called to replace SongBeamer's specific (html) tags with OpenLP's specific (html) tags.
|
This can be called to replace SongBeamer's specific (html) tags with OpenLP's specific (html) tags.
|
||||||
|
@ -159,7 +238,7 @@ class SongBeamerImport(SongImport):
|
||||||
for pair in SongBeamerImport.HTML_TAG_PAIRS:
|
for pair in SongBeamerImport.HTML_TAG_PAIRS:
|
||||||
self.current_verse = pair[0].sub(pair[1], self.current_verse)
|
self.current_verse = pair[0].sub(pair[1], self.current_verse)
|
||||||
|
|
||||||
def parseTags(self, line):
|
def parse_tags(self, line):
|
||||||
"""
|
"""
|
||||||
Parses a meta data line.
|
Parses a meta data line.
|
||||||
|
|
||||||
|
@ -176,8 +255,10 @@ class SongBeamerImport(SongImport):
|
||||||
self.add_copyright(tag_val[1])
|
self.add_copyright(tag_val[1])
|
||||||
elif tag_val[0] == '#AddCopyrightInfo':
|
elif tag_val[0] == '#AddCopyrightInfo':
|
||||||
pass
|
pass
|
||||||
|
elif tag_val[0] == '#AudioFile':
|
||||||
|
self.parse_audio_file(tag_val[1])
|
||||||
elif tag_val[0] == '#Author':
|
elif tag_val[0] == '#Author':
|
||||||
self.parse_author(tag_val[1])
|
self.parse_author(tag_val[1], 'words')
|
||||||
elif tag_val[0] == '#BackgroundImage':
|
elif tag_val[0] == '#BackgroundImage':
|
||||||
pass
|
pass
|
||||||
elif tag_val[0] == '#Bible':
|
elif tag_val[0] == '#Bible':
|
||||||
|
@ -187,12 +268,15 @@ class SongBeamerImport(SongImport):
|
||||||
elif tag_val[0] == '#CCLI':
|
elif tag_val[0] == '#CCLI':
|
||||||
self.ccli_number = tag_val[1]
|
self.ccli_number = tag_val[1]
|
||||||
elif tag_val[0] == '#Chords':
|
elif tag_val[0] == '#Chords':
|
||||||
pass
|
self.chord_table = self.parse_chords(tag_val[1])
|
||||||
elif tag_val[0] == '#ChurchSongID':
|
elif tag_val[0] == '#ChurchSongID':
|
||||||
pass
|
pass
|
||||||
elif tag_val[0] == '#ColorChords':
|
elif tag_val[0] == '#ColorChords':
|
||||||
pass
|
pass
|
||||||
elif tag_val[0] == '#Comments':
|
elif tag_val[0] == '#Comments':
|
||||||
|
try:
|
||||||
|
self.comments = base64.b64decode(tag_val[1]).decode(self.input_file_encoding)
|
||||||
|
except ValueError:
|
||||||
self.comments = tag_val[1]
|
self.comments = tag_val[1]
|
||||||
elif tag_val[0] == '#Editor':
|
elif tag_val[0] == '#Editor':
|
||||||
pass
|
pass
|
||||||
|
@ -217,7 +301,7 @@ class SongBeamerImport(SongImport):
|
||||||
elif tag_val[0] == '#LangCount':
|
elif tag_val[0] == '#LangCount':
|
||||||
pass
|
pass
|
||||||
elif tag_val[0] == '#Melody':
|
elif tag_val[0] == '#Melody':
|
||||||
self.parse_author(tag_val[1])
|
self.parse_author(tag_val[1], 'music')
|
||||||
elif tag_val[0] == '#NatCopyright':
|
elif tag_val[0] == '#NatCopyright':
|
||||||
pass
|
pass
|
||||||
elif tag_val[0] == '#OTitle':
|
elif tag_val[0] == '#OTitle':
|
||||||
|
@ -243,7 +327,7 @@ class SongBeamerImport(SongImport):
|
||||||
elif tag_val[0] == '#TextAlign':
|
elif tag_val[0] == '#TextAlign':
|
||||||
pass
|
pass
|
||||||
elif tag_val[0] == '#Title':
|
elif tag_val[0] == '#Title':
|
||||||
self.title = str(tag_val[1]).strip()
|
self.title = tag_val[1].strip()
|
||||||
elif tag_val[0] == '#TitleAlign':
|
elif tag_val[0] == '#TitleAlign':
|
||||||
pass
|
pass
|
||||||
elif tag_val[0] == '#TitleFontSize':
|
elif tag_val[0] == '#TitleFontSize':
|
||||||
|
@ -263,25 +347,80 @@ class SongBeamerImport(SongImport):
|
||||||
elif tag_val[0] == '#Version':
|
elif tag_val[0] == '#Version':
|
||||||
pass
|
pass
|
||||||
elif tag_val[0] == '#VerseOrder':
|
elif tag_val[0] == '#VerseOrder':
|
||||||
# TODO: add the verse order.
|
verse_order = tag_val[1].strip()
|
||||||
pass
|
for verse_mark in verse_order.split(','):
|
||||||
|
new_verse_mark = self.convert_verse_marks(verse_mark)
|
||||||
|
if new_verse_mark:
|
||||||
|
self.verse_order_list.append(new_verse_mark)
|
||||||
|
|
||||||
def check_verse_marks(self, line):
|
def check_verse_marks(self, line):
|
||||||
"""
|
"""
|
||||||
Check and add the verse's MarkType. Returns ``True`` if the given line contains a correct verse mark otherwise
|
Check and add the verse's MarkType. Returns ``True`` if the given line contains a correct verse mark otherwise
|
||||||
``False``.
|
``False``.
|
||||||
|
|
||||||
:param line: The line to check for marks (unicode).
|
:param line: The line to check for marks.
|
||||||
"""
|
"""
|
||||||
marks = line.split(' ')
|
new_verse_mark = self.convert_verse_marks(line)
|
||||||
if len(marks) <= 2 and marks[0].lower() in SongBeamerTypes.MarkTypes:
|
if new_verse_mark:
|
||||||
self.current_verse_type = SongBeamerTypes.MarkTypes[marks[0].lower()]
|
self.current_verse_type = new_verse_mark
|
||||||
if len(marks) == 2:
|
|
||||||
# If we have a digit, we append it to current_verse_type.
|
|
||||||
if marks[1].isdigit():
|
|
||||||
self.current_verse_type += marks[1]
|
|
||||||
return True
|
|
||||||
elif marks[0].lower().startswith('$$m='): # this verse-mark cannot be numbered
|
|
||||||
self.current_verse_type = SongBeamerTypes.MarkTypes['$$m=']
|
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def convert_verse_marks(self, line):
|
||||||
|
"""
|
||||||
|
Convert the verse's MarkType. Returns the OpenLP versemark if the given line contains a correct SongBeamer verse
|
||||||
|
mark otherwise ``None``.
|
||||||
|
|
||||||
|
:param line: The line to check for marks.
|
||||||
|
"""
|
||||||
|
new_verse_mark = None
|
||||||
|
marks = line.split(' ')
|
||||||
|
if len(marks) <= 2 and marks[0].lower() in SongBeamerTypes.MarkTypes:
|
||||||
|
new_verse_mark = SongBeamerTypes.MarkTypes[marks[0].lower()]
|
||||||
|
if len(marks) == 2:
|
||||||
|
# If we have a digit, we append it to the converted verse mark
|
||||||
|
if marks[1].isdigit():
|
||||||
|
new_verse_mark += marks[1]
|
||||||
|
elif marks[0].lower().startswith('$$m='): # this verse-mark cannot be numbered
|
||||||
|
new_verse_mark = SongBeamerTypes.MarkTypes['$$m=']
|
||||||
|
return new_verse_mark
|
||||||
|
|
||||||
|
def parse_chords(self, chords):
|
||||||
|
"""
|
||||||
|
Parse chords. The chords are in a base64 encode string. The decoded string is an index of chord placement
|
||||||
|
separated by "\r", like this: "<linecolumn>,<linenumber>,<chord>\r"
|
||||||
|
|
||||||
|
:param chords: Chords in a base64 encoded string
|
||||||
|
"""
|
||||||
|
chord_list = base64.b64decode(chords).decode(self.input_file_encoding).split('\r')
|
||||||
|
chord_table = {}
|
||||||
|
for chord_index in chord_list:
|
||||||
|
if not chord_index:
|
||||||
|
continue
|
||||||
|
[col_str, line_str, chord] = chord_index.split(',')
|
||||||
|
col = float(col_str)
|
||||||
|
line = int(line_str)
|
||||||
|
if line not in chord_table:
|
||||||
|
chord_table[line] = {}
|
||||||
|
chord_table[line][col] = chord
|
||||||
|
return chord_table
|
||||||
|
|
||||||
|
def parse_audio_file(self, audio_file_path):
|
||||||
|
"""
|
||||||
|
Parse audio file. The path is relative to the SongsBeamer Songs folder.
|
||||||
|
|
||||||
|
:param audio_file_path: Path to the audio file
|
||||||
|
"""
|
||||||
|
# The path is relative to SongBeamers Song folder
|
||||||
|
if is_win():
|
||||||
|
user_doc_folder = os.path.expandvars('$DOCUMENTS')
|
||||||
|
elif is_macosx():
|
||||||
|
user_doc_folder = os.path.join(os.path.expanduser('~'), 'Documents')
|
||||||
|
else:
|
||||||
|
# SongBeamer only runs on mac and win...
|
||||||
|
return
|
||||||
|
audio_file_path = os.path.normpath(os.path.join(user_doc_folder, 'SongBeamer', 'Songs', audio_file_path))
|
||||||
|
if os.path.isfile(audio_file_path):
|
||||||
|
self.add_media_file(audio_file_path)
|
||||||
|
else:
|
||||||
|
log.debug('Could not import mediafile "%s" since it does not exists!' % audio_file_path)
|
||||||
|
|
|
@ -242,7 +242,7 @@ class SongImport(QtCore.QObject):
|
||||||
self.copyright += ' '
|
self.copyright += ' '
|
||||||
self.copyright += copyright
|
self.copyright += copyright
|
||||||
|
|
||||||
def parse_author(self, text):
|
def parse_author(self, text, type=None):
|
||||||
"""
|
"""
|
||||||
Add the author. OpenLP stores them individually so split by 'and', '&' and comma. However need to check
|
Add the author. OpenLP stores them individually so split by 'and', '&' and comma. However need to check
|
||||||
for 'Mr and Mrs Smith' and turn it to 'Mr Smith' and 'Mrs Smith'.
|
for 'Mr and Mrs Smith' and turn it to 'Mr Smith' and 'Mrs Smith'.
|
||||||
|
@ -256,6 +256,9 @@ class SongImport(QtCore.QObject):
|
||||||
if author2.endswith('.'):
|
if author2.endswith('.'):
|
||||||
author2 = author2[:-1]
|
author2 = author2[:-1]
|
||||||
if author2:
|
if author2:
|
||||||
|
if type:
|
||||||
|
self.add_author(author2, type)
|
||||||
|
else:
|
||||||
self.add_author(author2)
|
self.add_author(author2)
|
||||||
|
|
||||||
def add_author(self, author, type=None):
|
def add_author(self, author, type=None):
|
||||||
|
@ -304,11 +307,22 @@ class SongImport(QtCore.QObject):
|
||||||
if verse_def not in self.verse_order_list_generated:
|
if verse_def not in self.verse_order_list_generated:
|
||||||
self.verse_order_list_generated.append(verse_def)
|
self.verse_order_list_generated.append(verse_def)
|
||||||
|
|
||||||
def repeat_verse(self):
|
def repeat_verse(self, verse_def=None):
|
||||||
"""
|
"""
|
||||||
Repeat the previous verse in the verse order
|
Repeat the verse with the given verse_def or default to repeating the previous verse in the verse order
|
||||||
|
|
||||||
|
:param verse_def: verse_def of the verse to be repeated
|
||||||
"""
|
"""
|
||||||
if self.verse_order_list_generated:
|
if self.verse_order_list_generated:
|
||||||
|
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.append(self.verse_order_list_generated[-1])
|
||||||
self.verse_order_list_generated_useful = True
|
self.verse_order_list_generated_useful = True
|
||||||
|
|
||||||
|
|
|
@ -26,8 +26,9 @@ exproted from Lyrix."""
|
||||||
import logging
|
import logging
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
from openlp.core.common import translate
|
from openlp.core.common import translate, Settings
|
||||||
from openlp.plugins.songs.lib.importers.songimport import SongImport
|
from openlp.plugins.songs.lib.importers.songimport import SongImport
|
||||||
from openlp.plugins.songs.lib.db import AuthorType
|
from openlp.plugins.songs.lib.db import AuthorType
|
||||||
|
|
||||||
|
@ -123,7 +124,11 @@ class VideoPsalmImport(SongImport):
|
||||||
for verse in song['Verses']:
|
for verse in song['Verses']:
|
||||||
if 'Text' not in verse:
|
if 'Text' not in verse:
|
||||||
continue
|
continue
|
||||||
self.add_verse(verse['Text'], 'v')
|
verse_text = verse['Text']
|
||||||
|
# Strip out chords if set up to
|
||||||
|
if not Settings().value('songs/enable chords') or Settings().value('songs/disable chords import'):
|
||||||
|
verse_text = re.sub(r'\[.*?\]', '', verse_text)
|
||||||
|
self.add_verse(verse_text, 'v')
|
||||||
if not self.finish():
|
if not self.finish():
|
||||||
self.log_error('Could not import {title}'.format(title=self.title))
|
self.log_error('Could not import {title}'.format(title=self.title))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
|
@ -61,7 +61,7 @@ import re
|
||||||
|
|
||||||
from lxml import etree, objectify
|
from lxml import etree, objectify
|
||||||
|
|
||||||
from openlp.core.common import translate
|
from openlp.core.common import translate, Settings
|
||||||
from openlp.core.common.versionchecker import get_application_version
|
from openlp.core.common.versionchecker import get_application_version
|
||||||
from openlp.core.lib import FormattingTags
|
from openlp.core.lib import FormattingTags
|
||||||
from openlp.plugins.songs.lib import VerseType, clean_song
|
from openlp.plugins.songs.lib import VerseType, clean_song
|
||||||
|
@ -154,7 +154,7 @@ class OpenLyrics(object):
|
||||||
OpenLP does not support the attribute *lang*.
|
OpenLP does not support the attribute *lang*.
|
||||||
|
|
||||||
``<chord>``
|
``<chord>``
|
||||||
This property is not supported.
|
This property is fully supported.
|
||||||
|
|
||||||
``<comments>``
|
``<comments>``
|
||||||
The ``<comments>`` property is fully supported. But comments in lyrics are not supported.
|
The ``<comments>`` property is fully supported. But comments in lyrics are not supported.
|
||||||
|
@ -323,7 +323,19 @@ class OpenLyrics(object):
|
||||||
# Do not add the break attribute to the last lines element.
|
# Do not add the break attribute to the last lines element.
|
||||||
if index < len(optional_verses) - 1:
|
if index < len(optional_verses) - 1:
|
||||||
lines_element.set('break', 'optional')
|
lines_element.set('break', 'optional')
|
||||||
return self._extract_xml(song_xml).decode()
|
xml_text = self._extract_xml(song_xml).decode()
|
||||||
|
return self._chordpro_to_openlyrics(xml_text)
|
||||||
|
|
||||||
|
def _chordpro_to_openlyrics(self, text):
|
||||||
|
"""
|
||||||
|
Convert chords from Chord Pro format to Open Lyrics format
|
||||||
|
|
||||||
|
:param text: the lyric with chords
|
||||||
|
:return: the lyrics with the converted chords
|
||||||
|
"""
|
||||||
|
# Process chords.
|
||||||
|
new_text = re.sub(r'\[(\w.*?)\]', r'<chord name="\1"/>', text)
|
||||||
|
return new_text
|
||||||
|
|
||||||
def _get_missing_tags(self, text):
|
def _get_missing_tags(self, text):
|
||||||
"""
|
"""
|
||||||
|
@ -595,8 +607,7 @@ class OpenLyrics(object):
|
||||||
|
|
||||||
def _process_lines_mixed_content(self, element, newlines=True):
|
def _process_lines_mixed_content(self, element, newlines=True):
|
||||||
"""
|
"""
|
||||||
Converts the xml text with mixed content to OpenLP representation. Chords are skipped and formatting tags are
|
Converts the xml text with mixed content to OpenLP representation. Chords and formatting tags are converted.
|
||||||
converted.
|
|
||||||
|
|
||||||
:param element: The property object (lxml.etree.Element).
|
:param element: The property object (lxml.etree.Element).
|
||||||
:param newlines: The switch to enable/disable processing of line breaks <br/>. The <br/> is used since
|
:param newlines: The switch to enable/disable processing of line breaks <br/>. The <br/> is used since
|
||||||
|
@ -608,12 +619,14 @@ class OpenLyrics(object):
|
||||||
# TODO: Verify format() with template variables
|
# TODO: Verify format() with template variables
|
||||||
if element.tag == NSMAP % 'comment':
|
if element.tag == NSMAP % 'comment':
|
||||||
if element.tail:
|
if element.tail:
|
||||||
# Append tail text at chord element.
|
# Append tail text at comment element.
|
||||||
text += element.tail
|
text += element.tail
|
||||||
return text
|
return text
|
||||||
# Skip <chord> element - not yet supported.
|
# Convert chords to ChordPro format which OpenLP uses internally
|
||||||
# TODO: Verify format() with template variables
|
# TODO: Verify format() with template variables
|
||||||
elif element.tag == NSMAP % 'chord':
|
elif element.tag == NSMAP % 'chord':
|
||||||
|
if Settings().value('songs/enable chords') and not Settings().value('songs/disable chords import'):
|
||||||
|
text += '[{chord}]'.format(chord=element.get('name'))
|
||||||
if element.tail:
|
if element.tail:
|
||||||
# Append tail text at chord element.
|
# Append tail text at chord element.
|
||||||
text += element.tail
|
text += element.tail
|
||||||
|
@ -666,7 +679,7 @@ class OpenLyrics(object):
|
||||||
text = self._process_lines_mixed_content(element)
|
text = self._process_lines_mixed_content(element)
|
||||||
# OpenLyrics version <= 0.7 contains <line> elements to represent lines. First child element is tested.
|
# OpenLyrics version <= 0.7 contains <line> elements to represent lines. First child element is tested.
|
||||||
else:
|
else:
|
||||||
# Loop over the "line" elements removing comments and chords.
|
# Loop over the "line" elements removing comments
|
||||||
for line in element:
|
for line in element:
|
||||||
# Skip comment lines.
|
# Skip comment lines.
|
||||||
# TODO: Verify format() with template variables
|
# TODO: Verify format() with template variables
|
||||||
|
|
|
@ -60,6 +60,35 @@ class SongsTab(SettingsTab):
|
||||||
self.display_copyright_check_box.setObjectName('copyright_check_box')
|
self.display_copyright_check_box.setObjectName('copyright_check_box')
|
||||||
self.mode_layout.addWidget(self.display_copyright_check_box)
|
self.mode_layout.addWidget(self.display_copyright_check_box)
|
||||||
self.left_layout.addWidget(self.mode_group_box)
|
self.left_layout.addWidget(self.mode_group_box)
|
||||||
|
# Chords group box
|
||||||
|
self.chords_group_box = QtWidgets.QGroupBox(self.left_column)
|
||||||
|
self.chords_group_box.setObjectName('chords_group_box')
|
||||||
|
self.chords_group_box.setCheckable(True)
|
||||||
|
self.chords_layout = QtWidgets.QVBoxLayout(self.chords_group_box)
|
||||||
|
self.chords_layout.setObjectName('chords_layout')
|
||||||
|
self.chords_info_label = QtWidgets.QLabel(self.chords_group_box)
|
||||||
|
self.chords_info_label.setWordWrap(True)
|
||||||
|
self.chords_layout.addWidget(self.chords_info_label)
|
||||||
|
self.mainview_chords_check_box = QtWidgets.QCheckBox(self.mode_group_box)
|
||||||
|
self.mainview_chords_check_box.setObjectName('mainview_chords_check_box')
|
||||||
|
self.chords_layout.addWidget(self.mainview_chords_check_box)
|
||||||
|
self.disable_chords_import_check_box = QtWidgets.QCheckBox(self.mode_group_box)
|
||||||
|
self.disable_chords_import_check_box.setObjectName('disable_chords_import_check_box')
|
||||||
|
self.chords_layout.addWidget(self.disable_chords_import_check_box)
|
||||||
|
# Chords notation group box
|
||||||
|
self.chord_notation_label = QtWidgets.QLabel(self.chords_group_box)
|
||||||
|
self.chord_notation_label.setWordWrap(True)
|
||||||
|
self.chords_layout.addWidget(self.chord_notation_label)
|
||||||
|
self.english_notation_radio_button = QtWidgets.QRadioButton(self.chords_group_box)
|
||||||
|
self.english_notation_radio_button.setObjectName('english_notation_radio_button')
|
||||||
|
self.chords_layout.addWidget(self.english_notation_radio_button)
|
||||||
|
self.german_notation_radio_button = QtWidgets.QRadioButton(self.chords_group_box)
|
||||||
|
self.german_notation_radio_button.setObjectName('german_notation_radio_button')
|
||||||
|
self.chords_layout.addWidget(self.german_notation_radio_button)
|
||||||
|
self.neolatin_notation_radio_button = QtWidgets.QRadioButton(self.chords_group_box)
|
||||||
|
self.neolatin_notation_radio_button.setObjectName('neolatin_notation_radio_button')
|
||||||
|
self.chords_layout.addWidget(self.neolatin_notation_radio_button)
|
||||||
|
self.left_layout.addWidget(self.chords_group_box)
|
||||||
self.left_layout.addStretch()
|
self.left_layout.addStretch()
|
||||||
self.right_layout.addStretch()
|
self.right_layout.addStretch()
|
||||||
self.tool_bar_active_check_box.stateChanged.connect(self.on_tool_bar_active_check_box_changed)
|
self.tool_bar_active_check_box.stateChanged.connect(self.on_tool_bar_active_check_box_changed)
|
||||||
|
@ -68,6 +97,11 @@ class SongsTab(SettingsTab):
|
||||||
self.display_songbook_check_box.stateChanged.connect(self.on_songbook_check_box_changed)
|
self.display_songbook_check_box.stateChanged.connect(self.on_songbook_check_box_changed)
|
||||||
self.display_written_by_check_box.stateChanged.connect(self.on_written_by_check_box_changed)
|
self.display_written_by_check_box.stateChanged.connect(self.on_written_by_check_box_changed)
|
||||||
self.display_copyright_check_box.stateChanged.connect(self.on_copyright_check_box_changed)
|
self.display_copyright_check_box.stateChanged.connect(self.on_copyright_check_box_changed)
|
||||||
|
self.mainview_chords_check_box.stateChanged.connect(self.on_mainview_chords_check_box_changed)
|
||||||
|
self.disable_chords_import_check_box.stateChanged.connect(self.on_disable_chords_import_check_box_changed)
|
||||||
|
self.english_notation_radio_button.clicked.connect(self.on_english_notation_button_clicked)
|
||||||
|
self.german_notation_radio_button.clicked.connect(self.on_german_notation_button_clicked)
|
||||||
|
self.neolatin_notation_radio_button.clicked.connect(self.on_neolatin_notation_button_clicked)
|
||||||
|
|
||||||
def retranslateUi(self):
|
def retranslateUi(self):
|
||||||
self.mode_group_box.setTitle(translate('SongsPlugin.SongsTab', 'Song related settings'))
|
self.mode_group_box.setTitle(translate('SongsPlugin.SongsTab', 'Song related settings'))
|
||||||
|
@ -82,6 +116,17 @@ class SongsTab(SettingsTab):
|
||||||
self.display_copyright_check_box.setText(translate('SongsPlugin.SongsTab',
|
self.display_copyright_check_box.setText(translate('SongsPlugin.SongsTab',
|
||||||
'Display "{symbol}" symbol before copyright '
|
'Display "{symbol}" symbol before copyright '
|
||||||
'info').format(symbol=SongStrings.CopyrightSymbol))
|
'info').format(symbol=SongStrings.CopyrightSymbol))
|
||||||
|
self.chords_info_label.setText(translate('SongsPlugin.SongsTab', 'If enabled all text between "[" and "]" will '
|
||||||
|
'be regarded as chords.'))
|
||||||
|
self.chords_group_box.setTitle(translate('SongsPlugin.SongsTab', 'Chords'))
|
||||||
|
self.mainview_chords_check_box.setText(translate('SongsPlugin.SongsTab', 'Display chords in the main view'))
|
||||||
|
self.disable_chords_import_check_box.setText(translate('SongsPlugin.SongsTab',
|
||||||
|
'Ignore chords when importing songs'))
|
||||||
|
self.chord_notation_label.setText(translate('SongsPlugin.SongsTab', 'Chord notation to use:'))
|
||||||
|
self.english_notation_radio_button.setText(translate('SongsPlugin.SongsTab', 'English') + ' (C-D-E-F-G-A-B)')
|
||||||
|
self.german_notation_radio_button.setText(translate('SongsPlugin.SongsTab', 'German') + ' (C-D-E-F-G-A-H)')
|
||||||
|
self.neolatin_notation_radio_button.setText(
|
||||||
|
translate('SongsPlugin.SongsTab', 'Neo-Latin') + ' (Do-Re-Mi-Fa-Sol-La-Si)')
|
||||||
|
|
||||||
def on_search_as_type_check_box_changed(self, check_state):
|
def on_search_as_type_check_box_changed(self, check_state):
|
||||||
self.song_search = (check_state == QtCore.Qt.Checked)
|
self.song_search = (check_state == QtCore.Qt.Checked)
|
||||||
|
@ -104,6 +149,21 @@ class SongsTab(SettingsTab):
|
||||||
def on_copyright_check_box_changed(self, check_state):
|
def on_copyright_check_box_changed(self, check_state):
|
||||||
self.display_copyright_symbol = (check_state == QtCore.Qt.Checked)
|
self.display_copyright_symbol = (check_state == QtCore.Qt.Checked)
|
||||||
|
|
||||||
|
def on_mainview_chords_check_box_changed(self, check_state):
|
||||||
|
self.mainview_chords = (check_state == QtCore.Qt.Checked)
|
||||||
|
|
||||||
|
def on_disable_chords_import_check_box_changed(self, check_state):
|
||||||
|
self.disable_chords_import = (check_state == QtCore.Qt.Checked)
|
||||||
|
|
||||||
|
def on_english_notation_button_clicked(self):
|
||||||
|
self.chord_notation = 'english'
|
||||||
|
|
||||||
|
def on_german_notation_button_clicked(self):
|
||||||
|
self.chord_notation = 'german'
|
||||||
|
|
||||||
|
def on_neolatin_notation_button_clicked(self):
|
||||||
|
self.chord_notation = 'neo-latin'
|
||||||
|
|
||||||
def load(self):
|
def load(self):
|
||||||
settings = Settings()
|
settings = Settings()
|
||||||
settings.beginGroup(self.settings_section)
|
settings.beginGroup(self.settings_section)
|
||||||
|
@ -113,12 +173,25 @@ class SongsTab(SettingsTab):
|
||||||
self.display_songbook = settings.value('display songbook')
|
self.display_songbook = settings.value('display songbook')
|
||||||
self.display_written_by = settings.value('display written by')
|
self.display_written_by = settings.value('display written by')
|
||||||
self.display_copyright_symbol = settings.value('display copyright symbol')
|
self.display_copyright_symbol = settings.value('display copyright symbol')
|
||||||
|
self.enable_chords = settings.value('enable chords')
|
||||||
|
self.chord_notation = settings.value('chord notation')
|
||||||
|
self.mainview_chords = settings.value('mainview chords')
|
||||||
|
self.disable_chords_import = settings.value('disable chords import')
|
||||||
self.tool_bar_active_check_box.setChecked(self.tool_bar)
|
self.tool_bar_active_check_box.setChecked(self.tool_bar)
|
||||||
self.update_on_edit_check_box.setChecked(self.update_edit)
|
self.update_on_edit_check_box.setChecked(self.update_edit)
|
||||||
self.add_from_service_check_box.setChecked(self.update_load)
|
self.add_from_service_check_box.setChecked(self.update_load)
|
||||||
self.display_songbook_check_box.setChecked(self.display_songbook)
|
self.display_songbook_check_box.setChecked(self.display_songbook)
|
||||||
self.display_written_by_check_box.setChecked(self.display_written_by)
|
self.display_written_by_check_box.setChecked(self.display_written_by)
|
||||||
self.display_copyright_check_box.setChecked(self.display_copyright_symbol)
|
self.display_copyright_check_box.setChecked(self.display_copyright_symbol)
|
||||||
|
self.chords_group_box.setChecked(self.enable_chords)
|
||||||
|
self.mainview_chords_check_box.setChecked(self.mainview_chords)
|
||||||
|
self.disable_chords_import_check_box.setChecked(self.disable_chords_import)
|
||||||
|
if self.chord_notation == 'german':
|
||||||
|
self.german_notation_radio_button.setChecked(True)
|
||||||
|
elif self.chord_notation == 'neo-latin':
|
||||||
|
self.neolatin_notation_radio_button.setChecked(True)
|
||||||
|
else:
|
||||||
|
self.english_notation_radio_button.setChecked(True)
|
||||||
settings.endGroup()
|
settings.endGroup()
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
|
@ -130,6 +203,10 @@ class SongsTab(SettingsTab):
|
||||||
settings.setValue('display songbook', self.display_songbook)
|
settings.setValue('display songbook', self.display_songbook)
|
||||||
settings.setValue('display written by', self.display_written_by)
|
settings.setValue('display written by', self.display_written_by)
|
||||||
settings.setValue('display copyright symbol', self.display_copyright_symbol)
|
settings.setValue('display copyright symbol', self.display_copyright_symbol)
|
||||||
|
settings.setValue('enable chords', self.chords_group_box.isChecked())
|
||||||
|
settings.setValue('mainview chords', self.mainview_chords)
|
||||||
|
settings.setValue('disable chords import', self.disable_chords_import)
|
||||||
|
settings.setValue('chord notation', self.chord_notation)
|
||||||
settings.endGroup()
|
settings.endGroup()
|
||||||
if self.tab_visited:
|
if self.tab_visited:
|
||||||
self.settings_form.register_post_process('songs_config_updated')
|
self.settings_form.register_post_process('songs_config_updated')
|
||||||
|
|
|
@ -66,7 +66,11 @@ __default_settings__ = {
|
||||||
'songs/last directory export': '',
|
'songs/last directory export': '',
|
||||||
'songs/songselect username': '',
|
'songs/songselect username': '',
|
||||||
'songs/songselect password': '',
|
'songs/songselect password': '',
|
||||||
'songs/songselect searches': ''
|
'songs/songselect searches': '',
|
||||||
|
'songs/enable chords': True,
|
||||||
|
'songs/chord notation': 'english', # Can be english, german or neo-latin
|
||||||
|
'songs/mainview chords': False,
|
||||||
|
'songs/disable chords import': False,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,7 @@ from PyQt5 import QtCore, QtWidgets
|
||||||
from openlp.core.common import translate
|
from openlp.core.common import translate
|
||||||
from openlp.core.lib import build_icon
|
from openlp.core.lib import build_icon
|
||||||
from openlp.core.lib.ui import create_button_box
|
from openlp.core.lib.ui import create_button_box
|
||||||
|
from openlp.core.ui.lib import PathEdit, PathType
|
||||||
|
|
||||||
|
|
||||||
class Ui_SongUsageDetailDialog(object):
|
class Ui_SongUsageDetailDialog(object):
|
||||||
|
@ -68,20 +69,13 @@ class Ui_SongUsageDetailDialog(object):
|
||||||
self.file_horizontal_layout.setSpacing(8)
|
self.file_horizontal_layout.setSpacing(8)
|
||||||
self.file_horizontal_layout.setContentsMargins(8, 8, 8, 8)
|
self.file_horizontal_layout.setContentsMargins(8, 8, 8, 8)
|
||||||
self.file_horizontal_layout.setObjectName('file_horizontal_layout')
|
self.file_horizontal_layout.setObjectName('file_horizontal_layout')
|
||||||
self.file_line_edit = QtWidgets.QLineEdit(self.file_group_box)
|
self.report_path_edit = PathEdit(self.file_group_box, path_type = PathType.Directories, show_revert=False)
|
||||||
self.file_line_edit.setObjectName('file_line_edit')
|
self.file_horizontal_layout.addWidget(self.report_path_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.vertical_layout.addWidget(self.file_group_box)
|
self.vertical_layout.addWidget(self.file_group_box)
|
||||||
self.button_box = create_button_box(song_usage_detail_dialog, 'button_box', ['cancel', 'ok'])
|
self.button_box = create_button_box(song_usage_detail_dialog, 'button_box', ['cancel', 'ok'])
|
||||||
self.vertical_layout.addWidget(self.button_box)
|
self.vertical_layout.addWidget(self.button_box)
|
||||||
self.retranslateUi(song_usage_detail_dialog)
|
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):
|
def retranslateUi(self, song_usage_detail_dialog):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -54,25 +54,20 @@ class SongUsageDetailForm(QtWidgets.QDialog, Ui_SongUsageDetailDialog, RegistryP
|
||||||
"""
|
"""
|
||||||
self.from_date_calendar.setSelectedDate(Settings().value(self.plugin.settings_section + '/from date'))
|
self.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.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
|
Triggered when the Directory selection button is clicked
|
||||||
"""
|
"""
|
||||||
path = QtWidgets.QFileDialog.getExistingDirectory(
|
Settings().setValue(self.plugin.settings_section + '/last directory export', file_path)
|
||||||
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)
|
|
||||||
|
|
||||||
def accept(self):
|
def accept(self):
|
||||||
"""
|
"""
|
||||||
Ok was triggered so lets save the data and run the report
|
Ok was triggered so lets save the data and run the report
|
||||||
"""
|
"""
|
||||||
log.debug('accept')
|
log.debug('accept')
|
||||||
path = self.file_line_edit.text()
|
path = self.report_path_edit.path
|
||||||
if not path:
|
if not path:
|
||||||
self.main_window.error_message(
|
self.main_window.error_message(
|
||||||
translate('SongUsagePlugin.SongUsageDetailForm', 'Output Path Not Selected'),
|
translate('SongUsagePlugin.SongUsageDetailForm', 'Output Path Not Selected'),
|
||||||
|
|
|
@ -250,5 +250,6 @@ def main():
|
||||||
print_qt_image_formats()
|
print_qt_image_formats()
|
||||||
print_enchant_backends_and_languages()
|
print_enchant_backends_and_languages()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
|
|
@ -217,5 +217,6 @@ def main():
|
||||||
else:
|
else:
|
||||||
parser.print_help()
|
parser.print_help()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
[pep8]
|
[pep8]
|
||||||
exclude=resources.py,vlc.py
|
exclude=resources.py,vlc.py
|
||||||
max-line-length = 120
|
max-line-length = 120
|
||||||
ignore = E402
|
ignore = E402,E722
|
||||||
|
|
|
@ -25,13 +25,8 @@ Base directory for tests
|
||||||
import sys
|
import sys
|
||||||
from PyQt5 import QtWidgets
|
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.
|
# Only one QApplication can be created. Use QtWidgets.QApplication.instance() when you need to "create" a QApplication.
|
||||||
application = QtWidgets.QApplication([])
|
application = QtWidgets.QApplication([])
|
||||||
application.setApplicationName('OpenLP')
|
application.setApplicationName('OpenLP')
|
||||||
|
|
||||||
__all__ = ['ANY', 'MagicMock', 'patch', 'mock_open', 'call', 'application', 'PropertyMock']
|
__all__ = ['application']
|
||||||
|
|
|
@ -23,12 +23,13 @@
|
||||||
Package to test the openlp.core.common.actions package.
|
Package to test the openlp.core.common.actions package.
|
||||||
"""
|
"""
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
from PyQt5 import QtGui, QtCore, QtWidgets
|
from PyQt5 import QtGui, QtCore, QtWidgets
|
||||||
|
|
||||||
from openlp.core.common import Settings
|
from openlp.core.common import Settings
|
||||||
from openlp.core.common.actions import CategoryActionList, ActionList
|
from openlp.core.common.actions import CategoryActionList, ActionList
|
||||||
from tests.functional import MagicMock
|
|
||||||
from tests.helpers.testmixin import TestMixin
|
from tests.helpers.testmixin import TestMixin
|
||||||
|
|
||||||
|
|
||||||
|
@ -120,11 +121,11 @@ class TestCategoryActionList(TestCase):
|
||||||
self.list.add(self.action2)
|
self.list.add(self.action2)
|
||||||
|
|
||||||
# WHEN: Iterating over the list
|
# WHEN: Iterating over the list
|
||||||
l = [a for a in self.list]
|
list = [a for a in self.list]
|
||||||
# THEN: Make sure they are returned in correct order
|
# THEN: Make sure they are returned in correct order
|
||||||
self.assertEquals(len(self.list), 2)
|
self.assertEquals(len(self.list), 2)
|
||||||
self.assertIs(l[0], self.action1)
|
self.assertIs(list[0], self.action1)
|
||||||
self.assertIs(l[1], self.action2)
|
self.assertIs(list[1], self.action2)
|
||||||
|
|
||||||
def test_remove(self):
|
def test_remove(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -25,9 +25,9 @@ Functional tests to test the AppLocation class and related methods.
|
||||||
import copy
|
import copy
|
||||||
import os
|
import os
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
from openlp.core.common import AppLocation, get_frozen_path
|
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']
|
FILE_LIST = ['file1', 'file2', 'file3.txt', 'file4.txt', 'file5.mp3', 'file6.mp3']
|
||||||
|
|
||||||
|
|
|
@ -22,12 +22,13 @@
|
||||||
"""
|
"""
|
||||||
Functional tests to test the AppLocation class and related methods.
|
Functional tests to test the AppLocation class and related methods.
|
||||||
"""
|
"""
|
||||||
|
from pathlib import Path
|
||||||
from unittest import TestCase
|
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, \
|
from openlp.core import common
|
||||||
is_linux, clean_button_text
|
from openlp.core.common import check_directory_exists, clean_button_text, de_hump, extension_loader, is_macosx, \
|
||||||
from tests.functional import MagicMock, patch
|
is_linux, is_win, path_to_module, trace_error_handler, translate
|
||||||
|
|
||||||
|
|
||||||
class TestCommonFunctions(TestCase):
|
class TestCommonFunctions(TestCase):
|
||||||
|
@ -73,6 +74,72 @@ class TestCommonFunctions(TestCase):
|
||||||
mocked_exists.assert_called_with(directory_to_check)
|
mocked_exists.assert_called_with(directory_to_check)
|
||||||
self.assertRaises(ValueError, check_directory_exists, 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):
|
def test_de_hump_conversion(self):
|
||||||
"""
|
"""
|
||||||
Test the de_hump function with a class name
|
Test the de_hump function with a class name
|
||||||
|
@ -84,7 +151,7 @@ class TestCommonFunctions(TestCase):
|
||||||
new_string = de_hump(string)
|
new_string = de_hump(string)
|
||||||
|
|
||||||
# THEN: the new string should be converted to python format
|
# 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):
|
def test_de_hump_static(self):
|
||||||
"""
|
"""
|
||||||
|
@ -97,7 +164,20 @@ class TestCommonFunctions(TestCase):
|
||||||
new_string = de_hump(string)
|
new_string = de_hump(string)
|
||||||
|
|
||||||
# THEN: the new string should be converted to python format
|
# 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):
|
def test_trace_error_handler(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -26,10 +26,10 @@ import os
|
||||||
import tempfile
|
import tempfile
|
||||||
import socket
|
import socket
|
||||||
from unittest import TestCase
|
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 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
|
from tests.helpers.testmixin import TestMixin
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -25,10 +25,11 @@ Functional tests to test the AppLocation class and related methods.
|
||||||
import os
|
import os
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from unittest import TestCase
|
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, \
|
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
|
get_uno_command, get_uno_instance, split_filename
|
||||||
from tests.functional import MagicMock, PropertyMock, call, patch
|
|
||||||
from tests.helpers.testmixin import TestMixin
|
from tests.helpers.testmixin import TestMixin
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -23,8 +23,8 @@
|
||||||
Functional tests to test the AppLocation class and related methods.
|
Functional tests to test the AppLocation class and related methods.
|
||||||
"""
|
"""
|
||||||
from unittest import TestCase
|
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
|
from openlp.core.common.languagemanager import get_locale_key, get_natural_key
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -24,9 +24,9 @@ Package to test the openlp.core.lib package.
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
from openlp.core.common import Registry
|
from openlp.core.common import Registry
|
||||||
from tests.functional import MagicMock
|
|
||||||
|
|
||||||
TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '../', '..', 'resources'))
|
TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '../', '..', 'resources'))
|
||||||
|
|
||||||
|
|
|
@ -23,11 +23,10 @@
|
||||||
Test the registry properties
|
Test the registry properties
|
||||||
"""
|
"""
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
from openlp.core.common import Registry, RegistryProperties
|
from openlp.core.common import Registry, RegistryProperties
|
||||||
|
|
||||||
from tests.functional import MagicMock, patch
|
|
||||||
|
|
||||||
|
|
||||||
class TestRegistryProperties(TestCase, RegistryProperties):
|
class TestRegistryProperties(TestCase, RegistryProperties):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -23,10 +23,11 @@
|
||||||
Package to test the openlp.core.lib.settings package.
|
Package to test the openlp.core.lib.settings package.
|
||||||
"""
|
"""
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
from openlp.core.common import Settings
|
from openlp.core.common import Settings
|
||||||
from openlp.core.common.settings import recent_files_conv
|
from openlp.core.common.settings import recent_files_conv
|
||||||
from tests.functional import patch
|
|
||||||
from tests.helpers.testmixin import TestMixin
|
from tests.helpers.testmixin import TestMixin
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -23,10 +23,11 @@
|
||||||
Package to test the openlp.core.common.versionchecker package.
|
Package to test the openlp.core.common.versionchecker package.
|
||||||
"""
|
"""
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
from openlp.core.common.settings import Settings
|
from openlp.core.common.settings import Settings
|
||||||
from openlp.core.common.versionchecker import VersionThread
|
from openlp.core.common.versionchecker import VersionThread
|
||||||
from tests.functional import MagicMock, patch
|
|
||||||
from tests.helpers.testmixin import TestMixin
|
from tests.helpers.testmixin import TestMixin
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -24,13 +24,13 @@ Package to test the openlp.core.lib package.
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
from unittest.mock import patch, MagicMock
|
||||||
|
|
||||||
from sqlalchemy.pool import NullPool
|
from sqlalchemy.pool import NullPool
|
||||||
from sqlalchemy.orm.scoping import ScopedSession
|
from sqlalchemy.orm.scoping import ScopedSession
|
||||||
from sqlalchemy import MetaData
|
from sqlalchemy import MetaData
|
||||||
|
|
||||||
from openlp.core.lib.db import init_db, get_upgrade_op, delete_database
|
from openlp.core.lib.db import init_db, get_upgrade_op, delete_database
|
||||||
from tests.functional import patch, MagicMock
|
|
||||||
|
|
||||||
|
|
||||||
class TestDB(TestCase):
|
class TestDB(TestCase):
|
||||||
|
|
|
@ -1,11 +1,31 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# OpenLP - Open Source Lyrics Projection #
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Copyright (c) 2008-2017 OpenLP Developers #
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# This program is free software; you can redistribute it and/or modify it #
|
||||||
|
# under the terms of the GNU General Public License as published by the Free #
|
||||||
|
# Software Foundation; version 2 of the License. #
|
||||||
|
# #
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT #
|
||||||
|
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
|
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
|
||||||
|
# more details. #
|
||||||
|
# #
|
||||||
|
# You should have received a copy of the GNU General Public License along #
|
||||||
|
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||||
|
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||||
|
###############################################################################
|
||||||
"""
|
"""
|
||||||
Package to test the openlp.core.lib.filedialog package.
|
Package to test the openlp.core.lib.filedialog package.
|
||||||
"""
|
"""
|
||||||
from unittest import TestCase
|
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 openlp.core.lib.filedialog import FileDialog
|
||||||
from tests.functional import MagicMock, call, patch
|
|
||||||
|
|
||||||
|
|
||||||
class TestFileDialog(TestCase):
|
class TestFileDialog(TestCase):
|
||||||
|
|
|
@ -24,10 +24,9 @@ Package to test the openlp.core.lib.formattingtags package.
|
||||||
"""
|
"""
|
||||||
import copy
|
import copy
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
from openlp.core.lib import FormattingTags
|
from openlp.core.lib import FormattingTags
|
||||||
from tests.functional import patch
|
|
||||||
|
|
||||||
|
|
||||||
TAG = {
|
TAG = {
|
||||||
'end tag': '{/aa}',
|
'end tag': '{/aa}',
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
"""
|
"""
|
||||||
Package to test the openlp.core.lib.htmlbuilder module.
|
Package to test the openlp.core.lib.htmlbuilder module.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
from PyQt5 import QtCore, QtWebKit
|
from PyQt5 import QtCore, QtWebKit
|
||||||
|
|
||||||
from openlp.core.common import Settings
|
from openlp.core.common import Settings
|
||||||
from openlp.core.lib.htmlbuilder import build_html, build_background_css, build_lyrics_css, build_lyrics_outline_css, \
|
from openlp.core.lib.htmlbuilder import build_html, build_background_css, build_lyrics_css, build_lyrics_outline_css, \
|
||||||
build_lyrics_format_css, build_footer_css, webkit_version
|
build_lyrics_format_css, build_footer_css, webkit_version, build_chords_css
|
||||||
from openlp.core.lib.theme import HorizontalType, VerticalType
|
from openlp.core.lib.theme import HorizontalType, VerticalType
|
||||||
from tests.functional import MagicMock, patch
|
|
||||||
from tests.helpers.testmixin import TestMixin
|
from tests.helpers.testmixin import TestMixin
|
||||||
|
|
||||||
HTML = """
|
HTML = """
|
||||||
|
@ -60,6 +60,29 @@ HTML = """
|
||||||
position: relative;
|
position: relative;
|
||||||
top: -0.3em;
|
top: -0.3em;
|
||||||
}
|
}
|
||||||
|
/* Chords css */
|
||||||
|
.chordline {
|
||||||
|
line-height: 1.0em;
|
||||||
|
}
|
||||||
|
.chordline span.chord span {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.chordline span.chord span strong {
|
||||||
|
position: absolute;
|
||||||
|
top: -0.8em;
|
||||||
|
left: 0;
|
||||||
|
font-size: 75%;
|
||||||
|
font-weight: normal;
|
||||||
|
line-height: normal;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.firstchordline {
|
||||||
|
line-height: 1.0em;
|
||||||
|
}
|
||||||
|
.ws {
|
||||||
|
display: none;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<script>
|
<script>
|
||||||
var timer = null;
|
var timer = null;
|
||||||
|
@ -211,6 +234,34 @@ FOOTER_CSS_BASE = """
|
||||||
FOOTER_CSS = FOOTER_CSS_BASE % ('nowrap')
|
FOOTER_CSS = FOOTER_CSS_BASE % ('nowrap')
|
||||||
FOOTER_CSS_WRAP = FOOTER_CSS_BASE % ('normal')
|
FOOTER_CSS_WRAP = FOOTER_CSS_BASE % ('normal')
|
||||||
FOOTER_CSS_INVALID = ''
|
FOOTER_CSS_INVALID = ''
|
||||||
|
CHORD_CSS_ENABLED = """
|
||||||
|
.chordline {
|
||||||
|
line-height: 2.0em;
|
||||||
|
}
|
||||||
|
.chordline span.chord span {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.chordline span.chord span strong {
|
||||||
|
position: absolute;
|
||||||
|
top: -0.8em;
|
||||||
|
left: 0;
|
||||||
|
font-size: 75%;
|
||||||
|
font-weight: normal;
|
||||||
|
line-height: normal;
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
.firstchordline {
|
||||||
|
line-height: 2.1em;
|
||||||
|
}
|
||||||
|
.ws {
|
||||||
|
display: inline;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}"""
|
||||||
|
|
||||||
|
__default_settings__ = {
|
||||||
|
'songs/mainview chords': False,
|
||||||
|
'songs/enable chords': True
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class Htmbuilder(TestCase, TestMixin):
|
class Htmbuilder(TestCase, TestMixin):
|
||||||
|
@ -222,6 +273,7 @@ class Htmbuilder(TestCase, TestMixin):
|
||||||
Create the UI
|
Create the UI
|
||||||
"""
|
"""
|
||||||
self.build_settings()
|
self.build_settings()
|
||||||
|
Settings().extend_default_settings(__default_settings__)
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
"""
|
"""
|
||||||
|
@ -403,3 +455,17 @@ class Htmbuilder(TestCase, TestMixin):
|
||||||
# WHEN: Retrieving the webkit version
|
# WHEN: Retrieving the webkit version
|
||||||
# THEN: Webkit versions should match
|
# THEN: Webkit versions should match
|
||||||
self.assertEquals(webkit_version(), webkit_ver, "The returned webkit version doesn't match the installed one")
|
self.assertEquals(webkit_version(), webkit_ver, "The returned webkit version doesn't match the installed one")
|
||||||
|
|
||||||
|
def test_build_chords_css(self):
|
||||||
|
"""
|
||||||
|
Test the build_chords_css() function
|
||||||
|
"""
|
||||||
|
# GIVEN: A setting that activates chords on the mainview
|
||||||
|
Settings().setValue('songs/enable chords', True)
|
||||||
|
Settings().setValue('songs/mainview chords', True)
|
||||||
|
|
||||||
|
# WHEN: Building the chord CSS
|
||||||
|
chord_css = build_chords_css()
|
||||||
|
|
||||||
|
# THEN: The build css should look as expected
|
||||||
|
self.assertEqual(CHORD_CSS_ENABLED, chord_css, 'The chord CSS should look as expected')
|
||||||
|
|
|
@ -25,14 +25,15 @@ Package to test the openlp.core.ui package.
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
from threading import Lock
|
from threading import Lock
|
||||||
|
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
from PyQt5 import QtGui
|
from PyQt5 import QtGui
|
||||||
|
|
||||||
from openlp.core.common import Registry
|
from openlp.core.common import Registry
|
||||||
from openlp.core.lib import ImageManager, ScreenList
|
from openlp.core.lib import ImageManager, ScreenList
|
||||||
from openlp.core.lib.imagemanager import Priority
|
from openlp.core.lib.imagemanager import Priority
|
||||||
from tests.functional import patch
|
|
||||||
from tests.helpers.testmixin import TestMixin
|
from tests.helpers.testmixin import TestMixin
|
||||||
|
|
||||||
TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'resources'))
|
TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'resources'))
|
||||||
|
|
|
@ -23,15 +23,16 @@
|
||||||
Package to test the openlp.core.lib package.
|
Package to test the openlp.core.lib package.
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from unittest import TestCase
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
from unittest import TestCase
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
from PyQt5 import QtCore, QtGui
|
from PyQt5 import QtCore, QtGui
|
||||||
|
|
||||||
|
from openlp.core.lib import FormattingTags, expand_chords_for_printing
|
||||||
from openlp.core.lib import build_icon, check_item_selected, clean_tags, create_thumb, create_separated_list, \
|
from openlp.core.lib import build_icon, check_item_selected, clean_tags, create_thumb, create_separated_list, \
|
||||||
expand_tags, get_text_file_string, image_to_byte, resize_image, str_to_bool, validate_thumb
|
expand_tags, get_text_file_string, image_to_byte, resize_image, str_to_bool, validate_thumb, expand_chords, \
|
||||||
from tests.functional import MagicMock, patch
|
compare_chord_lyric, find_formatting_tags
|
||||||
|
|
||||||
TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'resources'))
|
TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'resources'))
|
||||||
|
|
||||||
|
@ -746,3 +747,116 @@ class TestLib(TestCase):
|
||||||
# THEN: We should have "Author 1, Author 2 and Author 3"
|
# THEN: We should have "Author 1, Author 2 and Author 3"
|
||||||
self.assertEqual(string_result, 'Author 1, Author 2 and Author 3', 'The string should be "Author 1, '
|
self.assertEqual(string_result, 'Author 1, Author 2 and Author 3', 'The string should be "Author 1, '
|
||||||
'Author 2, and Author 3".')
|
'Author 2, and Author 3".')
|
||||||
|
|
||||||
|
def test_expand_chords(self):
|
||||||
|
"""
|
||||||
|
Test that the expanding of chords works as expected.
|
||||||
|
"""
|
||||||
|
# GIVEN: A lyrics-line with chords
|
||||||
|
text_with_chords = 'H[C]alleluya.[F] [G]'
|
||||||
|
|
||||||
|
# WHEN: Expanding the chords
|
||||||
|
text_with_expanded_chords = expand_chords(text_with_chords)
|
||||||
|
|
||||||
|
# THEN: We should get html that looks like below
|
||||||
|
expected_html = '<span class="chordline firstchordline">H<span class="chord"><span><strong>C</strong></span>' \
|
||||||
|
'</span>alleluya.<span class="chord"><span><strong>F</strong></span></span><span class="ws">' \
|
||||||
|
' </span> <span class="chord"><span><strong>G</strong></span></span></span>'
|
||||||
|
self.assertEqual(expected_html, text_with_expanded_chords, 'The expanded chords should look as expected!')
|
||||||
|
|
||||||
|
def test_expand_chords2(self):
|
||||||
|
"""
|
||||||
|
Test that the expanding of chords works as expected when special chars are involved.
|
||||||
|
"""
|
||||||
|
import html
|
||||||
|
# GIVEN: A lyrics-line with chords
|
||||||
|
text_with_chords = "I[D]'M NOT MOVED BY WHAT I SEE HALLE[F]LUJA[C]H"
|
||||||
|
|
||||||
|
# WHEN: Expanding the chords
|
||||||
|
text_with_expanded_chords = expand_tags(text_with_chords, True)
|
||||||
|
|
||||||
|
# THEN: We should get html that looks like below
|
||||||
|
expected_html = '<span class="chordline firstchordline">I<span class="chord"><span><strong>D</strong></span>' \
|
||||||
|
'</span>'M NOT MOVED BY WHAT I SEE HALLE<span class="chord"><span><strong>F</strong>' \
|
||||||
|
'</span></span>LUJA<span class="chord"><span><strong>C</strong></span></span>H</span>'
|
||||||
|
self.assertEqual(expected_html, text_with_expanded_chords, 'The expanded chords should look as expected!')
|
||||||
|
|
||||||
|
def test_compare_chord_lyric_short_chord(self):
|
||||||
|
"""
|
||||||
|
Test that the chord/lyric comparing works.
|
||||||
|
"""
|
||||||
|
# GIVEN: A chord and some lyric
|
||||||
|
chord = 'C'
|
||||||
|
lyrics = 'alleluya'
|
||||||
|
|
||||||
|
# WHEN: Comparing the chord and lyrics
|
||||||
|
ret = compare_chord_lyric(chord, lyrics)
|
||||||
|
|
||||||
|
# THEN: The returned value should 0 because the lyric is longer than the chord
|
||||||
|
self.assertEquals(0, ret, 'The returned value should 0 because the lyric is longer than the chord')
|
||||||
|
|
||||||
|
def test_compare_chord_lyric_long_chord(self):
|
||||||
|
"""
|
||||||
|
Test that the chord/lyric comparing works.
|
||||||
|
"""
|
||||||
|
# GIVEN: A chord and some lyric
|
||||||
|
chord = 'Gsus'
|
||||||
|
lyrics = 'me'
|
||||||
|
|
||||||
|
# WHEN: Comparing the chord and lyrics
|
||||||
|
ret = compare_chord_lyric(chord, lyrics)
|
||||||
|
|
||||||
|
# THEN: The returned value should 4 because the chord is longer than the lyric
|
||||||
|
self.assertEquals(4, ret, 'The returned value should 4 because the chord is longer than the lyric')
|
||||||
|
|
||||||
|
def test_find_formatting_tags(self):
|
||||||
|
"""
|
||||||
|
Test that find_formatting_tags works as expected
|
||||||
|
"""
|
||||||
|
# GIVEN: Lyrics with formatting tags and a empty list of formatting tags
|
||||||
|
lyrics = '{st}Amazing {r}grace{/r} how sweet the sound'
|
||||||
|
tags = []
|
||||||
|
FormattingTags.load_tags()
|
||||||
|
|
||||||
|
# WHEN: Detecting active formatting tags
|
||||||
|
active_tags = find_formatting_tags(lyrics, tags)
|
||||||
|
|
||||||
|
# THEN: The list of active tags should contain only 'st'
|
||||||
|
self.assertListEqual(['st'], active_tags, 'The list of active tags should contain only "st"')
|
||||||
|
|
||||||
|
def test_expand_chords_for_printing(self):
|
||||||
|
"""
|
||||||
|
Test that the expanding of chords for printing works as expected.
|
||||||
|
"""
|
||||||
|
# GIVEN: A lyrics-line with chords
|
||||||
|
text_with_chords = '{st}[D]Amazing {r}gr[D7]ace{/r} how [G]sweet the [D]sound [F]{/st}'
|
||||||
|
FormattingTags.load_tags()
|
||||||
|
|
||||||
|
# WHEN: Expanding the chords
|
||||||
|
text_with_expanded_chords = expand_chords_for_printing(text_with_chords, '{br}')
|
||||||
|
|
||||||
|
# THEN: We should get html that looks like below
|
||||||
|
expected_html = '<table class="line" width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td><table ' \
|
||||||
|
'class="segment" cellpadding="0" cellspacing="0" border="0" align="left"><tr class="chordrow">'\
|
||||||
|
'<td class="chord"> </td><td class="chord">D</td></tr><tr><td class="lyrics">{st}{/st}' \
|
||||||
|
'</td><td class="lyrics">{st}Amazing {/st}</td></tr></table><table class="segment" ' \
|
||||||
|
'cellpadding="0" cellspacing="0" border="0" align="left"><tr class="chordrow">' \
|
||||||
|
'<td class="chord"> </td><td class="chord">D7</td></tr><tr><td class="lyrics">{st}{r}gr' \
|
||||||
|
'{/r}{/st}</td><td class="lyrics">{r}{st}ace{/r} {/st}</td></tr></table><table ' \
|
||||||
|
'class="segment" cellpadding="0" cellspacing="0" border="0" align="left"><tr class="chordrow">'\
|
||||||
|
'<td class="chord"> </td></tr><tr><td class="lyrics">{st} {/st}</td></tr></table>' \
|
||||||
|
'<table class="segment" cellpadding="0" cellspacing="0" border="0" align="left"><tr ' \
|
||||||
|
'class="chordrow"><td class="chord"> </td></tr><tr><td class="lyrics">{st}how {/st}' \
|
||||||
|
'</td></tr></table><table class="segment" cellpadding="0" cellspacing="0" border="0" ' \
|
||||||
|
'align="left"><tr class="chordrow"><td class="chord">G</td></tr><tr><td class="lyrics">{st}' \
|
||||||
|
'sweet {/st}</td></tr></table><table class="segment" cellpadding="0" cellspacing="0" ' \
|
||||||
|
'border="0" align="left"><tr class="chordrow"><td class="chord"> </td></tr><tr><td ' \
|
||||||
|
'class="lyrics">{st}the {/st}</td></tr></table><table class="segment" cellpadding="0" ' \
|
||||||
|
'cellspacing="0" border="0" align="left"><tr class="chordrow"><td class="chord">D</td></tr>' \
|
||||||
|
'<tr><td class="lyrics">{st}sound {/st}</td></tr></table><table class="segment" ' \
|
||||||
|
'cellpadding="0" cellspacing="0" border="0" align="left"><tr class="chordrow"><td ' \
|
||||||
|
'class="chord"> </td></tr><tr><td class="lyrics">{st} {/st}</td></tr></table>' \
|
||||||
|
'<table class="segment" cellpadding="0" cellspacing="0" border="0" align="left"><tr ' \
|
||||||
|
'class="chordrow"><td class="chord">F</td></tr><tr><td class="lyrics">{st}{/st} </td>' \
|
||||||
|
'</tr></table></td></tr></table>'
|
||||||
|
self.assertEqual(expected_html, text_with_expanded_chords, 'The expanded chords should look as expected!')
|
||||||
|
|
|
@ -23,10 +23,10 @@
|
||||||
Package to test the openlp.core.lib.mediamanageritem package.
|
Package to test the openlp.core.lib.mediamanageritem package.
|
||||||
"""
|
"""
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
from openlp.core.lib import MediaManagerItem
|
from openlp.core.lib import MediaManagerItem
|
||||||
|
|
||||||
from tests.functional import MagicMock, patch
|
|
||||||
from tests.helpers.testmixin import TestMixin
|
from tests.helpers.testmixin import TestMixin
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -23,11 +23,11 @@
|
||||||
Package to test the openlp.core.lib.pluginmanager package.
|
Package to test the openlp.core.lib.pluginmanager package.
|
||||||
"""
|
"""
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
from openlp.core.common import Registry, Settings
|
from openlp.core.common import Registry, Settings
|
||||||
from openlp.core.lib.pluginmanager import PluginManager
|
from openlp.core.lib.pluginmanager import PluginManager
|
||||||
from openlp.core.lib import PluginStatus
|
from openlp.core.lib import PluginStatus
|
||||||
from tests.functional import MagicMock
|
|
||||||
|
|
||||||
|
|
||||||
class TestPluginManager(TestCase):
|
class TestPluginManager(TestCase):
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# OpenLP - Open Source Lyrics Projection #
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Copyright (c) 2008-2015 OpenLP Developers #
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# This program is free software; you can redistribute it and/or modify it #
|
||||||
|
# under the terms of the GNU General Public License as published by the Free #
|
||||||
|
# Software Foundation; version 2 of the License. #
|
||||||
|
# #
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT #
|
||||||
|
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
|
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
|
||||||
|
# more details. #
|
||||||
|
# #
|
||||||
|
# You should have received a copy of the GNU General Public License along #
|
||||||
|
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||||
|
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||||
|
###############################################################################
|
||||||
|
"""
|
||||||
|
Package to test the openlp.core.lib.projector.constants package.
|
||||||
|
"""
|
||||||
|
from unittest import TestCase, skip
|
||||||
|
|
||||||
|
|
||||||
|
class TestProjectorConstants(TestCase):
|
||||||
|
"""
|
||||||
|
Test specific functions in the projector constants module.
|
||||||
|
"""
|
||||||
|
def build_pjlink_video_label_test(self):
|
||||||
|
"""
|
||||||
|
Test building PJLINK_DEFAULT_CODES dictionary
|
||||||
|
"""
|
||||||
|
# GIVEN: Test data
|
||||||
|
from tests.resources.projector.data import TEST_VIDEO_CODES
|
||||||
|
|
||||||
|
# WHEN: Import projector PJLINK_DEFAULT_CODES
|
||||||
|
from openlp.core.lib.projector.constants import PJLINK_DEFAULT_CODES
|
||||||
|
|
||||||
|
# THEN: Verify dictionary was build correctly
|
||||||
|
self.assertEquals(PJLINK_DEFAULT_CODES, TEST_VIDEO_CODES, 'PJLink video strings should match')
|
|
@ -22,17 +22,16 @@
|
||||||
"""
|
"""
|
||||||
Package to test the openlp.core.lib.projector.pjlink1 package.
|
Package to test the openlp.core.lib.projector.pjlink1 package.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
from unittest.mock import call, patch, MagicMock
|
||||||
|
|
||||||
from openlp.core.lib.projector.pjlink1 import PJLink1
|
from openlp.core.lib.projector.pjlink1 import PJLink
|
||||||
from openlp.core.lib.projector.constants import E_PARAMETER, ERROR_STRING, S_OFF, S_STANDBY, S_WARMUP, S_ON, \
|
from openlp.core.lib.projector.constants import E_PARAMETER, ERROR_STRING, S_OFF, S_STANDBY, S_ON, \
|
||||||
S_COOLDOWN, PJLINK_POWR_STATUS
|
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
|
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):
|
class TestPJLink(TestCase):
|
||||||
|
@ -165,7 +164,13 @@ class TestPJLink(TestCase):
|
||||||
'Lamp 3 hours should have been set to 33333')
|
'Lamp 3 hours should have been set to 33333')
|
||||||
|
|
||||||
@patch.object(pjlink_test, 'projectorReceivedData')
|
@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
|
Test status power to ON
|
||||||
"""
|
"""
|
||||||
|
@ -178,9 +183,17 @@ class TestPJLink(TestCase):
|
||||||
|
|
||||||
# THEN: Power should be set to ON
|
# THEN: Power should be set to ON
|
||||||
self.assertEquals(pjlink.power, S_ON, 'Power should have been 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')
|
@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
|
Test status power to STANDBY
|
||||||
"""
|
"""
|
||||||
|
@ -193,6 +206,8 @@ class TestPJLink(TestCase):
|
||||||
|
|
||||||
# THEN: Power should be set to STANDBY
|
# THEN: Power should be set to STANDBY
|
||||||
self.assertEquals(pjlink.power, S_STANDBY, 'Power should have been 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')
|
@patch.object(pjlink_test, 'projectorUpdateIcons')
|
||||||
def test_projector_process_avmt_closed_unmuted(self, mock_projectorReceivedData):
|
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
|
# THEN: send_command should have the proper authentication
|
||||||
self.assertEquals("{test}".format(test=mock_send_command.call_args),
|
self.assertEquals("{test}".format(test=mock_send_command.call_args),
|
||||||
"call(data='{hash}%1CLSS ?\\r')".format(hash=TEST_HASH))
|
"call(data='{hash}%1CLSS ?\\r')".format(hash=TEST_HASH))
|
||||||
|
|
||||||
|
@patch.object(pjlink_test, '_not_implemented')
|
||||||
|
def not_implemented_test(self, mock_not_implemented):
|
||||||
|
"""
|
||||||
|
Test PJLink._not_implemented method being called
|
||||||
|
"""
|
||||||
|
# GIVEN: test object
|
||||||
|
pjlink = pjlink_test
|
||||||
|
test_cmd = 'TESTMEONLY'
|
||||||
|
|
||||||
|
# WHEN: A future command is called that is not implemented yet
|
||||||
|
pjlink.process_command(test_cmd, "Garbage data for test only")
|
||||||
|
|
||||||
|
# THEN: PJLink.__not_implemented should have been called with test_cmd
|
||||||
|
mock_not_implemented.assert_called_with(test_cmd)
|
||||||
|
|
||||||
|
@patch.object(pjlink_test, 'disconnect_from_host')
|
||||||
|
def socket_abort_test(self, mock_disconnect):
|
||||||
|
"""
|
||||||
|
Test PJLink.socket_abort calls disconnect_from_host
|
||||||
|
"""
|
||||||
|
# GIVEN: Test object
|
||||||
|
pjlink = pjlink_test
|
||||||
|
|
||||||
|
# WHEN: Calling socket_abort
|
||||||
|
pjlink.socket_abort()
|
||||||
|
|
||||||
|
# THEN: disconnect_from_host should be called
|
||||||
|
self.assertTrue(mock_disconnect.called, 'Should have called disconnect_from_host')
|
||||||
|
|
||||||
|
def poll_loop_not_connected_test(self):
|
||||||
|
"""
|
||||||
|
Test PJLink.poll_loop not connected return
|
||||||
|
"""
|
||||||
|
# GIVEN: Test object and mocks
|
||||||
|
pjlink = pjlink_test
|
||||||
|
pjlink.state = MagicMock()
|
||||||
|
pjlink.timer = MagicMock()
|
||||||
|
pjlink.state.return_value = False
|
||||||
|
pjlink.ConnectedState = True
|
||||||
|
|
||||||
|
# WHEN: PJLink.poll_loop called
|
||||||
|
pjlink.poll_loop()
|
||||||
|
|
||||||
|
# THEN: poll_loop should exit without calling any other method
|
||||||
|
self.assertFalse(pjlink.timer.called, 'Should have returned without calling any other method')
|
||||||
|
|
||||||
|
@patch.object(pjlink_test, 'send_command')
|
||||||
|
def poll_loop_start_test(self, mock_send_command):
|
||||||
|
"""
|
||||||
|
Test PJLink.poll_loop makes correct calls
|
||||||
|
"""
|
||||||
|
# GIVEN: test object and test data
|
||||||
|
pjlink = pjlink_test
|
||||||
|
pjlink.state = MagicMock()
|
||||||
|
pjlink.timer = MagicMock()
|
||||||
|
pjlink.timer.interval = MagicMock()
|
||||||
|
pjlink.timer.setInterval = MagicMock()
|
||||||
|
pjlink.timer.start = MagicMock()
|
||||||
|
pjlink.poll_time = 20
|
||||||
|
pjlink.power = S_ON
|
||||||
|
pjlink.source_available = None
|
||||||
|
pjlink.other_info = None
|
||||||
|
pjlink.manufacturer = None
|
||||||
|
pjlink.model = None
|
||||||
|
pjlink.pjlink_name = None
|
||||||
|
pjlink.ConnectedState = S_CONNECTED
|
||||||
|
pjlink.timer.interval.return_value = 10
|
||||||
|
pjlink.state.return_value = S_CONNECTED
|
||||||
|
call_list = [
|
||||||
|
call('POWR', queue=True),
|
||||||
|
call('ERST', queue=True),
|
||||||
|
call('LAMP', queue=True),
|
||||||
|
call('AVMT', queue=True),
|
||||||
|
call('INPT', queue=True),
|
||||||
|
call('INST', queue=True),
|
||||||
|
call('INFO', queue=True),
|
||||||
|
call('INF1', queue=True),
|
||||||
|
call('INF2', queue=True),
|
||||||
|
call('NAME', queue=True),
|
||||||
|
]
|
||||||
|
|
||||||
|
# WHEN: PJLink.poll_loop is called
|
||||||
|
pjlink.poll_loop()
|
||||||
|
|
||||||
|
# THEN: proper calls were made to retrieve projector data
|
||||||
|
# First, call to update the timer with the next interval
|
||||||
|
self.assertTrue(pjlink.timer.setInterval.called, 'Should have updated the timer')
|
||||||
|
# Next, should have called the timer to start
|
||||||
|
self.assertTrue(pjlink.timer.start.called, 'Should have started the timer')
|
||||||
|
# Finally, should have called send_command with a list of projetctor status checks
|
||||||
|
mock_send_command.assert_has_calls(call_list, 'Should have queued projector queries')
|
||||||
|
|
|
@ -26,13 +26,15 @@ record functions.
|
||||||
PREREQUISITE: add_record() and get_all() functions validated.
|
PREREQUISITE: add_record() and get_all() functions validated.
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
|
import shutil
|
||||||
from unittest import TestCase
|
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.db import Manufacturer, Model, Projector, ProjectorDB, ProjectorSource, Source
|
||||||
from openlp.core.lib.projector.constants import PJLINK_PORT
|
from openlp.core.lib.projector.constants import PJLINK_PORT
|
||||||
|
|
||||||
from tests.functional import MagicMock, patch
|
from tests.resources.projector.data import TEST_DB_PJLINK1, TEST_DB, TEST1_DATA, TEST2_DATA, TEST3_DATA
|
||||||
from tests.resources.projector.data import TEST_DB, TEST1_DATA, TEST2_DATA, TEST3_DATA
|
from tests.utils.constants import TEST_RESOURCES_PATH
|
||||||
|
|
||||||
|
|
||||||
def compare_data(one, two):
|
def compare_data(one, two):
|
||||||
|
@ -45,7 +47,11 @@ def compare_data(one, two):
|
||||||
one.port == two.port and \
|
one.port == two.port and \
|
||||||
one.name == two.name and \
|
one.name == two.name and \
|
||||||
one.location == two.location 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):
|
def compare_source(one, two):
|
||||||
|
@ -168,6 +174,10 @@ class TestProjectorDB(TestCase):
|
||||||
record.name = TEST3_DATA['name']
|
record.name = TEST3_DATA['name']
|
||||||
record.location = TEST3_DATA['location']
|
record.location = TEST3_DATA['location']
|
||||||
record.notes = TEST3_DATA['notes']
|
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)
|
updated = self.projector.update_projector(record)
|
||||||
self.assertTrue(updated, 'Save updated record should have returned True')
|
self.assertTrue(updated, 'Save updated record should have returned True')
|
||||||
record = self.projector.get_projector_by_ip(TEST3_DATA['ip'])
|
record = self.projector.get_projector_by_ip(TEST3_DATA['ip'])
|
||||||
|
@ -246,7 +256,8 @@ class TestProjectorDB(TestCase):
|
||||||
projector = Projector()
|
projector = Projector()
|
||||||
|
|
||||||
# WHEN: projector() is populated
|
# 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.source_list should return an empty list
|
||||||
projector.id = 0
|
projector.id = 0
|
||||||
projector.ip = '127.0.0.1'
|
projector.ip = '127.0.0.1'
|
||||||
|
@ -262,8 +273,9 @@ class TestProjectorDB(TestCase):
|
||||||
self.assertEqual(str(projector),
|
self.assertEqual(str(projector),
|
||||||
'< Projector(id="0", ip="127.0.0.1", port="4352", pin="None", name="Test One", '
|
'< 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", '
|
'location="Somewhere over the rainbow", notes="Not again", pjlink_name="TEST", '
|
||||||
'manufacturer="IN YOUR DREAMS", model="OpenLP", other="None", sources="None", '
|
'manufacturer="IN YOUR DREAMS", model="OpenLP", serial_no="None", other="None", '
|
||||||
'source_list="[]") >',
|
'sources="None", source_list="[]", model_filter="None", model_lamp="None", '
|
||||||
|
'sw_version="None") >',
|
||||||
'Projector.__repr__() should have returned a proper representation string')
|
'Projector.__repr__() should have returned a proper representation string')
|
||||||
|
|
||||||
def test_projectorsource_repr(self):
|
def test_projectorsource_repr(self):
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
Package to test the openlp.core.ui.renderer package.
|
Package to test the openlp.core.ui.renderer package.
|
||||||
"""
|
"""
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
from PyQt5 import QtCore
|
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.renderer import words_split, get_start_tags
|
||||||
from openlp.core.lib.theme import ThemeXML
|
from openlp.core.lib.theme import ThemeXML
|
||||||
|
|
||||||
from tests.functional import MagicMock, patch
|
|
||||||
|
|
||||||
SCREEN = {
|
SCREEN = {
|
||||||
'primary': False,
|
'primary': False,
|
||||||
|
|
|
@ -23,12 +23,12 @@
|
||||||
Package to test the openlp.core.lib.screenlist package.
|
Package to test the openlp.core.lib.screenlist package.
|
||||||
"""
|
"""
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
from PyQt5 import QtCore, QtWidgets
|
from PyQt5 import QtCore, QtWidgets
|
||||||
|
|
||||||
from openlp.core.common import Registry
|
from openlp.core.common import Registry
|
||||||
from openlp.core.lib import ScreenList
|
from openlp.core.lib import ScreenList
|
||||||
from tests.functional import MagicMock
|
|
||||||
|
|
||||||
SCREEN = {
|
SCREEN = {
|
||||||
'primary': False,
|
'primary': False,
|
||||||
|
|
|
@ -24,12 +24,12 @@ Package to test the openlp.core.lib package.
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
from tests.functional import MagicMock, patch
|
|
||||||
from tests.utils import assert_length, convert_file_service_item
|
|
||||||
|
|
||||||
from openlp.core.common import Registry, md5_hash
|
from openlp.core.common import Registry, md5_hash
|
||||||
from openlp.core.lib import ItemCapabilities, ServiceItem, ServiceItemType
|
from openlp.core.lib import ItemCapabilities, ServiceItem, ServiceItemType, FormattingTags
|
||||||
|
|
||||||
|
from tests.utils import assert_length, convert_file_service_item
|
||||||
|
|
||||||
VERSE = 'The Lord said to {r}Noah{/r}: \n'\
|
VERSE = 'The Lord said to {r}Noah{/r}: \n'\
|
||||||
'There\'s gonna be a {su}floody{/su}, {sb}floody{/sb}\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'\
|
'Get those children out of the muddy, muddy \n'\
|
||||||
'{r}C{/r}{b}h{/b}{bl}i{/bl}{y}l{/y}{g}d{/g}{pk}'\
|
'{r}C{/r}{b}h{/b}{bl}i{/bl}{y}l{/y}{g}d{/g}{pk}'\
|
||||||
'r{/pk}{o}e{/o}{pp}n{/pp} of the Lord\n'
|
'r{/pk}{o}e{/o}{pp}n{/pp} of the Lord\n'
|
||||||
|
CLEANED_VERSE = 'The Lord said to Noah: \n'\
|
||||||
|
'There\'s gonna be a floody, floody\n'\
|
||||||
|
'The Lord said to Noah:\n'\
|
||||||
|
'There\'s gonna be a floody, floody\n'\
|
||||||
|
'Get those children out of the muddy, muddy \n'\
|
||||||
|
'Children of the Lord\n'
|
||||||
|
RENDERED_VERSE = 'The Lord said to <span style="-webkit-text-fill-color:red">Noah</span>: \n'\
|
||||||
|
'There's gonna be a <sup>floody</sup>, <sub>floody</sub>\n'\
|
||||||
|
'The Lord said to <span style="-webkit-text-fill-color:green">Noah</span>:\n'\
|
||||||
|
'There's gonna be a <strong>floody</strong>, <em>floody</em>\n'\
|
||||||
|
'Get those children out of the muddy, muddy \n'\
|
||||||
|
'<span style="-webkit-text-fill-color:red">C</span><span style="-webkit-text-fill-color:black">h' \
|
||||||
|
'</span><span style="-webkit-text-fill-color:blue">i</span>'\
|
||||||
|
'<span style="-webkit-text-fill-color:yellow">l</span><span style="-webkit-text-fill-color:green">d'\
|
||||||
|
'</span><span style="-webkit-text-fill-color:#FFC0CB">r</span>'\
|
||||||
|
'<span style="-webkit-text-fill-color:#FFA500">e</span><span style="-webkit-text-fill-color:#800080">'\
|
||||||
|
'n</span> of the Lord\n'
|
||||||
FOOTER = ['Arky Arky (Unknown)', 'Public Domain', 'CCLI 123456']
|
FOOTER = ['Arky Arky (Unknown)', 'Public Domain', 'CCLI 123456']
|
||||||
TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'resources', 'service'))
|
TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'resources', 'service'))
|
||||||
|
|
||||||
|
@ -74,6 +91,7 @@ class TestServiceItem(TestCase):
|
||||||
# GIVEN: A new service item and a mocked add icon function
|
# GIVEN: A new service item and a mocked add icon function
|
||||||
service_item = ServiceItem(None)
|
service_item = ServiceItem(None)
|
||||||
service_item.add_icon = MagicMock()
|
service_item.add_icon = MagicMock()
|
||||||
|
FormattingTags.load_tags()
|
||||||
|
|
||||||
# WHEN: We add a custom from a saved service
|
# WHEN: We add a custom from a saved service
|
||||||
line = convert_file_service_item(TEST_PATH, 'serviceitem_custom_1.osj')
|
line = convert_file_service_item(TEST_PATH, 'serviceitem_custom_1.osj')
|
||||||
|
@ -89,9 +107,9 @@ class TestServiceItem(TestCase):
|
||||||
|
|
||||||
# THEN: The frames should also be valid
|
# THEN: The frames should also be valid
|
||||||
self.assertEqual('Test Custom', service_item.get_display_title(), 'The title should be "Test Custom"')
|
self.assertEqual('Test Custom', service_item.get_display_title(), 'The title should be "Test Custom"')
|
||||||
self.assertEqual(VERSE[:-1], service_item.get_frames()[0]['text'],
|
self.assertEqual(CLEANED_VERSE[:-1], service_item.get_frames()[0]['text'],
|
||||||
'The returned text matches the input, except the last line feed')
|
'The returned text matches the input, except the last line feed')
|
||||||
self.assertEqual(VERSE.split('\n', 1)[0], service_item.get_rendered_frame(1),
|
self.assertEqual(RENDERED_VERSE.split('\n', 1)[0], service_item.get_rendered_frame(1),
|
||||||
'The first line has been returned')
|
'The first line has been returned')
|
||||||
self.assertEqual('Slide 1', service_item.get_frame_title(0), '"Slide 1" has been returned as the title')
|
self.assertEqual('Slide 1', service_item.get_frame_title(0), '"Slide 1" has been returned as the title')
|
||||||
self.assertEqual('Slide 2', service_item.get_frame_title(1), '"Slide 2" has been returned as the title')
|
self.assertEqual('Slide 2', service_item.get_frame_title(1), '"Slide 2" has been returned as the title')
|
||||||
|
@ -300,6 +318,7 @@ class TestServiceItem(TestCase):
|
||||||
# GIVEN: A new service item and a mocked add icon function
|
# GIVEN: A new service item and a mocked add icon function
|
||||||
service_item = ServiceItem(None)
|
service_item = ServiceItem(None)
|
||||||
service_item.add_icon = MagicMock()
|
service_item.add_icon = MagicMock()
|
||||||
|
FormattingTags.load_tags()
|
||||||
|
|
||||||
# WHEN: We add a custom from a saved service
|
# WHEN: We add a custom from a saved service
|
||||||
line = convert_file_service_item(TEST_PATH, 'serviceitem-song-linked-audio.osj')
|
line = convert_file_service_item(TEST_PATH, 'serviceitem-song-linked-audio.osj')
|
||||||
|
@ -315,9 +334,9 @@ class TestServiceItem(TestCase):
|
||||||
|
|
||||||
# THEN: The frames should also be valid
|
# THEN: The frames should also be valid
|
||||||
self.assertEqual('Amazing Grace', service_item.get_display_title(), 'The title should be "Amazing Grace"')
|
self.assertEqual('Amazing Grace', service_item.get_display_title(), 'The title should be "Amazing Grace"')
|
||||||
self.assertEqual(VERSE[:-1], service_item.get_frames()[0]['text'],
|
self.assertEqual(CLEANED_VERSE[:-1], service_item.get_frames()[0]['text'],
|
||||||
'The returned text matches the input, except the last line feed')
|
'The returned text matches the input, except the last line feed')
|
||||||
self.assertEqual(VERSE.split('\n', 1)[0], service_item.get_rendered_frame(1),
|
self.assertEqual(RENDERED_VERSE.split('\n', 1)[0], service_item.get_rendered_frame(1),
|
||||||
'The first line has been returned')
|
'The first line has been returned')
|
||||||
self.assertEqual('Amazing Grace! how sweet the s', service_item.get_frame_title(0),
|
self.assertEqual('Amazing Grace! how sweet the s', service_item.get_frame_title(0),
|
||||||
'"Amazing Grace! how sweet the s" has been returned as the title')
|
'"Amazing Grace! how sweet the s" has been returned as the title')
|
||||||
|
|
|
@ -22,14 +22,15 @@
|
||||||
"""
|
"""
|
||||||
Package to test the openlp.core.lib.ui package.
|
Package to test the openlp.core.lib.ui package.
|
||||||
"""
|
"""
|
||||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
|
||||||
from unittest import TestCase
|
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.common import UiStrings, translate
|
||||||
from openlp.core.lib.ui import add_welcome_page, create_button_box, create_horizontal_adjusting_combo_box, \
|
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, \
|
create_button, create_action, create_valign_selection_widgets, find_and_set_in_combo_box, create_widget_action, \
|
||||||
set_case_insensitive_completer
|
set_case_insensitive_completer
|
||||||
from tests.functional import MagicMock, patch
|
|
||||||
|
|
||||||
|
|
||||||
class TestUi(TestCase):
|
class TestUi(TestCase):
|
||||||
|
|
|
@ -24,10 +24,10 @@ Package to test the openlp.core.ui.firsttimeform package.
|
||||||
"""
|
"""
|
||||||
import datetime
|
import datetime
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
from openlp.core.ui.aboutform import AboutForm
|
from openlp.core.ui.aboutform import AboutForm
|
||||||
|
|
||||||
from tests.functional import patch
|
|
||||||
from tests.helpers.testmixin import TestMixin
|
from tests.helpers.testmixin import TestMixin
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -26,15 +26,13 @@ Package to test the openlp.core.ui.exeptionform package.
|
||||||
import os
|
import os
|
||||||
import tempfile
|
import tempfile
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
from unittest.mock import mock_open
|
from unittest.mock import mock_open, patch
|
||||||
|
|
||||||
from openlp.core.common import Registry
|
from openlp.core.common import Registry
|
||||||
|
|
||||||
from tests.functional import patch
|
|
||||||
from tests.helpers.testmixin import TestMixin
|
|
||||||
|
|
||||||
from openlp.core.ui import exceptionform
|
from openlp.core.ui import exceptionform
|
||||||
|
|
||||||
|
from tests.helpers.testmixin import TestMixin
|
||||||
|
|
||||||
exceptionform.WEBKIT_VERSION = 'Webkit Test'
|
exceptionform.WEBKIT_VERSION = 'Webkit Test'
|
||||||
exceptionform.MIGRATE_VERSION = 'Migrate Test'
|
exceptionform.MIGRATE_VERSION = 'Migrate Test'
|
||||||
exceptionform.CHARDET_VERSION = 'CHARDET Test'
|
exceptionform.CHARDET_VERSION = 'CHARDET Test'
|
||||||
|
|
|
@ -22,17 +22,16 @@
|
||||||
"""
|
"""
|
||||||
Package to test the openlp.core.utils.__init__ package.
|
Package to test the openlp.core.utils.__init__ package.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from unittest import TestCase
|
|
||||||
import urllib.request
|
import urllib.request
|
||||||
import urllib.error
|
import urllib.error
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
from unittest import TestCase
|
||||||
from tests.functional import patch
|
from unittest.mock import patch
|
||||||
from tests.helpers.testmixin import TestMixin
|
|
||||||
|
|
||||||
from openlp.core.common.httputils import CONNECTION_RETRIES, get_web_page
|
from openlp.core.common.httputils import CONNECTION_RETRIES, get_web_page
|
||||||
|
|
||||||
|
from tests.helpers.testmixin import TestMixin
|
||||||
|
|
||||||
|
|
||||||
class TestFirstTimeWizard(TestMixin, TestCase):
|
class TestFirstTimeWizard(TestMixin, TestCase):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -26,11 +26,11 @@ import os
|
||||||
import tempfile
|
import tempfile
|
||||||
import urllib
|
import urllib
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
from openlp.core.common import Registry
|
from openlp.core.common import Registry
|
||||||
from openlp.core.ui.firsttimeform import FirstTimeForm
|
from openlp.core.ui.firsttimeform import FirstTimeForm
|
||||||
|
|
||||||
from tests.functional import MagicMock, patch
|
|
||||||
from tests.helpers.testmixin import TestMixin
|
from tests.helpers.testmixin import TestMixin
|
||||||
|
|
||||||
FAKE_CONFIG = b"""
|
FAKE_CONFIG = b"""
|
||||||
|
|
|
@ -23,8 +23,7 @@
|
||||||
Package to test the openlp.core.ui.formattingtagsform package.
|
Package to test the openlp.core.ui.formattingtagsform package.
|
||||||
"""
|
"""
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
from unittest.mock import MagicMock, patch, call
|
||||||
from tests.functional import MagicMock, patch, call
|
|
||||||
|
|
||||||
from openlp.core.ui.formattingtagform import FormattingTagForm
|
from openlp.core.ui.formattingtagform import FormattingTagForm
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
Package to test the openlp.core.ui.slidecontroller package.
|
Package to test the openlp.core.ui.slidecontroller package.
|
||||||
"""
|
"""
|
||||||
from unittest import TestCase, skipUnless
|
from unittest import TestCase, skipUnless
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
from PyQt5 import QtCore
|
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 openlp.core.ui.maindisplay import TRANSPARENT_STYLESHEET, OPAQUE_STYLESHEET
|
||||||
|
|
||||||
from tests.helpers.testmixin import TestMixin
|
from tests.helpers.testmixin import TestMixin
|
||||||
from tests.functional import MagicMock, patch
|
|
||||||
|
|
||||||
if is_macosx():
|
if is_macosx():
|
||||||
from ctypes import pythonapi, c_void_p, c_char_p, py_object
|
from ctypes import pythonapi, c_void_p, c_char_p, py_object
|
||||||
|
|
||||||
from sip import voidptr
|
from sip import voidptr
|
||||||
from objc import objc_object
|
from objc import objc_object
|
||||||
from AppKit import NSMainMenuWindowLevel, NSWindowCollectionBehaviorManaged
|
from AppKit import NSMainMenuWindowLevel, NSWindowCollectionBehaviorManaged
|
||||||
|
|
|
@ -23,8 +23,8 @@
|
||||||
Package to test openlp.core.ui.mainwindow package.
|
Package to test openlp.core.ui.mainwindow package.
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
from PyQt5 import QtWidgets
|
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.lib.ui import UiStrings
|
||||||
from openlp.core.common.registry import Registry
|
from openlp.core.common.registry import Registry
|
||||||
|
|
||||||
from tests.functional import MagicMock, patch
|
|
||||||
from tests.helpers.testmixin import TestMixin
|
from tests.helpers.testmixin import TestMixin
|
||||||
from tests.utils.constants import TEST_RESOURCES_PATH
|
from tests.utils.constants import TEST_RESOURCES_PATH
|
||||||
|
|
||||||
|
|
|
@ -22,12 +22,13 @@
|
||||||
"""
|
"""
|
||||||
Package to test the openlp.core.ui package.
|
Package to test the openlp.core.ui package.
|
||||||
"""
|
"""
|
||||||
from PyQt5 import QtCore
|
|
||||||
from unittest import TestCase
|
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 openlp.core.ui.media import get_media_players, parse_optical_path
|
||||||
|
|
||||||
from tests.functional import MagicMock, patch
|
|
||||||
from tests.helpers.testmixin import TestMixin
|
from tests.helpers.testmixin import TestMixin
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -24,15 +24,14 @@ Package to test the openlp.core.ui.slidecontroller package.
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
import PyQt5
|
import PyQt5
|
||||||
|
|
||||||
from openlp.core.common import Registry, ThemeLevel
|
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.lib import ServiceItem, ServiceItemType, ItemCapabilities
|
||||||
from openlp.core.ui import ServiceManager
|
from openlp.core.ui import ServiceManager
|
||||||
|
from openlp.core.ui.lib.toolbar import OpenLPToolbar
|
||||||
from tests.functional import MagicMock, patch
|
|
||||||
|
|
||||||
|
|
||||||
class TestServiceManager(TestCase):
|
class TestServiceManager(TestCase):
|
||||||
|
|
|
@ -22,14 +22,14 @@
|
||||||
"""
|
"""
|
||||||
Package to test the openlp.core.ui.settingsform package.
|
Package to test the openlp.core.ui.settingsform package.
|
||||||
"""
|
"""
|
||||||
from PyQt5 import QtWidgets
|
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
from PyQt5 import QtWidgets
|
||||||
|
|
||||||
from openlp.core.common import Registry
|
from openlp.core.common import Registry
|
||||||
from openlp.core.ui.settingsform import SettingsForm
|
from openlp.core.ui.settingsform import SettingsForm
|
||||||
|
|
||||||
from tests.functional import MagicMock, patch
|
|
||||||
|
|
||||||
|
|
||||||
class TestSettingsForm(TestCase):
|
class TestSettingsForm(TestCase):
|
||||||
|
|
||||||
|
|
|
@ -22,12 +22,12 @@
|
||||||
"""
|
"""
|
||||||
Package to test the openlp.core.ui.shortcutlistdialog package.
|
Package to test the openlp.core.ui.shortcutlistdialog package.
|
||||||
"""
|
"""
|
||||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
from PyQt5 import QtCore
|
||||||
|
|
||||||
from openlp.core.ui.shortcutlistdialog import CaptureShortcutButton, ShortcutTreeWidget
|
from openlp.core.ui.shortcutlistdialog import CaptureShortcutButton, ShortcutTreeWidget
|
||||||
|
|
||||||
from tests.interfaces import MagicMock, patch
|
|
||||||
|
|
||||||
|
|
||||||
def test_key_press_event():
|
def test_key_press_event():
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -22,16 +22,16 @@
|
||||||
"""
|
"""
|
||||||
Package to test the openlp.core.ui.slidecontroller package.
|
Package to test the openlp.core.ui.slidecontroller package.
|
||||||
"""
|
"""
|
||||||
|
from unittest import TestCase
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
from PyQt5 import QtCore, QtGui
|
from PyQt5 import QtCore, QtGui
|
||||||
|
|
||||||
from unittest import TestCase
|
|
||||||
from openlp.core import Registry
|
from openlp.core import Registry
|
||||||
from openlp.core.lib import ImageSource, ServiceItemAction
|
from openlp.core.lib import ImageSource, ServiceItemAction
|
||||||
from openlp.core.ui import SlideController, LiveController, PreviewController
|
from openlp.core.ui import SlideController, LiveController, PreviewController
|
||||||
from openlp.core.ui.slidecontroller import InfoLabel, WIDE_MENU, NON_TEXT_MENU
|
from openlp.core.ui.slidecontroller import InfoLabel, WIDE_MENU, NON_TEXT_MENU
|
||||||
|
|
||||||
from tests.functional import MagicMock, patch
|
|
||||||
|
|
||||||
|
|
||||||
class TestSlideController(TestCase):
|
class TestSlideController(TestCase):
|
||||||
|
|
||||||
|
|
|
@ -22,72 +22,31 @@
|
||||||
"""
|
"""
|
||||||
Package to test the openlp.core.ui.themeform package.
|
Package to test the openlp.core.ui.themeform package.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
from openlp.core.ui import ThemeForm
|
from openlp.core.ui import ThemeForm
|
||||||
|
|
||||||
from tests.functional import MagicMock, patch
|
|
||||||
|
|
||||||
|
|
||||||
class TestThemeManager(TestCase):
|
class TestThemeManager(TestCase):
|
||||||
"""
|
"""
|
||||||
Test the functions in the ThemeManager Class
|
Test the functions in the ThemeManager Class
|
||||||
"""
|
"""
|
||||||
def test_select_image_file_dialog_cancelled(self):
|
def setUp(self):
|
||||||
"""
|
with patch('openlp.core.ui.ThemeForm._setup'):
|
||||||
Test the select image file dialog when the user presses cancel
|
self.instance = ThemeForm(None)
|
||||||
"""
|
|
||||||
# GIVEN: An instance of Theme Form and mocked QFileDialog which returns an empty string (similating a user
|
|
||||||
# pressing cancel)
|
|
||||||
with patch('openlp.core.ui.ThemeForm._setup'),\
|
|
||||||
patch('openlp.core.ui.themeform.get_images_filter',
|
|
||||||
**{'return_value': 'Image Files (*.bmp; *.gif)(*.bmp *.gif)'}),\
|
|
||||||
patch('openlp.core.ui.themeform.QtWidgets.QFileDialog.getOpenFileName',
|
|
||||||
**{'return_value': ('', '')}) as mocked_get_open_file_name,\
|
|
||||||
patch('openlp.core.ui.themeform.translate', **{'return_value': 'Translated String'}),\
|
|
||||||
patch('openlp.core.ui.ThemeForm.set_background_page_values') as mocked_set_background_page_values:
|
|
||||||
instance = ThemeForm(None)
|
|
||||||
mocked_image_file_edit = MagicMock()
|
|
||||||
mocked_image_file_edit.text.return_value = '/original_path/file.ext'
|
|
||||||
instance.image_file_edit = mocked_image_file_edit
|
|
||||||
|
|
||||||
# WHEN: on_image_browse_button is clicked
|
def test_on_image_path_edit_path_changed(self):
|
||||||
instance.on_image_browse_button_clicked()
|
"""
|
||||||
|
Test the `image_path_edit.pathChanged` handler
|
||||||
|
"""
|
||||||
|
# GIVEN: An instance of Theme Form
|
||||||
|
with patch.object(self.instance, 'set_background_page_values') as mocked_set_background_page_values:
|
||||||
|
self.instance.theme = MagicMock()
|
||||||
|
|
||||||
# THEN: The QFileDialog getOpenFileName and set_background_page_values moethods should have been called
|
# WHEN: `on_image_path_edit_path_changed` is clicked
|
||||||
# with known arguments
|
self.instance.on_image_path_edit_path_changed('/new/pat.h')
|
||||||
mocked_get_open_file_name.assert_called_once_with(instance, 'Translated String', '/original_path/file.ext',
|
|
||||||
'Image Files (*.bmp; *.gif)(*.bmp *.gif);;'
|
# THEN: The theme background file should be set and `set_background_page_values` should have been called
|
||||||
'All Files (*.*)')
|
self.assertEqual(self.instance.theme.background_filename, '/new/pat.h')
|
||||||
mocked_set_background_page_values.assert_called_once_with()
|
mocked_set_background_page_values.assert_called_once_with()
|
||||||
|
|
||||||
def test_select_image_file_dialog_new_file(self):
|
|
||||||
"""
|
|
||||||
Test the select image file dialog when the user presses ok
|
|
||||||
"""
|
|
||||||
# GIVEN: An instance of Theme Form and mocked QFileDialog which returns a file path
|
|
||||||
with patch('openlp.core.ui.ThemeForm._setup'),\
|
|
||||||
patch('openlp.core.ui.themeform.get_images_filter',
|
|
||||||
**{'return_value': 'Image Files (*.bmp; *.gif)(*.bmp *.gif)'}),\
|
|
||||||
patch('openlp.core.ui.themeform.QtWidgets.QFileDialog.getOpenFileName',
|
|
||||||
**{'return_value': ('/new_path/file.ext', '')}) as mocked_get_open_file_name,\
|
|
||||||
patch('openlp.core.ui.themeform.translate', **{'return_value': 'Translated String'}),\
|
|
||||||
patch('openlp.core.ui.ThemeForm.set_background_page_values') as mocked_background_page_values:
|
|
||||||
instance = ThemeForm(None)
|
|
||||||
mocked_image_file_edit = MagicMock()
|
|
||||||
mocked_image_file_edit.text.return_value = '/original_path/file.ext'
|
|
||||||
instance.image_file_edit = mocked_image_file_edit
|
|
||||||
instance.theme = MagicMock()
|
|
||||||
|
|
||||||
# WHEN: on_image_browse_button is clicked
|
|
||||||
instance.on_image_browse_button_clicked()
|
|
||||||
|
|
||||||
# THEN: The QFileDialog getOpenFileName and set_background_page_values moethods should have been called
|
|
||||||
# with known arguments and theme.background_filename should be set
|
|
||||||
mocked_get_open_file_name.assert_called_once_with(instance, 'Translated String', '/original_path/file.ext',
|
|
||||||
'Image Files (*.bmp; *.gif)(*.bmp *.gif);;'
|
|
||||||
'All Files (*.*)')
|
|
||||||
self.assertEqual(instance.theme.background_filename, '/new_path/file.ext',
|
|
||||||
'theme.background_filename should be set to the path that the file dialog returns')
|
|
||||||
mocked_background_page_values.assert_called_once_with()
|
|
||||||
|
|
|
@ -24,18 +24,16 @@ Package to test the openlp.core.ui.thememanager package.
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
from unittest import TestCase
|
|
||||||
from tempfile import mkdtemp
|
from tempfile import mkdtemp
|
||||||
|
from unittest import TestCase
|
||||||
|
from unittest.mock import ANY, MagicMock, patch
|
||||||
|
|
||||||
from PyQt5 import QtWidgets
|
from PyQt5 import QtWidgets
|
||||||
from tempfile import mkdtemp
|
|
||||||
|
|
||||||
from openlp.core.ui import ThemeManager
|
from openlp.core.ui import ThemeManager
|
||||||
from openlp.core.common import Registry
|
from openlp.core.common import Registry
|
||||||
|
|
||||||
from tests.utils.constants import TEST_RESOURCES_PATH
|
from tests.utils.constants import TEST_RESOURCES_PATH
|
||||||
from tests.functional import ANY, MagicMock, patch
|
|
||||||
|
|
||||||
|
|
||||||
class TestThemeManager(TestCase):
|
class TestThemeManager(TestCase):
|
||||||
|
|
|
@ -23,13 +23,13 @@
|
||||||
Package to test the openlp.core.ui.ThemeTab package.
|
Package to test the openlp.core.ui.ThemeTab package.
|
||||||
"""
|
"""
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
from openlp.core.common import Registry
|
from openlp.core.common import Registry
|
||||||
from openlp.core.ui.themestab import ThemesTab
|
from openlp.core.ui.themestab import ThemesTab
|
||||||
from openlp.core.ui.settingsform import SettingsForm
|
from openlp.core.ui.settingsform import SettingsForm
|
||||||
|
|
||||||
from tests.helpers.testmixin import TestMixin
|
from tests.helpers.testmixin import TestMixin
|
||||||
from tests.functional import MagicMock
|
|
||||||
|
|
||||||
|
|
||||||
class TestThemeTab(TestCase, TestMixin):
|
class TestThemeTab(TestCase, TestMixin):
|
||||||
|
|
|
@ -20,12 +20,12 @@
|
||||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||||
###############################################################################
|
###############################################################################
|
||||||
"""
|
"""
|
||||||
This module contains tests for the openlp.core.lib.filedialog module
|
This module contains tests for the openlp.core.ui.lib.colorbutton module
|
||||||
"""
|
"""
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
from unittest.mock import MagicMock, call, patch
|
||||||
|
|
||||||
from openlp.core.ui.lib.colorbutton import ColorButton
|
from openlp.core.ui.lib import ColorButton
|
||||||
from tests.functional import MagicMock, call, patch
|
|
||||||
|
|
||||||
|
|
||||||
class TestColorDialog(TestCase):
|
class TestColorDialog(TestCase):
|
||||||
|
@ -148,10 +148,9 @@ class TestColorDialog(TestCase):
|
||||||
widget.on_clicked()
|
widget.on_clicked()
|
||||||
|
|
||||||
# THEN: change_color should not have been called and the colorChanged signal should not have been emitted
|
# THEN: change_color should not have been called and the colorChanged signal should not have been emitted
|
||||||
self.assertEqual(
|
self.assertFalse(self.mocked_change_color.called,
|
||||||
self.mocked_change_color.call_count, 0, 'change_color should not have been called with an invalid color')
|
'change_color should not have been called with an invalid color')
|
||||||
self.assertEqual(
|
self.assertFalse(self.mocked_color_changed.emit.called,
|
||||||
self.mocked_color_changed.emit.call_count, 0,
|
|
||||||
'colorChange signal should not have been emitted with an invalid color')
|
'colorChange signal should not have been emitted with an invalid color')
|
||||||
|
|
||||||
def test_on_clicked_same_color(self):
|
def test_on_clicked_same_color(self):
|
||||||
|
@ -171,11 +170,9 @@ class TestColorDialog(TestCase):
|
||||||
widget.on_clicked()
|
widget.on_clicked()
|
||||||
|
|
||||||
# THEN: change_color should not have been called and the colorChanged signal should not have been emitted
|
# THEN: change_color should not have been called and the colorChanged signal should not have been emitted
|
||||||
self.assertEqual(
|
self.assertFalse(self.mocked_change_color.called,
|
||||||
self.mocked_change_color.call_count, 0,
|
|
||||||
'change_color should not have been called when the color has not changed')
|
'change_color should not have been called when the color has not changed')
|
||||||
self.assertEqual(
|
self.assertFalse(self.mocked_color_changed.emit.called,
|
||||||
self.mocked_color_changed.emit.call_count, 0,
|
|
||||||
'colorChange signal should not have been emitted when the color has not changed')
|
'colorChange signal should not have been emitted when the color has not changed')
|
||||||
|
|
||||||
def test_on_clicked_new_color(self):
|
def test_on_clicked_new_color(self):
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue