forked from openlp/openlp
HEAD
This commit is contained in:
commit
e2abca9a03
@ -8,7 +8,7 @@
|
||||
*.nja
|
||||
*.orig
|
||||
*.pyc
|
||||
*.qm
|
||||
resources/i18n/*.ts
|
||||
*.rej
|
||||
*.ropeproject
|
||||
*.~\?~
|
||||
|
@ -16,3 +16,4 @@ include copyright.txt
|
||||
include LICENSE
|
||||
include README.txt
|
||||
include openlp/.version
|
||||
include package.json
|
||||
|
@ -60,7 +60,7 @@ module.exports = function(config) {
|
||||
|
||||
// start these browsers
|
||||
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
|
||||
browsers: ["PhantomJS"],
|
||||
browsers: ["Firefox"],
|
||||
|
||||
// Continuous Integration mode
|
||||
// if true, Karma captures browsers, runs the tests and exits
|
||||
|
@ -172,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
|
||||
@ -385,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'),
|
||||
|
@ -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:
|
||||
|
@ -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__ = super().__new__(cls)
|
||||
cls.__instance__.load()
|
||||
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 & 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 ')
|
||||
|
@ -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
|
||||
|
@ -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', '', []),
|
||||
|
@ -281,8 +281,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
|
||||
@ -333,7 +334,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();
|
||||
@ -372,9 +373,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];
|
||||
@ -390,11 +390,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();
|
||||
@ -426,9 +424,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;
|
||||
@ -476,25 +475,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;
|
||||
}
|
||||
},
|
||||
/**
|
||||
@ -502,8 +504,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;
|
||||
}
|
||||
},
|
||||
/**
|
||||
@ -511,8 +514,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;
|
||||
}
|
||||
},
|
||||
/**
|
||||
@ -520,24 +524,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 */
|
||||
}
|
||||
},
|
||||
@ -619,7 +626,6 @@ var Display = {
|
||||
},
|
||||
setTheme: function (theme) {
|
||||
this._theme = theme;
|
||||
var slidesDiv = $(".slides")
|
||||
// Set the background
|
||||
var globalBackground = $("#global-background")[0];
|
||||
var backgroundStyle = {};
|
||||
|
@ -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}'
|
||||
@ -482,6 +482,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
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
"""
|
||||
|
@ -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 = {}
|
||||
|
@ -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
|
||||
"""
|
||||
|
@ -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)
|
||||
|
@ -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__ = super().__new__(cls)
|
||||
cls.__instance__.load()
|
||||
return cls.__instance__
|
||||
|
||||
def load(self):
|
||||
def __init__(self):
|
||||
"""
|
||||
These are the font icons used in the code.
|
||||
"""
|
||||
@ -165,6 +155,7 @@ class UiIcons(object):
|
||||
'volunteer': {'icon': 'fa.group'}
|
||||
}
|
||||
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():
|
||||
|
@ -635,7 +635,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()
|
||||
|
@ -219,13 +219,13 @@ class PrintServiceForm(QtWidgets.QDialog, Ui_PrintServiceDialog, RegistryPropert
|
||||
verse_def = None
|
||||
verse_html = None
|
||||
for slide in item.get_frames():
|
||||
if not verse_def or verse_def != slide['verseTag'] or verse_html == slide['printing_html']:
|
||||
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')
|
||||
|
@ -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'))
|
||||
|
||||
|
@ -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
|
||||
@ -828,7 +829,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
|
||||
|
@ -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,
|
||||
|
@ -210,9 +210,13 @@ 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)
|
||||
|
@ -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))
|
||||
|
@ -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):
|
||||
"""
|
||||
|
@ -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):
|
||||
"""
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -291,6 +291,7 @@ class Ui_EditSongDialog(object):
|
||||
self.warning_label.setObjectName('warning_label')
|
||||
self.bottom_layout.addWidget(self.warning_label)
|
||||
self.button_box = create_button_box(edit_song_dialog, 'button_box', ['cancel', 'save'])
|
||||
self.save_button = self.button_box.button(QtWidgets.QDialogButtonBox.Save)
|
||||
self.bottom_layout.addWidget(self.button_box)
|
||||
self.dialog_layout.addLayout(self.bottom_layout)
|
||||
self.retranslate_ui(edit_song_dialog)
|
||||
@ -341,6 +342,7 @@ class Ui_EditSongDialog(object):
|
||||
translate('SongsPlugin.EditSongForm', '<strong>Warning:</strong> Not all of the verses are in use.')
|
||||
self.no_verse_order_entered_warning = \
|
||||
translate('SongsPlugin.EditSongForm', '<strong>Warning:</strong> You have not entered a verse order.')
|
||||
self.save_button.setText(UiStrings().SaveAndPreview)
|
||||
|
||||
|
||||
def create_combo_box(parent, name, editable=True):
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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')
|
||||
|
11
package.json
11
package.json
@ -11,15 +11,12 @@
|
||||
"karma": "^3.1.4",
|
||||
"karma-coverage": "^1.1.2",
|
||||
"karma-jasmine": "^1.1.0",
|
||||
"karma-phantomjs-launcher": "^1.0.4",
|
||||
"phantomjs-prebuilt": "^2.1.16"
|
||||
"karma-firefox-launcher": "^1.2.0",
|
||||
"karma-log-reporter": "0.0.4"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "karma start"
|
||||
"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"
|
||||
}
|
||||
|
10977
resources/i18n/af.ts
10977
resources/i18n/af.ts
File diff suppressed because it is too large
Load Diff
10975
resources/i18n/bg.ts
10975
resources/i18n/bg.ts
File diff suppressed because it is too large
Load Diff
11003
resources/i18n/cs.ts
11003
resources/i18n/cs.ts
File diff suppressed because it is too large
Load Diff
10996
resources/i18n/da.ts
10996
resources/i18n/da.ts
File diff suppressed because it is too large
Load Diff
10995
resources/i18n/de.ts
10995
resources/i18n/de.ts
File diff suppressed because it is too large
Load Diff
10973
resources/i18n/el.ts
10973
resources/i18n/el.ts
File diff suppressed because it is too large
Load Diff
11075
resources/i18n/en.ts
11075
resources/i18n/en.ts
File diff suppressed because it is too large
Load Diff
11073
resources/i18n/en_GB.ts
11073
resources/i18n/en_GB.ts
File diff suppressed because it is too large
Load Diff
10995
resources/i18n/en_ZA.ts
10995
resources/i18n/en_ZA.ts
File diff suppressed because it is too large
Load Diff
11055
resources/i18n/es.ts
11055
resources/i18n/es.ts
File diff suppressed because it is too large
Load Diff
10995
resources/i18n/et.ts
10995
resources/i18n/et.ts
File diff suppressed because it is too large
Load Diff
11251
resources/i18n/fi.ts
11251
resources/i18n/fi.ts
File diff suppressed because it is too large
Load Diff
10990
resources/i18n/fr.ts
10990
resources/i18n/fr.ts
File diff suppressed because it is too large
Load Diff
11050
resources/i18n/hu.ts
11050
resources/i18n/hu.ts
File diff suppressed because it is too large
Load Diff
11015
resources/i18n/id.ts
11015
resources/i18n/id.ts
File diff suppressed because it is too large
Load Diff
10975
resources/i18n/ja.ts
10975
resources/i18n/ja.ts
File diff suppressed because it is too large
Load Diff
10975
resources/i18n/ko.ts
10975
resources/i18n/ko.ts
File diff suppressed because it is too large
Load Diff
11065
resources/i18n/lt.ts
11065
resources/i18n/lt.ts
File diff suppressed because it is too large
Load Diff
11053
resources/i18n/nb.ts
11053
resources/i18n/nb.ts
File diff suppressed because it is too large
Load Diff
10993
resources/i18n/nl.ts
10993
resources/i18n/nl.ts
File diff suppressed because it is too large
Load Diff
10995
resources/i18n/pl.ts
10995
resources/i18n/pl.ts
File diff suppressed because it is too large
Load Diff
11008
resources/i18n/pt_BR.ts
11008
resources/i18n/pt_BR.ts
File diff suppressed because it is too large
Load Diff
10995
resources/i18n/ru.ts
10995
resources/i18n/ru.ts
File diff suppressed because it is too large
Load Diff
10994
resources/i18n/sk.ts
10994
resources/i18n/sk.ts
File diff suppressed because it is too large
Load Diff
10985
resources/i18n/sv.ts
10985
resources/i18n/sv.ts
File diff suppressed because it is too large
Load Diff
10974
resources/i18n/ta_LK.ts
10974
resources/i18n/ta_LK.ts
File diff suppressed because it is too large
Load Diff
10974
resources/i18n/th_TH.ts
10974
resources/i18n/th_TH.ts
File diff suppressed because it is too large
Load Diff
10971
resources/i18n/zh_CN.ts
10971
resources/i18n/zh_CN.ts
File diff suppressed because it is too large
Load Diff
10976
resources/i18n/zh_TW.ts
10976
resources/i18n/zh_TW.ts
File diff suppressed because it is too large
Load Diff
9
scripts/.tx/config
Normal file
9
scripts/.tx/config
Normal 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
|
46
scripts/pull_translations
Executable file
46
scripts/pull_translations
Executable 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
52
scripts/push_translations
Executable 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.
|
||||
|
@ -22,210 +22,29 @@
|
||||
##########################################################################
|
||||
|
||||
"""
|
||||
This script is used to maintain the translation files in OpenLP. It downloads
|
||||
the latest translation files from the Transifex translation server, updates the
|
||||
local translation files from both the source code and the files from Transifex,
|
||||
and can also generate the compiled translation files.
|
||||
|
||||
Create New Language
|
||||
-------------------
|
||||
|
||||
To create a new language, simply run this script with the ``-c`` command line
|
||||
option::
|
||||
|
||||
@:~$ ./translation_utils.py -c
|
||||
|
||||
Update Translation Files
|
||||
------------------------
|
||||
|
||||
The best way to update the translations is to download the files from Transifex,
|
||||
and then update the local files using both the downloaded files and the source.
|
||||
This is done easily via the ``-d``, ``-p`` and ``-u`` options::
|
||||
|
||||
@:~$ ./translation_utils.py -dpu
|
||||
|
||||
This script is used to maintain the translation files in OpenLP.
|
||||
It generates the base en.ts file used to drive all translations
|
||||
on Transifex.
|
||||
For more details on the translation process see the Translation pages on the
|
||||
Wiki
|
||||
"""
|
||||
import base64
|
||||
import glob
|
||||
import json
|
||||
|
||||
import os
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
import webbrowser
|
||||
from argparse import ArgumentParser
|
||||
from getpass import getpass
|
||||
|
||||
from lxml import objectify
|
||||
from PyQt5 import QtCore
|
||||
|
||||
SERVER_URL = 'http://www.transifex.com/api/2/project/openlp/resource/openlp-26x/'
|
||||
IGNORED_PATHS = ['scripts']
|
||||
IGNORED_PATHS = ['scripts', 'tests']
|
||||
IGNORED_FILES = ['setup.py']
|
||||
|
||||
verbose_mode = False
|
||||
quiet_mode = False
|
||||
username = ''
|
||||
password = ''
|
||||
|
||||
|
||||
class Command(object):
|
||||
"""
|
||||
Provide an enumeration of commands.
|
||||
"""
|
||||
Download = 1
|
||||
Create = 2
|
||||
Prepare = 3
|
||||
Update = 4
|
||||
Generate = 5
|
||||
Check = 6
|
||||
|
||||
|
||||
class CommandStack(object):
|
||||
"""
|
||||
This class provides an iterable stack.
|
||||
"""
|
||||
def __init__(self):
|
||||
self.current_index = 0
|
||||
self.data = []
|
||||
|
||||
def __len__(self):
|
||||
return len(self.data)
|
||||
|
||||
def __getitem__(self, index):
|
||||
if index not in self.data:
|
||||
return None
|
||||
elif self.data[index].get('arguments'):
|
||||
return self.data[index]['command'], self.data[index]['arguments']
|
||||
else:
|
||||
return self.data[index]['command']
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
def __next__(self):
|
||||
if self.current_index == len(self.data):
|
||||
raise StopIteration
|
||||
else:
|
||||
current_item = self.data[self.current_index]['command']
|
||||
self.current_index += 1
|
||||
return current_item
|
||||
|
||||
def append(self, command, **kwargs):
|
||||
data = {'command': command}
|
||||
if 'arguments' in kwargs:
|
||||
data['arguments'] = kwargs['arguments']
|
||||
self.data.append(data)
|
||||
|
||||
def reset(self):
|
||||
self.current_index = 0
|
||||
|
||||
def arguments(self):
|
||||
if self.data[self.current_index - 1].get('arguments'):
|
||||
return self.data[self.current_index - 1]['arguments']
|
||||
else:
|
||||
return []
|
||||
|
||||
def __repr__(self):
|
||||
results = []
|
||||
for item in self.data:
|
||||
if item.get('arguments'):
|
||||
results.append(str((item['command'], item['arguments'])))
|
||||
else:
|
||||
results.append(str((item['command'], )))
|
||||
return '[%s]' % ', '.join(results)
|
||||
|
||||
|
||||
def print_quiet(text, linefeed=True):
|
||||
"""
|
||||
This method checks to see if we are in quiet mode, and if not prints ``text`` out.
|
||||
|
||||
:param text: The text to print.
|
||||
:param linefeed: Linefeed required
|
||||
"""
|
||||
global quiet_mode
|
||||
if not quiet_mode:
|
||||
if linefeed:
|
||||
print(text)
|
||||
else:
|
||||
print(text, end=' ')
|
||||
|
||||
|
||||
def print_verbose(text):
|
||||
"""
|
||||
This method checks to see if we are in verbose mode, and if so prints ``text`` out.
|
||||
|
||||
:param text: The text to print.
|
||||
"""
|
||||
global verbose_mode, quiet_mode
|
||||
if not quiet_mode and verbose_mode:
|
||||
print(' %s' % text)
|
||||
|
||||
|
||||
def run(command):
|
||||
"""
|
||||
This method runs an external application.
|
||||
|
||||
:param command: The command to run.
|
||||
"""
|
||||
print_verbose(command)
|
||||
process = QtCore.QProcess()
|
||||
process.start(command)
|
||||
while process.waitForReadyRead():
|
||||
print_verbose('ReadyRead: %s' % process.readAll())
|
||||
print_verbose('Error(s):\n%s' % process.readAllStandardError())
|
||||
print_verbose('Output:\n%s' % process.readAllStandardOutput())
|
||||
|
||||
|
||||
def download_translations():
|
||||
"""
|
||||
This method downloads the translation files from the Pootle server.
|
||||
|
||||
**Note:** URLs and headers need to remain strings, not unicode.
|
||||
"""
|
||||
global username, password
|
||||
print_quiet('Download translation files from Transifex')
|
||||
if not username:
|
||||
username = input(' Transifex username: ')
|
||||
if not password:
|
||||
password = getpass(' Transifex password: ')
|
||||
# First get the list of languages
|
||||
base64string = base64.encodebytes(('%s:%s' % (username, password)).encode())[:-1]
|
||||
auth_header = 'Basic %s' % base64string.decode()
|
||||
request = urllib.request.Request(SERVER_URL + '?details')
|
||||
request.add_header('Authorization', auth_header)
|
||||
print_verbose('Downloading list of languages from: %s' % SERVER_URL)
|
||||
try:
|
||||
json_response = urllib.request.urlopen(request)
|
||||
except urllib.error.HTTPError:
|
||||
print_quiet('Username or password incorrect.')
|
||||
return False
|
||||
json_dict = json.loads(json_response.read().decode())
|
||||
languages = [lang['code'] for lang in json_dict['available_languages']]
|
||||
for language in languages:
|
||||
lang_url = SERVER_URL + 'translation/%s/?file' % language
|
||||
request = urllib.request.Request(lang_url)
|
||||
request.add_header('Authorization', auth_header)
|
||||
filename = os.path.join(os.path.abspath('..'), 'resources', 'i18n', language + '.ts')
|
||||
print_verbose('Get Translation File: %s' % filename)
|
||||
response = urllib.request.urlopen(request)
|
||||
fd = open(filename, 'wb')
|
||||
fd.write(response.read())
|
||||
fd.close()
|
||||
print_quiet(' Done.')
|
||||
return True
|
||||
|
||||
|
||||
def prepare_project():
|
||||
"""
|
||||
This method creates the project file needed to update the translation files and compile them into .qm files.
|
||||
"""
|
||||
print_quiet('Generating the openlp.pro file')
|
||||
print('Generating the openlp.pro file')
|
||||
lines = []
|
||||
start_dir = os.path.abspath('..')
|
||||
start_dir = start_dir + os.sep
|
||||
print_verbose('Starting directory: %s' % start_dir)
|
||||
print('Starting directory: %s' % start_dir)
|
||||
for root, dirs, files in os.walk(start_dir):
|
||||
for file in files:
|
||||
path = root.replace(start_dir, '').replace('\\', '/')
|
||||
@ -250,180 +69,14 @@ def prepare_project():
|
||||
line = '%s/%s' % (path, file)
|
||||
else:
|
||||
line = file
|
||||
print_verbose('Parsing "%s"' % line)
|
||||
print('Parsing "%s"' % line)
|
||||
lines.append('SOURCES += %s' % line)
|
||||
elif file.endswith('.ts'):
|
||||
line = '%s/%s' % (path, file)
|
||||
print_verbose('Parsing "%s"' % line)
|
||||
lines.append('TRANSLATIONS += %s' % line)
|
||||
lines.append('TRANSLATIONS += resources/i18n/en.ts')
|
||||
lines.sort()
|
||||
file = open(os.path.join(start_dir, 'openlp.pro'), 'w')
|
||||
file.write('\n'.join(lines))
|
||||
file.close()
|
||||
print_quiet(' Done.')
|
||||
|
||||
|
||||
def update_translations():
|
||||
print_quiet('Update the translation files')
|
||||
if not os.path.exists(os.path.join(os.path.abspath('..'), 'openlp.pro')):
|
||||
print('You have not generated a project file yet, please run this script with the -p option.')
|
||||
return
|
||||
else:
|
||||
os.chdir(os.path.abspath('..'))
|
||||
run('pylupdate5 -verbose -noobsolete openlp.pro')
|
||||
os.chdir(os.path.abspath('scripts'))
|
||||
|
||||
|
||||
def generate_binaries():
|
||||
print_quiet('Generate the related *.qm files')
|
||||
if not os.path.exists(os.path.join(os.path.abspath('..'), 'openlp.pro')):
|
||||
print('You have not generated a project file yet, please run this script with the -p option. It is also ' +
|
||||
'recommended that you this script with the -u option to update the translation files as well.')
|
||||
return
|
||||
else:
|
||||
os.chdir(os.path.abspath('..'))
|
||||
run('lrelease openlp.pro')
|
||||
print_quiet(' Done.')
|
||||
|
||||
|
||||
def create_translation():
|
||||
"""
|
||||
This method opens a browser to the OpenLP project page at Transifex so
|
||||
that the user can request a new language.
|
||||
"""
|
||||
print_quiet('Please request a new language at the OpenLP project on Transifex.')
|
||||
webbrowser.open('https://www.transifex.net/projects/p/openlp/resource/ents/')
|
||||
print_quiet('Opening browser to OpenLP project...')
|
||||
|
||||
|
||||
def check_format_strings():
|
||||
"""
|
||||
This method runs through the ts-files and looks for mismatches between format strings in the original text
|
||||
and in the translations.
|
||||
"""
|
||||
is_ok = True
|
||||
path = os.path.join(os.path.abspath('..'), 'resources', 'i18n', '*.ts')
|
||||
file_list = glob.glob(path)
|
||||
for filename in file_list:
|
||||
print_quiet('Checking %s' % filename)
|
||||
file = open(filename, 'rb')
|
||||
tree = objectify.parse(file)
|
||||
root = tree.getroot()
|
||||
for tag in root.iter('message'):
|
||||
location = tag.location.get('filename')
|
||||
line = tag.location.get('line')
|
||||
org_text = tag.source.text
|
||||
translation = tag.translation.text
|
||||
if not translation:
|
||||
for num in tag.iter('numerusform'):
|
||||
print_verbose('parsed numerusform: location: %s, source: %s, translation: %s' % (
|
||||
location, org_text, num.text))
|
||||
if num and org_text.count('%') != num.text.count('%'):
|
||||
is_ok = False
|
||||
print_quiet(
|
||||
'ERROR: Translation from %s at line %s has a mismatch of format input:\n%s\n%s\n' % (
|
||||
location, line, org_text, num.text))
|
||||
else:
|
||||
print_verbose('parsed: location: %s, source: %s, translation: %s' % (location, org_text, translation))
|
||||
if org_text.count('%') != translation.count('%'):
|
||||
is_ok = False
|
||||
print_quiet('ERROR: Translation from %s at line %s has a mismatch of format input:\n%s\n%s\n' % (
|
||||
location, line, org_text, translation))
|
||||
return is_ok
|
||||
|
||||
|
||||
def process_stack(command_stack):
|
||||
"""
|
||||
This method looks at the commands in the command stack, and processes them
|
||||
in the order they are in the stack.
|
||||
|
||||
``command_stack``
|
||||
The command stack to process.
|
||||
"""
|
||||
is_success = True
|
||||
if command_stack:
|
||||
print_quiet('Processing %d commands...' % len(command_stack))
|
||||
for command in command_stack:
|
||||
print_quiet('%d.' % (command_stack.current_index), False)
|
||||
if command == Command.Download:
|
||||
if not download_translations():
|
||||
return
|
||||
elif command == Command.Prepare:
|
||||
prepare_project()
|
||||
elif command == Command.Update:
|
||||
update_translations()
|
||||
elif command == Command.Generate:
|
||||
generate_binaries()
|
||||
elif command == Command.Create:
|
||||
create_translation()
|
||||
elif command == Command.Check:
|
||||
is_success = check_format_strings()
|
||||
print_quiet('Finished processing commands.')
|
||||
else:
|
||||
print_quiet('No commands to process.')
|
||||
return is_success
|
||||
|
||||
|
||||
def main():
|
||||
global verbose_mode, quiet_mode, username, password
|
||||
# Set up command line options.
|
||||
usage = '%(prog)s [options]\nOptions are parsed in the order they are ' + \
|
||||
'listed below. If no options are given, "-dpug" will be used.\n\n' + \
|
||||
'This script is used to manage OpenLP\'s translation files.'
|
||||
parser = ArgumentParser(usage=usage)
|
||||
parser.add_argument('-U', '--username', dest='username', metavar='USERNAME',
|
||||
help='Transifex username, used for authentication')
|
||||
parser.add_argument('-P', '--password', dest='password', metavar='PASSWORD',
|
||||
help='Transifex password, used for authentication')
|
||||
parser.add_argument('-d', '--download-ts', dest='download',
|
||||
action='store_true', help='download language files from Transifex')
|
||||
parser.add_argument('-c', '--create', dest='create', action='store_true',
|
||||
help='go to Transifex to request a new translation file')
|
||||
parser.add_argument('-p', '--prepare', dest='prepare', action='store_true',
|
||||
help='generate a project file, used to update the translations')
|
||||
parser.add_argument('-u', '--update', action='store_true', dest='update',
|
||||
help='update translation files (needs a project file)')
|
||||
parser.add_argument('-g', '--generate', dest='generate', action='store_true',
|
||||
help='compile .ts files into .qm files')
|
||||
parser.add_argument('-v', '--verbose', dest='verbose', action='store_true',
|
||||
help='show extra information while processing translations')
|
||||
parser.add_argument('-q', '--quiet', dest='quiet', action='store_true',
|
||||
help='suppress all output other than errors')
|
||||
parser.add_argument('-f', '--check-format-strings', dest='check', action='store_true',
|
||||
help='check that format strings are matching in translations')
|
||||
args = parser.parse_args()
|
||||
# Create and populate the command stack
|
||||
command_stack = CommandStack()
|
||||
if args.download:
|
||||
command_stack.append(Command.Download)
|
||||
if args.create:
|
||||
command_stack.append(Command.Create, arguments=[args.create])
|
||||
if args.prepare:
|
||||
command_stack.append(Command.Prepare)
|
||||
if args.update:
|
||||
command_stack.append(Command.Update)
|
||||
if args.generate:
|
||||
command_stack.append(Command.Generate)
|
||||
if args.check:
|
||||
command_stack.append(Command.Check)
|
||||
verbose_mode = args.verbose
|
||||
quiet_mode = args.quiet
|
||||
if args.username:
|
||||
username = args.username
|
||||
if args.password:
|
||||
password = args.password
|
||||
if not command_stack:
|
||||
command_stack.append(Command.Download)
|
||||
command_stack.append(Command.Prepare)
|
||||
command_stack.append(Command.Update)
|
||||
command_stack.append(Command.Generate)
|
||||
# Process the commands
|
||||
return process_stack(command_stack)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if os.path.split(os.path.abspath('.'))[1] != 'scripts':
|
||||
print('You need to run this script from the scripts directory.')
|
||||
else:
|
||||
if not main():
|
||||
sys.exit(1)
|
||||
prepare_project()
|
||||
|
@ -26,7 +26,7 @@ from pathlib import Path
|
||||
from unittest import TestCase
|
||||
from unittest.mock import MagicMock, call, patch
|
||||
|
||||
from openlp.core.common import clean_button_text, de_hump, extension_loader, is_linux, is_macosx, is_win, \
|
||||
from openlp.core.common import Singleton, clean_button_text, de_hump, extension_loader, is_linux, is_macosx, is_win, \
|
||||
normalize_str, path_to_module, trace_error_handler
|
||||
|
||||
|
||||
@ -163,6 +163,48 @@ class TestCommonFunctions(TestCase):
|
||||
mocked_logger.error.assert_called_with(
|
||||
'OpenLP Error trace\n File openlp.fake at line 56 \n\t called trace_error_handler_test')
|
||||
|
||||
def test_singleton_metaclass_multiple_init(self):
|
||||
"""
|
||||
Test that a class using the Singleton Metaclass is only initialised once despite being called several times and
|
||||
that the same instance is returned each time..
|
||||
"""
|
||||
# GIVEN: The Singleton Metaclass and a test class using it
|
||||
class SingletonClass(metaclass=Singleton):
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
with patch.object(SingletonClass, '__init__', return_value=None) as patched_init:
|
||||
|
||||
# WHEN: Initialising the class multiple times
|
||||
inst_1 = SingletonClass()
|
||||
inst_2 = SingletonClass()
|
||||
|
||||
# THEN: The __init__ method of the SingletonClass should have only been called once, and both returned values
|
||||
# should be the same instance.
|
||||
assert inst_1 is inst_2
|
||||
assert patched_init.call_count == 1
|
||||
|
||||
def test_singleton_metaclass_multiple_classes(self):
|
||||
"""
|
||||
Test that multiple classes using the Singleton Metaclass return the different an appropriate instances.
|
||||
"""
|
||||
# GIVEN: Two different classes using the Singleton Metaclass
|
||||
class SingletonClass1(metaclass=Singleton):
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
class SingletonClass2(metaclass=Singleton):
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
# WHEN: Initialising both classes
|
||||
s_c1 = SingletonClass1()
|
||||
s_c2 = SingletonClass2()
|
||||
|
||||
# THEN: The instances should be an instance of the appropriate class
|
||||
assert isinstance(s_c1, SingletonClass1)
|
||||
assert isinstance(s_c2, SingletonClass2)
|
||||
|
||||
def test_is_win(self):
|
||||
"""
|
||||
Test the is_win() function
|
||||
|
@ -182,4 +182,4 @@ class TestTheme(TestCase):
|
||||
assert 0 == theme.display_vertical_align, 'display_vertical_align should be 0'
|
||||
assert theme.font_footer_bold is False, 'font_footer_bold should be False'
|
||||
assert 'Arial' == theme.font_main_name, 'font_main_name should be "Arial"'
|
||||
assert 47 == len(theme.__dict__), 'The theme should have 47 attributes'
|
||||
assert 48 == len(theme.__dict__), 'The theme should have 48 attributes'
|
||||
|
@ -33,7 +33,7 @@ from tests.helpers.testmixin import TestMixin
|
||||
|
||||
class TestIcons(TestCase, TestMixin):
|
||||
|
||||
@patch('openlp.core.ui.icons.UiIcons.load')
|
||||
@patch('openlp.core.ui.icons.UiIcons.__init__', return_value=None)
|
||||
def test_simple_icon(self, _):
|
||||
# GIVEN: an basic set of icons
|
||||
icons = UiIcons()
|
||||
|
@ -1,3 +1,12 @@
|
||||
function _createDiv(attrs) {
|
||||
var div = document.createElement("div");
|
||||
for (key in attrs) {
|
||||
div.setAttribute(key, attrs[key]);
|
||||
}
|
||||
document.body.appendChild(div);
|
||||
return div;
|
||||
}
|
||||
|
||||
describe("The enumeration object", function () {
|
||||
it("BackgroundType should exist", function () {
|
||||
expect(BackgroundType).toBeDefined();
|
||||
@ -22,9 +31,7 @@ describe("The enumeration object", function () {
|
||||
|
||||
describe("The function", function () {
|
||||
it("$() should return the right element", function () {
|
||||
var div = document.createElement("div");
|
||||
div.setAttribute("id", "dollar-test");
|
||||
document.body.appendChild(div);
|
||||
var div = _createDiv({"id": "dollar-test"});
|
||||
expect($("#dollar-test")[0]).toBe(div);
|
||||
});
|
||||
|
||||
@ -39,10 +46,8 @@ describe("The function", function () {
|
||||
});
|
||||
|
||||
it("_getStyle should return the correct style on an element", function () {
|
||||
var div = document.createElement("div");
|
||||
var div = _createDiv({"id": "style-test"});
|
||||
div.style.setProperty("width", "100px");
|
||||
div.setAttribute("id", "style-test");
|
||||
document.body.appendChild(div);
|
||||
expect(_getStyle($("#style-test")[0], "width")).toBe("100px");
|
||||
});
|
||||
|
||||
@ -120,10 +125,8 @@ describe("The Display object", function () {
|
||||
expect(Display.clearSlides).toBeDefined();
|
||||
|
||||
document.body.innerHTML = "";
|
||||
var slidesDiv = document.createElement("div");
|
||||
slidesDiv.setAttribute("class", "slides");
|
||||
var slidesDiv = _createDiv({"class": "slides"});
|
||||
slidesDiv.innerHTML = "<section><p></p></section>";
|
||||
document.body.appendChild(slidesDiv);
|
||||
|
||||
Display.clearSlides();
|
||||
expect($(".slides")[0].innerHTML).toEqual("");
|
||||
@ -143,17 +146,18 @@ describe("The Display object", function () {
|
||||
describe("Display.addTextSlide", function () {
|
||||
beforeEach(function() {
|
||||
document.body.innerHTML = "";
|
||||
var slidesDiv = document.createElement("div");
|
||||
slidesDiv.setAttribute("class", "slides");
|
||||
document.body.appendChild(slidesDiv);
|
||||
_createDiv({"class": "slides"});
|
||||
_createDiv({"class": "footer"});
|
||||
Display._slides = {};
|
||||
});
|
||||
|
||||
it("should add a new slide", function () {
|
||||
var verse = "v1", text = "Amazing grace,\nhow sweet the sound";
|
||||
var verse = "v1",
|
||||
text = "Amazing grace,\nhow sweet the sound",
|
||||
footer = "Public Domain";
|
||||
spyOn(Display, "reinit");
|
||||
|
||||
Display.addTextSlide(verse, text);
|
||||
Display.addTextSlide(verse, text, footer);
|
||||
|
||||
expect(Display._slides[verse]).toEqual(0);
|
||||
expect($(".slides > section").length).toEqual(1);
|
||||
@ -162,10 +166,12 @@ describe("Display.addTextSlide", function () {
|
||||
});
|
||||
|
||||
it("should add a new slide without calling reinit()", function () {
|
||||
var verse = "v1", text = "Amazing grace,\nhow sweet the sound";
|
||||
var verse = "v1",
|
||||
text = "Amazing grace,\nhow sweet the sound",
|
||||
footer = "Public Domain";
|
||||
spyOn(Display, "reinit");
|
||||
|
||||
Display.addTextSlide(verse, text, false);
|
||||
Display.addTextSlide(verse, text, footer, false);
|
||||
|
||||
expect(Display._slides[verse]).toEqual(0);
|
||||
expect($(".slides > section").length).toEqual(1);
|
||||
@ -174,8 +180,10 @@ describe("Display.addTextSlide", function () {
|
||||
});
|
||||
|
||||
it("should update an existing slide", function () {
|
||||
var verse = "v1", text = "Amazing grace, how sweet the sound\nThat saved a wretch like me";
|
||||
Display.addTextSlide(verse, "Amazing grace,\nhow sweet the sound", false);
|
||||
var verse = "v1",
|
||||
text = "Amazing grace, how sweet the sound\nThat saved a wretch like me",
|
||||
footer = "Public Domain";
|
||||
Display.addTextSlide(verse, "Amazing grace,\nhow sweet the sound", footer, false);
|
||||
spyOn(Display, "reinit");
|
||||
|
||||
Display.addTextSlide(verse, text, true);
|
||||
@ -190,18 +198,9 @@ describe("Display.addTextSlide", function () {
|
||||
describe("Display.setTextSlides", function () {
|
||||
beforeEach(function() {
|
||||
document.body.innerHTML = "";
|
||||
var slidesDiv = document.createElement("div");
|
||||
slidesDiv.setAttribute("class", "slides");
|
||||
document.body.appendChild(slidesDiv);
|
||||
|
||||
var background = document.createElement("div");
|
||||
background.setAttribute("id", "global-background");
|
||||
document.body.appendChild(background);
|
||||
|
||||
var footer = document.createElement("div");
|
||||
footer.setAttribute("class", "footer");
|
||||
document.body.appendChild(footer);
|
||||
|
||||
_createDiv({"class": "slides"});
|
||||
_createDiv({"class": "footer"});
|
||||
_createDiv({"id": "global-background"});
|
||||
Display._slides = {};
|
||||
});
|
||||
|
||||
@ -210,12 +209,14 @@ describe("Display.setTextSlides", function () {
|
||||
{
|
||||
"verse": "v1",
|
||||
"text": "Amazing grace, how sweet the sound\nThat saved a wretch like me\n" +
|
||||
"I once was lost, but now I'm found\nWas blind but now I see"
|
||||
"I once was lost, but now I'm found\nWas blind but now I see",
|
||||
"footer": "Public Domain"
|
||||
},
|
||||
{
|
||||
"verse": "v2",
|
||||
"text": "'twas Grace that taught, my heart to fear\nAnd grace, my fears relieved.\n" +
|
||||
"How precious did that grace appear,\nthe hour I first believed."
|
||||
"How precious did that grace appear,\nthe hour I first believed.",
|
||||
"footer": "Public Domain"
|
||||
}
|
||||
];
|
||||
spyOn(Display, "clearSlides");
|
||||
@ -232,29 +233,27 @@ describe("Display.setTextSlides", function () {
|
||||
expect(Reveal.slide).toHaveBeenCalledWith(0);
|
||||
});
|
||||
|
||||
it("should correctly set outline width", function () {
|
||||
xit("should correctly set outline width (skipped for now)", function () {
|
||||
const slides = [
|
||||
{
|
||||
"verse": "v1",
|
||||
"text": "Amazing grace, how sweet the sound\nThat saved a wretch like me\n" +
|
||||
"I once was lost, but now I'm found\nWas blind but now I see"
|
||||
"I once was lost, but now I'm found\nWas blind but now I see",
|
||||
"footer": "Public Domain"
|
||||
}
|
||||
];
|
||||
|
||||
const theme = {
|
||||
'font_main_color': 'yellow',
|
||||
'font_main_outline': true,
|
||||
'font_main_outline_size': 42,
|
||||
'font_main_outline_color': 'red'
|
||||
};
|
||||
|
||||
spyOn(Display, "reinit");
|
||||
|
||||
Display.setTextSlides(slides);
|
||||
Display.setTheme(theme);
|
||||
|
||||
const slidesDiv = $(".slides")[0];
|
||||
|
||||
expect(slidesDiv.style['-webkit-text-stroke']).toEqual('42pt red');
|
||||
expect(slidesDiv.style['padding-left']).toEqual('84pt');
|
||||
expect(slidesDiv.style['-webkit-text-fill-color']).toEqual('yellow');
|
||||
@ -264,12 +263,9 @@ describe("Display.setTextSlides", function () {
|
||||
describe("Display.setImageSlides", function () {
|
||||
beforeEach(function() {
|
||||
document.body.innerHTML = "";
|
||||
var slidesDiv = document.createElement("div");
|
||||
slidesDiv.setAttribute("class", "slides");
|
||||
document.body.appendChild(slidesDiv);
|
||||
var backgroundDiv = document.createElement("div");
|
||||
backgroundDiv.setAttribute("id", "global-background");
|
||||
document.body.appendChild(backgroundDiv);
|
||||
_createDiv({"class": "slides"});
|
||||
_createDiv({"class": "footer"});
|
||||
_createDiv({"id": "global-background"});
|
||||
Display._slides = {};
|
||||
});
|
||||
|
||||
@ -286,7 +282,9 @@ describe("Display.setImageSlides", function () {
|
||||
expect($(".slides > section").length).toEqual(2);
|
||||
expect($(".slides > section > img").length).toEqual(2);
|
||||
expect($(".slides > section > img")[0].getAttribute("src")).toEqual("file:///openlp1.jpg")
|
||||
expect($(".slides > section > img")[0].getAttribute("style")).toEqual("max-width: 100%; max-height: 100%; margin: 0; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);")
|
||||
expect($(".slides > section > img")[1].getAttribute("src")).toEqual("file:///openlp2.jpg")
|
||||
expect($(".slides > section > img")[1].getAttribute("style")).toEqual("max-width: 100%; max-height: 100%; margin: 0; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);")
|
||||
expect(Display.reinit).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@ -294,12 +292,8 @@ describe("Display.setImageSlides", function () {
|
||||
describe("Display.setVideo", function () {
|
||||
beforeEach(function() {
|
||||
document.body.innerHTML = "";
|
||||
var slidesDiv = document.createElement("div");
|
||||
slidesDiv.setAttribute("class", "slides");
|
||||
document.body.appendChild(slidesDiv);
|
||||
var backgroundDiv = document.createElement("div");
|
||||
backgroundDiv.setAttribute("id", "global-background");
|
||||
document.body.appendChild(backgroundDiv);
|
||||
_createDiv({"class": "slides"});
|
||||
_createDiv({"id": "global-background"});
|
||||
Display._slides = {};
|
||||
});
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user