This commit is contained in:
Raoul Snyman 2019-09-11 21:41:43 -07:00
commit 83cd5c1a88
117 changed files with 1951 additions and 320959 deletions

View File

@ -8,7 +8,7 @@
*.nja
*.orig
*.pyc
*.qm
resources/i18n/*.ts
*.rej
*.ropeproject
*.~\?~

View File

@ -16,3 +16,5 @@ include copyright.txt
include LICENSE
include README.txt
include openlp/.version
include package.json
include karma.conf.js

View File

@ -26,18 +26,21 @@ module.exports = function(config) {
// source files, that you wanna generate coverage for
// do not include tests or libraries
// (these files will be instrumented by Istanbul)
"display.js": ["coverage"]
// "display.js": ["coverage"]
},
// test results reporter to use
// possible values: "dots", "progress"
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
reporters: ["progress", "coverage"],
reporters: ["dots", "junit"],
// configure the coverateReporter
coverageReporter: {
/* coverageReporter: {
type : "html",
dir : "htmlcov/"
}, */
junitReporter: {
outputFile: "test-results.xml"
},
// web server port
@ -60,11 +63,11 @@ module.exports = function(config) {
// start these browsers
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
browsers: ["PhantomJS"],
browsers: ["Chromium"],
// Continuous Integration mode
// if true, Karma captures browsers, runs the tests and exits
singleRun: false,
singleRun: true,
// Concurrency level
// how many browser should be started simultaneous

86
openlp/__main__.py Normal file
View File

@ -0,0 +1,86 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
##########################################################################
# OpenLP - Open Source Lyrics Projection #
# ---------------------------------------------------------------------- #
# Copyright (c) 2008-2019 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, either version 3 of the License, or #
# (at your option) any later version. #
# #
# 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, see <https://www.gnu.org/licenses/>. #
##########################################################################
"""
The entrypoint for OpenLP
"""
import atexit
import faulthandler
import logging
import multiprocessing
import sys
# from OpenGL import GL
from openlp.core.app import main
from openlp.core.common import is_macosx, is_win
from openlp.core.common.applocation import AppLocation
from openlp.core.common.path import create_paths
log = logging.getLogger(__name__)
error_log_file = None
def tear_down_fault_handling():
"""
When Python exits, close the file we were using for the faulthandler
"""
global error_log_file
error_log_file.close()
def set_up_fault_handling():
"""
Set up the Python fault handler
"""
global error_log_file
# Create the cache directory if it doesn't exist, and enable the fault handler to log to an error log file
try:
create_paths(AppLocation.get_directory(AppLocation.CacheDir))
error_log_file = (AppLocation.get_directory(AppLocation.CacheDir) / 'error.log').open('wb')
atexit.register(tear_down_fault_handling)
faulthandler.enable(error_log_file)
except OSError:
log.exception('An exception occurred when enabling the fault handler')
atexit.unregister(tear_down_fault_handling)
if error_log_file:
error_log_file.close()
def start():
"""
Instantiate and run the application.
"""
set_up_fault_handling()
# Add support for using multiprocessing from frozen Windows executable (built using PyInstaller),
# see https://docs.python.org/3/library/multiprocessing.html#multiprocessing.freeze_support
if is_win():
multiprocessing.freeze_support()
# Mac OS X passes arguments like '-psn_XXXX' to the application. This argument is actually a process serial number.
# However, this causes a conflict with other OpenLP arguments. Since we do not use this argument we can delete it
# to avoid any potential conflicts.
if is_macosx():
sys.argv = [x for x in sys.argv if not x.startswith('-psn')]
main()
if __name__ == '__main__':
start()

View File

@ -24,7 +24,7 @@ The :mod:`~openlp.core.api.tab` module contains the settings tab for the API
"""
from PyQt5 import QtCore, QtGui, QtWidgets
from openlp.core.common import get_local_ip4
from openlp.core.common import get_network_interfaces
from openlp.core.common.i18n import UiStrings, translate
from openlp.core.common.registry import Registry
from openlp.core.common.settings import Settings
@ -194,8 +194,7 @@ class ApiTab(SettingsTab):
http_url_temp = http_url + 'main'
self.live_url.setText('<a href="{url}">{url}</a>'.format(url=http_url_temp))
@staticmethod
def get_ip_address(ip_address):
def get_ip_address(self, ip_address):
"""
returns the IP address in dependency of the passed address
ip_address == 0.0.0.0: return the IP address of the first valid interface
@ -203,9 +202,8 @@ class ApiTab(SettingsTab):
"""
if ip_address == ZERO_URL:
# In case we have more than one interface
ifaces = get_local_ip4()
for key in iter(ifaces):
ip_address = ifaces.get(key)['ip']
for _, interface in get_network_interfaces().items():
ip_address = interface['ip']
# We only want the first interface returned
break
return ip_address

View File

@ -0,0 +1,99 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
##########################################################################
# OpenLP - Open Source Lyrics Projection #
# ---------------------------------------------------------------------- #
# Copyright (c) 2008-2019 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, either version 3 of the License, or #
# (at your option) any later version. #
# #
# 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, see <https://www.gnu.org/licenses/>. #
##########################################################################
"""
The :mod:`~openlp.core.api.zeroconf` module runs a Zerconf server so that OpenLP can advertise the
RESTful API for devices on the network to discover.
"""
import socket
from time import sleep
from zeroconf import ServiceInfo, Zeroconf
from openlp.core.common import get_network_interfaces
from openlp.core.common.registry import Registry
from openlp.core.common.settings import Settings
from openlp.core.threading import ThreadWorker, run_thread
class ZeroconfWorker(ThreadWorker):
"""
This thread worker runs a Zeroconf service
"""
address = None
http_port = 4316
ws_port = 4317
_can_run = False
def __init__(self, ip_address, http_port=4316, ws_port=4317):
"""
Create the worker for the Zeroconf service
"""
super().__init__()
self.address = socket.inet_aton(ip_address)
self.http_port = http_port
self.ws_port = ws_port
def can_run(self):
"""
Check if the worker can continue to run. This is mostly so that we can override this method
and test the class.
"""
return self._can_run
def start(self):
"""
Start the service
"""
http_info = ServiceInfo('_http._tcp.local.', 'OpenLP._http._tcp.local.',
address=self.address, port=self.http_port, properties={})
ws_info = ServiceInfo('_ws._tcp.local.', 'OpenLP._ws._tcp.local.',
address=self.address, port=self.ws_port, properties={})
zc = Zeroconf()
zc.register_service(http_info)
zc.register_service(ws_info)
self._can_run = True
while self.can_run():
sleep(0.1)
zc.unregister_service(http_info)
zc.unregister_service(ws_info)
zc.close()
self.quit.emit()
def stop(self):
"""
Stop the service
"""
self._can_run = False
def start_zeroconf():
"""
Start the Zeroconf service
"""
# When we're running tests, just skip this set up if this flag is set
if Registry().get_flag('no_web_server'):
return
http_port = Settings().value('api/port')
ws_port = Settings().value('api/websocket port')
for name, interface in get_network_interfaces().items():
worker = ZeroconfWorker(interface['ip'], http_port, ws_port)
run_thread(worker, 'api_zeroconf_{name}'.format(name=name))

View File

@ -391,7 +391,11 @@ def main():
vlc_lib = 'libvlc.dylib'
elif is_win():
vlc_lib = 'libvlc.dll'
os.environ['PYTHON_VLC_LIB_PATH'] = str(AppLocation.get_directory(AppLocation.AppDir) / vlc_lib)
# Path to libvlc
os.environ['PYTHON_VLC_LIB_PATH'] = str(AppLocation.get_directory(AppLocation.AppDir) / 'vlc' / vlc_lib)
log.debug('VLC Path: {}'.format(os.environ['PYTHON_VLC_LIB_PATH']))
# Path to VLC directory containing VLC's "plugins" directory
os.environ['PYTHON_VLC_MODULE_PATH'] = str(AppLocation.get_directory(AppLocation.AppDir) / 'vlc')
log.debug('VLC Path: {}'.format(os.environ['PYTHON_VLC_LIB_PATH']))
# Initialise the Registry
Registry.create()

View File

@ -45,15 +45,16 @@ log = logging.getLogger(__name__ + '.__init__')
FIRST_CAMEL_REGEX = re.compile('(.)([A-Z][a-z]+)')
SECOND_CAMEL_REGEX = re.compile('([a-z0-9])([A-Z])')
CONTROL_CHARS = re.compile(r'[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F]')
INVALID_FILE_CHARS = re.compile(r'[\\/:\*\?"<>\|\+\[\]%]')
INVALID_FILE_CHARS = re.compile(r'[\\/:*?"<>|+\[\]%]')
IMAGES_FILTER = None
REPLACMENT_CHARS_MAP = str.maketrans({'\u2018': '\'', '\u2019': '\'', '\u201c': '"', '\u201d': '"', '\u2026': '...',
'\u2013': '-', '\u2014': '-', '\v': '\n\n', '\f': '\n\n'})
NEW_LINE_REGEX = re.compile(r' ?(\r\n?|\n) ?')
WHITESPACE_REGEX = re.compile(r'[ \t]+')
INTERFACE_FILTER = re.compile('lo|loopback|docker|tun', re.IGNORECASE)
def get_local_ip4():
def get_network_interfaces():
"""
Creates a dictionary of local IPv4 interfaces on local machine.
If no active interfaces available, returns a dict of localhost IPv4 information
@ -61,43 +62,33 @@ def get_local_ip4():
:returns: Dict of interfaces
"""
log.debug('Getting local IPv4 interface(es) information')
my_ip4 = {}
for iface in QNetworkInterface.allInterfaces():
interfaces = {}
for interface in QNetworkInterface.allInterfaces():
interface_name = interface.name()
if INTERFACE_FILTER.search(interface_name):
log.debug('Filtering out interfaces we don\'t care about: {name}'.format(name=interface_name))
continue
log.debug('Checking for isValid and flags == IsUP | IsRunning')
if not iface.isValid() or not (iface.flags() & (QNetworkInterface.IsUp | QNetworkInterface.IsRunning)):
if not interface.isValid() or not (interface.flags() & (QNetworkInterface.IsUp | QNetworkInterface.IsRunning)):
continue
log.debug('Checking address(es) protocol')
for address in iface.addressEntries():
for address in interface.addressEntries():
ip = address.ip()
log.debug('Checking for protocol == IPv4Protocol')
if ip.protocol() == QAbstractSocket.IPv4Protocol:
log.debug('Getting interface information')
my_ip4[iface.name()] = {'ip': ip.toString(),
'broadcast': address.broadcast().toString(),
'netmask': address.netmask().toString(),
'prefix': address.prefixLength(),
'localnet': QHostAddress(address.netmask().toIPv4Address() &
ip.toIPv4Address()).toString()
}
log.debug('Adding {iface} to active list'.format(iface=iface.name()))
if len(my_ip4) == 0:
interfaces[interface_name] = {
'ip': ip.toString(),
'broadcast': address.broadcast().toString(),
'netmask': address.netmask().toString(),
'prefix': address.prefixLength(),
'localnet': QHostAddress(address.netmask().toIPv4Address() &
ip.toIPv4Address()).toString()
}
log.debug('Adding {interface} to active list'.format(interface=interface.name()))
if len(interfaces) == 0:
log.warning('No active IPv4 network interfaces detected')
return my_ip4
if 'localhost' in my_ip4:
log.debug('Renaming windows localhost to lo')
my_ip4['lo'] = my_ip4['localhost']
my_ip4.pop('localhost')
if len(my_ip4) == 1:
if 'lo' in my_ip4:
# No active interfaces - so leave localhost in there
log.warning('No active IPv4 interfaces found except localhost')
else:
# Since we have a valid IP4 interface, remove localhost
if 'lo' in my_ip4:
log.debug('Found at least one IPv4 interface, removing localhost')
my_ip4.pop('lo')
return my_ip4
return interfaces
def trace_error_handler(logger):
@ -112,21 +103,21 @@ def trace_error_handler(logger):
logger.error(log_string)
def extension_loader(glob_pattern, excluded_files=[]):
def extension_loader(glob_pattern, excluded_files=None):
"""
A utility function to find and load OpenLP extensions, such as plugins, presentation and media controllers and
importers.
:param str glob_pattern: A glob pattern used to find the extension(s) to be imported. Should be relative to the
application directory. i.e. plugins/*/*plugin.py
:param list[str] excluded_files: A list of file names to exclude that the glob pattern may find.
:param list[str] | None excluded_files: A list of file names to exclude that the glob pattern may find.
:rtype: None
"""
from openlp.core.common.applocation import AppLocation
app_dir = AppLocation.get_directory(AppLocation.AppDir)
for extension_path in app_dir.glob(glob_pattern):
extension_path = extension_path.relative_to(app_dir)
if extension_path.name in excluded_files:
if extension_path.name in (excluded_files or []):
continue
log.debug('Attempting to import %s', extension_path)
module_name = path_to_module(extension_path)
@ -181,6 +172,21 @@ class SlideLimits(object):
Next = 3
class Singleton(type):
"""
Provide a `Singleton` metaclass https://stackoverflow.com/questions/6760685/creating-a-singleton-in-python
"""
_instances = {}
def __call__(cls, *args, **kwargs):
"""
Create a new instance if one does not already exist.
"""
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
def de_hump(name):
"""
Change any Camel Case string to python string
@ -394,7 +400,7 @@ def get_images_filter():
global IMAGES_FILTER
if not IMAGES_FILTER:
log.debug('Generating images filter.')
formats = list(map(bytes.decode, list(map(bytes, QtGui.QImageReader.supportedImageFormats()))))
formats = list(map(bytes.decode, map(bytes, QtGui.QImageReader.supportedImageFormats())))
visible_formats = '(*.{text})'.format(text='; *.'.join(formats))
actual_formats = '(*.{text})'.format(text=' *.'.join(formats))
IMAGES_FILTER = '{text} {visible} {actual}'.format(text=translate('OpenLP', 'Image Files'),

View File

@ -260,7 +260,7 @@ class ActionList(object):
return
# We have to do this to ensure that the loaded shortcut list e. g. STRG+O (German) is converted to CTRL+O,
# which is only done when we convert the strings in this way (QKeySequencet -> uncode).
shortcuts = list(map(QtGui.QKeySequence.toString, list(map(QtGui.QKeySequence, shortcuts))))
shortcuts = list(map(QtGui.QKeySequence.toString, map(QtGui.QKeySequence, shortcuts)))
# Check the alternate shortcut first, to avoid problems when the alternate shortcut becomes the primary shortcut
# after removing the (initial) primary shortcut due to conflicts.
if len(shortcuts) == 2:

View File

@ -29,7 +29,7 @@ from collections import namedtuple
from PyQt5 import QtCore, QtWidgets
from openlp.core.common import is_macosx, is_win
from openlp.core.common import Singleton, is_macosx, is_win
from openlp.core.common.applocation import AppLocation
from openlp.core.common.settings import Settings
@ -327,22 +327,11 @@ class LanguageManager(object):
return LanguageManager.__qm_list__
class UiStrings(object):
class UiStrings(metaclass=Singleton):
"""
Provide standard strings for objects to use.
"""
__instance__ = None
def __new__(cls):
"""
Override the default object creation method to return a single instance.
"""
if not cls.__instance__:
cls.__instance__ = object.__new__(cls)
cls.load(cls)
return cls.__instance__
def load(self):
def __init__(self):
"""
These strings should need a good reason to be retranslated elsewhere.
Should some/more/less of these have an &amp; attached?
@ -436,6 +425,7 @@ class UiStrings(object):
self.ResetLiveBG = translate('OpenLP.Ui', 'Reset live background.')
self.RequiredShowInFooter = translate('OpenLP.Ui', 'Required, this will be displayed in footer.')
self.Seconds = translate('OpenLP.Ui', 's', 'The abbreviated unit for seconds')
self.SaveAndClose = translate('OpenLP.ui', translate('SongsPlugin.EditSongForm', '&Save && Close'))
self.SaveAndPreview = translate('OpenLP.Ui', 'Save && Preview')
self.Search = translate('OpenLP.Ui', 'Search')
self.SearchThemes = translate('OpenLP.Ui', 'Search Themes...', 'Search bar place holder text ')
@ -503,7 +493,7 @@ def format_time(text, local_time):
"""
return local_time.strftime(match.group())
return re.sub(r'\%[a-zA-Z]', match_formatting, text)
return re.sub(r'%[a-zA-Z]', match_formatting, text)
def get_locale_key(string, numeric=False):

View File

@ -23,29 +23,19 @@
Provide Registry Services
"""
import logging
import sys
from openlp.core.common import de_hump, trace_error_handler
from openlp.core.common import Singleton, de_hump, trace_error_handler
log = logging.getLogger(__name__)
class Registry(object):
class Registry(metaclass=Singleton):
"""
This is the Component Registry. It is a singleton object and is used to provide a look up service for common
objects.
"""
log.info('Registry loaded')
__instance__ = None
def __new__(cls):
"""
Re-implement the __new__ method to make sure we create a true singleton.
"""
if not cls.__instance__:
cls.__instance__ = object.__new__(cls)
return cls.__instance__
@classmethod
def create(cls):
@ -57,20 +47,9 @@ class Registry(object):
registry.service_list = {}
registry.functions_list = {}
registry.working_flags = {}
# Allow the tests to remove Registry entries but not the live system
registry.running_under_test = 'nose' in sys.argv[0] or 'pytest' in sys.argv[0]
registry.initialising = True
return registry
@classmethod
def destroy(cls):
"""
Destroy the Registry.
"""
if cls.__instance__.running_under_test:
del cls.__instance__
cls.__instance__ = None
def get(self, key):
"""
Extracts the registry value from the list based on the key passed in

View File

@ -90,12 +90,13 @@ def upgrade_screens(number, x_position, y_position, height, width, can_override,
number: {
'number': number,
geometry_key: {
'x': x_position,
'y': y_position,
'height': height,
'width': width
'x': int(x_position),
'y': int(y_position),
'height': int(height),
'width': int(width)
},
'is_display': is_display_screen
'is_display': is_display_screen,
'is_primary': can_override
}
}
@ -309,7 +310,7 @@ class Settings(QtCore.QSettings):
('songuasge/db hostname', 'songusage/db hostname', []),
('songuasge/db database', 'songusage/db database', []),
('presentations / Powerpoint Viewer', '', []),
(['core/monitor', 'core/x position', 'core/y position', 'core/height', 'core/width', 'core/override',
(['core/monitor', 'core/x position', 'core/y position', 'core/height', 'core/width', 'core/override position',
'core/display on monitor'], 'core/screens', [(upgrade_screens, [1, 0, 0, None, None, False, False])]),
('bibles/proxy name', '', []), # Just remove these bible proxy settings. They weren't used in 2.4!
('bibles/proxy address', '', []),

View File

@ -384,8 +384,9 @@ var Display = {
* Checks if the present slide content fits within the slide
*/
doesContentFit: function () {
console.debug("scrollHeight: " + $(".slides")[0].scrollHeight + ", clientHeight: " + $(".slides")[0].clientHeight);
return $(".slides")[0].clientHeight >= $(".slides")[0].scrollHeight;
var currSlide = $(".slides")[0];
console.debug("scrollHeight: " + currSlide.scrollHeight + ", clientHeight: " + currSlide.clientHeight);
return currSlide.clientHeight >= currSlide.scrollHeight;
},
/**
* Generate the OpenLP startup splashscreen
@ -436,7 +437,7 @@ var Display = {
/**
* Set fullscreen image from base64 data
* @param {string} bg_color - The background color
* @param {string} image - Path to the image
* @param {string} image_data - base64 encoded image data
*/
setFullscreenImageFromData: function(bg_color, image_data) {
Display.clearSlides();
@ -596,9 +597,8 @@ var Display = {
* @param {string} verse - The verse number, e.g. "v1"
* @param {string} text - The HTML for the verse, e.g. "line1<br>line2"
* @param {string} footer_text - The HTML for the footer"
* @param {bool} [reinit=true] - Re-initialize Reveal. Defaults to true.
*/
addTextSlide: function (verse, text, footer_text) {
addTextSlide: function (verse, text, footerText) {
var html = _prepareText(text);
if (this._slides.hasOwnProperty(verse)) {
var slide = $("#" + verse)[0];
@ -614,11 +614,9 @@ var Display = {
slidesDiv.appendChild(slide);
var slides = $(".slides > section");
this._slides[verse] = slides.length - 1;
console.debug(" footer_text: " + footer_text);
var footerDiv = $(".footer")[0];
footerDiv.innerHTML = footer_text;
if (footerText) {
$(".footer")[0].innerHTML = footerText;
}
}
if ((arguments.length > 3) && (arguments[3] === true)) {
this.reinit();
@ -650,9 +648,10 @@ var Display = {
var section = document.createElement("section");
section.setAttribute("id", index);
section.setAttribute("data-background", "#000");
section.setAttribute("style", "height: 100%; width: 100%;");
var img = document.createElement('img');
img.src = slide["path"];
img.setAttribute("style", "height: 100%; width: 100%;");
img.setAttribute("style", "max-width: 100%; max-height: 100%; margin: 0; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);");
section.appendChild(img);
slidesDiv.appendChild(section);
Display._slides[index.toString()] = index;
@ -700,25 +699,28 @@ var Display = {
* Play a video
*/
playVideo: function () {
if ($("#video").length == 1) {
$("#video")[0].play();
var videoElem = $("#video");
if (videoElem.length == 1) {
videoElem[0].play();
}
},
/**
* Pause a video
*/
pauseVideo: function () {
if ($("#video").length == 1) {
$("#video")[0].pause();
var videoElem = $("#video");
if (videoElem.length == 1) {
videoElem[0].pause();
}
},
/**
* Stop a video
*/
stopVideo: function () {
if ($("#video").length == 1) {
$("#video")[0].pause();
$("#video")[0].currentTime = 0.0;
var videoElem = $("#video");
if (videoElem.length == 1) {
videoElem[0].pause();
videoElem[0].currentTime = 0.0;
}
},
/**
@ -726,8 +728,9 @@ var Display = {
* @param seconds The position in seconds to seek to
*/
seekVideo: function (seconds) {
if ($("#video").length == 1) {
$("#video")[0].currentTime = seconds;
var videoElem = $("#video");
if (videoElem.length == 1) {
videoElem[0].currentTime = seconds;
}
},
/**
@ -735,8 +738,9 @@ var Display = {
* @param rate A Double of the rate. 1.0 => 100% speed, 0.75 => 75% speed, 1.25 => 125% speed, etc.
*/
setPlaybackRate: function (rate) {
if ($("#video").length == 1) {
$("#video")[0].playbackRate = rate;
var videoElem = $("#video");
if (videoElem.length == 1) {
videoElem[0].playbackRate = rate;
}
},
/**
@ -744,24 +748,27 @@ var Display = {
* @param level The volume level from 0 to 100.
*/
setVideoVolume: function (level) {
if ($("#video").length == 1) {
$("#video")[0].volume = level / 100.0;
var videoElem = $("#video");
if (videoElem.length == 1) {
videoElem[0].volume = level / 100.0;
}
},
/**
* Mute the volume
*/
toggleVideoMute: function () {
if ($("#video").length == 1) {
$("#video")[0].muted = !$("#video")[0].muted;
var videoElem = $("#video");
if (videoElem.length == 1) {
videoElem[0].muted = !videoElem[0].muted;
}
},
/**
* Clear the background audio playlist
*/
clearPlaylist: function () {
if ($("#background-audio").length == 1) {
var audio = $("#background-audio")[0];
var backgroundAudoElem = $("#background-audio");
if (backgroundAudoElem.length == 1) {
var audio = backgroundAudoElem[0];
/* audio.playList */
}
},
@ -843,7 +850,6 @@ var Display = {
},
setTheme: function (theme) {
this._theme = theme;
var slidesDiv = $(".slides")
// Set the background
var globalBackground = $("#global-background")[0];
var backgroundStyle = {};

View File

@ -47,7 +47,7 @@ log = logging.getLogger(__name__)
SLIM_CHARS = 'fiíIÍjlĺľrtť.,;/ ()|"\'!:\\'
CHORD_LINE_MATCH = re.compile(r'\[(.*?)\]([\u0080-\uFFFF,\w]*)'
r'([\u0080-\uFFFF,\w,\s,\.,\,,\!,\?,\;,\:,\|,\",\',\-,\_]*)(\Z)?')
r'([\u0080-\uFFFF\w\s\.\,\!\?\;\:\|\"\'\-\_]*)(\Z)?')
CHORD_TEMPLATE = '<span class="chordline">{chord}</span>'
FIRST_CHORD_TEMPLATE = '<span class="chordline firstchordline">{chord}</span>'
CHORD_LINE_TEMPLATE = '<span class="chord"><span><strong>{chord}</strong></span></span>{tail}{whitespace}{remainder}'
@ -67,6 +67,15 @@ FOOTER_COPYRIGHT = 'Public Domain'
CCLI_NO = '123456'
def remove_chords(text):
"""
Remove chords from the text
:param text: Text to be cleaned
"""
return re.sub(r'\[.+?\]', r'', text)
def remove_tags(text, can_remove_chords=False):
"""
Remove Tags from text for display
@ -82,7 +91,7 @@ def remove_tags(text, can_remove_chords=False):
text = text.replace(tag['end tag'], '')
# Remove ChordPro tags
if can_remove_chords:
text = re.sub(r'\[.+?\]', r'', text)
text = remove_chords(text)
return text
@ -377,6 +386,8 @@ def render_tags(text, can_render_chords=False, is_printing=False):
text = render_chords_for_printing(text, '{br}')
else:
text = render_chords(text)
else:
text = remove_chords(text)
for tag in FormattingTags.get_html_tags():
text = text.replace(tag['start tag'], tag['start html'])
text = text.replace(tag['end tag'], tag['end html'])
@ -482,6 +493,7 @@ class ThemePreviewRenderer(LogMixin, DisplayWindow):
:param theme_data: The theme to generated a preview for.
:param force_page: Flag to tell message lines per page need to be generated.
:param generate_screenshot: Do I need to generate a screen shot?
:rtype: QtGui.QPixmap
"""
# save value for use in format_slide
@ -502,6 +514,26 @@ class ThemePreviewRenderer(LogMixin, DisplayWindow):
self.force_page = False
return None
def get_theme(self, item):
"""
:param item: The :class:`~openlp.core.lib.serviceitem.ServiceItem` item object
:return string: The name of the theme to be used
"""
# Just assume we use the global theme.
theme_name = Registry().get('theme_manager').global_theme
# The theme level is either set to Service or Item. Use the service theme if one is set. We also have to use the
# service theme, even when the theme level is set to Item, because the item does not necessarily have to have a
# theme.
if self.theme_level != ThemeLevel.Global:
# When the theme level is at Service and we actually have a service theme then use it.
if self.theme_level == ThemeLevel.Service:
theme_name = Registry().get('service_manager').service_theme
# If we have Item level and have an item theme then use it.
if self.theme_level == ThemeLevel.Song and item.theme:
theme_name = item.theme
return theme_name
def format_slide(self, text, item):
"""
Calculate how much text can fit on a slide.
@ -514,7 +546,7 @@ class ThemePreviewRenderer(LogMixin, DisplayWindow):
QtWidgets.QApplication.instance().processEvents()
self.log_debug('format slide')
if item:
theme_name = item.theme if item.theme else Registry().get('theme_manager').global_theme
theme_name = self.get_theme(item)
theme_data = Registry().get('theme_manager').get_theme_data(theme_name)
self.theme_height = theme_data.font_main_height
# Set theme for preview

View File

@ -28,6 +28,7 @@ from functools import cmp_to_key
from PyQt5 import QtCore, QtWidgets
from openlp.core.common import Singleton
from openlp.core.common.i18n import translate
from openlp.core.common.registry import Registry
from openlp.core.common.settings import Settings
@ -133,8 +134,13 @@ class Screen(object):
self.number = int(screen_dict['number'])
self.is_display = screen_dict['is_display']
self.is_primary = screen_dict['is_primary']
self.geometry = QtCore.QRect(screen_dict['geometry']['x'], screen_dict['geometry']['y'],
screen_dict['geometry']['width'], screen_dict['geometry']['height'])
try:
self.geometry = QtCore.QRect(screen_dict['geometry']['x'], screen_dict['geometry']['y'],
screen_dict['geometry']['width'], screen_dict['geometry']['height'])
except KeyError:
# Preserve the current values as this has come from the settings update which does not have
# the geometry information
pass
if 'custom_geometry' in screen_dict:
self.custom_geometry = QtCore.QRect(screen_dict['custom_geometry']['x'],
screen_dict['custom_geometry']['y'],
@ -142,24 +148,15 @@ class Screen(object):
screen_dict['custom_geometry']['height'])
class ScreenList(object):
class ScreenList(metaclass=Singleton):
"""
Wrapper to handle the parameters of the display screen.
To get access to the screen list call ``ScreenList()``.
"""
log.info('Screen loaded')
__instance__ = None
screens = []
def __new__(cls):
"""
Re-implement __new__ to create a true singleton.
"""
if not cls.__instance__:
cls.__instance__ = object.__new__(cls)
return cls.__instance__
def __iter__(self):
"""
Convert this object into an iterable, so that we can iterate over it instead of the inner list

View File

@ -67,7 +67,7 @@ def database_exists(url):
create_database(engine.url)
database_exists(engine.url) #=> True
Borrowed from SQLAlchemy_Utils (v0.32.14 )since we only need this one function.
Borrowed from SQLAlchemy_Utils (v0.32.14) since we only need this one function.
"""
url = copy(make_url(url))
@ -265,7 +265,7 @@ def upgrade_db(url, upgrade):
"""
if not database_exists(url):
log.warning("Database {db} doesn't exist - skipping upgrade checks".format(db=url))
return (0, 0)
return 0, 0
log.debug('Checking upgrades for DB {db}'.format(db=url))

View File

@ -59,97 +59,98 @@ class FormattingTags(object):
"""
temporary_tags = [tag for tag in FormattingTags.html_expands if tag.get('temporary')]
FormattingTags.html_expands = []
base_tags = []
base_tags = [
{
'desc': translate('OpenLP.FormattingTags', 'Red'),
'start tag': '{r}',
'start html': '<span style="-webkit-text-fill-color:red">',
'end tag': '{/r}', 'end html': '</span>', 'protected': True,
'temporary': False
}, {
'desc': translate('OpenLP.FormattingTags', 'Black'),
'start tag': '{b}',
'start html': '<span style="-webkit-text-fill-color:black">',
'end tag': '{/b}', 'end html': '</span>', 'protected': True,
'temporary': False
}, {
'desc': translate('OpenLP.FormattingTags', 'Blue'),
'start tag': '{bl}',
'start html': '<span style="-webkit-text-fill-color:blue">',
'end tag': '{/bl}', 'end html': '</span>', 'protected': True,
'temporary': False
}, {
'desc': translate('OpenLP.FormattingTags', 'Yellow'),
'start tag': '{y}',
'start html': '<span style="-webkit-text-fill-color:yellow">',
'end tag': '{/y}', 'end html': '</span>', 'protected': True,
'temporary': False
}, {
'desc': translate('OpenLP.FormattingTags', 'Green'),
'start tag': '{g}',
'start html': '<span style="-webkit-text-fill-color:green">',
'end tag': '{/g}', 'end html': '</span>', 'protected': True,
'temporary': False
}, {
'desc': translate('OpenLP.FormattingTags', 'Pink'),
'start tag': '{pk}',
'start html': '<span style="-webkit-text-fill-color:#FFC0CB">',
'end tag': '{/pk}', 'end html': '</span>', 'protected': True,
'temporary': False
}, {
'desc': translate('OpenLP.FormattingTags', 'Orange'),
'start tag': '{o}',
'start html': '<span style="-webkit-text-fill-color:#FFA500">',
'end tag': '{/o}', 'end html': '</span>', 'protected': True,
'temporary': False
}, {
'desc': translate('OpenLP.FormattingTags', 'Purple'),
'start tag': '{pp}',
'start html': '<span style="-webkit-text-fill-color:#800080">',
'end tag': '{/pp}', 'end html': '</span>', 'protected': True,
'temporary': False
}, {
'desc': translate('OpenLP.FormattingTags', 'White'),
'start tag': '{w}',
'start html': '<span style="-webkit-text-fill-color:white">',
'end tag': '{/w}', 'end html': '</span>', 'protected': True,
'temporary': False
}, {
'desc': translate('OpenLP.FormattingTags', 'Superscript'),
'start tag': '{su}', 'start html': '<sup>',
'end tag': '{/su}', 'end html': '</sup>', 'protected': True,
'temporary': False
}, {
'desc': translate('OpenLP.FormattingTags', 'Subscript'),
'start tag': '{sb}', 'start html': '<sub>',
'end tag': '{/sb}', 'end html': '</sub>', 'protected': True,
'temporary': False
}, {
'desc': translate('OpenLP.FormattingTags', 'Paragraph'),
'start tag': '{p}', 'start html': '<p>', 'end tag': '{/p}',
'end html': '</p>', 'protected': True,
'temporary': False
}, {
'desc': translate('OpenLP.FormattingTags', 'Bold'),
'start tag': '{st}', 'start html': '<strong>',
'end tag': '{/st}', 'end html': '</strong>',
'protected': True, 'temporary': False
}, {
'desc': translate('OpenLP.FormattingTags', 'Italics'),
'start tag': '{it}', 'start html': '<em>', 'end tag': '{/it}',
'end html': '</em>', 'protected': True, 'temporary': False
}, {
'desc': translate('OpenLP.FormattingTags', 'Underline'),
'start tag': '{u}',
'start html': '<span style="text-decoration: underline;">',
'end tag': '{/u}', 'end html': '</span>', 'protected': True,
'temporary': False
}, {
'desc': translate('OpenLP.FormattingTags', 'Break'),
'start tag': '{br}', 'start html': '<br>', 'end tag': '',
'end html': '', 'protected': True,
'temporary': False
}]
# Append the base tags.
base_tags.append({
'desc': translate('OpenLP.FormattingTags', 'Red'),
'start tag': '{r}',
'start html': '<span style="-webkit-text-fill-color:red">',
'end tag': '{/r}', 'end html': '</span>', 'protected': True,
'temporary': False})
base_tags.append({
'desc': translate('OpenLP.FormattingTags', 'Black'),
'start tag': '{b}',
'start html': '<span style="-webkit-text-fill-color:black">',
'end tag': '{/b}', 'end html': '</span>', 'protected': True,
'temporary': False})
base_tags.append({
'desc': translate('OpenLP.FormattingTags', 'Blue'),
'start tag': '{bl}',
'start html': '<span style="-webkit-text-fill-color:blue">',
'end tag': '{/bl}', 'end html': '</span>', 'protected': True,
'temporary': False})
base_tags.append({
'desc': translate('OpenLP.FormattingTags', 'Yellow'),
'start tag': '{y}',
'start html': '<span style="-webkit-text-fill-color:yellow">',
'end tag': '{/y}', 'end html': '</span>', 'protected': True,
'temporary': False})
base_tags.append({
'desc': translate('OpenLP.FormattingTags', 'Green'),
'start tag': '{g}',
'start html': '<span style="-webkit-text-fill-color:green">',
'end tag': '{/g}', 'end html': '</span>', 'protected': True,
'temporary': False})
base_tags.append({
'desc': translate('OpenLP.FormattingTags', 'Pink'),
'start tag': '{pk}',
'start html': '<span style="-webkit-text-fill-color:#FFC0CB">',
'end tag': '{/pk}', 'end html': '</span>', 'protected': True,
'temporary': False})
base_tags.append({
'desc': translate('OpenLP.FormattingTags', 'Orange'),
'start tag': '{o}',
'start html': '<span style="-webkit-text-fill-color:#FFA500">',
'end tag': '{/o}', 'end html': '</span>', 'protected': True,
'temporary': False})
base_tags.append({
'desc': translate('OpenLP.FormattingTags', 'Purple'),
'start tag': '{pp}',
'start html': '<span style="-webkit-text-fill-color:#800080">',
'end tag': '{/pp}', 'end html': '</span>', 'protected': True,
'temporary': False})
base_tags.append({
'desc': translate('OpenLP.FormattingTags', 'White'),
'start tag': '{w}',
'start html': '<span style="-webkit-text-fill-color:white">',
'end tag': '{/w}', 'end html': '</span>', 'protected': True,
'temporary': False})
base_tags.append({
'desc': translate('OpenLP.FormattingTags', 'Superscript'),
'start tag': '{su}', 'start html': '<sup>',
'end tag': '{/su}', 'end html': '</sup>', 'protected': True,
'temporary': False})
base_tags.append({
'desc': translate('OpenLP.FormattingTags', 'Subscript'),
'start tag': '{sb}', 'start html': '<sub>',
'end tag': '{/sb}', 'end html': '</sub>', 'protected': True,
'temporary': False})
base_tags.append({
'desc': translate('OpenLP.FormattingTags', 'Paragraph'),
'start tag': '{p}', 'start html': '<p>', 'end tag': '{/p}',
'end html': '</p>', 'protected': True,
'temporary': False})
base_tags.append({
'desc': translate('OpenLP.FormattingTags', 'Bold'),
'start tag': '{st}', 'start html': '<strong>',
'end tag': '{/st}', 'end html': '</strong>',
'protected': True, 'temporary': False})
base_tags.append({
'desc': translate('OpenLP.FormattingTags', 'Italics'),
'start tag': '{it}', 'start html': '<em>', 'end tag': '{/it}',
'end html': '</em>', 'protected': True, 'temporary': False})
base_tags.append({
'desc': translate('OpenLP.FormattingTags', 'Underline'),
'start tag': '{u}',
'start html': '<span style="text-decoration: underline;">',
'end tag': '{/u}', 'end html': '</span>', 'protected': True,
'temporary': False})
base_tags.append({
'desc': translate('OpenLP.FormattingTags', 'Break'),
'start tag': '{br}', 'start html': '<br>', 'end tag': '',
'end html': '', 'protected': True,
'temporary': False})
FormattingTags.add_html_tags(base_tags)
FormattingTags.add_html_tags(temporary_tags)
user_expands_string = str(Settings().value('formattingTags/html_tags'))

View File

@ -39,7 +39,7 @@ from openlp.core.common.applocation import AppLocation
from openlp.core.common.i18n import translate
from openlp.core.common.mixins import RegistryProperties
from openlp.core.common.settings import Settings
from openlp.core.display.render import remove_tags, render_tags
from openlp.core.display.render import remove_tags, render_tags, render_chords_for_printing
from openlp.core.lib import ItemCapabilities
from openlp.core.ui.icons import UiIcons
@ -74,6 +74,7 @@ class ServiceItem(RegistryProperties):
self.name = plugin.name
self._rendered_slides = None
self._display_slides = None
self._print_slides = None
self.title = ''
self.slides = []
self.processor = None
@ -185,7 +186,7 @@ class ServiceItem(RegistryProperties):
self._rendered_slides.append(rendered_slide)
display_slide = {
'title': raw_slide['title'],
'text': remove_tags(page),
'text': remove_tags(page, can_remove_chords=True),
'verse': verse_tag,
}
self._display_slides.append(display_slide)
@ -209,6 +210,34 @@ class ServiceItem(RegistryProperties):
self._create_slides()
return self._display_slides
@property
def print_slides(self):
"""
Render the frames for printing and return them
:param can_render_chords: bool Whether or not to render the chords
"""
if not self._print_slides:
self._print_slides = []
previous_pages = {}
index = 0
for raw_slide in self.slides:
verse_tag = raw_slide['verse']
if verse_tag in previous_pages and previous_pages[verse_tag][0] == raw_slide:
pages = previous_pages[verse_tag][1]
else:
pages = self.renderer.format_slide(raw_slide['text'], self)
previous_pages[verse_tag] = (raw_slide, pages)
for page in pages:
slide = {
'title': raw_slide['title'],
'text': render_chords_for_printing(remove_tags(page), '\n'),
'verse': index,
'footer': self.raw_footer,
}
self._print_slides.append(slide)
return self._print_slides
def add_from_image(self, path, title, background=None, thumbnail=None):
"""
Add an image slide to the service item.
@ -321,6 +350,13 @@ class ServiceItem(RegistryProperties):
'display_title': slide['display_title'], 'notes': slide['notes']})
return {'header': service_header, 'data': service_data}
def render_text_items(self):
"""
This method forces the display to be regenerated
"""
self._display_slides = []
self._rendered_slides = []
def set_from_service(self, service_item, path=None):
"""
This method takes a service item from a saved service file (passed from the ServiceManager) and extracts the
@ -503,7 +539,6 @@ class ServiceItem(RegistryProperties):
:param row: The service item slide to be returned
"""
if self.service_item_type == ServiceItemType.Text:
# return self.display_frames[row]['html'].split('\n')[0]
return self.rendered_slides[row]['text']
elif self.service_item_type == ServiceItemType.Image:
return self.slides[row]['path']

View File

@ -170,6 +170,7 @@ class Theme(object):
jsn = get_text_file_string(json_path)
self.load_theme(jsn)
self.background_filename = None
self.version = 2
def expand_json(self, var, prev=None):
"""

View File

@ -344,14 +344,14 @@ class ProjectorManager(QtWidgets.QWidget, RegistryBase, UiProjectorManager, LogM
"""
log.debug('Checking for UDP port {port} listener deletion'.format(port=port))
if port not in self.pjlink_udp:
log.warn('UDP listener for port {port} not there - skipping delete'.format(port=port))
log.warning('UDP listener for port {port} not there - skipping delete'.format(port=port))
return
keep_port = False
for item in self.projector_list:
if port == item.link.port:
keep_port = True
if keep_port:
log.warn('UDP listener for port {port} needed for other projectors - skipping delete'.format(port=port))
log.warning('UDP listener for port {port} needed for other projectors - skipping delete'.format(port=port))
return
Registry().execute('udp_broadcast_remove', port=port)
del self.pjlink_udp[port]

View File

@ -552,7 +552,7 @@ class PJLink(QtNetwork.QTcpSocket):
data = data_in.strip()
self.receive_data_signal()
# Initial packet checks
if (len(data) < 7):
if len(data) < 7:
self._trash_buffer(msg='get_data(): Invalid packet - length')
return
elif len(data) > self.max_size:

View File

@ -177,7 +177,7 @@ class FingerTabBarWidget(QtWidgets.QTabBar):
:param height: Remove default height parameter in kwargs
"""
self.tabSize = QtCore.QSize(kwargs.pop('width', 100), kwargs.pop('height', 25))
QtWidgets.QTabBar.__init__(self, parent, *args, **kwargs)
super().__init__(parent)
def paintEvent(self, event):
"""
@ -215,11 +215,11 @@ class FingerTabWidget(QtWidgets.QTabWidget):
Based on thread discussion
http://www.riverbankcomputing.com/pipermail/pyqt/2005-December/011724.html
"""
def __init__(self, parent, *args):
def __init__(self, parent):
"""
Initialize FingerTabWidget instance
"""
QtWidgets.QTabWidget.__init__(self, parent, *args)
super().__init__(parent)
self.setTabBar(FingerTabBarWidget(self))

View File

@ -28,6 +28,7 @@ contained within the openlp.core module.
"""
import logging
from openlp.core.common import Singleton
from openlp.core.common.registry import Registry
from openlp.core.common.mixins import LogMixin
from openlp.core.lib.plugin import PluginStatus
@ -52,17 +53,7 @@ class StateModule(LogMixin):
self.text = None
class State(LogMixin):
__instance__ = None
def __new__(cls):
"""
Re-implement the __new__ method to make sure we create a true singleton.
"""
if not cls.__instance__:
cls.__instance__ = object.__new__(cls)
return cls.__instance__
class State(LogMixin, metaclass=Singleton):
def load_settings(self):
self.modules = {}

View File

@ -24,10 +24,11 @@ The :mod:`openlp.core.threading` module contains some common threading code
"""
from PyQt5 import QtCore
from openlp.core.common.mixins import LogMixin
from openlp.core.common.registry import Registry
class ThreadWorker(QtCore.QObject):
class ThreadWorker(QtCore.QObject, LogMixin):
"""
The :class:`~openlp.core.threading.ThreadWorker` class provides a base class for all worker objects
"""

View File

@ -93,4 +93,4 @@ class SingleColumnTableWidget(QtWidgets.QTableWidget):
self.resizeRowsToContents()
__all__ = ['SingleColumnTableWidget', 'DisplayControllerType']
__all__ = ['AlertLocation', 'DisplayControllerType', 'HideMode', 'SingleColumnTableWidget']

File diff suppressed because it is too large Load Diff

View File

@ -50,19 +50,20 @@ class AboutForm(QtWidgets.QDialog, UiAboutDialog):
Set up the dialog. This method is mocked out in tests.
"""
self.setup_ui(self)
self.button_box.buttons()[0].setFocus()
application_version = get_version()
about_text = self.about_text_edit.toPlainText()
about_text = about_text.replace('<version>', application_version['version'])
about_text = self.about_text_edit.toHtml()
about_text = about_text.replace('{version}', application_version['version'])
if application_version['build']:
build_text = translate('OpenLP.AboutForm', ' build {version}').format(version=application_version['build'])
else:
build_text = ''
about_text = about_text.replace('<revision>', build_text)
self.about_text_edit.setPlainText(about_text)
self.volunteer_button.clicked.connect(self.on_volunteer_button_clicked)
about_text = about_text.replace('{revision}', build_text)
self.about_text_edit.setHtml(about_text)
self.contribute_button.clicked.connect(self.on_contribute_button_clicked)
def on_volunteer_button_clicked(self):
def on_contribute_button_clicked(self):
"""
Launch a web browser and go to the contribute page on the site.
"""
webbrowser.open_new('http://openlp.org/en/contribute')
webbrowser.open_new('http://openlp.org/contribute')

View File

@ -81,7 +81,7 @@ class AdvancedTab(SettingsTab):
self.ui_layout.addRow(self.media_plugin_check_box)
self.hide_mouse_check_box = QtWidgets.QCheckBox(self.ui_group_box)
self.hide_mouse_check_box.setObjectName('hide_mouse_check_box')
self.ui_layout.addWidget(self.hide_mouse_check_box)
self.ui_layout.addRow(self.hide_mouse_check_box)
self.double_click_live_check_box = QtWidgets.QCheckBox(self.ui_group_box)
self.double_click_live_check_box.setObjectName('double_click_live_check_box')
self.ui_layout.addRow(self.double_click_live_check_box)

View File

@ -40,7 +40,7 @@ class FormattingTagController(object):
"""
self.html_tag_regex = re.compile(
r'<(?:(?P<close>/(?=[^\s/>]+>))?'
r'(?P<tag>[^\s/!\?>]+)(?:\s+[^\s=]+="[^"]*")*\s*(?P<empty>/)?'
r'(?P<tag>[^\s/!?>]+)(?:\s+[^\s=]+="[^"]*")*\s*(?P<empty>/)?'
r'|(?P<cdata>!\[CDATA\[(?:(?!\]\]>).)*\]\])'
r'|(?P<procinst>\?(?:(?!\?>).)*\?)'
r'|(?P<comment>!--(?:(?!-->).)*--))>')

View File

@ -27,6 +27,7 @@ import logging
import qtawesome as qta
from PyQt5 import QtGui, QtWidgets
from openlp.core.common import Singleton
from openlp.core.common.applocation import AppLocation
from openlp.core.lib import build_icon
@ -34,22 +35,11 @@ from openlp.core.lib import build_icon
log = logging.getLogger(__name__)
class UiIcons(object):
class UiIcons(metaclass=Singleton):
"""
Provide standard icons for objects to use.
"""
__instance__ = None
def __new__(cls):
"""
Override the default object creation method to return a single instance.
"""
if not cls.__instance__:
cls.__instance__ = object.__new__(cls)
cls.load(cls)
return cls.__instance__
def load(self):
def __init__(self):
"""
These are the font icons used in the code.
"""
@ -164,7 +154,8 @@ class UiIcons(object):
'video': {'icon': 'fa.file-video-o'},
'volunteer': {'icon': 'fa.group'}
}
self.load_icons(self, icon_list)
self.load_icons(icon_list)
self.main_icon = build_icon(':/icon/openlp-logo.svg')
def load_icons(self, icon_list):
"""
@ -184,7 +175,6 @@ class UiIcons(object):
setattr(self, key, qta.icon('fa.plus-circle', color='red'))
except Exception:
setattr(self, key, qta.icon('fa.plus-circle', color='red'))
self.main_icon = build_icon(':/icon/openlp-logo.svg')
@staticmethod
def _print_icons():

View File

@ -33,8 +33,9 @@ from tempfile import gettempdir
from PyQt5 import QtCore, QtGui, QtWidgets
from openlp.core.state import State
from openlp.core.api import websockets
from openlp.core.api.http import server
from openlp.core.api.websockets import WebSocketServer
from openlp.core.api.http.server import HttpServer
from openlp.core.api.zeroconf import start_zeroconf
from openlp.core.common import add_actions, is_macosx, is_win
from openlp.core.common.actions import ActionList, CategoryOrder
from openlp.core.common.applocation import AppLocation
@ -495,8 +496,9 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, LogMixin, RegistryPropert
self.copy_data = False
Settings().set_up_default_values()
self.about_form = AboutForm(self)
self.ws_server = websockets.WebSocketServer()
self.http_server = server.HttpServer(self)
self.ws_server = WebSocketServer()
self.http_server = HttpServer(self)
start_zeroconf()
SettingsForm(self)
self.formatting_tag_form = FormattingTagForm(self)
self.shortcut_form = ShortcutListForm(self)
@ -544,7 +546,8 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, LogMixin, RegistryPropert
Wait for the threads
"""
# Sometimes the threads haven't finished, let's wait for them
wait_dialog = QtWidgets.QProgressDialog('Waiting for some things to finish...', '', 0, 0, self)
wait_dialog = QtWidgets.QProgressDialog(translate('OpenLP.MainWindow', 'Waiting for some things to finish...'),
'', 0, 0, self)
wait_dialog.setWindowModality(QtCore.Qt.WindowModal)
wait_dialog.setAutoClose(False)
wait_dialog.setCancelButton(None)
@ -633,7 +636,10 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, LogMixin, RegistryPropert
# if self.live_controller.display.isVisible():
# self.live_controller.display.setFocus()
self.activateWindow()
if self.application.args:
# We have -disable-web-security added by our code.
# If a file is passed in we will have that as well so count of 2
# If not we need to see if we want to use the previous file.so count of 1
if self.application.args and len(self.application.args) > 1:
self.open_cmd_line_files(self.application.args)
elif Settings().value(self.general_settings_section + '/auto open'):
self.service_manager_contents.load_last_file()

View File

@ -218,14 +218,14 @@ class PrintServiceForm(QtWidgets.QDialog, Ui_PrintServiceDialog, RegistryPropert
if item.is_text():
verse_def = None
verse_html = None
for slide in item.get_frames():
if not verse_def or verse_def != slide['verseTag'] or verse_html == slide['printing_html']:
for slide in item.print_slides:
if not verse_def or verse_def != slide['verse'] or verse_html == slide['text']:
text_div = self._add_element('div', parent=div, class_id='itemText')
elif 'chordspacing' not in slide['printing_html']:
elif 'chordspacing' not in slide['text']:
self._add_element('br', parent=text_div)
self._add_element('span', slide['printing_html'], text_div)
verse_def = slide['verseTag']
verse_html = slide['printing_html']
self._add_element('span', slide['text'], text_div)
verse_def = slide['verse']
verse_html = slide['text']
# Break the page before the div element.
if index != 0 and self.page_break_after_text.isChecked():
div.set('class', 'item newPage')

View File

@ -53,7 +53,6 @@ class ScreensTab(SettingsTab):
self.setObjectName('self')
self.tab_layout = QtWidgets.QVBoxLayout(self)
self.tab_layout.setObjectName('tab_layout')
self.screen_selection_widget = ScreenSelectionWidget(self, ScreenList())
self.tab_layout.addWidget(self.screen_selection_widget)
self.generic_group_box = QtWidgets.QGroupBox(self)
@ -63,13 +62,11 @@ class ScreensTab(SettingsTab):
self.display_on_monitor_check.setObjectName('monitor_combo_box')
self.generic_group_layout.addWidget(self.display_on_monitor_check)
self.tab_layout.addWidget(self.generic_group_box)
Registry().register_function('config_screen_changed', self.screen_selection_widget.load)
self.retranslate_ui()
def retranslate_ui(self):
self.setWindowTitle(translate('self', 'self')) # TODO: ???
self.generic_group_box.setTitle(translate('OpenLP.ScreensTab', 'Generic screen settings'))
self.display_on_monitor_check.setText(translate('OpenLP.ScreensTab', 'Display if a single screen'))

View File

@ -34,6 +34,7 @@ from tempfile import NamedTemporaryFile
from PyQt5 import QtCore, QtGui, QtWidgets
from openlp.core.state import State
from openlp.core.common import ThemeLevel, delete_file
from openlp.core.common.actions import ActionList, CategoryOrder
from openlp.core.common.applocation import AppLocation
@ -45,7 +46,7 @@ from openlp.core.common.settings import Settings
from openlp.core.lib import build_icon
from openlp.core.lib.exceptions import ValidationError
from openlp.core.lib.plugin import PluginStatus
from openlp.core.lib.serviceitem import ItemCapabilities, ServiceItem
from openlp.core.lib.serviceitem import ItemCapabilities, ServiceItem, ServiceItemType
from openlp.core.lib.ui import create_widget_action, critical_error_message_box, find_and_set_in_combo_box
from openlp.core.ui.icons import UiIcons
from openlp.core.ui.media import AUDIO_EXT, VIDEO_EXT
@ -748,9 +749,7 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
if theme:
find_and_set_in_combo_box(self.theme_combo_box, theme, set_missing=False)
if theme == self.theme_combo_box.currentText():
# TODO: Use a local display widget
# self.preview_display.set_theme(get_theme_from_name(theme))
pass
self.service_theme = theme
else:
if self._save_lite:
service_item.set_from_service(item)
@ -828,7 +827,7 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
self.auto_start_action.setIcon(UiIcons().inactive)
self.auto_start_action.setText(translate('OpenLP.ServiceManager', '&Auto Start - inactive'))
if service_item['service_item'].is_text():
for plugin in self.plugin_manager.plugins:
for plugin in State().list_plugins():
if plugin.name == 'custom' and plugin.status == PluginStatus.Active:
self.create_custom_action.setVisible(True)
break
@ -1196,9 +1195,9 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
service_item_from_item = item['service_item']
tree_widget_item = QtWidgets.QTreeWidgetItem(self.service_manager_list)
if service_item_from_item.is_valid:
icon = service_item_from_item.icon.pixmap(80, 80).toImage()
icon = icon.scaled(80, 80, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)
if service_item_from_item.notes:
icon = service_item_from_item.icon.pixmap(80, 80).toImage()
icon = icon.scaled(80, 80, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)
overlay = UiIcons().notes.pixmap(40, 40).toImage()
overlay = overlay.scaled(40, 40, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)
painter = QtGui.QPainter(icon)
@ -1206,8 +1205,6 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
painter.end()
tree_widget_item.setIcon(0, build_icon(icon))
elif service_item_from_item.temporary_edit:
icon = service_item_from_item.icon.pixmap(80, 80).toImage()
icon = icon.scaled(80, 80, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)
overlay = QtGui.QImage(UiIcons().upload)
overlay = overlay.scaled(40, 40, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)
painter = QtGui.QPainter(icon)
@ -1241,13 +1238,14 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
tree_widget_item.setData(0, QtCore.Qt.UserRole, item['order'])
tree_widget_item.setSelected(item['selected'])
# Add the children to their parent tree_widget_item.
for slide_index, slide in enumerate(service_item_from_item.slides):
for slide_index, slide in enumerate(service_item_from_item.get_frames()):
child = QtWidgets.QTreeWidgetItem(tree_widget_item)
# prefer to use a display_title
if service_item_from_item.is_capable(ItemCapabilities.HasDisplayTitle):
text = slide['display_title'].replace('\n', ' ')
else:
if service_item_from_item.is_capable(ItemCapabilities.HasDisplayTitle) or \
service_item_from_item.service_item_type == ServiceItemType.Image:
text = slide['title'].replace('\n', ' ')
else:
text = service_item_from_item.get_rendered_frame(slide_index)
child.setText(0, text[:40])
child.setData(0, QtCore.Qt.UserRole, slide_index)
if service_item == item_index:
@ -1274,8 +1272,6 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
:param current_index: The combo box index for the selected item
"""
self.service_theme = self.theme_combo_box.currentText()
# TODO: Use a local display widget
# self.preview_display.set_theme(get_theme_from_name(theme))
Settings().setValue(self.main_window.service_manager_settings_section + '/service theme', self.service_theme)
self.regenerate_service_items(True)
@ -1334,7 +1330,7 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
"""
for item_count, item in enumerate(self.service_items):
if item['service_item'].edit_id == new_item.edit_id and item['service_item'].name == new_item.name:
new_item.render()
new_item.create_slides()
new_item.merge(item['service_item'])
item['service_item'] = new_item
self.repaint_service_list(item_count + 1, 0)
@ -1367,7 +1363,7 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
self.repaint_service_list(s_item, child)
self.live_controller.replace_service_manager_item(item)
else:
# item.render()
item.render_text_items()
# nothing selected for dnd
if self.drop_position == -1:
if isinstance(item, list):
@ -1616,8 +1612,6 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
theme_group.addAction(create_widget_action(self.theme_menu, theme, text=theme, checked=False,
triggers=self.on_theme_change_action))
find_and_set_in_combo_box(self.theme_combo_box, self.service_theme)
# TODO: Sort this out
# self.renderer.set_service_theme(self.service_theme)
self.regenerate_service_items()
def on_theme_change_action(self):

View File

@ -101,7 +101,7 @@ class Ui_ShortcutListDialog(object):
self.primary_push_button = CaptureShortcutButton(shortcutListDialog)
self.primary_push_button.setObjectName('primary_push_button')
self.primary_push_button.setMinimumSize(QtCore.QSize(84, 0))
self.primary_push_button.setIcon(UiIcons.shortcuts)
self.primary_push_button.setIcon(UiIcons().shortcuts)
self.primary_layout.addWidget(self.primary_push_button)
self.clear_primary_button = QtWidgets.QToolButton(shortcutListDialog)
self.clear_primary_button.setObjectName('clear_primary_button')

View File

@ -34,7 +34,7 @@ from openlp.core.common.settings import Settings
from openlp.core.ui.shortcutlistdialog import Ui_ShortcutListDialog
REMOVE_AMPERSAND = re.compile(r'&{1}')
REMOVE_AMPERSAND = re.compile(r'&')
log = logging.getLogger(__name__)

View File

@ -30,6 +30,7 @@ from xml.etree.ElementTree import XML, ElementTree
from PyQt5 import QtCore, QtWidgets
from openlp.core.state import State
from openlp.core.common import delete_file
from openlp.core.common.applocation import AppLocation
from openlp.core.common.i18n import UiStrings, get_locale_key, translate
@ -293,7 +294,7 @@ class ThemeManager(QtWidgets.QWidget, RegistryBase, Ui_ThemeManager, LogMixin, R
old_theme_data = self.get_theme_data(old_theme_name)
self.clone_theme_data(old_theme_data, new_theme_name)
self.delete_theme(old_theme_name)
for plugin in self.plugin_manager.plugins:
for plugin in State().list_plugins():
if plugin.uses_theme(old_theme_name):
plugin.rename_theme(old_theme_name, new_theme_name)
self.renderer.set_theme(self.get_theme_data(new_theme_name))
@ -612,7 +613,7 @@ class ThemeManager(QtWidgets.QWidget, RegistryBase, Ui_ThemeManager, LogMixin, R
self.log_exception('Importing theme from zip failed {name}'.format(name=file_path))
critical_error_message_box(
translate('OpenLP.ThemeManager', 'Import Error'),
translate('OpenLP.ThemeManager', 'There was a problem imoorting {file_name}.\n\nIt is corrupt,'
translate('OpenLP.ThemeManager', 'There was a problem importing {file_name}.\n\nIt is corrupt, '
'inaccessible or not a valid theme.').format(file_name=file_path))
finally:
if not abort_import:
@ -771,7 +772,7 @@ class ThemeManager(QtWidgets.QWidget, RegistryBase, Ui_ThemeManager, LogMixin, R
# check for use in the system else where.
if test_plugin:
plugin_usage = ""
for plugin in self.plugin_manager.plugins:
for plugin in State().list_plugins():
used_count = plugin.uses_theme(theme)
if used_count:
plugin_usage = "{plug}{text}".format(plug=plugin_usage,

View File

@ -80,7 +80,7 @@ class ListPreviewWidget(QtWidgets.QTableWidget, RegistryProperties):
An empty ``ServiceItem`` is used by default. replace_service_manager_item() needs to be called to make this
widget display something.
"""
super(QtWidgets.QTableWidget, self).__init__(parent)
super().__init__(parent)
self._setup(screen_ratio)
def _setup(self, screen_ratio):
@ -124,7 +124,7 @@ class ListPreviewWidget(QtWidgets.QTableWidget, RegistryProperties):
max_img_row_height = Settings().value('advanced/slide max height')
# Adjust for row height cap if in use.
if isinstance(max_img_row_height, int):
if max_img_row_height > 0 and height > max_img_row_height:
if 0 < max_img_row_height < height:
height = max_img_row_height
elif max_img_row_height < 0:
# If auto setting, show that number of slides, or if the resulting slides too small, 100px.
@ -210,16 +210,20 @@ class ListPreviewWidget(QtWidgets.QTableWidget, RegistryProperties):
pixmap = QtGui.QPixmap(str(slide['image']))
else:
pixmap = QtGui.QPixmap(str(slide['path']))
if pixmap.height() > 0:
pixmap_ratio = pixmap.width() / pixmap.height()
else:
pixmap_ratio = 1
label.setPixmap(pixmap)
container = QtWidgets.QWidget()
layout = AspectRatioLayout(container, self.screen_ratio)
layout = AspectRatioLayout(container, pixmap_ratio)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(label)
container.setLayout(layout)
slide_height = width // self.screen_ratio
max_img_row_height = Settings().value('advanced/slide max height')
if isinstance(max_img_row_height, int):
if max_img_row_height > 0 and slide_height > max_img_row_height:
if 0 < max_img_row_height < slide_height:
slide_height = max_img_row_height
elif max_img_row_height < 0:
# If auto setting, show that number of slides, or if the resulting slides too small, 100px.

View File

@ -194,11 +194,11 @@ class ScreenButton(QtWidgets.QPushButton):
class ScreenSelectionWidget(QtWidgets.QWidget):
def __init__(self, parent=None, screens=[]):
def __init__(self, parent=None, screens=None):
super().__init__(parent)
self.current_screen = None
self.identify_labels = []
self.screens = screens
self.screens = screens or []
self.timer = QtCore.QTimer()
self.timer.setSingleShot(True)
self.timer.setInterval(3000)

View File

@ -26,6 +26,7 @@ plugin.
import logging
import re
from openlp.core.common import Singleton
from openlp.core.common.i18n import translate
from openlp.core.common.settings import Settings
@ -64,20 +65,10 @@ class LanguageSelection(object):
English = 2
class BibleStrings(object):
class BibleStrings(metaclass=Singleton):
"""
Provide standard strings for objects to use.
"""
__instance__ = None
def __new__(cls):
"""
Override the default object creation method to return a single instance.
"""
if not cls.__instance__:
cls.__instance__ = object.__new__(cls)
return cls.__instance__
def __init__(self):
"""
These strings should need a good reason to be retranslated elsewhere.
@ -336,11 +327,13 @@ def parse_reference(reference, bible, language_selection, book_ref_id=False):
log.debug('Matched reference {text}'.format(text=reference))
book = match.group('book')
if not book_ref_id:
book_ref_id = bible.get_book_ref_id_by_localised_name(book, language_selection)
book_ref_ids = bible.get_book_ref_id_by_localised_name(book, language_selection)
elif not bible.get_book_by_book_ref_id(book_ref_id):
return []
else:
book_ref_ids = [book_ref_id]
# We have not found the book so do not continue
if not book_ref_id:
if not book_ref_ids:
return []
ranges = match.group('ranges')
range_list = get_reference_match('range_separator').split(ranges)
@ -381,22 +374,23 @@ def parse_reference(reference, bible, language_selection, book_ref_id=False):
to_chapter = to_verse
to_verse = None
# Append references to the list
if has_range:
if not from_verse:
from_verse = 1
if not to_verse:
to_verse = -1
if to_chapter and to_chapter > from_chapter:
ref_list.append((book_ref_id, from_chapter, from_verse, -1))
for i in range(from_chapter + 1, to_chapter):
ref_list.append((book_ref_id, i, 1, -1))
ref_list.append((book_ref_id, to_chapter, 1, to_verse))
elif to_verse >= from_verse or to_verse == -1:
ref_list.append((book_ref_id, from_chapter, from_verse, to_verse))
elif from_verse:
ref_list.append((book_ref_id, from_chapter, from_verse, from_verse))
else:
ref_list.append((book_ref_id, from_chapter, 1, -1))
for book_ref_id in book_ref_ids:
if has_range:
if not from_verse:
from_verse = 1
if not to_verse:
to_verse = -1
if to_chapter and to_chapter > from_chapter:
ref_list.append((book_ref_id, from_chapter, from_verse, -1))
for i in range(from_chapter + 1, to_chapter):
ref_list.append((book_ref_id, i, 1, -1))
ref_list.append((book_ref_id, to_chapter, 1, to_verse))
elif to_verse >= from_verse or to_verse == -1:
ref_list.append((book_ref_id, from_chapter, from_verse, to_verse))
elif from_verse:
ref_list.append((book_ref_id, from_chapter, from_verse, from_verse))
else:
ref_list.append((book_ref_id, from_chapter, 1, -1))
return ref_list
else:
log.debug('Invalid reference: {text}'.format(text=reference))

View File

@ -281,13 +281,14 @@ class BibleDB(Manager):
log.debug('BibleDB.get_book("{book}")'.format(book=book))
return self.get_object_filtered(Book, Book.name.like(book + '%'))
def get_books(self):
def get_books(self, book=None):
"""
A wrapper so both local and web bibles have a get_books() method that
manager can call. Used in the media manager advanced search tab.
"""
log.debug('BibleDB.get_books()')
return self.get_all_objects(Book, order_by_ref=Book.id)
log.debug('BibleDB.get_books("{book}")'.format(book=book))
filter = Book.name.like(book + '%') if book else None
return self.get_all_objects(Book, filter_clause=filter, order_by_ref=Book.id)
def get_book_by_book_ref_id(self, ref_id):
"""
@ -300,39 +301,35 @@ class BibleDB(Manager):
def get_book_ref_id_by_localised_name(self, book, language_selection):
"""
Return the id of a named book.
Return the ids of a matching named book.
:param book: The name of the book, according to the selected language.
:param language_selection: The language selection the user has chosen in the settings section of the Bible.
:rtype: list[int]
"""
log.debug('get_book_ref_id_by_localised_name("{book}", "{lang}")'.format(book=book, lang=language_selection))
from openlp.plugins.bibles.lib import LanguageSelection, BibleStrings
book_names = BibleStrings().BookNames
# escape reserved characters
book_escaped = book
for character in RESERVED_CHARACTERS:
book_escaped = book_escaped.replace(character, '\\' + character)
book_escaped = book.replace(character, '\\' + character)
regex_book = re.compile('\\s*{book}\\s*'.format(book='\\s*'.join(book_escaped.split())), re.IGNORECASE)
if language_selection == LanguageSelection.Bible:
db_book = self.get_book(book)
if db_book:
return db_book.book_reference_id
elif language_selection == LanguageSelection.Application:
books = [key for key in list(book_names.keys()) if regex_book.match(str(book_names[key]))]
books = [_f for _f in map(BiblesResourcesDB.get_book, books) if _f]
for value in books:
if self.get_book_by_book_ref_id(value['id']):
return value['id']
elif language_selection == LanguageSelection.English:
books = BiblesResourcesDB.get_books_like(book)
if books:
book_list = [value for value in books if regex_book.match(value['name'])]
if not book_list:
book_list = books
for value in book_list:
if self.get_book_by_book_ref_id(value['id']):
return value['id']
return False
db_books = self.get_books(book)
return [db_book.book_reference_id for db_book in db_books]
else:
book_list = []
if language_selection == LanguageSelection.Application:
books = [key for key in list(book_names.keys()) if regex_book.match(book_names[key])]
book_list = [_f for _f in map(BiblesResourcesDB.get_book, books) if _f]
elif language_selection == LanguageSelection.English:
books = BiblesResourcesDB.get_books_like(book)
if books:
book_list = [value for value in books if regex_book.match(value['name'])]
if not book_list:
book_list = books
return [value['id'] for value in book_list if self.get_book_by_book_ref_id(value['id'])]
return []
def get_verses(self, reference_list, show_error=True):
"""

View File

@ -240,8 +240,10 @@ class BibleManager(LogMixin, RegistryProperties):
book=book,
chapter=chapter))
language_selection = self.get_language_selection(bible)
book_ref_id = self.db_cache[bible].get_book_ref_id_by_localised_name(book, language_selection)
return self.db_cache[bible].get_verse_count(book_ref_id, chapter)
book_ref_ids = self.db_cache[bible].get_book_ref_id_by_localised_name(book, language_selection)
if book_ref_ids:
return self.db_cache[bible].get_verse_count(book_ref_ids[0], chapter)
return 0
def get_verse_count_by_book_ref_id(self, bible, book_ref_id, chapter):
"""
@ -401,4 +403,4 @@ class BibleManager(LogMixin, RegistryProperties):
self.db_cache[bible].finalise()
__all__ = ['BibleFormat']
__all__ = ['BibleFormat', 'BibleManager']

View File

@ -109,7 +109,7 @@ class BibleMediaItem(MediaManagerItem):
:param kwargs: Keyword arguments to pass to the super method. (dict)
"""
self.clear_icon = UiIcons().square
self.save_results_icon = UiIcons.save
self.save_results_icon = UiIcons().save
self.sort_icon = UiIcons().sort
self.bible = None
self.second_bible = None

View File

@ -97,6 +97,7 @@ class Ui_CustomEditDialog(object):
self.preview_button = QtWidgets.QPushButton()
self.button_box = create_button_box(custom_edit_dialog, 'button_box', ['cancel', 'save'],
[self.preview_button])
self.save_button = self.button_box.button(QtWidgets.QDialogButtonBox.Save)
self.dialog_layout.addWidget(self.button_box)
self.retranslate_ui(custom_edit_dialog)
@ -112,3 +113,4 @@ class Ui_CustomEditDialog(object):
self.theme_label.setText(translate('CustomPlugin.EditCustomForm', 'The&me:'))
self.credit_label.setText(translate('CustomPlugin.EditCustomForm', '&Credits:'))
self.preview_button.setText(UiStrings().SaveAndPreview)
self.save_button.setText(UiStrings().SaveAndClose)

View File

@ -349,7 +349,7 @@ class CustomMediaItem(MediaManagerItem):
custom.credits = ''
custom_xml = CustomXMLBuilder()
for (idx, slide) in enumerate(item.slides):
custom_xml.add_verse_to_lyrics('custom', str(idx + 1), slide['raw_slide'])
custom_xml.add_verse_to_lyrics('custom', str(idx + 1), slide['text'])
custom.text = str(custom_xml.extract_xml(), 'utf-8')
self.plugin.db_manager.save_object(custom)
self.on_search_text_button_clicked()

View File

@ -55,7 +55,7 @@ if is_win():
class SlideShowListenerImport(XSlideShowListenerObj.__class__):
pass
except (AttributeError, pywintypes.com_error):
class SlideShowListenerImport():
class SlideShowListenerImport(object):
pass
# Declare an empty exception to match the exception imported from UNO
@ -76,7 +76,7 @@ else:
except ImportError:
uno_available = False
class SlideShowListenerImport():
class SlideShowListenerImport(object):
pass
log = logging.getLogger(__name__)
@ -233,9 +233,7 @@ class ImpressController(PresentationController):
self.conf_provider = self.manager.createInstanceWithContext(
'com.sun.star.configuration.ConfigurationProvider', uno.getComponentContext())
# Setup lookup properties to get Impress settings
properties = []
properties.append(self.create_property('nodepath', 'org.openoffice.Office.Impress'))
properties = tuple(properties)
properties = tuple(self.create_property('nodepath', 'org.openoffice.Office.Impress'))
try:
# Get an updateable configuration view
impress_conf_props = self.conf_provider.createInstanceWithArguments(
@ -311,9 +309,7 @@ class ImpressDocument(PresentationDocument):
if desktop is None:
return False
self.desktop = desktop
properties = []
properties.append(self.controller.create_property('Hidden', True))
properties = tuple(properties)
properties = tuple(self.controller.create_property('Hidden', True))
try:
self.document = desktop.loadComponentFromURL(url, '_blank', 0, properties)
except Exception:
@ -338,9 +334,7 @@ class ImpressDocument(PresentationDocument):
return
temp_folder_path = self.get_temp_folder()
thumb_dir_url = temp_folder_path.as_uri()
properties = []
properties.append(self.controller.create_property('FilterName', 'impress_png_Export'))
properties = tuple(properties)
properties = tuple(self.controller.create_property('FilterName', 'impress_png_Export'))
doc = self.document
pages = doc.getDrawPages()
if not pages:

View File

@ -27,6 +27,7 @@ from Pyro4 import Proxy
from openlp.core.common import delete_file, is_macosx
from openlp.core.common.applocation import AppLocation
from openlp.core.common.mixins import LogMixin
from openlp.core.common.path import Path
from openlp.core.common.registry import Registry
from openlp.core.display.screens import ScreenList
@ -47,7 +48,7 @@ log = logging.getLogger(__name__)
register_classes()
class MacLOController(PresentationController):
class MacLOController(PresentationController, LogMixin):
"""
Class to control interactions with MacLO presentations on Mac OS X via Pyro4. It starts the Pyro4 nameserver,
starts the LibreOfficeServer, and then controls MacLO via Pyro4.

View File

@ -246,6 +246,9 @@ class PresentationMediaItem(MediaManagerItem):
:rtype: None
"""
for cidx in self.controllers:
if not self.controllers[cidx].enabled():
# skip presentation controllers that are not enabled
continue
file_ext = file_path.suffix[1:]
if file_ext in self.controllers[cidx].supports or file_ext in self.controllers[cidx].also_supports:
doc = self.controllers[cidx].add_document(file_path)

View File

@ -29,6 +29,7 @@ from shutil import copyfile
from PyQt5 import QtCore, QtWidgets
from openlp.core.state import State
from openlp.core.common.applocation import AppLocation
from openlp.core.common.i18n import UiStrings, get_natural_key, translate
from openlp.core.common.mixins import RegistryProperties
@ -416,7 +417,7 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties):
Load the media files into a combobox.
"""
self.from_media_button.setVisible(False)
for plugin in self.plugin_manager.plugins:
for plugin in State().list_plugins():
if plugin.name == 'media' and plugin.status == PluginStatus.Active:
self.from_media_button.setVisible(True)
self.media_form.populate_files(plugin.media_item.get_list(MediaType.Audio))

View File

@ -615,14 +615,16 @@ def transpose_chord(chord, transpose_value, notation):
: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']
notes_sharp_notation = {
'german': ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'H'],
'english': ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'],
'neo-latin': ['Do', 'Do#', 'Re', 'Re#', 'Mi', 'Fa', 'Fa#', 'Sol', 'Sol#', 'La', 'La#', 'Si']
}
notes_flat_notation = {
'german': ['C', 'Db', 'D', 'Eb', 'Fb', 'F', 'Gb', 'G', 'Ab', 'A', 'B', 'H'],
'english': ['C', 'Db', 'D', 'Eb', 'Fb', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B'],
'neo-latin': ['Do', 'Reb', 'Re', 'Mib', 'Fab', 'Fa', 'Solb', 'Sol', 'Lab', 'La', 'Sib', 'Si']
}
chord_split = chord.replace('', 'b').split('/')
transposed_chord = ''
last_chord = ''

View File

@ -374,7 +374,9 @@ def init_schema(url):
mapper(SongBookEntry, songs_songbooks_table, properties={
'songbook': relation(Book)
})
mapper(Book, song_books_table)
mapper(Book, song_books_table, properties={
'songs': relation(Song, secondary=songs_songbooks_table)
})
mapper(MediaFile, media_files_table)
mapper(Song, songs_table, properties={
# Use the authors_songs relation when you need access to the 'author_type' attribute

View File

@ -146,7 +146,9 @@ class CCLIFileImport(SongImport):
"""
log.debug('USR file text: {text}'.format(text=text_list))
song_author = ''
song_fields = ''
song_topics = ''
song_words = ''
for line in text_list:
if line.startswith('[S '):
ccli, line = line.split(']', 1)

View File

@ -28,6 +28,7 @@ import re
from openlp.core.common.settings import Settings
from openlp.plugins.songs.lib.importers.songimport import SongImport
from openlp.plugins.songs.lib.db import AuthorType
log = logging.getLogger(__name__)
@ -39,6 +40,7 @@ class ChordProImport(SongImport):
This importer is based on the information available on these webpages:
- https://www.chordpro.org
- 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
@ -73,6 +75,29 @@ class ChordProImport(SongImport):
self.title = tag_value
elif tag_name in ['subtitle', 'su', 'st']:
self.alternate_title = tag_value
elif tag_name == 'composer':
self.parse_author(tag_value, AuthorType.Music)
elif tag_name in ['lyricist', 'artist', 'author']: # author is not an official directive
self.parse_author(tag_value, AuthorType.Words)
elif tag_name == 'meta':
meta_tag_name, meta_tag_value = tag_value.split(' ', 1)
# Skip, if no value
if not meta_tag_value:
continue
# The meta-tag can contain anything. We check for the ones above and a few more
if meta_tag_name in ['title', 't']:
self.title = meta_tag_value
elif meta_tag_name in ['subtitle', 'su', 'st']:
self.alternate_title = meta_tag_value
elif meta_tag_name == 'composer':
self.parse_author(meta_tag_value, AuthorType.Music)
elif meta_tag_name in ['lyricist', 'artist', 'author']:
self.parse_author(meta_tag_value, AuthorType.Words)
elif meta_tag_name in ['topic', 'topics']:
for topic in meta_tag_value.split(','):
self.topics.append(topic.strip())
elif 'ccli' in meta_tag_name:
self.ccli_number = meta_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'):
@ -156,6 +181,13 @@ class ChordProImport(SongImport):
'songs/disable chords import'):
current_verse = re.sub(r'\[.*?\]', '', current_verse)
self.add_verse(current_verse.rstrip(), current_verse_type)
# if no title was in directives, get it from the first line
if not self.title:
(verse_def, verse_text, lang) = self.verses[0]
# strip any chords from the title
self.title = re.sub(r'\[.*?\]', '', verse_text.split('\n')[0])
# strip the last char if it a punctuation
self.title = re.sub(r'[^\w\s]$', '', self.title)
if not self.finish():
self.log_error(song_file.name)

View File

@ -87,6 +87,7 @@ class DreamBeamImport(SongImport):
if self.stop_import_flag:
return
self.set_defaults()
author_copyright = ''
parser = etree.XMLParser(remove_blank_text=True)
try:
with file_path.open('r') as xml_file:
@ -142,7 +143,7 @@ class DreamBeamImport(SongImport):
author_copyright = song_xml.Text2.Text.text
if author_copyright:
author_copyright = str(author_copyright)
if author_copyright.find(str(SongStrings.CopyrightSymbol)) >= 0:
if author_copyright.find(SongStrings.CopyrightSymbol) >= 0:
self.add_copyright(author_copyright)
else:
self.parse_author(author_copyright)

View File

@ -137,9 +137,11 @@ class EasySlidesImport(SongImport):
except UnicodeDecodeError:
log.exception('Unicode decode error while decoding Contents')
self._success = False
return
except AttributeError:
log.exception('no Contents')
self._success = False
return
lines = lyrics.split('\n')
# we go over all lines first, to determine information,
# which tells us how to parse verses later

View File

@ -118,8 +118,8 @@ class EasyWorshipSongImport(SongImport):
# 40/48/56 Entry count int32le 4 Number of items in the schedule
# 44/52/60 Entry length int16le 2 Length of schedule entries: 0x0718 = 1816
# Get file version
type, = struct.unpack('<38s', self.ews_file.read(38))
version = type.decode()[-3:]
file_type, = struct.unpack('<38s', self.ews_file.read(38))
version = file_type.decode()[-3:]
# Set fileposition based on filetype/version
file_pos = 0
if version == ' 5':
@ -268,13 +268,13 @@ class EasyWorshipSongImport(SongImport):
self.db_set_record_struct(field_descriptions)
# Pick out the field description indexes we will need
try:
success = True
fi_title = self.db_find_field(b'Title')
fi_author = self.db_find_field(b'Author')
fi_copy = self.db_find_field(b'Copyright')
fi_admin = self.db_find_field(b'Administrator')
fi_words = self.db_find_field(b'Words')
fi_ccli = self.db_find_field(b'Song Number')
success = True
except IndexError:
# This is the wrong table
success = False

View File

@ -302,7 +302,7 @@ class FoilPresenter(object):
break
author_temp = []
for author in strings:
temp = re.split(r',(?=\D{2})|(?<=\D),|\/(?=\D{3,})|(?<=\D);', author)
temp = re.split(r',(?=\D{2})|(?<=\D),|/(?=\D{3,})|(?<=\D);', author)
for tempx in temp:
author_temp.append(tempx)
for author in author_temp:

View File

@ -145,9 +145,7 @@ class OpenOfficeImport(SongImport):
"""
self.file_path = file_path
url = file_path.as_uri()
properties = []
properties.append(self.create_property('Hidden', True))
properties = tuple(properties)
properties = tuple(self.create_property('Hidden', True))
try:
self.document = self.desktop.loadComponentFromURL(url, '_blank', 0, properties)
if not self.document.supportsService("com.sun.star.presentation.PresentationDocument") and not \

View File

@ -136,8 +136,8 @@ class OPSProImport(SongImport):
verse_text = re.sub(r'^\d+\r\n', '', verse_text)
verse_def = 'v' + verse_number.group(1)
# Detect verse tags
elif re.match(r'^.+?\:\r\n', verse_text):
tag_match = re.match(r'^(.+?)\:\r\n(.*)', verse_text, flags=re.DOTALL)
elif re.match(r'^.+?:\r\n', verse_text):
tag_match = re.match(r'^(.+?):\r\n(.*)', verse_text, flags=re.DOTALL)
tag = tag_match.group(1).lower()
tag = tag.split(' ')[0]
verse_text = tag_match.group(2)

View File

@ -80,7 +80,7 @@ class ProPresenterImport(SongImport):
self.parse_author(author)
# ProPresenter 4
if(self.version >= 400 and self.version < 500):
if 400 <= self.version < 500:
self.copyright = root.get('CCLICopyrightInfo')
self.ccli_number = root.get('CCLILicenseNumber')
count = 0
@ -95,7 +95,7 @@ class ProPresenterImport(SongImport):
self.add_verse(words, "v{count}".format(count=count))
# ProPresenter 5
elif(self.version >= 500 and self.version < 600):
elif 500 <= self.version < 600:
self.copyright = root.get('CCLICopyrightInfo')
self.ccli_number = root.get('CCLILicenseNumber')
count = 0
@ -111,7 +111,7 @@ class ProPresenterImport(SongImport):
self.add_verse(words, "v{count:d}".format(count=count))
# ProPresenter 6
elif(self.version >= 600 and self.version < 700):
elif 600 <= self.version < 700:
self.copyright = root.get('CCLICopyrightYear')
self.ccli_number = root.get('CCLISongNumber')
count = 0
@ -128,7 +128,7 @@ class ProPresenterImport(SongImport):
b64Data = contents.text
data = base64.standard_b64decode(b64Data)
words = None
if(contents.get('rvXMLIvarName') == "RTFData"):
if contents.get('rvXMLIvarName') == "RTFData":
words, encoding = strip_rtf(data.decode())
break
if words:

View File

@ -128,7 +128,7 @@ class SongBeamerImport(SongImport):
# The encoding should only be ANSI (cp1252), UTF-8, Unicode, Big-Endian-Unicode.
# So if it doesn't start with 'u' we default to cp1252. See:
# https://forum.songbeamer.com/viewtopic.php?p=419&sid=ca4814924e37c11e4438b7272a98b6f2
if not self.input_file_encoding.lower().startswith('u'):
if self.input_file_encoding and not self.input_file_encoding.lower().startswith('u'):
self.input_file_encoding = 'cp1252'
with file_path.open(encoding=self.input_file_encoding) as song_file:
song_data = song_file.readlines()

View File

@ -75,7 +75,7 @@ class VideoPsalmImport(SongImport):
c = next(file_content_it)
processed_content += '"' + c
# Remove control characters
elif (c < chr(32)):
elif c < chr(32):
processed_content += ' '
# Handle escaped characters
elif c == '\\':

View File

@ -221,7 +221,7 @@ class OpenLyrics(object):
"""
IMPLEMENTED_VERSION = '0.8'
START_TAGS_REGEX = re.compile(r'\{(\w+)\}')
END_TAGS_REGEX = re.compile(r'\{\/(\w+)\}')
END_TAGS_REGEX = re.compile(r'\{/(\w+)\}')
VERSE_TAG_SPLITTER = re.compile('([a-zA-Z]+)([0-9]*)([a-zA-Z]?)')
def __init__(self, manager):

View File

@ -34,7 +34,7 @@ class SongStrings(object):
Author = translate('OpenLP.Ui', 'Author', 'Singular')
Authors = translate('OpenLP.Ui', 'Authors', 'Plural')
AuthorUnknown = translate('OpenLP.Ui', 'Author Unknown') # Used to populate the database.
CopyrightSymbol = translate('OpenLP.Ui', '\xa9', 'Copyright symbol.')
CopyrightSymbol = '\xa9'
SongBook = translate('OpenLP.Ui', 'Songbook', 'Singular')
SongBooks = translate('OpenLP.Ui', 'Songbooks', 'Plural')
SongIncomplete = translate('OpenLP.Ui', 'Title and/or verses not found')

View File

@ -9,17 +9,16 @@
"dependencies": {
"jasmine-core": "^2.6.4",
"karma": "^3.1.4",
"karma-chrome-launcher": "^3.1.0",
"karma-coverage": "^1.1.2",
"karma-firefox-launcher": "^1.2.0",
"karma-jasmine": "^1.1.0",
"karma-phantomjs-launcher": "^1.0.4",
"phantomjs-prebuilt": "^2.1.16"
"karma-junit-reporter": "^1.2.0",
"karma-log-reporter": "0.0.4"
},
"scripts": {
"test": "karma start --single-run"
},
"author": "OpenLP Developers",
"license": "GPL-3.0-or-later",
"devDependencies": {
"karma-log-reporter": "0.0.4"
}
"license": "GPL-3.0-or-later"
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -21,51 +21,9 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
##########################################################################
"""
The entrypoint for OpenLP
A compatibility entrypoint for OpenLP
"""
import faulthandler
import logging
import multiprocessing
import sys
# from OpenGL import GL
from openlp.core.app import main
from openlp.core.common import is_macosx, is_win
from openlp.core.common.applocation import AppLocation
from openlp.core.common.path import create_paths
log = logging.getLogger(__name__)
def set_up_fault_handling():
"""
Set up the Python fault handler
"""
# Create the cache directory if it doesn't exist, and enable the fault handler to log to an error log file
try:
create_paths(AppLocation.get_directory(AppLocation.CacheDir))
faulthandler.enable((AppLocation.get_directory(AppLocation.CacheDir) / 'error.log').open('wb'))
except OSError:
log.exception('An exception occurred when enabling the fault handler')
def start():
"""
Instantiate and run the application.
"""
set_up_fault_handling()
# Add support for using multiprocessing from frozen Windows executable (built using PyInstaller),
# see https://docs.python.org/3/library/multiprocessing.html#multiprocessing.freeze_support
if is_win():
multiprocessing.freeze_support()
# Mac OS X passes arguments like '-psn_XXXX' to the application. This argument is actually a process serial number.
# However, this causes a conflict with other OpenLP arguments. Since we do not use this argument we can delete it
# to avoid any potential conflicts.
if is_macosx():
sys.argv = [x for x in sys.argv if not x.startswith('-psn')]
main()
from openlp import __main__
if __name__ == '__main__':
start()
__main__.start()

9
scripts/.tx/config Normal file
View File

@ -0,0 +1,9 @@
[main]
host = https://www.transifex.com
[openlp.openlp-30x]
file_filter = ../resources/i18n/<lang>.ts
minimum_perc = 0
source_file = ../resources/i18n/en.ts
source_lang = en
type = qt

View File

@ -18,7 +18,7 @@ environment:
install:
# Install dependencies from pypi
- "%PYTHON%\\python.exe -m pip install sqlalchemy alembic appdirs chardet beautifulsoup4 lxml Mako mysql-connector-python pytest mock pyodbc psycopg2 pypiwin32 websockets asyncio waitress six webob requests QtAwesome PyQt5 PyQtWebEngine pymediainfo PyMuPDF QDarkStyle python-vlc Pyro4"
- "%PYTHON%\\python.exe -m pip install sqlalchemy alembic appdirs chardet beautifulsoup4 lxml Mako mysql-connector-python pytest mock pyodbc psycopg2 pypiwin32 websockets asyncio waitress six webob requests QtAwesome PyQt5 PyQtWebEngine pymediainfo PyMuPDF QDarkStyle python-vlc Pyro4 zeroconf"
build: off

View File

@ -90,13 +90,14 @@ MODULES = [
'requests',
'qtawesome',
'pymediainfo',
'vlc'
'vlc',
'zeroconf'
]
OPTIONAL_MODULES = [
('qdarkstyle', '(dark style support)'),
('mysql.connector', '(MySQL support)'),
('pymysql', '(MySQL support)'),
('pyodbc', '(ODBC support)'),
('psycopg2', '(PostgreSQL support)'),
('enchant', '(spell checker)'),

46
scripts/pull_translations Executable file
View File

@ -0,0 +1,46 @@
#!/bin/sh
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
##########################################################################
# OpenLP - Open Source Lyrics Projection #
# ---------------------------------------------------------------------- #
# Copyright (c) 2008-2019 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, either version 3 of the License, or #
# (at your option) any later version. #
# #
# 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, see <https://www.gnu.org/licenses/>. #
##########################################################################
#
# This script automates the update of the translations on OpenLP.
#
# It uses the tx client from Transifex for all the heavy lifting
# All download *.ts files are converted to *.qm files which are used by
# OpenLP.
#
###############################################################################
pwd=`pwd`
result=${PWD##*/}; echo $result
if [ $result != 'scripts' ] ; then
echo 'This script must be run from the scripts directory'
exit
fi
rm ../resources/i18n/*.ts
echo
echo Downloading the translated files
echo
tx pull -a --minimum-perc=45
echo Translation update complete

52
scripts/push_translations Executable file
View File

@ -0,0 +1,52 @@
##########################################################################
# OpenLP - Open Source Lyrics Projection #
# ---------------------------------------------------------------------- #
# Copyright (c) 2008-2019 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, either version 3 of the License, or #
# (at your option) any later version. #
# #
# 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, see <https://www.gnu.org/licenses/>. #
##########################################################################
#
# This script automates the update of the translations on OpenLP.
#
# It uses the tx client from Transifex for all the heavy lifting
# All download *.ts files are converted to *.qm files which are used by
# OpenLP.
#
###############################################################################
pwd=`pwd`
result=${PWD##*/}; echo $result
if [ $result != 'scripts' ] ; then
echo 'This script must be run from the scripts directory'
exit
fi
echo
echo Generation translation control file
echo
rm ../resources/i18n/*.ts
python3 $pwd/translation_utils.py -p
echo Creating base translation file
cd ..
pylupdate5 -verbose -noobsolete openlp.pro
cd scripts
echo Check of invalid characters in push file
grep -axv '.*' ../resources/i18n/en.ts
tx push -s
echo New translation file pushed.

Some files were not shown because too many files have changed in this diff Show More