This commit is contained in:
Tim Bentley 2019-02-14 19:40:35 +00:00
commit c0d328dc06
365 changed files with 15112 additions and 6165 deletions

View File

@ -46,4 +46,11 @@ resources/innosetup/Output
resources/windows/warnOpenLP.txt
*.ropeproject
tags
output
htmlcov
node_modules
openlp-test-projectordb.sqlite
package-lock.json
.cache
test
tests.kdev4

77
karma.conf.js Normal file
View File

@ -0,0 +1,77 @@
module.exports = function(config) {
config.set({
// base path that will be used to resolve all patterns (eg. files, exclude)
basePath: "",
// frameworks to use
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
frameworks: ["jasmine"],
// list of files / patterns to load in the browser
files: [
"tests/js/polyfill.js",
"tests/js/fake_webchannel.js",
"openlp/core/display/html/reveal.js",
"openlp/core/display/html/display.js",
"tests/js/test_*.js"
],
// list of files to exclude
exclude: [
],
// preprocess matching files before serving them to the browser
// available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
preprocessors: {
// source files, that you wanna generate coverage for
// do not include tests or libraries
// (these files will be instrumented by Istanbul)
"display.js": ["coverage"]
},
// test results reporter to use
// possible values: "dots", "progress"
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
reporters: ["progress", "coverage"],
// configure the coverateReporter
coverageReporter: {
type : "html",
dir : "htmlcov/"
},
// web server port
port: 9876,
// enable / disable colors in the output (reporters and logs)
colors: true,
// level of logging
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
logLevel: config.LOG_DEBUG,
// loggers
/* loggers: [
{"type": "file", "filename": "karma.log"}
],*/
// enable / disable watching file and executing tests whenever any file changes
autoWatch: true,
// start these browsers
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
browsers: ["PhantomJS"],
// Continuous Integration mode
// if true, Karma captures browsers, runs the tests and exits
singleRun: false,
// Concurrency level
// how many browser should be started simultaneous
concurrency: Infinity,
client: {
captureConsole: true
}
})
}

View File

@ -1,5 +1,5 @@
[unittest]
verbose = True
verbose = true
plugins = nose2.plugins.mp
[log-capture]
@ -9,19 +9,19 @@ filter = -nose
log-level = ERROR
[test-result]
always-on = True
descriptions = True
always-on = true
descriptions = true
[coverage]
always-on = True
always-on = true
coverage = openlp
coverage-report = html
[multiprocess]
always-on = False
always-on = false
processes = 4
[output-buffer]
always-on = True
stderr = True
stdout = False
always-on = true
stderr = true
stdout = true

View File

@ -1 +1 @@
2.9.0
2.5.dev2856

View File

@ -25,7 +25,7 @@ Download and "install" the remote web client
from zipfile import ZipFile
from openlp.core.common.applocation import AppLocation
from openlp.core.common.httputils import download_file, get_web_page, get_url_file_size
from openlp.core.common.httputils import download_file, get_url_file_size, get_web_page
from openlp.core.common.registry import Registry

View File

@ -34,6 +34,7 @@ from openlp.core.common.settings import Settings
from openlp.core.lib import create_thumb
from openlp.core.lib.serviceitem import ItemCapabilities
log = logging.getLogger(__name__)
controller_endpoint = Endpoint('controller')
@ -48,7 +49,7 @@ def controller_text(request):
:param request: the http request - not used
"""
log.debug("controller_text ")
log.debug('controller_text')
live_controller = Registry().get('live_controller')
current_item = live_controller.service_item
data = []
@ -57,13 +58,14 @@ def controller_text(request):
item = {}
# Handle text (songs, custom, bibles)
if current_item.is_text():
if frame['verseTag']:
item['tag'] = str(frame['verseTag'])
if frame['verse']:
item['tag'] = str(frame['verse'])
else:
item['tag'] = str(index + 1)
item['chords_text'] = str(frame['chords_text'])
item['text'] = str(frame['text'])
item['html'] = str(frame['html'])
# TODO: Figure out rendering chords
item['chords_text'] = str(frame.get('chords_text', ''))
item['text'] = frame['text']
item['html'] = current_item.get_rendered_frame(index)
# Handle images, unless a custom thumbnail is given or if thumbnails is disabled
elif current_item.is_image() and not frame.get('image', '') and Settings().value('api/thumbnails'):
item['tag'] = str(index + 1)

View File

@ -30,8 +30,8 @@ from openlp.core.api.http.endpoint import Endpoint
from openlp.core.common.i18n import UiStrings, translate
from openlp.core.common.registry import Registry
from openlp.core.lib import image_to_byte
from openlp.core.lib.plugin import StringContent
from openlp.core.lib.plugin import PluginStatus
from openlp.core.lib.plugin import PluginStatus, StringContent
template_dir = 'templates'
static_dir = 'static'

View File

@ -24,6 +24,7 @@ import logging
from openlp.core.api.endpoint.core import TRANSLATED_STRINGS
from openlp.core.api.http.endpoint import Endpoint
log = logging.getLogger(__name__)
remote_endpoint = Endpoint('remote', template_dir='remotes')

View File

@ -26,6 +26,7 @@ from openlp.core.api.http import requires_auth
from openlp.core.api.http.endpoint import Endpoint
from openlp.core.common.registry import Registry
log = logging.getLogger(__name__)
service_endpoint = Endpoint('service')

View File

@ -30,22 +30,21 @@ from PyQt5 import QtCore, QtWidgets
from waitress.server import create_server
from openlp.core.api.deploy import download_and_check, download_sha256
from openlp.core.api.endpoint.controller import controller_endpoint, api_controller_endpoint
from openlp.core.api.endpoint.core import chords_endpoint, stage_endpoint, blank_endpoint, main_endpoint
from openlp.core.api.endpoint.controller import api_controller_endpoint, controller_endpoint
from openlp.core.api.endpoint.core import blank_endpoint, chords_endpoint, main_endpoint, stage_endpoint
from openlp.core.api.endpoint.remote import remote_endpoint
from openlp.core.api.endpoint.service import service_endpoint, api_service_endpoint
from openlp.core.api.http import application
from openlp.core.api.http import register_endpoint
from openlp.core.api.endpoint.service import api_service_endpoint, service_endpoint
from openlp.core.api.http import application, register_endpoint
from openlp.core.api.poll import Poller
from openlp.core.common.applocation import AppLocation
from openlp.core.common.i18n import UiStrings
from openlp.core.common.i18n import translate
from openlp.core.common.i18n import UiStrings, translate
from openlp.core.common.mixins import LogMixin, RegistryProperties
from openlp.core.common.path import create_paths
from openlp.core.common.registry import Registry, RegistryBase
from openlp.core.common.settings import Settings
from openlp.core.threading import ThreadWorker, run_thread
log = logging.getLogger(__name__)

View File

@ -33,6 +33,7 @@ from webob.static import DirectoryApp
from openlp.core.api.http.errors import HttpError, NotFound, ServerError
from openlp.core.common.applocation import AppLocation
ARGS_REGEX = re.compile(r'''\{(\w+)(?::([^}]+))?\}''', re.VERBOSE)
log = logging.getLogger(__name__)

View File

@ -31,6 +31,7 @@ from openlp.core.common.settings import Settings
from openlp.core.lib.settingstab import SettingsTab
from openlp.core.ui.icons import UiIcons
ZERO_URL = '0.0.0.0'
@ -43,9 +44,9 @@ class ApiTab(SettingsTab):
advanced_translated = translate('OpenLP.AdvancedTab', 'Advanced')
super(ApiTab, self).__init__(parent, 'api', advanced_translated)
def setupUi(self):
def setup_ui(self):
self.setObjectName('ApiTab')
super(ApiTab, self).setupUi()
super(ApiTab, self).setup_ui()
self.server_settings_group_box = QtWidgets.QGroupBox(self.left_column)
self.server_settings_group_box.setObjectName('server_settings_group_box')
self.server_settings_layout = QtWidgets.QFormLayout(self.server_settings_group_box)
@ -154,7 +155,7 @@ class ApiTab(SettingsTab):
self.thumbnails_check_box.stateChanged.connect(self.on_thumbnails_check_box_changed)
self.address_edit.textChanged.connect(self.set_urls)
def retranslateUi(self):
def retranslate_ui(self):
self.tab_title_visible = translate('RemotePlugin.RemoteTab', 'Remote Interface')
self.server_settings_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'Server Settings'))
self.address_label.setText(translate('RemotePlugin.RemoteTab', 'Serve on IP address:'))

View File

@ -35,6 +35,7 @@ from openlp.core.common.registry import Registry
from openlp.core.common.settings import Settings
from openlp.core.threading import ThreadWorker, run_thread
log = logging.getLogger(__name__)

View File

@ -33,27 +33,28 @@ import time
from datetime import datetime
from traceback import format_exception
from PyQt5 import QtCore, QtWidgets
from PyQt5 import QtCore, QtWebEngineWidgets, QtWidgets # noqa
from openlp.core.state import State
from openlp.core.common import is_macosx, is_win
from openlp.core.common.applocation import AppLocation
from openlp.core.loader import loader
from openlp.core.common.i18n import LanguageManager, UiStrings, translate
from openlp.core.common.path import create_paths, copytree
from openlp.core.common.path import copytree, create_paths
from openlp.core.common.registry import Registry
from openlp.core.common.settings import Settings
from openlp.core.display.screens import ScreenList
from openlp.core.resources import qInitResources
from openlp.core.ui.splashscreen import SplashScreen
from openlp.core.server import Server
from openlp.core.ui.exceptionform import ExceptionForm
from openlp.core.ui.firsttimeform import FirstTimeForm
from openlp.core.ui.firsttimelanguageform import FirstTimeLanguageForm
from openlp.core.ui.mainwindow import MainWindow
from openlp.core.ui.splashscreen import SplashScreen
from openlp.core.ui.style import get_application_stylesheet
from openlp.core.server import Server
from openlp.core.version import check_for_update, get_version
__all__ = ['OpenLP', 'main']
@ -74,7 +75,8 @@ class OpenLP(QtWidgets.QApplication):
"""
self.is_event_loop_active = True
result = QtWidgets.QApplication.exec()
self.server.close_server()
if hasattr(self, 'server'):
self.server.close_server()
return result
def run(self, args):
@ -317,7 +319,7 @@ def set_up_logging(log_path):
file_path = log_path / 'openlp.log'
# TODO: FileHandler accepts a Path object in Py3.6
logfile = logging.FileHandler(str(file_path), 'w', encoding='UTF-8')
logfile.setFormatter(logging.Formatter('%(asctime)s %(name)-55s %(levelname)-8s %(message)s'))
logfile.setFormatter(logging.Formatter('%(asctime)s %(threadName)s %(name)-55s %(levelname)-8s %(message)s'))
log.addHandler(logfile)
if log.isEnabledFor(logging.DEBUG):
print('Logging to: {name}'.format(name=file_path))
@ -330,7 +332,8 @@ def main(args=None):
:param args: Some args
"""
args = parse_options(args)
qt_args = []
qt_args = ['--disable-web-security']
# qt_args = []
if args and args.loglevel.lower() in ['d', 'debug']:
log.setLevel(logging.DEBUG)
elif args and args.loglevel.lower() in ['w', 'warning']:

View File

@ -29,6 +29,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets
from openlp.core.common.settings import Settings
log = logging.getLogger(__name__)
@ -113,7 +114,6 @@ class CategoryActionList(object):
if item[1] == action:
self.actions.remove(item)
return
log.warning('Action "{action}" does not exist.'.format(action=action))
class CategoryList(object):

View File

@ -29,10 +29,11 @@ import sys
import appdirs
import openlp
from openlp.core.common import get_frozen_path, is_win, is_macosx
from openlp.core.common import get_frozen_path, is_macosx, is_win
from openlp.core.common.path import Path, create_paths
from openlp.core.common.settings import Settings
log = logging.getLogger(__name__)
FROZEN_APP_PATH = Path(sys.argv[0]).parent

View File

@ -27,6 +27,7 @@ from copy import deepcopy
import sqlalchemy
log = logging.getLogger(__name__)

View File

@ -34,6 +34,7 @@ from openlp.core.common import trace_error_handler
from openlp.core.common.registry import Registry
from openlp.core.common.settings import ProxyMode, Settings
log = logging.getLogger(__name__ + '.__init__')
USER_AGENTS = {

View File

@ -29,10 +29,11 @@ from collections import namedtuple
from PyQt5 import QtCore, QtWidgets
from openlp.core.common import is_win, is_macosx
from openlp.core.common import is_macosx, is_win
from openlp.core.common.applocation import AppLocation
from openlp.core.common.settings import Settings
log = logging.getLogger(__name__)

View File

@ -28,6 +28,7 @@ import logging
from openlp.core.common import is_win, trace_error_handler
from openlp.core.common.registry import Registry
DO_NOT_TRACE_EVENTS = ['timerEvent', 'paintEvent', 'drag_enter_event', 'drop_event', 'on_controller_size_changed',
'preview_size_changed', 'resizeEvent']

View File

@ -25,6 +25,7 @@ from contextlib import suppress
from openlp.core.common import is_win
if is_win():
from pathlib import WindowsPath as PathVariant # pragma: nocover
else:

View File

@ -27,6 +27,7 @@ import sys
from openlp.core.common import de_hump, trace_error_handler
log = logging.getLogger(__name__)
@ -143,6 +144,7 @@ class Registry(object):
if event in self.functions_list:
for function in self.functions_list[event]:
try:
log.debug('Running function {} for {}'.format(function, event))
result = function(*args, **kwargs)
if result:
results.append(result)

View File

@ -33,7 +33,8 @@ from PyQt5 import QtCore, QtGui
from openlp.core.common import SlideLimits, ThemeLevel, is_linux, is_win
from openlp.core.common.json import OpenLPJsonDecoder, OpenLPJsonEncoder
from openlp.core.common.path import Path, str_to_path, files_to_paths
from openlp.core.common.path import Path, files_to_paths, str_to_path
log = logging.getLogger(__name__)
@ -70,6 +71,34 @@ def media_players_conv(string):
return string
def upgrade_screens(number, x_position, y_position, height, width, can_override, is_display_screen):
"""
Upgrade them monitor setting from a few single entries to a composite JSON entry
:param int number: The old monitor number
:param int x_position: The X position
:param int y_position: The Y position
:param bool can_override: Are the screen positions overridden
:param bool is_display_screen: Is this a display screen
:returns dict: Dictionary with the new value
"""
geometry_key = 'geometry'
if can_override:
geometry_key = 'custom_geometry'
return {
number: {
'number': number,
geometry_key: {
'x': x_position,
'y': y_position,
'height': height,
'width': width
},
'is_display': is_display_screen
}
}
class Settings(QtCore.QSettings):
"""
Class to wrap QSettings.
@ -175,6 +204,7 @@ class Settings(QtCore.QSettings):
# circular dependency.
'core/display on monitor': True,
'core/override position': False,
'core/monitor': {},
'core/application version': '0.0',
'images/background color': '#000000',
'media/media auto start': QtCore.Qt.Unchecked,
@ -276,6 +306,8 @@ 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/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', '', []),
('bibles/proxy username', '', []),
@ -547,7 +579,7 @@ class Settings(QtCore.QSettings):
:param value: The value to save
:rtype: None
"""
if isinstance(value, Path) or (isinstance(value, list) and value and isinstance(value[0], Path)):
if isinstance(value, (Path, dict)) or (isinstance(value, list) and value and isinstance(value[0], Path)):
value = json.dumps(value, cls=OpenLPJsonEncoder)
super().setValue(key, value)
@ -570,8 +602,11 @@ class Settings(QtCore.QSettings):
# An empty list saved to the settings results in a None type being returned.
elif isinstance(default_value, list):
return []
# An empty dictionary saved to the settings results in a None type being returned.
elif isinstance(default_value, dict):
return {}
elif isinstance(setting, str):
if '__Path__' in setting:
if '__Path__' in setting or setting.startswith('{'):
return json.loads(setting, cls=OpenLPJsonDecoder)
# Convert the setting to the correct type.
if isinstance(default_value, bool):
@ -580,6 +615,8 @@ class Settings(QtCore.QSettings):
# Sometimes setting is string instead of a boolean.
return setting == 'true'
if isinstance(default_value, int):
if setting is None:
return 0
return int(setting)
return setting

View File

@ -0,0 +1,292 @@
/**
* Black theme for reveal.js. This is the opposite of the 'white' theme.
*
* By Hakim El Hattab, http://hakim.se
*/
@import url(../../lib/font/source-sans-pro/source-sans-pro.css);
section.has-light-background, section.has-light-background h1, section.has-light-background h2, section.has-light-background h3, section.has-light-background h4, section.has-light-background h5, section.has-light-background h6 {
color: #222; }
/*********************************************
* GLOBAL STYLES
*********************************************/
body {
background: #222;
background-color: #222; }
.reveal {
font-family: "Source Sans Pro", Helvetica, sans-serif;
font-size: 42px;
font-weight: normal;
color: #fff; }
::selection {
color: #fff;
background: #bee4fd;
text-shadow: none; }
::-moz-selection {
color: #fff;
background: #bee4fd;
text-shadow: none; }
.reveal .slides > section,
.reveal .slides > section > section {
line-height: 1.3;
font-weight: inherit; }
/*********************************************
* HEADERS
*********************************************/
.reveal h1,
.reveal h2,
.reveal h3,
.reveal h4,
.reveal h5,
.reveal h6 {
margin: 0 0 20px 0;
color: #fff;
font-family: "Source Sans Pro", Helvetica, sans-serif;
font-weight: 600;
line-height: 1.2;
letter-spacing: normal;
text-transform: uppercase;
text-shadow: none;
word-wrap: break-word; }
.reveal h1 {
font-size: 2.5em; }
.reveal h2 {
font-size: 1.6em; }
.reveal h3 {
font-size: 1.3em; }
.reveal h4 {
font-size: 1em; }
.reveal h1 {
text-shadow: none; }
/*********************************************
* OTHER
*********************************************/
.reveal p {
margin: 20px 0;
line-height: 1.3; }
/* Ensure certain elements are never larger than the slide itself */
.reveal img,
.reveal video,
.reveal iframe {
max-width: 95%;
max-height: 95%; }
.reveal strong,
.reveal b {
font-weight: bold; }
.reveal em {
font-style: italic; }
.reveal ol,
.reveal dl,
.reveal ul {
display: inline-block;
text-align: left;
margin: 0 0 0 1em; }
.reveal ol {
list-style-type: decimal; }
.reveal ul {
list-style-type: disc; }
.reveal ul ul {
list-style-type: square; }
.reveal ul ul ul {
list-style-type: circle; }
.reveal ul ul,
.reveal ul ol,
.reveal ol ol,
.reveal ol ul {
display: block;
margin-left: 40px; }
.reveal dt {
font-weight: bold; }
.reveal dd {
margin-left: 40px; }
.reveal q,
.reveal blockquote {
quotes: none; }
.reveal blockquote {
display: block;
position: relative;
width: 70%;
margin: 20px auto;
padding: 5px;
font-style: italic;
background: rgba(255, 255, 255, 0.05);
box-shadow: 0px 0px 2px rgba(0, 0, 0, 0.2); }
.reveal blockquote p:first-child,
.reveal blockquote p:last-child {
display: inline-block; }
.reveal q {
font-style: italic; }
.reveal pre {
display: block;
position: relative;
width: 90%;
margin: 20px auto;
text-align: left;
font-size: 0.55em;
font-family: monospace;
line-height: 1.2em;
word-wrap: break-word;
box-shadow: 0px 0px 6px rgba(0, 0, 0, 0.3); }
.reveal code {
font-family: monospace; }
.reveal pre code {
display: block;
padding: 5px;
overflow: auto;
max-height: 400px;
word-wrap: normal; }
.reveal table {
margin: auto;
border-collapse: collapse;
border-spacing: 0; }
.reveal table th {
font-weight: bold; }
.reveal table th,
.reveal table td {
text-align: left;
padding: 0.2em 0.5em 0.2em 0.5em;
border-bottom: 1px solid; }
.reveal table th[align="center"],
.reveal table td[align="center"] {
text-align: center; }
.reveal table th[align="right"],
.reveal table td[align="right"] {
text-align: right; }
.reveal table tbody tr:last-child th,
.reveal table tbody tr:last-child td {
border-bottom: none; }
.reveal sup {
vertical-align: super; }
.reveal sub {
vertical-align: sub; }
.reveal small {
display: inline-block;
font-size: 0.6em;
line-height: 1.2em;
vertical-align: top; }
.reveal small * {
vertical-align: top; }
/*********************************************
* LINKS
*********************************************/
.reveal a {
color: #42affa;
text-decoration: none;
-webkit-transition: color .15s ease;
-moz-transition: color .15s ease;
transition: color .15s ease; }
.reveal a:hover {
color: #8dcffc;
text-shadow: none;
border: none; }
.reveal .roll span:after {
color: #fff;
background: #068de9; }
/*********************************************
* IMAGES
*********************************************/
.reveal section img {
margin: 15px 0px;
background: rgba(255, 255, 255, 0.12);
border: 4px solid #fff;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.15); }
.reveal section img.plain {
border: 0;
box-shadow: none; }
.reveal a img {
-webkit-transition: all .15s linear;
-moz-transition: all .15s linear;
transition: all .15s linear; }
.reveal a:hover img {
background: rgba(255, 255, 255, 0.2);
border-color: #42affa;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.55); }
/*********************************************
* NAVIGATION CONTROLS
*********************************************/
.reveal .controls .navigate-left,
.reveal .controls .navigate-left.enabled {
border-right-color: #42affa; }
.reveal .controls .navigate-right,
.reveal .controls .navigate-right.enabled {
border-left-color: #42affa; }
.reveal .controls .navigate-up,
.reveal .controls .navigate-up.enabled {
border-bottom-color: #42affa; }
.reveal .controls .navigate-down,
.reveal .controls .navigate-down.enabled {
border-top-color: #42affa; }
.reveal .controls .navigate-left.enabled:hover {
border-right-color: #8dcffc; }
.reveal .controls .navigate-right.enabled:hover {
border-left-color: #8dcffc; }
.reveal .controls .navigate-up.enabled:hover {
border-bottom-color: #8dcffc; }
.reveal .controls .navigate-down.enabled:hover {
border-top-color: #8dcffc; }
/*********************************************
* PROGRESS BAR
*********************************************/
.reveal .progress {
background: rgba(0, 0, 0, 0.2); }
.reveal .progress span {
background: #42affa;
-webkit-transition: width 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985);
-moz-transition: width 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985);
transition: width 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985); }

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -0,0 +1,39 @@
<!DOCTYPE html>
<html>
<head>
<title>Display Window</title>
<link href="reveal.css" rel="stylesheet">
<style type="text/css">
body {
background: transparent !important;
color: #fff !important;
}
sup {
vertical-align: super !important;
font-size: smaller !important;
}
.reveal .slides > section,
.reveal .slides > section > section {
padding: 0;
}
.reveal > .backgrounds > .present {
visibility: hidden !important;
}
#global-background {
display: block;
visibility: visible;
z-index: -1;
}
</style>
<script type="text/javascript" src="qrc:///qtwebchannel/qwebchannel.js"></script>
<script type="text/javascript" src="reveal.js"></script>
<script type="text/javascript" src="display.js"></script>
</head>
<body>
<div class="reveal">
<div id="global-background" class="slide-background present" data-loaded="true"></div>
<div class="slides"></div>
<div class="footer"></div>
</div>
</body>
</html>

View File

@ -0,0 +1,789 @@
/**
* display.js is the main Javascript file that is used to drive the display.
*/
/**
* Background type enumeration
*/
var BackgroundType = {
Transparent: "transparent",
Solid: "solid",
Gradient: "gradient",
Video: "video",
Image: "image"
};
/**
* Gradient type enumeration
*/
var GradientType = {
Horizontal: "horizontal",
LeftTop: "leftTop",
LeftBottom: "leftBottom",
Vertical: "vertical",
Circular: "circular"
};
/**
* Horizontal alignment enumeration
*/
var HorizontalAlign = {
Left: "left",
Right: "right",
Center: "center",
Justify: "justify"
};
/**
* Vertical alignment enumeration
*/
var VerticalAlign = {
Top: "top",
Middle: "middle",
Bottom: "bottom"
};
/**
* Audio state enumeration
*/
var AudioState = {
Playing: "playing",
Paused: "paused",
Stopped: "stopped"
};
/**
* Return an array of elements based on the selector query
* @param {string} selector - The selector to find elements
* @returns {array} An array of matching elements
*/
function $(selector) {
return Array.from(document.querySelectorAll(selector));
}
/**
* Build linear gradient CSS
* @private
* @param {string} startDir - Starting direction
* @param {string} endDir - Ending direction
* @param {string} startColor - The starting color
* @param {string} endColor - The ending color
* @returns {string} A string of the gradient CSS
*/
function _buildLinearGradient(startDir, endDir, startColor, endColor) {
return "-webkit-gradient(linear, " + startDir + ", " + endDir + ", from(" + startColor + "), to(" + endColor + ")) fixed";
}
/**
* Build radial gradient CSS
* @private
* @param {string} width - Width of the gradient
* @param {string} startColor - The starting color
* @param {string} endColor - The ending color
* @returns {string} A string of the gradient CSS
*/
function _buildRadialGradient(width, startColor, endColor) {
return "-webkit-gradient(radial, " + width + " 50%, 100, " + width + " 50%, " + width + ", from(" + startColor + "), to(" + endColor + ")) fixed";
}
/**
* Get a style value from an element (computed or manual)
* @private
* @param {Object} element - The element whose style we want
* @param {string} style - The name of the style we want
* @returns {(Number|string)} The style value (type depends on the style)
*/
function _getStyle(element, style) {
return document.defaultView.getComputedStyle(element).getPropertyValue(style);
}
/**
* Convert newlines to <br> tags
* @private
* @param {string} text - The text to parse
* @returns {string} The text now with <br> tags
*/
function _nl2br(text) {
return text.replace("\r\n", "\n").replace("\n", "<br>");
}
/**
* Prepare text by creating paragraphs and calling _nl2br to convert newlines to <br> tags
* @private
* @param {string} text - The text to parse
* @returns {string} The text now with <p> and <br> tags
*/
function _prepareText(text) {
return "<p>" + _nl2br(text) + "</p>";
}
/**
* The paths we get are JSON versions of Python Path objects, so let's just fix that.
* @private
* @param {object} path - The Path object
* @returns {string} The actual file path
*/
function _pathToString(path) {
var filename = path.__Path__.join("/").replace("//", "/");
if (!filename.startsWith("/")) {
filename = "/" + filename;
}
return filename;
}
/**
* An audio player with a play list
*/
var AudioPlayer = function (audioElement) {
this._audioElement = null;
this._eventListeners = {};
this._playlist = [];
this._currentTrack = null;
this._canRepeat = false;
this._state = AudioState.Stopped;
this.createAudioElement();
};
/**
* Call all listeners associated with this event
* @private
* @param {object} event - The event that was emitted
*/
AudioPlayer.prototype._callListener = function (event) {
if (this._eventListeners.hasOwnProperty(event.type)) {
this._eventListeners[event.type].forEach(function (listener) {
listener(event);
});
}
else {
console.warn("Received unknown event \"" + event.type + "\", doing nothing.");
}
};
/**
* Create the <audio> element that is used to play the audio
*/
AudioPlayer.prototype.createAudioElement = function () {
this._audioElement = document.createElement("audio");
this._audioElement.addEventListener("ended", this.onEnded);
this._audioElement.addEventListener("ended", this._callListener);
this._audioElement.addEventListener("timeupdate", this._callListener);
this._audioElement.addEventListener("volumechange", this._callListener);
this._audioElement.addEventListener("durationchange", this._callListener);
this._audioElement.addEventListener("loadeddata", this._callListener);
document.addEventListener("complete", function(event) {
document.body.appendChild(this._audioElement);
});
};
AudioPlayer.prototype.addEventListener = function (eventType, listener) {
this._eventListeners[eventType] = this._eventListeners[eventType] || [];
this._eventListeners[eventType].push(listener);
};
AudioPlayer.prototype.onEnded = function (event) {
this.nextTrack();
};
AudioPlayer.prototype.setCanRepeat = function (canRepeat) {
this._canRepeat = canRepeat;
};
AudioPlayer.prototype.clearTracks = function () {
this._playlist = [];
};
AudioPlayer.prototype.addTrack = function (track) {
this._playlist.push(track);
};
AudioPlayer.prototype.nextTrack = function () {
if (!!this._currentTrack) {
var trackIndex = this._playlist.indexOf(this._currentTrack);
if ((trackIndex + 1 >= this._playlist.length) && this._canRepeat) {
this.play(this._playlist[0]);
}
else if (trackIndex + 1 < this._playlist.length) {
this.play(this._playlist[trackIndex + 1]);
}
else {
this.stop();
}
}
else if (this._playlist.length > 0) {
this.play(this._playlist[0]);
}
else {
console.warn("No tracks in playlist, doing nothing.");
}
};
AudioPlayer.prototype.play = function () {
if (arguments.length > 0) {
this._currentTrack = arguments[0];
this._audioElement.src = this._currentTrack;
this._audioElement.play();
this._state = AudioState.Playing;
}
else if (this._state == AudioState.Paused) {
this._audioElement.play();
this._state = AudioState.Playing;
}
else {
console.warn("No track currently paused and no track specified, doing nothing.");
}
};
/**
* Pause
*/
AudioPlayer.prototype.pause = function () {
this._audioElement.pause();
this._state = AudioState.Paused;
};
/**
* Stop playing
*/
AudioPlayer.prototype.stop = function () {
this._audioElement.pause();
this._audioElement.src = "";
this._state = AudioState.Stopped;
};
/**
* The Display object is what we use from OpenLP
*/
var Display = {
_slides: {},
_revealConfig: {
margin: 0.0,
minScale: 1.0,
maxScale: 1.0,
controls: false,
progress: false,
history: false,
overview: false,
center: false,
help: false,
transition: "none",
backgroundTransition: "none",
viewDistance: 9999,
width: "100%",
height: "100%"
},
/**
* Start up reveal and do any other initialisation
*/
init: function () {
Reveal.initialize(this._revealConfig);
},
/**
* Reinitialise Reveal
*/
reinit: function () {
Reveal.reinitialize();
},
/**
* Set the transition type
* @param {string} transitionType - Can be one of "none", "fade", "slide", "convex", "concave", "zoom"
*/
setTransition: function (transitionType) {
Reveal.configure({"transition": transitionType});
},
/**
* Clear the current list of slides
*/
clearSlides: function () {
$(".slides")[0].innerHTML = "";
this._slides = {};
},
/**
* 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;
},
/**
* Generate the OpenLP startup splashscreen
* @param {string} bg_color - The background color
* @param {string} image - Path to the splash image
*/
setStartupSplashScreen: function(bg_color, image) {
Display.clearSlides();
var globalBackground = $("#global-background")[0];
globalBackground.style.cssText = "";
globalBackground.style.setProperty("background", bg_color);
var slidesDiv = $(".slides")[0];
var section = document.createElement("section");
section.setAttribute("id", 0);
section.setAttribute("data-background", bg_color);
section.setAttribute("style", "height: 100%; width: 100%; position: relative;");
var img = document.createElement('img');
img.src = image;
img.setAttribute("style", "position: absolute; top: 0; bottom: 0; left: 0; right: 0; margin: auto;");
section.appendChild(img);
slidesDiv.appendChild(section);
Display._slides['0'] = 0;
Display.reinit();
},
/**
* Set fullscreen image from path
* @param {string} bg_color - The background color
* @param {string} image - Path to the image
*/
setFullscreenImage: function(bg_color, image) {
Display.clearSlides();
var globalBackground = $("#global-background")[0];
globalBackground.style.cssText = "";
globalBackground.style.setProperty("background", bg_color);
var slidesDiv = $(".slides")[0];
var section = document.createElement("section");
section.setAttribute("id", 0);
section.setAttribute("data-background", bg_color);
section.setAttribute("style", "height: 100%; width: 100%;");
var img = document.createElement('img');
img.src = image;
img.setAttribute("style", "height: 100%; width: 100%");
section.appendChild(img);
slidesDiv.appendChild(section);
Display._slides['0'] = 0;
Display.reinit();
},
/**
* Set fullscreen image from base64 data
* @param {string} bg_color - The background color
* @param {string} image - Path to the image
*/
setFullscreenImageFromData: function(bg_color, image_data) {
Display.clearSlides();
var globalBackground = $("#global-background")[0];
globalBackground.style.cssText = "";
globalBackground.style.setProperty("background", bg_color);
var slidesDiv = $(".slides")[0];
var section = document.createElement("section");
section.setAttribute("id", 0);
section.setAttribute("data-background", bg_color);
section.setAttribute("style", "height: 100%; width: 100%;");
var img = document.createElement('img');
img.src = 'data:image/png;base64,' + image_data;
img.setAttribute("style", "height: 100%; width: 100%");
section.appendChild(img);
slidesDiv.appendChild(section);
Display._slides['0'] = 0;
Display.reinit();
},
/**
* Display an alert
* @param {string} text - The alert text
* @param {int} location - The location of the text (top, middle or bottom)
*/
alert: function (text, location) {
console.debug(" alert text: " + text, ", location: " + location);
/*
* The implementation should show an alert.
* It should be able to handle receiving a new alert before a previous one is "finished", basically queueing it.
*/
return;
},
/**
* Add a slides. If the slide exists but the HTML is different, update the slide.
* @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) {
var html = _prepareText(text);
if (this._slides.hasOwnProperty(verse)) {
var slide = $("#" + verse)[0];
if (slide.innerHTML != html) {
slide.innerHTML = html;
}
}
else {
var slidesDiv = $(".slides")[0];
var slide = document.createElement("section");
slide.setAttribute("id", verse);
slide.innerHTML = html;
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 ((arguments.length > 3) && (arguments[3] === true)) {
this.reinit();
}
else if (arguments.length == 3) {
this.reinit();
}
},
/**
* Set text slides.
* @param {Object[]} slides - A list of slides to add as JS objects: {"verse": "v1", "text": "line 1\nline2"}
*/
setTextSlides: function (slides) {
Display.clearSlides();
slides.forEach(function (slide) {
Display.addTextSlide(slide.verse, slide.text, slide.footer, false);
});
Display.reinit();
Display.goToSlide(0);
},
/**
* Set image slides
* @param {Object[]} slides - A list of images to add as JS objects [{"path": "url/to/file"}]
*/
setImageSlides: function (slides) {
Display.clearSlides();
var slidesDiv = $(".slides")[0];
slides.forEach(function (slide, index) {
var section = document.createElement("section");
section.setAttribute("id", index);
section.setAttribute("data-background", "#000");
var img = document.createElement('img');
img.src = slide["path"];
img.setAttribute("style", "height: 100%; width: 100%;");
section.appendChild(img);
slidesDiv.appendChild(section);
Display._slides[index.toString()] = index;
});
Display.reinit();
},
/**
* Set a video
* @param {Object} video - The video to show as a JS object: {"path": "url/to/file"}
*/
setVideo: function (video) {
this.clearSlides();
var section = document.createElement("section");
section.setAttribute("data-background", "#000");
var videoElement = document.createElement("video");
videoElement.src = video["path"];
videoElement.preload = "auto";
videoElement.setAttribute("id", "video");
videoElement.setAttribute("style", "height: 100%; width: 100%;");
videoElement.autoplay = false;
// All the update methods below are Python functions, hence not camelCase
videoElement.addEventListener("durationchange", function (event) {
mediaWatcher.update_duration(event.target.duration);
});
videoElement.addEventListener("timeupdate", function (event) {
mediaWatcher.update_progress(event.target.currentTime);
});
videoElement.addEventListener("volumeupdate", function (event) {
mediaWatcher.update_volume(event.target.volume);
});
videoElement.addEventListener("ratechange", function (event) {
mediaWatcher.update_playback_rate(event.target.playbackRate);
});
videoElement.addEventListener("ended", function (event) {
mediaWatcher.has_ended(event.target.ended);
});
videoElement.addEventListener("muted", function (event) {
mediaWatcher.has_muted(event.target.muted);
});
section.appendChild(videoElement);
$(".slides")[0].appendChild(section);
this.reinit();
},
/**
* Play a video
*/
playVideo: function () {
if ($("#video").length == 1) {
$("#video")[0].play();
}
},
/**
* Pause a video
*/
pauseVideo: function () {
if ($("#video").length == 1) {
$("#video")[0].pause();
}
},
/**
* Stop a video
*/
stopVideo: function () {
if ($("#video").length == 1) {
$("#video")[0].pause();
$("#video")[0].currentTime = 0.0;
}
},
/**
* Go to a particular time in a video
* @param seconds The position in seconds to seek to
*/
seekVideo: function (seconds) {
if ($("#video").length == 1) {
$("#video")[0].currentTime = seconds;
}
},
/**
* Set the playback rate of a video
* @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;
}
},
/**
* Set the volume
* @param level The volume level from 0 to 100.
*/
setVideoVolume: function (level) {
if ($("#video").length == 1) {
$("#video")[0].volume = level / 100.0;
}
},
/**
* Mute the volume
*/
toggleVideoMute: function () {
if ($("#video").length == 1) {
$("#video")[0].muted = !$("#video")[0].muted;
}
},
/**
* Clear the background audio playlist
*/
clearPlaylist: function () {
if ($("#background-audio").length == 1) {
var audio = $("#background-audio")[0];
/* audio.playList */
}
},
/**
* Add background audio
* @param files The list of files as objects in an array
*/
addBackgroundAudio: function (files) {
},
/**
* Go to a slide.
* @param slide The slide number or name, e.g. "v1", 0
*/
goToSlide: function (slide) {
if (this._slides.hasOwnProperty(slide)) {
Reveal.slide(this._slides[slide]);
}
else {
Reveal.slide(slide);
}
},
/**
* Go to the next slide in the list
*/
next: Reveal.next,
/**
* Go to the previous slide in the list
*/
prev: Reveal.prev,
/**
* Blank the screen
*/
blankToBlack: function () {
if (!Reveal.isPaused()) {
Reveal.togglePause();
}
// var slidesDiv = $(".slides")[0];
},
/**
* Blank to theme
*/
blankToTheme: function () {
var slidesDiv = $(".slides")[0];
slidesDiv.style.visibility = "hidden";
var footerDiv = $(".footer")[0];
footerDiv.style.visibility = "hidden";
if (Reveal.isPaused()) {
Reveal.togglePause();
}
},
/**
* Show the screen
*/
show: function () {
var slidesDiv = $(".slides")[0];
slidesDiv.style.visibility = "visible";
var footerDiv = $(".footer")[0];
footerDiv.style.visibility = "visible";
if (Reveal.isPaused()) {
Reveal.togglePause();
}
},
/**
* Figure out how many lines can fit on a slide given the font size
* @param fontSize The font size in pts
*/
calculateLineCount: function (fontSize) {
var p = $(".slides > section > p");
if (p.length == 0) {
this.addSlide("v1", "Arky arky");
p = $(".slides > section > p");
}
p = p[0];
p.style.fontSize = "" + fontSize + "pt";
var d = $(".slides")[0];
var lh = parseFloat(_getStyle(p, "line-height"));
var dh = parseFloat(_getStyle(d, "height"));
return Math.floor(dh / lh);
},
setTheme: function (theme) {
this._theme = theme;
var slidesDiv = $(".slides")
// Set the background
var globalBackground = $("#global-background")[0];
var backgroundStyle = {};
var backgroundHtml = "";
switch (theme.background_type) {
case BackgroundType.Transparent:
backgroundStyle["background"] = "transparent";
break;
case BackgroundType.Solid:
backgroundStyle["background"] = theme.background_color;
break;
case BackgroundType.Gradient:
switch (theme.background_direction) {
case GradientType.Horizontal:
backgroundStyle["background"] = _buildLinearGradient("left top", "left bottom",
theme.background_start_color,
theme.background_end_color);
break;
case GradientType.Vertical:
backgroundStyle["background"] = _buildLinearGradient("left top", "right top",
theme.background_start_color,
theme.background_end_color);
break;
case GradientType.LeftTop:
backgroundStyle["background"] = _buildLinearGradient("left top", "right bottom",
theme.background_start_color,
theme.background_end_color);
break;
case GradientType.LeftBottom:
backgroundStyle["background"] = _buildLinearGradient("left bottom", "right top",
theme.background_start_color,
theme.background_end_color);
break;
case GradientType.Circular:
backgroundStyle["background"] = _buildRadialGradient(window.innerWidth / 2, theme.background_start_color,
theme.background_end_color);
break;
default:
backgroundStyle["background"] = "#000";
}
break;
case BackgroundType.Image:
background_filename = _pathToString(theme.background_filename);
backgroundStyle["background-image"] = "url('file://" + background_filename + "')";
break;
case BackgroundType.Video:
background_filename = _pathToString(theme.background_filename);
backgroundStyle["background-color"] = theme.background_border_color;
backgroundHtml = "<video loop autoplay muted><source src='file://" + background_filename + "'></video>";
break;
default:
backgroundStyle["background"] = "#000";
}
globalBackground.style.cssText = "";
for (var key in backgroundStyle) {
if (backgroundStyle.hasOwnProperty(key)) {
globalBackground.style.setProperty(key, backgroundStyle[key]);
}
}
if (!!backgroundHtml) {
globalBackground.innerHTML = backgroundHtml;
}
// set up the main area
mainStyle = {
"word-wrap": "break-word",
/*"margin": "0",
"padding": "0"*/
};
if (!!theme.font_main_outline) {
mainStyle["-webkit-text-stroke"] = "" + theme.font_main_outline_size + "pt " +
theme.font_main_outline_color;
mainStyle["-webkit-text-fill-color"] = theme.font_main_color;
}
mainStyle["font-family"] = theme.font_main_name;
mainStyle["font-size"] = "" + theme.font_main_size + "pt";
mainStyle["font-style"] = !!theme.font_main_italics ? "italic" : "";
mainStyle["font-weight"] = !!theme.font_main_bold ? "bold" : "";
mainStyle["color"] = theme.font_main_color;
mainStyle["line-height"] = "" + (100 + theme.font_main_line_adjustment) + "%";
mainStyle["text-align"] = theme.display_horizontal_align;
if (theme.display_horizontal_align != HorizontalAlign.Justify) {
mainStyle["white-space"] = "pre-wrap";
}
mainStyle["vertical-align"] = theme.display_vertical_align;
if (theme.hasOwnProperty('font_main_shadow_size')) {
mainStyle["text-shadow"] = theme.font_main_shadow_color + " " + theme.font_main_shadow_size + "px " +
theme.font_main_shadow_size + "px";
}
mainStyle["padding-bottom"] = theme.display_vertical_align == VerticalAlign.Bottom ? "0.5em" : "0";
mainStyle["padding-left"] = !!theme.font_main_outline ? "" + (theme.font_main_outline_size * 2) + "pt" : "0";
// These need to be fixed, in the Python they use a width passed in as a parameter
mainStyle["position"] = "absolute";
mainStyle["width"] = "" + (window.innerWidth - (theme.font_main_outline_size * 4)) + "px";
mainStyle["height"] = "" + (window.innerHeight - (theme.font_main_outline_size * 4)) + "px";
mainStyle["left"] = "" + theme.font_main_x + "px";
mainStyle["top"] = "" + theme.font_main_y + "px";
var slidesDiv = $(".slides")[0];
slidesDiv.style.cssText = "";
for (var key in mainStyle) {
if (mainStyle.hasOwnProperty(key)) {
slidesDiv.style.setProperty(key, mainStyle[key]);
}
}
// Set up the footer
footerStyle = {
"text-align": "left"
};
footerStyle["position"] = "absolute";
footerStyle["left"] = "" + theme.font_footer_x + "px";
footerStyle["top"] = "" + theme.font_footer_y + "px";
footerStyle["bottom"] = "" + (window.innerHeight - theme.font_footer_y - theme.font_footer_height) + "px";
footerStyle["width"] = "" + theme.font_footer_width + "px";
footerStyle["font-family"] = theme.font_footer_name;
footerStyle["font-size"] = "" + theme.font_footer_size + "pt";
footerStyle["color"] = theme.font_footer_color;
footerStyle["white-space"] = theme.font_footer_wrap ? "normal" : "nowrap";
var footer = $(".footer")[0];
footer.style.cssText = "";
for (var key in footerStyle) {
if (footerStyle.hasOwnProperty(key)) {
footer.style.setProperty(key, footerStyle[key]);
}
}
},
/**
* Return the video types supported by the video tag
*/
getVideoTypes: function () {
var videoElement = document.createElement('video');
var videoTypes = [];
if (videoElement.canPlayType('video/mp4; codecs="mp4v.20.8"') == "probably" ||
videoElement.canPlayType('video/mp4; codecs="avc1.42E01E"') == "pobably" ||
videoElement.canPlayType('video/mp4; codecs="avc1.42E01E, mp4a.40.2"') == "probably") {
videoTypes.push(['video/mp4', '*.mp4']);
}
if (videoElement.canPlayType('video/ogg; codecs="theora"') == "probably") {
videoTypes.push(['video/ogg', '*.ogv']);
}
if (videoElement.canPlayType('video/webm; codecs="vp8, vorbis"') == "probably") {
videoTypes.push(['video/webm', '*.webm']);
}
return videoTypes;
},
/**
* Sets the scale of the page - used to make preview widgets scale
*/
setScale: function(scale) {
document.body.style.zoom = scale+"%";
}
};
new QWebChannel(qt.webChannelTransport, function (channel) {
window.mediaWatcher = channel.objects.mediaWatcher;
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,237 @@
/**
* textFit v2.3.1
* Previously known as jQuery.textFit
* 11/2014 by STRML (strml.github.com)
* MIT License
*
* To use: textFit(document.getElementById('target-div'), options);
*
* Will make the *text* content inside a container scale to fit the container
* The container is required to have a set width and height
* Uses binary search to fit text with minimal layout calls.
* Version 2.0 does not use jQuery.
*/
/*global define:true, document:true, window:true, HTMLElement:true*/
(function(root, factory) {
"use strict";
// UMD shim
if (typeof define === "function" && define.amd) {
// AMD
define([], factory);
} else if (typeof exports === "object") {
// Node/CommonJS
module.exports = factory();
} else {
// Browser
root.textFit = factory();
}
}(typeof global === "object" ? global : this, function () {
"use strict";
var defaultSettings = {
alignVert: false, // if true, textFit will align vertically using css tables
alignHoriz: false, // if true, textFit will set text-align: center
multiLine: false, // if true, textFit will not set white-space: no-wrap
detectMultiLine: true, // disable to turn off automatic multi-line sensing
minFontSize: 6,
maxFontSize: 80,
reProcess: true, // if true, textFit will re-process already-fit nodes. Set to 'false' for better performance
widthOnly: false, // if true, textFit will fit text to element width, regardless of text height
alignVertWithFlexbox: false, // if true, textFit will use flexbox for vertical alignment
};
return function textFit(els, options) {
if (!options) options = {};
// Extend options.
var settings = {};
for(var key in defaultSettings){
if(options.hasOwnProperty(key)){
settings[key] = options[key];
} else {
settings[key] = defaultSettings[key];
}
}
// Convert jQuery objects into arrays
if (typeof els.toArray === "function") {
els = els.toArray();
}
// Support passing a single el
var elType = Object.prototype.toString.call(els);
if (elType !== '[object Array]' && elType !== '[object NodeList]' &&
elType !== '[object HTMLCollection]'){
els = [els];
}
// Process each el we've passed.
for(var i = 0; i < els.length; i++){
processItem(els[i], settings);
}
};
/**
* The meat. Given an el, make the text inside it fit its parent.
* @param {DOMElement} el Child el.
* @param {Object} settings Options for fit.
*/
function processItem(el, settings){
if (!isElement(el) || (!settings.reProcess && el.getAttribute('textFitted'))) {
return false;
}
// Set textFitted attribute so we know this was processed.
if(!settings.reProcess){
el.setAttribute('textFitted', 1);
}
var innerSpan, originalHeight, originalHTML, originalWidth;
var low, mid, high;
// Get element data.
originalHTML = el.innerHTML;
originalWidth = innerWidth(el);
originalHeight = innerHeight(el);
// Don't process if we can't find box dimensions
if (!originalWidth || (!settings.widthOnly && !originalHeight)) {
if(!settings.widthOnly)
throw new Error('Set a static height and width on the target element ' + el.outerHTML +
' before using textFit!');
else
throw new Error('Set a static width on the target element ' + el.outerHTML +
' before using textFit!');
}
// Add textFitted span inside this container.
if (originalHTML.indexOf('textFitted') === -1) {
innerSpan = document.createElement('span');
innerSpan.className = 'textFitted';
// Inline block ensure it takes on the size of its contents, even if they are enclosed
// in other tags like <p>
innerSpan.style['display'] = 'inline-block';
innerSpan.innerHTML = originalHTML;
el.innerHTML = '';
el.appendChild(innerSpan);
} else {
// Reprocessing.
innerSpan = el.querySelector('span.textFitted');
// Remove vertical align if we're reprocessing.
if (hasClass(innerSpan, 'textFitAlignVert')){
innerSpan.className = innerSpan.className.replace('textFitAlignVert', '');
innerSpan.style['height'] = '';
el.className.replace('textFitAlignVertFlex', '');
}
}
// Prepare & set alignment
if (settings.alignHoriz) {
el.style['text-align'] = 'center';
innerSpan.style['text-align'] = 'center';
}
// Check if this string is multiple lines
// Not guaranteed to always work if you use wonky line-heights
var multiLine = settings.multiLine;
if (settings.detectMultiLine && !multiLine &&
innerSpan.scrollHeight >= parseInt(window.getComputedStyle(innerSpan)['font-size'], 10) * 2){
multiLine = true;
}
// If we're not treating this as a multiline string, don't let it wrap.
if (!multiLine) {
el.style['white-space'] = 'nowrap';
}
low = settings.minFontSize + 1;
high = settings.maxFontSize + 1;
// Binary search for best fit
while (low <= high) {
mid = parseInt((low + high) / 2, 10);
innerSpan.style.fontSize = mid + 'px';
if(innerSpan.scrollWidth <= originalWidth && (settings.widthOnly || innerSpan.scrollHeight <= originalHeight)){
low = mid + 1;
} else {
high = mid - 1;
}
}
// Sub 1 at the very end, this is closer to what we wanted.
innerSpan.style.fontSize = (mid - 1) + 'px';
// Our height is finalized. If we are aligning vertically, set that up.
if (settings.alignVert) {
addStyleSheet();
var height = innerSpan.scrollHeight;
if (window.getComputedStyle(el)['position'] === "static"){
el.style['position'] = 'relative';
}
if (!hasClass(innerSpan, "textFitAlignVert")){
innerSpan.className = innerSpan.className + " textFitAlignVert";
}
innerSpan.style['height'] = height + "px";
if (settings.alignVertWithFlexbox && !hasClass(el, "textFitAlignVertFlex")) {
el.className = el.className + " textFitAlignVertFlex";
}
}
}
// Calculate height without padding.
function innerHeight(el){
var style = window.getComputedStyle(el, null);
return el.clientHeight -
parseInt(style.getPropertyValue('padding-top'), 10) -
parseInt(style.getPropertyValue('padding-bottom'), 10);
}
// Calculate width without padding.
function innerWidth(el){
var style = window.getComputedStyle(el, null);
return el.clientWidth -
parseInt(style.getPropertyValue('padding-left'), 10) -
parseInt(style.getPropertyValue('padding-right'), 10);
}
//Returns true if it is a DOM element
function isElement(o){
return (
typeof HTMLElement === "object" ? o instanceof HTMLElement : //DOM2
o && typeof o === "object" && o !== null && o.nodeType === 1 && typeof o.nodeName==="string"
);
}
function hasClass(element, cls) {
return (' ' + element.className + ' ').indexOf(' ' + cls + ' ') > -1;
}
// Better than a stylesheet dependency
function addStyleSheet() {
if (document.getElementById("textFitStyleSheet")) return;
var style = [
".textFitAlignVert{",
"position: absolute;",
"top: 0; right: 0; bottom: 0; left: 0;",
"margin: auto;",
"display: flex;",
"justify-content: center;",
"flex-direction: column;",
"}",
".textFitAlignVertFlex{",
"display: flex;",
"}",
".textFitAlignVertFlex .textFitAlignVert{",
"position: static;",
"}",].join("");
var css = document.createElement("style");
css.type = "text/css";
css.id = "textFitStyleSheet";
css.innerHTML = style;
document.body.appendChild(css);
}
}));

View File

@ -0,0 +1,746 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
###############################################################################
# OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- #
# Copyright (c) 2008-2018 OpenLP Developers #
# --------------------------------------------------------------------------- #
# This program is free software; you can redistribute it and/or modify it #
# under the terms of the GNU General Public License as published by the Free #
# Software Foundation; version 2 of the License. #
# #
# This program is distributed in the hope that it will be useful, but WITHOUT #
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
# more details. #
# #
# You should have received a copy of the GNU General Public License along #
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
"""
The :mod:`~openlp.display.render` module contains functions for rendering.
"""
import html
import logging
import math
import os
import re
import time
from PyQt5 import QtWidgets, QtGui
from openlp.core.common import ThemeLevel
from openlp.core.common.mixins import LogMixin, RegistryProperties
from openlp.core.common.registry import Registry, RegistryBase
from openlp.core.display.screens import ScreenList
from openlp.core.display.window import DisplayWindow
from openlp.core.lib import ItemCapabilities
from openlp.core.lib.formattingtags import FormattingTags
log = logging.getLogger(__name__)
SLIM_CHARS = 'fiíIÍjlĺľrtť.,;/ ()|"\'!:\\'
CHORD_LINE_MATCH = re.compile(r'\[(.*?)\]([\u0080-\uFFFF,\w]*)' # noqa
'([\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}'
WHITESPACE_TEMPLATE = '<span class="ws">{whitespaces}</span>'
VERSE = 'The Lord said to {r}Noah{/r}: \n' \
'There\'s gonna be a {su}floody{/su}, {sb}floody{/sb}\n' \
'The Lord said to {g}Noah{/g}:\n' \
'There\'s gonna be a {st}floody{/st}, {it}floody{/it}\n' \
'Get those children out of the muddy, muddy \n' \
'{r}C{/r}{b}h{/b}{bl}i{/bl}{y}l{/y}{g}d{/g}{pk}' \
'r{/pk}{o}e{/o}{pp}n{/pp} of the Lord\n'
VERSE_FOR_LINE_COUNT = '\n'.join(map(str, range(100)))
TITLE = 'Arky Arky (Unknown)'
FOOTER = ['Public Domain', 'CCLI 123456']
def remove_tags(text, can_remove_chords=False):
"""
Remove Tags from text for display
:param text: Text to be cleaned
:param can_remove_chords: Can we remove the chords too?
"""
text = text.replace('<br>', '\n')
text = text.replace('{br}', '\n')
text = text.replace('&nbsp;', ' ')
for tag in FormattingTags.get_html_tags():
text = text.replace(tag['start tag'], '')
text = text.replace(tag['end tag'], '')
# Remove ChordPro tags
if can_remove_chords:
text = re.sub(r'\[.+?\]', r'', text)
return text
def has_valid_tags(text):
"""
The :func:`~openlp.core.display.render.has_valid_tags` function validates the tags within ``text``.
:param str text: The string with formatting tags in it.
:returns bool: Returns True if tags are valid, False if there are parsing problems.
"""
return True
def render_chords_in_line(match):
"""
Render the chords in the line and align them using whitespaces.
NOTE: There is equivalent javascript code in chords.js, in the updateSlide function. Make sure to update both!
:param str match: The line which contains chords
:returns str: The line with rendered html-chords
"""
whitespaces = ''
chord_length = 0
tail_length = 0
# The match could be "[G]sweet the " from a line like "A[D]mazing [D7]grace! How [G]sweet the [D]sound!"
# The actual chord, would be "G" in match "[G]sweet the "
chord = match.group(1)
# The tailing word of the chord, would be "sweet" in match "[G]sweet the "
tail = match.group(2)
# The remainder of the line, until line end or next chord. Would be " the " in match "[G]sweet the "
remainder = match.group(3)
# Line end if found, else None
end = match.group(4)
# Based on char width calculate width of chord
for chord_char in chord:
if chord_char not in SLIM_CHARS:
chord_length += 2
else:
chord_length += 1
# Based on char width calculate width of tail
for tail_char in tail:
if tail_char not in SLIM_CHARS:
tail_length += 2
else:
tail_length += 1
# Based on char width calculate width of remainder
for remainder_char in remainder:
if remainder_char not in SLIM_CHARS:
tail_length += 2
else:
tail_length += 1
# If the chord is wider than the tail+remainder and the line goes on, some padding is needed
if chord_length >= tail_length and end is None:
# Decide if the padding should be "_" for drawing out words or spaces
if tail:
if not remainder:
for c in range(math.ceil((chord_length - tail_length) / 2) + 2):
whitespaces += '_'
else:
for c in range(chord_length - tail_length + 1):
whitespaces += '&nbsp;'
else:
if not remainder:
for c in range(math.floor((chord_length - tail_length) / 2)):
whitespaces += '_'
else:
for c in range(chord_length - tail_length + 1):
whitespaces += '&nbsp;'
else:
if not tail and remainder and remainder[0] == ' ':
for c in range(chord_length):
whitespaces += '&nbsp;'
if whitespaces:
if '_' in whitespaces:
ws_length = len(whitespaces)
if ws_length == 1:
whitespaces = '&ndash;'
else:
wsl_mod = ws_length // 2
ws_right = ws_left = ' ' * wsl_mod
whitespaces = ws_left + '&ndash;' + ws_right
whitespaces = WHITESPACE_TEMPLATE.format(whitespaces=whitespaces)
return CHORD_LINE_TEMPLATE.format(chord=html.escape(chord), tail=html.escape(tail), whitespace=whitespaces,
remainder=html.escape(remainder))
def render_chords(text):
"""
Render ChordPro tags
:param str text: The text containing the chords
:returns str: The text containing the rendered chords
"""
text_lines = text.split('{br}')
rendered_lines = []
chords_on_prev_line = False
for line in text_lines:
# If a ChordPro is detected in the line, replace it with a html-span tag and wrap the line in a span tag.
if '[' in line and ']' in line:
if chords_on_prev_line:
chord_template = CHORD_TEMPLATE
else:
chord_template = FIRST_CHORD_TEMPLATE
chords_on_prev_line = True
# Matches a chord, a tail, a remainder and a line end. See expand_and_align_chords_in_line() for more info.
new_line = chord_template.format(chord=CHORD_LINE_MATCH.sub(render_chords_in_line, line))
rendered_lines.append(new_line)
else:
chords_on_prev_line = False
rendered_lines.append(html.escape(line))
return '{br}'.join(rendered_lines)
def compare_chord_lyric_width(chord, lyric):
"""
Compare the width of chord and lyrics. If chord width is greater than the lyric width the diff is returned.
:param chord:
:param lyric:
:return:
"""
chord_length = 0
if chord == '&nbsp;':
return 0
chord = re.sub(r'\{.*?\}', r'', chord)
lyric = re.sub(r'\{.*?\}', r'', lyric)
for chord_char in chord:
if chord_char not in SLIM_CHARS:
chord_length += 2
else:
chord_length += 1
lyriclen = 0
for lyric_char in lyric:
if lyric_char not in SLIM_CHARS:
lyriclen += 2
else:
lyriclen += 1
if chord_length > lyriclen:
return chord_length - lyriclen
else:
return 0
def find_formatting_tags(text, active_formatting_tags):
"""
Look for formatting tags in lyrics and adds/removes them to/from the given list. Returns the update list.
:param text:
:param active_formatting_tags:
:return:
"""
if not re.search(r'\{.*?\}', text):
return active_formatting_tags
word_iterator = iter(text)
# Loop through lyrics to find any formatting tags
for char in word_iterator:
if char == '{':
tag = ''
char = next(word_iterator)
start_tag = True
if char == '/':
start_tag = False
char = next(word_iterator)
while char != '}':
tag += char
char = next(word_iterator)
# See if the found tag has an end tag
for formatting_tag in FormattingTags.get_html_tags():
if formatting_tag['start tag'] == '{' + tag + '}':
if formatting_tag['end tag']:
if start_tag:
# prepend the new tag to the list of active formatting tags
active_formatting_tags[:0] = [tag]
else:
# remove the tag from the list
active_formatting_tags.remove(tag)
# Break out of the loop matching the found tag against the tag list.
break
return active_formatting_tags
def render_chords_for_printing(text, line_split):
"""
Render ChordPro tags for printing
:param str text: The text containing the chords to be rendered.
:param str line_split: The character(s) used to split lines
:returns str: The rendered chords
"""
if not re.search(r'\[.*?\]', text):
return text
text_lines = text.split(line_split)
rendered_text_lines = []
for line in text_lines:
# If a ChordPro is detected in the line, build html tables.
new_line = '<table class="line" width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td>'
active_formatting_tags = []
if re.search(r'\[.*?\]', line):
words = line.split(' ')
in_chord = False
for word in words:
chords = []
lyrics = []
new_line += '<table class="segment" cellpadding="0" cellspacing="0" border="0" align="left">'
# If the word contains a chord, we need to handle it.
if re.search(r'\[.*?\]', word):
chord = ''
lyric = ''
# Loop over each character of the word
for char in word:
if char == '[':
in_chord = True
if lyric != '':
if chord == '':
chord = '&nbsp;'
chords.append(chord)
lyrics.append(lyric)
chord = ''
lyric = ''
elif char == ']' and in_chord:
in_chord = False
elif in_chord:
chord += char
else:
lyric += char
if lyric != '' or chord != '':
if chord == '':
chord = '&nbsp;'
if lyric == '':
lyric = '&nbsp;'
chords.append(chord)
lyrics.append(lyric)
new_chord_line = '<tr class="chordrow">'
new_lyric_line = '</tr><tr>'
for i in range(len(lyrics)):
spacer = compare_chord_lyric_width(chords[i], lyrics[i])
# Handle formatting tags
start_formatting_tags = ''
if active_formatting_tags:
start_formatting_tags = '{' + '}{'.join(active_formatting_tags) + '}'
# Update list of active formatting tags
active_formatting_tags = find_formatting_tags(lyrics[i], active_formatting_tags)
end_formatting_tags = ''
if active_formatting_tags:
end_formatting_tags = '{/' + '}{/'.join(active_formatting_tags) + '}'
new_chord_line += '<td class="chord">%s</td>' % chords[i]
# Check if this is the last column, if so skip spacing calc and instead insert a single space
if i + 1 == len(lyrics):
new_lyric_line += '<td class="lyrics">{starttags}{lyrics}&nbsp;{endtags}</td>'.format(
starttags=start_formatting_tags, lyrics=lyrics[i], endtags=end_formatting_tags)
else:
spacing = ''
if spacer > 0:
space = '&nbsp;' * int(math.ceil(spacer / 2))
spacing = '<span class="chordspacing">%s-%s</span>' % (space, space)
new_lyric_line += '<td class="lyrics">{starttags}{lyrics}{spacing}{endtags}</td>'.format(
starttags=start_formatting_tags, lyrics=lyrics[i], spacing=spacing,
endtags=end_formatting_tags)
new_line += new_chord_line + new_lyric_line + '</tr>'
else:
start_formatting_tags = ''
if active_formatting_tags:
start_formatting_tags = '{' + '}{'.join(active_formatting_tags) + '}'
active_formatting_tags = find_formatting_tags(word, active_formatting_tags)
end_formatting_tags = ''
if active_formatting_tags:
end_formatting_tags = '{/' + '}{/'.join(active_formatting_tags) + '}'
new_line += '<tr class="chordrow"><td class="chord">&nbsp;</td></tr><tr><td class="lyrics">' \
'{starttags}{lyrics}&nbsp;{endtags}</td></tr>'.format(
starttags=start_formatting_tags, lyrics=word, endtags=end_formatting_tags)
new_line += '</table>'
else:
new_line += line
new_line += '</td></tr></table>'
rendered_text_lines.append(new_line)
# the {br} tag used to split lines is not inserted again since the style of the line-tables makes them redundant.
return ''.join(rendered_text_lines)
def render_tags(text, can_render_chords=False, is_printing=False):
"""
The :func:`~openlp.core.display.render.render_tags` function takes a stirng with OpenLP-style tags in it
and replaces them with the HTML version.
:param str text: The string with OpenLP-style tags to be rendered.
:param bool can_render_chords: Should the chords be rendererd?
:param bool is_printing: Are we going to print this?
:returns str: The HTML version of the tags is returned as a string.
"""
if can_render_chords:
if is_printing:
text = render_chords_for_printing(text, '{br}')
else:
text = render_chords(text)
for tag in FormattingTags.get_html_tags():
text = text.replace(tag['start tag'], tag['start html'])
text = text.replace(tag['end tag'], tag['end html'])
return text
def words_split(line):
"""
Split the slide up by word so can wrap better
:param line: Line to be split
"""
# this parse we are to be wordy
return re.split(r'\s+', line)
def get_start_tags(raw_text):
"""
Tests the given text for not closed formatting tags and returns a tuple consisting of three unicode strings::
('{st}{r}Text text text{/r}{/st}', '{st}{r}', '<strong><span style="-webkit-text-fill-color:red">')
The first unicode string is the text, with correct closing tags. The second unicode string are OpenLP's opening
formatting tags and the third unicode string the html opening formatting tags.
:param raw_text: The text to test. The text must **not** contain html tags, only OpenLP formatting tags
are allowed::
{st}{r}Text text text
"""
raw_tags = []
html_tags = []
for tag in FormattingTags.get_html_tags():
if tag['start tag'] == '{br}':
continue
if raw_text.count(tag['start tag']) != raw_text.count(tag['end tag']):
raw_tags.append((raw_text.find(tag['start tag']), tag['start tag'], tag['end tag']))
html_tags.append((raw_text.find(tag['start tag']), tag['start html']))
# Sort the lists, so that the tags which were opened first on the first slide (the text we are checking) will be
# opened first on the next slide as well.
raw_tags.sort(key=lambda tag: tag[0])
html_tags.sort(key=lambda tag: tag[0])
# Create a list with closing tags for the raw_text.
end_tags = []
start_tags = []
for tag in raw_tags:
start_tags.append(tag[1])
end_tags.append(tag[2])
end_tags.reverse()
# Remove the indexes.
html_tags = [tag[1] for tag in html_tags]
return raw_text + ''.join(end_tags), ''.join(start_tags), ''.join(html_tags)
class Renderer(RegistryBase, LogMixin, RegistryProperties, DisplayWindow):
"""
A virtual display used for rendering thumbnails and other offscreen tasks
"""
def __init__(self, *args, **kwargs):
"""
Constructor
"""
super().__init__(*args, **kwargs)
self.force_page = False
for screen in ScreenList():
if screen.is_display:
self.setGeometry(screen.display_geometry.x(), screen.display_geometry.y(),
screen.display_geometry.width(), screen.display_geometry.height())
break
# If the display is not show'ed and hidden like this webegine will not render
self.show()
self.hide()
self.theme_height = 0
self.theme_level = ThemeLevel.Global
def set_theme_level(self, theme_level):
"""
Sets the theme level.
:param theme_level: The theme level to be used.
"""
self.theme_level = theme_level
def calculate_line_count(self):
"""
Calculate the number of lines that fits on one slide
"""
return self.run_javascript('Display.calculateLineCount();', is_sync=True)
def clear_slides(self):
"""
Clear slides
"""
return self.run_javascript('Display.clearSlides();')
def generate_preview(self, theme_data, force_page=False):
"""
Generate a preview of a theme.
:param theme_data: The theme to generated a preview for.
:param force_page: Flag to tell message lines per page need to be generated.
:rtype: QtGui.QPixmap
"""
# save value for use in format_slide
self.force_page = force_page
if not self.force_page:
self.set_theme(theme_data)
self.theme_height = theme_data.font_main_height
slides = self.format_slide(render_tags(VERSE), None)
verses = dict()
verses['title'] = TITLE
verses['text'] = slides[0]
verses['verse'] = 'V1'
self.load_verses([verses])
self.force_page = False
return self.save_screenshot()
self.force_page = False
return None
def format_slide(self, text, item):
"""
Calculate how much text can fit on a slide.
:param text: The words to go on the slides.
:param item: The :class:`~openlp.core.lib.serviceitem.ServiceItem` item object.
"""
while not self._is_initialised:
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_data = Registry().get('theme_manager').get_theme_data(theme_name)
self.theme_height = theme_data.font_main_height
# Set theme for preview
self.set_theme(theme_data)
# Add line endings after each line of text used for bibles.
line_end = '<br>'
if item and item.is_capable(ItemCapabilities.NoLineBreaks):
line_end = ' '
# Bibles
if item and item.is_capable(ItemCapabilities.CanWordSplit):
pages = self._paginate_slide_words(text.split('\n'), line_end)
# Songs and Custom
elif item is None or item.is_capable(ItemCapabilities.CanSoftBreak):
pages = []
if '[---]' in text:
# Remove Overflow split if at start of the text
if text.startswith('[---]'):
text = text[5:]
# Remove two or more option slide breaks next to each other (causing infinite loop).
while '\n[---]\n[---]\n' in text:
text = text.replace('\n[---]\n[---]\n', '\n[---]\n')
while ' [---]' in text:
text = text.replace(' [---]', '[---]')
while '[---] ' in text:
text = text.replace('[---] ', '[---]')
count = 0
# only loop 5 times as there will never be more than 5 incorrect logical splits on a single slide.
while True and count < 5:
slides = text.split('\n[---]\n', 2)
# If there are (at least) two occurrences of [---] we use the first two slides (and neglect the last
# for now).
if len(slides) == 3:
html_text = render_tags('\n'.join(slides[:2]))
# We check both slides to determine if the optional split is needed (there is only one optional
# split).
else:
html_text = render_tags('\n'.join(slides))
html_text = html_text.replace('\n', '<br>')
if self._text_fits_on_slide(html_text):
# The first two optional slides fit (as a whole) on one slide. Replace the first occurrence
# of [---].
text = text.replace('\n[---]', '', 1)
else:
# The first optional slide fits, which means we have to render the first optional slide.
text_contains_split = '[---]' in text
if text_contains_split:
try:
text_to_render, text = text.split('\n[---]\n', 1)
except ValueError:
text_to_render = text.split('\n[---]\n')[0]
text = ''
text_to_render, raw_tags, html_tags = get_start_tags(text_to_render)
if text:
text = raw_tags + text
else:
text_to_render = text
text = ''
lines = text_to_render.strip('\n').split('\n')
slides = self._paginate_slide(lines, line_end)
if len(slides) > 1 and text:
# Add all slides apart from the last one the list.
pages.extend(slides[:-1])
if text_contains_split:
text = slides[-1] + '\n[---]\n' + text
else:
text = slides[-1] + '\n' + text
text = text.replace('<br>', '\n')
else:
pages.extend(slides)
if '[---]' not in text:
lines = text.strip('\n').split('\n')
pages.extend(self._paginate_slide(lines, line_end))
break
count += 1
else:
# Clean up line endings.
pages = self._paginate_slide(text.split('\n'), line_end)
else:
pages = self._paginate_slide(text.split('\n'), line_end)
new_pages = []
for page in pages:
while page.endswith('<br>'):
page = page[:-4]
new_pages.append(page)
return new_pages
def _paginate_slide(self, lines, line_end):
"""
Figure out how much text can appear on a slide, using the current theme settings.
**Note:** The smallest possible "unit" of text for a slide is one line. If the line is too long it will be cut
off when displayed.
:param lines: The text to be fitted on the slide split into lines.
:param line_end: The text added after each line. Either ``' '`` or ``'<br>``.
"""
formatted = []
previous_html = ''
previous_raw = ''
separator = '<br>'
html_lines = list(map(render_tags, lines))
# Text too long so go to next page.
if not self._text_fits_on_slide(separator.join(html_lines)):
html_text, previous_raw = self._binary_chop(
formatted, previous_html, previous_raw, html_lines, lines, separator, '')
else:
previous_raw = separator.join(lines)
formatted.append(previous_raw)
return formatted
def _paginate_slide_words(self, lines, line_end):
"""
Figure out how much text can appear on a slide, using the current theme settings.
**Note:** The smallest possible "unit" of text for a slide is one word. If one line is too long it will be
processed word by word. This is sometimes need for **bible** verses.
:param lines: The text to be fitted on the slide split into lines.
:param line_end: The text added after each line. Either ``' '`` or ``'<br>``. This is needed for **bibles**.
"""
formatted = []
previous_html = ''
previous_raw = ''
for line in lines:
line = line.strip()
html_line = render_tags(line)
# Text too long so go to next page.
if not self._text_fits_on_slide(previous_html + html_line):
# Check if there was a verse before the current one and append it, when it fits on the page.
if previous_html:
if self._text_fits_on_slide(previous_html):
formatted.append(previous_raw)
previous_html = ''
previous_raw = ''
# Now check if the current verse will fit, if it does not we have to start to process the verse
# word by word.
if self._text_fits_on_slide(html_line):
previous_html = html_line + line_end
previous_raw = line + line_end
continue
# Figure out how many words of the line will fit on screen as the line will not fit as a whole.
raw_words = words_split(line)
html_words = list(map(render_tags, raw_words))
previous_html, previous_raw = \
self._binary_chop(formatted, previous_html, previous_raw, html_words, raw_words, ' ', line_end)
else:
previous_html += html_line + line_end
previous_raw += line + line_end
formatted.append(previous_raw)
return formatted
def _binary_chop(self, formatted, previous_html, previous_raw, html_list, raw_list, separator, line_end):
"""
This implements the binary chop algorithm for faster rendering. This algorithm works line based (line by line)
and word based (word by word). It is assumed that this method is **only** called, when the lines/words to be
rendered do **not** fit as a whole.
:param formatted: The list to append any slides.
:param previous_html: The html text which is know to fit on a slide, but is not yet added to the list of
slides. (unicode string)
:param previous_raw: The raw text (with formatting tags) which is know to fit on a slide, but is not yet added
to the list of slides. (unicode string)
:param html_list: The elements which do not fit on a slide and needs to be processed using the binary chop.
The text contains html.
:param raw_list: The elements which do not fit on a slide and needs to be processed using the binary chop.
The elements can contain formatting tags.
:param separator: The separator for the elements. For lines this is ``'<br>'`` and for words this is ``' '``.
:param line_end: The text added after each "element line". Either ``' '`` or ``'<br>``. This is needed for
bibles.
"""
smallest_index = 0
highest_index = len(html_list) - 1
index = highest_index // 2
while True:
if not self._text_fits_on_slide(previous_html + separator.join(html_list[:index + 1]).strip()):
# We know that it does not fit, so change/calculate the new index and highest_index accordingly.
highest_index = index
index = index - (index - smallest_index) // 2
else:
smallest_index = index
index = index + (highest_index - index) // 2
# We found the number of words which will fit.
if smallest_index == index or highest_index == index:
index = smallest_index
text = previous_raw.rstrip('<br>') + separator.join(raw_list[:index + 1])
text, raw_tags, html_tags = get_start_tags(text)
formatted.append(text)
previous_html = ''
previous_raw = ''
# Stop here as the theme line count was requested.
if self.force_page:
Registry().execute('theme_line_count', index + 1)
break
else:
continue
# Check if the remaining elements fit on the slide.
if self._text_fits_on_slide(html_tags + separator.join(html_list[index + 1:]).strip()):
previous_html = html_tags + separator.join(html_list[index + 1:]).strip() + line_end
previous_raw = raw_tags + separator.join(raw_list[index + 1:]).strip() + line_end
break
else:
# The remaining elements do not fit, thus reset the indexes, create a new list and continue.
raw_list = raw_list[index + 1:]
raw_list[0] = raw_tags + raw_list[0]
html_list = html_list[index + 1:]
html_list[0] = html_tags + html_list[0]
smallest_index = 0
highest_index = len(html_list) - 1
index = highest_index // 2
return previous_html, previous_raw
def _text_fits_on_slide(self, text):
"""
Checks if the given ``text`` fits on a slide. If it does ``True`` is returned, otherwise ``False``.
:param text: The text to check. It may contain HTML tags.
"""
self.clear_slides()
self.run_javascript('Display.addTextSlide("v1", "{text}", "Dummy Footer");'.format(text=text), is_sync=True)
does_text_fits = self.run_javascript('Display.doesContentFit();', is_sync=True)
return does_text_fits
def save_screenshot(self, fname=None):
"""
Save a screenshot, either returning it or saving it to file. Do some extra work to actually get a picture.
"""
self.setVisible(True)
pixmap = self.grab()
for i in range(0, 4):
QtWidgets.QApplication.instance().processEvents()
time.sleep(0.05)
QtWidgets.QApplication.instance().processEvents()
pixmap = self.grab()
self.setVisible(False)
pixmap = QtGui.QPixmap(self.webview.size())
self.webview.render(pixmap)
if fname:
ext = os.path.splitext(fname)[-1][1:]
pixmap.save(fname, ext)
else:
return pixmap

View File

@ -1,586 +0,0 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
###############################################################################
# OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- #
# Copyright (c) 2008-2018 OpenLP Developers #
# --------------------------------------------------------------------------- #
# This program is free software; you can redistribute it and/or modify it #
# under the terms of the GNU General Public License as published by the Free #
# Software Foundation; version 2 of the License. #
# #
# This program is distributed in the hope that it will be useful, but WITHOUT #
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
# more details. #
# #
# You should have received a copy of the GNU General Public License along #
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
import re
from string import Template
from PyQt5 import QtGui, QtCore, QtWebKitWidgets
from openlp.core.common import ThemeLevel
from openlp.core.common.mixins import LogMixin, RegistryProperties
from openlp.core.common.path import path_to_str
from openlp.core.common.registry import Registry, RegistryBase
from openlp.core.common.settings import Settings
from openlp.core.display.screens import ScreenList
from openlp.core.lib import ImageSource, expand_tags
from openlp.core.lib.htmlbuilder import build_chords_css, build_lyrics_format_css, \
build_lyrics_outline_css
from openlp.core.lib.formattingtags import FormattingTags
from openlp.core.lib.serviceitem import ServiceItem, ItemCapabilities
from openlp.core.ui.maindisplay import MainDisplay
VERSE = 'The Lord said to {r}Noah{/r}: \n' \
'There\'s gonna be a {su}floody{/su}, {sb}floody{/sb}\n' \
'The Lord said to {g}Noah{/g}:\n' \
'There\'s gonna be a {st}floody{/st}, {it}floody{/it}\n' \
'Get those children out of the muddy, muddy \n' \
'{r}C{/r}{b}h{/b}{bl}i{/bl}{y}l{/y}{g}d{/g}{pk}' \
'r{/pk}{o}e{/o}{pp}n{/pp} of the Lord\n'
VERSE_FOR_LINE_COUNT = '\n'.join(map(str, range(100)))
FOOTER = ['Arky Arky (Unknown)', 'Public Domain', 'CCLI 123456']
class Renderer(RegistryBase, LogMixin, RegistryProperties):
"""
Class to pull all Renderer interactions into one place. The plugins will call helper methods to do the rendering but
this class will provide display defense code.
"""
def __init__(self):
"""
Initialise the renderer.
"""
super(Renderer, self).__init__(None)
# Need live behaviour if this is also working as a pseudo MainDisplay.
self.screens = ScreenList()
self.theme_level = ThemeLevel.Global
self.global_theme_name = ''
self.service_theme_name = ''
self.item_theme_name = ''
self.force_page = False
self._theme_dimensions = {}
self._calculate_default()
self.web = QtWebKitWidgets.QWebView()
self.web.setVisible(False)
self.web_frame = self.web.page().mainFrame()
Registry().register_function('theme_update_global', self.set_global_theme)
def bootstrap_initialise(self):
"""
Initialise functions
"""
self.display = MainDisplay(self)
self.display.setup()
def update_display(self):
"""
Updates the renderer's information about the current screen.
"""
self._calculate_default()
if self.display:
self.display.close()
self.display = MainDisplay(self)
self.display.setup()
self._theme_dimensions = {}
def update_theme(self, theme_name, old_theme_name=None, only_delete=False):
"""
This method updates the theme in ``_theme_dimensions`` when a theme has been edited or renamed.
:param theme_name: The current theme name.
:param old_theme_name: The old theme name. Has only to be passed, when the theme has been renamed.
Defaults to *None*.
:param only_delete: Only remove the given ``theme_name`` from the ``_theme_dimensions`` list. This can be
used when a theme is permanently deleted.
"""
if old_theme_name is not None and old_theme_name in self._theme_dimensions:
del self._theme_dimensions[old_theme_name]
if theme_name in self._theme_dimensions:
del self._theme_dimensions[theme_name]
if not only_delete and theme_name:
self._set_theme(theme_name)
def _set_theme(self, theme_name):
"""
Helper method to save theme names and theme data.
:param theme_name: The theme name
"""
self.log_debug("_set_theme with theme {theme}".format(theme=theme_name))
if theme_name not in self._theme_dimensions:
theme_data = self.theme_manager.get_theme_data(theme_name)
main_rect = self.get_main_rectangle(theme_data)
footer_rect = self.get_footer_rectangle(theme_data)
self._theme_dimensions[theme_name] = [theme_data, main_rect, footer_rect]
else:
theme_data, main_rect, footer_rect = self._theme_dimensions[theme_name]
# if No file do not update cache
if theme_data.background_filename:
self.image_manager.add_image(path_to_str(theme_data.background_filename),
ImageSource.Theme, QtGui.QColor(theme_data.background_border_color))
def pre_render(self, override_theme_data=None):
"""
Set up the theme to be used before rendering an item.
:param override_theme_data: The theme data should be passed, when we want to use our own theme data, regardless
of the theme level. This should for example be used in the theme manager. **Note**, this is **not** to
be mixed up with the ``set_item_theme`` method.
"""
# Just assume we use the global theme.
theme_to_use = self.global_theme_name
# 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.service_theme_name:
theme_to_use = self.service_theme_name
# If we have Item level and have an item theme then use it.
if self.theme_level == ThemeLevel.Song and self.item_theme_name:
theme_to_use = self.item_theme_name
if override_theme_data is None:
if theme_to_use not in self._theme_dimensions:
self._set_theme(theme_to_use)
theme_data, main_rect, footer_rect = self._theme_dimensions[theme_to_use]
else:
# Ignore everything and use own theme data.
theme_data = override_theme_data
main_rect = self.get_main_rectangle(override_theme_data)
footer_rect = self.get_footer_rectangle(override_theme_data)
self._set_text_rectangle(theme_data, main_rect, footer_rect)
return theme_data, self._rect, self._rect_footer
def set_theme_level(self, theme_level):
"""
Sets the theme level.
:param theme_level: The theme level to be used.
"""
self.theme_level = theme_level
def set_global_theme(self):
"""
Set the global-level theme name.
"""
global_theme_name = Settings().value('themes/global theme')
self._set_theme(global_theme_name)
self.global_theme_name = global_theme_name
def set_service_theme(self, service_theme_name):
"""
Set the service-level theme.
:param service_theme_name: The service level theme's name.
"""
self._set_theme(service_theme_name)
self.service_theme_name = service_theme_name
def set_item_theme(self, item_theme_name):
"""
Set the item-level theme. **Note**, this has to be done for each item we are rendering.
:param item_theme_name: The item theme's name.
"""
self.log_debug("set_item_theme with theme {theme}".format(theme=item_theme_name))
self._set_theme(item_theme_name)
self.item_theme_name = item_theme_name
def generate_preview(self, theme_data, force_page=False):
"""
Generate a preview of a theme.
:param theme_data: The theme to generated a preview for.
:param force_page: Flag to tell message lines per page need to be generated.
:rtype: QtGui.QPixmap
"""
# save value for use in format_slide
self.force_page = force_page
# build a service item to generate preview
service_item = ServiceItem()
if self.force_page:
# make big page for theme edit dialog to get line count
service_item.add_from_text(VERSE_FOR_LINE_COUNT)
else:
service_item.add_from_text(VERSE)
service_item.raw_footer = FOOTER
# if No file do not update cache
if theme_data.background_filename:
self.image_manager.add_image(path_to_str(theme_data.background_filename),
ImageSource.Theme, QtGui.QColor(theme_data.background_border_color))
theme_data, main, footer = self.pre_render(theme_data)
service_item.theme_data = theme_data
service_item.main = main
service_item.footer = footer
service_item.render(True)
if not self.force_page:
self.display.build_html(service_item)
raw_html = service_item.get_rendered_frame(0)
self.display.text(raw_html, False)
return self.display.preview()
self.force_page = False
def format_slide(self, text, item):
"""
Calculate how much text can fit on a slide.
:param text: The words to go on the slides.
:param item: The :class:`~openlp.core.lib.serviceitem.ServiceItem` item object.
"""
self.log_debug('format slide')
# Add line endings after each line of text used for bibles.
line_end = '<br>'
if item.is_capable(ItemCapabilities.NoLineBreaks):
line_end = ' '
# Bibles
if item.is_capable(ItemCapabilities.CanWordSplit):
pages = self._paginate_slide_words(text.split('\n'), line_end)
# Songs and Custom
elif item.is_capable(ItemCapabilities.CanSoftBreak):
pages = []
if '[---]' in text:
# Remove Overflow split if at start of the text
if text.startswith('[---]'):
text = text[5:]
# Remove two or more option slide breaks next to each other (causing infinite loop).
while '\n[---]\n[---]\n' in text:
text = text.replace('\n[---]\n[---]\n', '\n[---]\n')
while ' [---]' in text:
text = text.replace(' [---]', '[---]')
while '[---] ' in text:
text = text.replace('[---] ', '[---]')
count = 0
# only loop 5 times as there will never be more than 5 incorrect logical splits on a single slide.
while True and count < 5:
slides = text.split('\n[---]\n', 2)
# If there are (at least) two occurrences of [---] we use the first two slides (and neglect the last
# for now).
if len(slides) == 3:
html_text = expand_tags('\n'.join(slides[:2]))
# We check both slides to determine if the optional split is needed (there is only one optional
# split).
else:
html_text = expand_tags('\n'.join(slides))
html_text = html_text.replace('\n', '<br>')
if self._text_fits_on_slide(html_text):
# The first two optional slides fit (as a whole) on one slide. Replace the first occurrence
# of [---].
text = text.replace('\n[---]', '', 1)
else:
# The first optional slide fits, which means we have to render the first optional slide.
text_contains_split = '[---]' in text
if text_contains_split:
try:
text_to_render, text = text.split('\n[---]\n', 1)
except ValueError:
text_to_render = text.split('\n[---]\n')[0]
text = ''
text_to_render, raw_tags, html_tags = get_start_tags(text_to_render)
if text:
text = raw_tags + text
else:
text_to_render = text
text = ''
lines = text_to_render.strip('\n').split('\n')
slides = self._paginate_slide(lines, line_end)
if len(slides) > 1 and text:
# Add all slides apart from the last one the list.
pages.extend(slides[:-1])
if text_contains_split:
text = slides[-1] + '\n[---]\n' + text
else:
text = slides[-1] + '\n' + text
text = text.replace('<br>', '\n')
else:
pages.extend(slides)
if '[---]' not in text:
lines = text.strip('\n').split('\n')
pages.extend(self._paginate_slide(lines, line_end))
break
count += 1
else:
# Clean up line endings.
pages = self._paginate_slide(text.split('\n'), line_end)
else:
pages = self._paginate_slide(text.split('\n'), line_end)
new_pages = []
for page in pages:
while page.endswith('<br>'):
page = page[:-4]
new_pages.append(page)
return new_pages
def _calculate_default(self):
"""
Calculate the default dimensions of the screen.
"""
screen_size = self.screens.current['size']
self.width = screen_size.width()
self.height = screen_size.height()
self.screen_ratio = self.height / self.width
self.log_debug('_calculate default {size}, {ratio:f}'.format(size=screen_size, ratio=self.screen_ratio))
# 90% is start of footer
self.footer_start = int(self.height * 0.90)
def get_main_rectangle(self, theme_data):
"""
Calculates the placement and size of the main rectangle.
:param theme_data: The theme information
"""
if not theme_data.font_main_override:
return QtCore.QRect(10, 0, self.width - 20, self.footer_start)
else:
return QtCore.QRect(theme_data.font_main_x, theme_data.font_main_y,
theme_data.font_main_width - 1, theme_data.font_main_height - 1)
def get_footer_rectangle(self, theme_data):
"""
Calculates the placement and size of the footer rectangle.
:param theme_data: The theme data.
"""
if not theme_data.font_footer_override:
return QtCore.QRect(10, self.footer_start, self.width - 20, self.height - self.footer_start)
else:
return QtCore.QRect(theme_data.font_footer_x,
theme_data.font_footer_y, theme_data.font_footer_width - 1,
theme_data.font_footer_height - 1)
def _set_text_rectangle(self, theme_data, rect_main, rect_footer):
"""
Sets the rectangle within which text should be rendered.
:param theme_data: The theme data.
:param rect_main: The main text block.
:param rect_footer: The footer text block.
"""
self.log_debug('_set_text_rectangle {main} , {footer}'.format(main=rect_main, footer=rect_footer))
self._rect = rect_main
self._rect_footer = rect_footer
self.page_width = self._rect.width()
self.page_height = self._rect.height()
if theme_data.font_main_shadow:
self.page_width -= int(theme_data.font_main_shadow_size)
self.page_height -= int(theme_data.font_main_shadow_size)
# For the life of my I don't know why we have to completely kill the QWebView in order for the display to work
# properly, but we do. See bug #1041366 for an example of what happens if we take this out.
self.web = None
self.web = QtWebKitWidgets.QWebView()
self.web.setVisible(False)
self.web.resize(self.page_width, self.page_height)
self.web_frame = self.web.page().mainFrame()
# Adjust width and height to account for shadow. outline done in css.
html = Template("""<!DOCTYPE html><html><head><script>
function show_text(newtext) {
var main = document.getElementById('main');
main.innerHTML = newtext;
// We need to be sure that the page is loaded, that is why we
// return the element's height (even though we do not use the
// returned value).
return main.offsetHeight;
}
</script>
<style>
*{margin: 0; padding: 0; border: 0;}
#main {position: absolute; top: 0px; ${format_css} ${outline_css}} ${chords_css}
</style></head>
<body><div id="main"></div></body></html>""")
self.web.setHtml(html.substitute(format_css=build_lyrics_format_css(theme_data,
self.page_width,
self.page_height),
outline_css=build_lyrics_outline_css(theme_data),
chords_css=build_chords_css()))
self.empty_height = self.web_frame.contentsSize().height()
def _paginate_slide(self, lines, line_end):
"""
Figure out how much text can appear on a slide, using the current theme settings.
**Note:** The smallest possible "unit" of text for a slide is one line. If the line is too long it will be cut
off when displayed.
:param lines: The text to be fitted on the slide split into lines.
:param line_end: The text added after each line. Either ``' '`` or ``'<br>``.
"""
formatted = []
previous_html = ''
previous_raw = ''
separator = '<br>'
html_lines = list(map(expand_tags, lines))
# Text too long so go to next page.
if not self._text_fits_on_slide(separator.join(html_lines)):
html_text, previous_raw = self._binary_chop(
formatted, previous_html, previous_raw, html_lines, lines, separator, '')
else:
previous_raw = separator.join(lines)
formatted.append(previous_raw)
return formatted
def _paginate_slide_words(self, lines, line_end):
"""
Figure out how much text can appear on a slide, using the current theme settings.
**Note:** The smallest possible "unit" of text for a slide is one word. If one line is too long it will be
processed word by word. This is sometimes need for **bible** verses.
:param lines: The text to be fitted on the slide split into lines.
:param line_end: The text added after each line. Either ``' '`` or ``'<br>``. This is needed for **bibles**.
"""
formatted = []
previous_html = ''
previous_raw = ''
for line in lines:
line = line.strip()
html_line = expand_tags(line)
# Text too long so go to next page.
if not self._text_fits_on_slide(previous_html + html_line):
# Check if there was a verse before the current one and append it, when it fits on the page.
if previous_html:
if self._text_fits_on_slide(previous_html):
formatted.append(previous_raw)
previous_html = ''
previous_raw = ''
# Now check if the current verse will fit, if it does not we have to start to process the verse
# word by word.
if self._text_fits_on_slide(html_line):
previous_html = html_line + line_end
previous_raw = line + line_end
continue
# Figure out how many words of the line will fit on screen as the line will not fit as a whole.
raw_words = words_split(line)
html_words = list(map(expand_tags, raw_words))
previous_html, previous_raw = \
self._binary_chop(formatted, previous_html, previous_raw, html_words, raw_words, ' ', line_end)
else:
previous_html += html_line + line_end
previous_raw += line + line_end
formatted.append(previous_raw)
return formatted
def _binary_chop(self, formatted, previous_html, previous_raw, html_list, raw_list, separator, line_end):
"""
This implements the binary chop algorithm for faster rendering. This algorithm works line based (line by line)
and word based (word by word). It is assumed that this method is **only** called, when the lines/words to be
rendered do **not** fit as a whole.
:param formatted: The list to append any slides.
:param previous_html: The html text which is know to fit on a slide, but is not yet added to the list of
slides. (unicode string)
:param previous_raw: The raw text (with formatting tags) which is know to fit on a slide, but is not yet added
to the list of slides. (unicode string)
:param html_list: The elements which do not fit on a slide and needs to be processed using the binary chop.
The text contains html.
:param raw_list: The elements which do not fit on a slide and needs to be processed using the binary chop.
The elements can contain formatting tags.
:param separator: The separator for the elements. For lines this is ``'<br>'`` and for words this is ``' '``.
:param line_end: The text added after each "element line". Either ``' '`` or ``'<br>``. This is needed for
bibles.
"""
smallest_index = 0
highest_index = len(html_list) - 1
index = highest_index // 2
while True:
if not self._text_fits_on_slide(previous_html + separator.join(html_list[:index + 1]).strip()):
# We know that it does not fit, so change/calculate the new index and highest_index accordingly.
highest_index = index
index = index - (index - smallest_index) // 2
else:
smallest_index = index
index = index + (highest_index - index) // 2
# We found the number of words which will fit.
if smallest_index == index or highest_index == index:
index = smallest_index
text = previous_raw.rstrip('<br>') + separator.join(raw_list[:index + 1])
text, raw_tags, html_tags = get_start_tags(text)
formatted.append(text)
previous_html = ''
previous_raw = ''
# Stop here as the theme line count was requested.
if self.force_page:
Registry().execute('theme_line_count', index + 1)
break
else:
continue
# Check if the remaining elements fit on the slide.
if self._text_fits_on_slide(html_tags + separator.join(html_list[index + 1:]).strip()):
previous_html = html_tags + separator.join(html_list[index + 1:]).strip() + line_end
previous_raw = raw_tags + separator.join(raw_list[index + 1:]).strip() + line_end
break
else:
# The remaining elements do not fit, thus reset the indexes, create a new list and continue.
raw_list = raw_list[index + 1:]
raw_list[0] = raw_tags + raw_list[0]
html_list = html_list[index + 1:]
html_list[0] = html_tags + html_list[0]
smallest_index = 0
highest_index = len(html_list) - 1
index = highest_index // 2
return previous_html, previous_raw
def _text_fits_on_slide(self, text):
"""
Checks if the given ``text`` fits on a slide. If it does ``True`` is returned, otherwise ``False``.
:param text: The text to check. It may contain HTML tags.
"""
self.web_frame.evaluateJavaScript('show_text'
'("{text}")'.format(text=text.replace('\\', '\\\\').replace('\"', '\\\"')))
return self.web_frame.contentsSize().height() <= self.empty_height
def words_split(line):
"""
Split the slide up by word so can wrap better
:param line: Line to be split
"""
# this parse we are to be wordy
return re.split(r'\s+', line)
def get_start_tags(raw_text):
"""
Tests the given text for not closed formatting tags and returns a tuple consisting of three unicode strings::
('{st}{r}Text text text{/r}{/st}', '{st}{r}', '<strong><span style="-webkit-text-fill-color:red">')
The first unicode string is the text, with correct closing tags. The second unicode string are OpenLP's opening
formatting tags and the third unicode string the html opening formatting tags.
:param raw_text: The text to test. The text must **not** contain html tags, only OpenLP formatting tags
are allowed::
{st}{r}Text text text
"""
raw_tags = []
html_tags = []
for tag in FormattingTags.get_html_tags():
if tag['start tag'] == '{br}':
continue
if raw_text.count(tag['start tag']) != raw_text.count(tag['end tag']):
raw_tags.append((raw_text.find(tag['start tag']), tag['start tag'], tag['end tag']))
html_tags.append((raw_text.find(tag['start tag']), tag['start html']))
# Sort the lists, so that the tags which were opened first on the first slide (the text we are checking) will be
# opened first on the next slide as well.
raw_tags.sort(key=lambda tag: tag[0])
html_tags.sort(key=lambda tag: tag[0])
# Create a list with closing tags for the raw_text.
end_tags = []
start_tags = []
for tag in raw_tags:
start_tags.append(tag[1])
end_tags.append(tag[2])
end_tags.reverse()
# Remove the indexes.
html_tags = [tag[1] for tag in html_tags]
return raw_text + ''.join(end_tags), ''.join(start_tags), ''.join(html_tags)

View File

@ -23,18 +23,125 @@
The :mod:`screen` module provides management functionality for a machines'
displays.
"""
import copy
import logging
from functools import cmp_to_key
from PyQt5 import QtCore
from PyQt5 import QtCore, QtWidgets
from openlp.core.common.i18n import translate
from openlp.core.common.registry import Registry
from openlp.core.common.settings import Settings
log = logging.getLogger(__name__)
class Screen(object):
"""
A Python representation of a screen
"""
def __init__(self, number=None, geometry=None, is_primary=False, is_display=False):
"""
Set up the screen object
:param int number: The Qt number of this screen
:param QRect geometry: The geometry of this screen as a QRect object
:param bool is_primary: Whether or not this screen is the primary screen
:param bool is_display: Whether or not this screen should be used to display lyrics
"""
self.number = int(number)
self.geometry = geometry
self.custom_geometry = None
self.is_primary = is_primary
self.is_display = is_display
def __str__(self):
"""
Return a string for displaying this screen
:return str: A nicely formatted string
"""
name = '{screen} {number:d}'.format(screen=translate('OpenLP.ScreenList', 'Screen'), number=self.number + 1)
if self.is_primary:
name = '{name} ({primary})'.format(name=name, primary=translate('OpenLP.ScreenList', 'primary'))
return name
def __repr__(self):
"""
Return a string representation of the object
"""
return '<{screen}>'.format(screen=self)
@property
def display_geometry(self):
"""
Returns the geometry to use when displaying. This property decides between the native and custom geometries
"""
return self.custom_geometry or self.geometry
@classmethod
def from_dict(cls, screen_dict):
"""
Create a screen object from a dictionary
:param dict screen_dict: The dictionary from which to make the Screen object
:return: A Screen object with the values from screen_dict
:rtype: openlp.core.display.screens.Screen
"""
screen_dict['geometry'] = QtCore.QRect(screen_dict['geometry']['x'], screen_dict['geometry']['y'],
screen_dict['geometry']['width'], screen_dict['geometry']['height'])
if 'custom_geometry' in screen_dict:
screen_dict['custom_geometry'] = QtCore.QRect(screen_dict['custom_geometry']['x'],
screen_dict['custom_geometry']['y'],
screen_dict['custom_geometry']['width'],
screen_dict['custom_geometry']['height'])
return cls(**screen_dict)
def to_dict(self):
"""
Convert a screen object to a dictionary
:return: A dictionary of this screen
:rtype: dict
"""
screen_dict = {
'number': self.number,
'geometry': {
'x': self.geometry.x(),
'y': self.geometry.y(),
'width': self.geometry.width(),
'height': self.geometry.height()
},
'is_primary': self.is_primary,
'is_display': self.is_display
}
if self.custom_geometry is not None:
screen_dict['custom_geometry'] = {
'x': self.custom_geometry.x(),
'y': self.custom_geometry.y(),
'width': self.custom_geometry.width(),
'height': self.custom_geometry.height()
}
return screen_dict
def update(self, screen_dict):
"""
Update this instance from a dictionary
:param dict screen_dict: The dictionary which we want to apply to the screen
"""
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'])
if 'custom_geometry' in screen_dict:
self.custom_geometry = QtCore.QRect(screen_dict['custom_geometry']['x'],
screen_dict['custom_geometry']['y'],
screen_dict['custom_geometry']['width'],
screen_dict['custom_geometry']['height'])
class ScreenList(object):
"""
Wrapper to handle the parameters of the display screen.
@ -43,6 +150,7 @@ class ScreenList(object):
"""
log.info('Screen loaded')
__instance__ = None
screens = []
def __new__(cls):
"""
@ -52,6 +160,50 @@ class ScreenList(object):
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
"""
for screen in self.screens:
yield screen
def __getitem__(self, key):
"""
Make sure this object is indexable, e.g. screen_list[1] => Screen(number=1)
"""
for screen in self:
if screen.number == key:
return screen
else:
raise IndexError('No screen with number {number} in list'.format(number=key))
def __len__(self):
"""
Make sure we can call "len" on this object
"""
return len(self.screens)
@property
def current(self):
"""
Return the first "current" desktop
NOTE: This is a HACK to ease the upgrade process
"""
# Get the first display screen
for screen in self.screens:
if screen.is_display:
return screen
# If there's no display screen, get the first primary screen
for screen in self.screens:
if screen.is_primary:
return screen
# Otherwise just return the first screen
if len(self.screens) > 0:
return self.screens[0]
else:
return None
@classmethod
def create(cls, desktop):
"""
@ -61,18 +213,121 @@ class ScreenList(object):
"""
screen_list = cls()
screen_list.desktop = desktop
screen_list.preview = None
screen_list.current = None
screen_list.override = None
screen_list.screen_list = []
screen_list.display_count = 0
screen_list.screen_count_changed()
screen_list.desktop.resized.connect(screen_list.on_screen_resolution_changed)
screen_list.desktop.screenCountChanged.connect(screen_list.on_screen_count_changed)
screen_list.desktop.primaryScreenChanged.connect(screen_list.on_primary_screen_changed)
screen_list.update_screens()
screen_list.load_screen_settings()
desktop.resized.connect(screen_list.screen_resolution_changed)
desktop.screenCountChanged.connect(screen_list.screen_count_changed)
return screen_list
def screen_resolution_changed(self, number):
def load_screen_settings(self):
"""
Loads the screen size and the screen number from the settings.
"""
# Add the screen settings to the settings dict. This has to be done here due to cyclic dependency.
# Do not do this anywhere else.
screen_settings = {
'core/screens': '{}'
}
Settings.extend_default_settings(screen_settings)
screen_settings = Settings().value('core/screens')
for number, screen_dict in screen_settings.items():
# Sometimes this loads as a string instead of an int
number = int(number)
if self.has_screen(number):
self[number].update(screen_dict)
else:
self.screens.append(Screen.from_dict(screen_dict))
def save_screen_settings(self):
"""
Saves the screen size and screen settings
"""
Settings().setValue('core/screens', {screen.number: screen.to_dict() for screen in self.screens})
def get_display_screen_list(self):
"""
Returns a list with the screens. This should only be used to display available screens to the user::
['Screen 1 (primary)', 'Screen 2']
"""
screen_list = []
for screen in self.screens:
screen_name = '{name} {number:d}'.format(name=translate('OpenLP.ScreenList', 'Screen'),
number=screen.number + 1)
if screen.is_primary:
screen_name = '{name} ({primary})'.format(name=screen_name,
primary=translate('OpenLP.ScreenList', 'primary'))
screen_list.append(screen_name)
return screen_list
def get_number_for_window(self, window):
"""
Return the screen number that the centre of the passed window is in.
:param window: A QWidget we are finding the location of.
"""
for screen in self.screens:
if screen.geometry == window.geometry() or screen.display_geometry == window.geometry():
return screen
return None
def set_display_screen(self, number, can_save=False):
"""
Set screen number ``number`` to be the display screen.
At the moment, this is forced to be only a single screen, but later when we support multiple monitors it
will need to be updated.
:param int number: The number of the screen
:param bool can_save: If the screen settings should be saved, defaults to False.
"""
for screen in self.screens:
if screen.number == number:
screen.is_display = True
else:
screen.is_display = False
if can_save:
self.save_screen_settings()
def has_screen(self, number):
"""
Confirms a screen is known.
:param number: The screen number (int).
"""
for screen in self.screens:
if screen.number == number:
return True
return False
def update_screens(self):
"""
Update the list of screens
"""
def _screen_compare(this, other):
"""
Compare screens. Can't use a key here because of the nested property and method to be called
"""
if this.geometry().x() < other.geometry().x():
return -1
elif this.geometry().x() > other.geometry().x():
return 1
else:
if this.geometry().y() < other.geometry().y():
return -1
elif this.geometry().y() > other.geometry().y():
return 1
else:
return 0
self.screens = []
os_screens = QtWidgets.QApplication.screens()
os_screens.sort(key=cmp_to_key(_screen_compare))
for number, screen in enumerate(os_screens):
self.screens.append(
Screen(number, screen.geometry(), self.desktop.primaryScreen() == number))
def on_screen_resolution_changed(self, number):
"""
Called when the resolution of a screen has changed.
@ -80,184 +335,31 @@ class ScreenList(object):
The number of the screen, which size has changed.
"""
log.info('screen_resolution_changed {number:d}'.format(number=number))
for screen in self.screen_list:
if number == screen['number']:
new_screen = {
'number': number,
'size': self.desktop.screenGeometry(number),
'primary': self.desktop.primaryScreen() == number
}
self.remove_screen(number)
self.add_screen(new_screen)
# The screen's default size is used, that is why we have to
# update the override screen.
if screen == self.override:
self.override = copy.deepcopy(new_screen)
self.set_override_display()
for screen in self.screens:
if number == screen.number:
screen.geometry = self.desktop.screenGeometry(number)
screen.is_primary = self.desktop.primaryScreen() == number
Registry().execute('config_screen_changed')
break
def screen_count_changed(self, changed_screen=-1):
def on_screen_count_changed(self, changed_screen=None):
"""
Called when a screen has been added or removed.
``changed_screen``
The screen's number which has been (un)plugged.
"""
# Do not log at start up.
if changed_screen != -1:
log.info('screen_count_changed {count:d}'.format(count=self.desktop.screenCount()))
# Remove unplugged screens.
for screen in copy.deepcopy(self.screen_list):
if screen['number'] == self.desktop.screenCount():
self.remove_screen(screen['number'])
# Add new screens.
for number in range(self.desktop.screenCount()):
if not self.screen_exists(number):
self.add_screen({
'number': number,
'size': self.desktop.screenGeometry(number),
'primary': (self.desktop.primaryScreen() == number)
})
# We do not want to send this message at start up.
if changed_screen != -1:
# Reload setting tabs to apply possible changes.
Registry().execute('config_screen_changed')
screen_count = self.desktop.screenCount()
log.info('screen_count_changed {count:d}'.format(count=screen_count))
# Update the list of screens
self.update_screens()
# Reload setting tabs to apply possible changes.
Registry().execute('config_screen_changed')
def get_screen_list(self):
def on_primary_screen_changed(self):
"""
Returns a list with the screens. This should only be used to display
available screens to the user::
['Screen 1 (primary)', 'Screen 2']
The primary screen has changed, let's sort it out and then notify everyone
"""
screen_list = []
for screen in self.screen_list:
screen_name = '{name} {number:d}'.format(name=translate('OpenLP.ScreenList', 'Screen'),
number=screen['number'] + 1)
if screen['primary']:
screen_name = '{name} ({primary})'.format(name=screen_name,
primary=translate('OpenLP.ScreenList', 'primary'))
screen_list.append(screen_name)
return screen_list
def add_screen(self, screen):
"""
Add a screen to the list of known screens.
:param screen: A dict with the screen properties:
::
{
'primary': True,
'number': 0,
'size': PyQt5.QtCore.QRect(0, 0, 1024, 768)
}
"""
log.info('Screen {number:d} found with resolution {size}'.format(number=screen['number'], size=screen['size']))
if screen['primary']:
self.current = screen
self.override = copy.deepcopy(self.current)
self.screen_list.append(screen)
self.display_count += 1
def remove_screen(self, number):
"""
Remove a screen from the list of known screens.
:param number: The screen number (int).
"""
log.info('remove_screen {number:d}'.format(number=number))
for screen in self.screen_list:
if screen['number'] == number:
self.screen_list.remove(screen)
self.display_count -= 1
break
def screen_exists(self, number):
"""
Confirms a screen is known.
:param number: The screen number (int).
"""
for screen in self.screen_list:
if screen['number'] == number:
return True
return False
def set_current_display(self, number):
"""
Set up the current screen dimensions.
:param number: The screen number (int).
"""
log.debug('set_current_display {number}'.format(number=number))
if number + 1 > self.display_count:
self.current = self.screen_list[0]
else:
self.current = self.screen_list[number]
self.preview = copy.deepcopy(self.current)
self.override = copy.deepcopy(self.current)
if self.display_count == 1:
self.preview = self.screen_list[0]
def set_override_display(self):
"""
Replace the current size with the override values, as the user wants to have their own screen attributes.
"""
log.debug('set_override_display')
self.current = copy.deepcopy(self.override)
self.preview = copy.deepcopy(self.current)
def reset_current_display(self):
"""
Replace the current values with the correct values, as the user wants to use the correct screen attributes.
"""
log.debug('reset_current_display')
self.set_current_display(self.current['number'])
def which_screen(self, window):
"""
Return the screen number that the centre of the passed window is in.
:param window: A QWidget we are finding the location of.
"""
x = window.x() + (window.width() // 2)
y = window.y() + (window.height() // 2)
for screen in self.screen_list:
size = screen['size']
if x >= size.x() and x <= (size.x() + size.width()) and y >= size.y() and y <= (size.y() + size.height()):
return screen['number']
def load_screen_settings(self):
"""
Loads the screen size and the monitor number from the settings.
"""
# Add the screen settings to the settings dict. This has to be done here due to cyclic dependency.
# Do not do this anywhere else.
screen_settings = {
'core/x position': self.current['size'].x(),
'core/y position': self.current['size'].y(),
'core/monitor': self.display_count - 1,
'core/height': self.current['size'].height(),
'core/width': self.current['size'].width()
}
Settings.extend_default_settings(screen_settings)
settings = Settings()
settings.beginGroup('core')
monitor = settings.value('monitor')
self.set_current_display(monitor)
self.display = settings.value('display on monitor')
override_display = settings.value('override position')
x = settings.value('x position')
y = settings.value('y position')
width = settings.value('width')
height = settings.value('height')
self.override['size'] = QtCore.QRect(x, y, width, height)
self.override['primary'] = False
settings.endGroup()
if override_display:
self.set_override_display()
else:
self.reset_current_display()
for screen in self.screens:
screen.is_primary = self.desktop.primaryScreen() == screen.number
Registry().execute('config_screen_changed')

View File

@ -0,0 +1,93 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
###############################################################################
# OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- #
# Copyright (c) 2008-2018 OpenLP Developers #
# --------------------------------------------------------------------------- #
# This program is free software; you can redistribute it and/or modify it #
# under the terms of the GNU General Public License as published by the Free #
# Software Foundation; version 2 of the License. #
# #
# This program is distributed in the hope that it will be useful, but WITHOUT #
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
# more details. #
# #
# You should have received a copy of the GNU General Public License along #
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
"""
Subclass of QWebEngineView. Adds some special eventhandling needed for screenshots/previews
Heavily inspired by https://stackoverflow.com/questions/33467776/qt-qwebengine-render-after-scrolling/33576100#33576100
"""
import logging
from PyQt5 import QtCore, QtWebEngineWidgets, QtWidgets
LOG_LEVELS = {
QtWebEngineWidgets.QWebEnginePage.InfoMessageLevel: logging.INFO,
QtWebEngineWidgets.QWebEnginePage.WarningMessageLevel: logging.WARNING,
QtWebEngineWidgets.QWebEnginePage.ErrorMessageLevel: logging.ERROR
}
log = logging.getLogger(__name__)
class WebEnginePage(QtWebEngineWidgets.QWebEnginePage):
"""
A custom WebEngine page to capture Javascript console logging
"""
def javaScriptConsoleMessage(self, level, message, line_number, source_id):
"""
Override the parent method in order to log the messages in OpenLP
"""
log.log(LOG_LEVELS[level], '{source_id}:{line_number} {message}'.format(source_id=source_id,
line_number=line_number,
message=message))
class WebEngineView(QtWebEngineWidgets.QWebEngineView):
"""
A sub-classed QWebEngineView to handle paint events of OpenGL (does not seem to work)
and set some attributtes.
"""
_child = None # QtWidgets.QOpenGLWidget or QWidget?
delegatePaint = QtCore.pyqtSignal()
def __init__(self, parent=None):
"""
Constructor
"""
super(WebEngineView, self).__init__(parent)
self.setPage(WebEnginePage(self))
self.settings().setAttribute(QtWebEngineWidgets.QWebEngineSettings.LocalStorageEnabled, True)
self.settings().setAttribute(QtWebEngineWidgets.QWebEngineSettings.LocalContentCanAccessFileUrls, True)
self.settings().setAttribute(QtWebEngineWidgets.QWebEngineSettings.LocalContentCanAccessRemoteUrls, True)
self.page().settings().setAttribute(QtWebEngineWidgets.QWebEngineSettings.LocalStorageEnabled, True)
self.page().settings().setAttribute(QtWebEngineWidgets.QWebEngineSettings.LocalContentCanAccessFileUrls, True)
self.page().settings().setAttribute(QtWebEngineWidgets.QWebEngineSettings.LocalContentCanAccessRemoteUrls, True)
def eventFilter(self, obj, ev):
"""
Emit delegatePaint on paint event of the last added QOpenGLWidget child
"""
if obj == self._child and ev.type() == QtCore.QEvent.Paint:
self.delegatePaint.emit()
return super(WebEngineView, self).eventFilter(obj, ev)
def event(self, ev):
"""
Handle events
"""
if ev.type() == QtCore.QEvent.ChildAdded:
# Only use QWidget child (used to be QOpenGLWidget)
w = ev.child()
if w and isinstance(w, QtWidgets.QWidget):
self._child = w
w.installEventFilter(self)
return super(WebEngineView, self).event(ev)

View File

@ -0,0 +1,402 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
###############################################################################
# OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- #
# Copyright (c) 2008-2018 OpenLP Developers #
# --------------------------------------------------------------------------- #
# This program is free software; you can redistribute it and/or modify it #
# under the terms of the GNU General Public License as published by the Free #
# Software Foundation; version 2 of the License. #
# #
# This program is distributed in the hope that it will be useful, but WITHOUT #
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
# more details. #
# #
# You should have received a copy of the GNU General Public License along #
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
"""
The :mod:`~openlp.core.display.window` module contains the display window
"""
import json
import logging
import os
import copy
from PyQt5 import QtCore, QtWebChannel, QtWidgets
from openlp.core.common.path import Path, path_to_str
from openlp.core.common.settings import Settings
from openlp.core.common.registry import Registry
from openlp.core.ui import HideMode
from openlp.core.display.screens import ScreenList
log = logging.getLogger(__name__)
DISPLAY_PATH = Path(__file__).parent / 'html' / 'display.html'
CHECKERBOARD_PATH = Path(__file__).parent / 'html' / 'checkerboard.png'
OPENLP_SPLASH_SCREEN_PATH = Path(__file__).parent / 'html' / 'openlp-splash-screen.png'
class MediaWatcher(QtCore.QObject):
"""
A class to watch media events in the display and emit signals for OpenLP
"""
progress = QtCore.pyqtSignal(float)
duration = QtCore.pyqtSignal(float)
volume = QtCore.pyqtSignal(float)
playback_rate = QtCore.pyqtSignal(float)
ended = QtCore.pyqtSignal(bool)
muted = QtCore.pyqtSignal(bool)
@QtCore.pyqtSlot(float)
def update_progress(self, time):
"""
Notify about the current position of the media
"""
log.warning(time)
self.progress.emit(time)
@QtCore.pyqtSlot(float)
def update_duration(self, time):
"""
Notify about the duration of the media
"""
log.warning(time)
self.duration.emit(time)
@QtCore.pyqtSlot(float)
def update_volume(self, level):
"""
Notify about the volume of the media
"""
log.warning(level)
level = level * 100
self.volume.emit(level)
@QtCore.pyqtSlot(float)
def update_playback_rate(self, rate):
"""
Notify about the playback rate of the media
"""
log.warning(rate)
self.playback_rate.emit(rate)
@QtCore.pyqtSlot(bool)
def has_ended(self, is_ended):
"""
Notify that the media has ended playing
"""
log.warning(is_ended)
self.ended.emit(is_ended)
@QtCore.pyqtSlot(bool)
def has_muted(self, is_muted):
"""
Notify that the media has been muted
"""
log.warning(is_muted)
self.muted.emit(is_muted)
class DisplayWindow(QtWidgets.QWidget):
"""
This is a window to show the output
"""
def __init__(self, parent=None, screen=None):
"""
Create the display window
"""
super(DisplayWindow, self).__init__(parent)
# Need to import this inline to get around a QtWebEngine issue
from openlp.core.display.webengine import WebEngineView
self._is_initialised = False
self._fbo = None
self.setWindowFlags(QtCore.Qt.FramelessWindowHint | QtCore.Qt.Tool | QtCore.Qt.WindowStaysOnTopHint)
self.setAttribute(QtCore.Qt.WA_TranslucentBackground)
self.setAutoFillBackground(True)
self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
self.layout = QtWidgets.QVBoxLayout(self)
self.layout.setContentsMargins(0, 0, 0, 0)
self.webview = WebEngineView(self)
self.webview.setAttribute(QtCore.Qt.WA_TranslucentBackground)
self.webview.page().setBackgroundColor(QtCore.Qt.transparent)
self.layout.addWidget(self.webview)
self.webview.loadFinished.connect(self.after_loaded)
self.set_url(QtCore.QUrl.fromLocalFile(path_to_str(DISPLAY_PATH)))
self.media_watcher = MediaWatcher(self)
self.channel = QtWebChannel.QWebChannel(self)
self.channel.registerObject('mediaWatcher', self.media_watcher)
self.webview.page().setWebChannel(self.channel)
self.is_display = False
self.scale = 1
self.hide_mode = None
if screen and screen.is_display:
Registry().register_function('live_display_hide', self.hide_display)
Registry().register_function('live_display_show', self.show_display)
self.update_from_screen(screen)
self.is_display = True
# Only make visible on single monitor setup if setting enabled.
if len(ScreenList()) > 1 or Settings().value('core/display on monitor'):
self.show()
def update_from_screen(self, screen):
"""
Update the number and the geometry from the screen.
:param Screen screen: A `~openlp.core.display.screens.Screen` instance
"""
self.setGeometry(screen.display_geometry)
self.screen_number = screen.number
def set_single_image(self, bg_color, image_path):
"""
:param str bg_color: Background color
:param Path image_path: Path to the image
"""
image_uri = image_path.as_uri()
self.run_javascript('Display.setFullscreenImage("{bg_color}", "{image}");'.format(bg_color=bg_color,
image=image_uri))
def set_single_image_data(self, bg_color, image_data):
self.run_javascript('Display.setFullscreenImageFromData("{bg_color}", '
'"{image_data}");'.format(bg_color=bg_color, image_data=image_data))
def set_startup_screen(self):
bg_color = Settings().value('core/logo background color')
image = Settings().value('core/logo file')
if path_to_str(image).startswith(':'):
image = OPENLP_SPLASH_SCREEN_PATH
image_uri = image.as_uri()
self.run_javascript('Display.setStartupSplashScreen("{bg_color}", "{image}");'.format(bg_color=bg_color,
image=image_uri))
def set_url(self, url):
"""
Set the URL of the webview
:param str url: The URL to set
"""
if not isinstance(url, QtCore.QUrl):
url = QtCore.QUrl(url)
self.webview.setUrl(url)
def set_html(self, html):
"""
Set the html
"""
self.webview.setHtml(html)
def after_loaded(self):
"""
Add stuff after page initialisation
"""
self.run_javascript('Display.init();')
self._is_initialised = True
self.set_startup_screen()
# Make sure the scale is set if it was attempted set before init
if self.scale != 1:
self.set_scale(self.scale)
def run_javascript(self, script, is_sync=False):
"""
Run some Javascript in the WebView
:param script: The script to run, a string
:param is_sync: Run the script synchronously. Defaults to False
"""
log.debug(script)
if not is_sync:
self.webview.page().runJavaScript(script)
else:
self.__script_done = False
self.__script_result = None
def handle_result(result):
"""
Handle the result from the asynchronous call
"""
self.__script_done = True
self.__script_result = result
self.webview.page().runJavaScript(script, handle_result)
while not self.__script_done:
# TODO: Figure out how to break out of a potentially infinite loop
QtWidgets.QApplication.instance().processEvents()
return self.__script_result
def go_to_slide(self, verse):
"""
Go to a particular slide.
:param str verse: The verse to go to, e.g. "V1" for songs, or just "0" for other types
"""
self.run_javascript('Display.goToSlide("{verse}");'.format(verse=verse))
def load_verses(self, verses):
"""
Set verses in the display
"""
json_verses = json.dumps(verses)
self.run_javascript('Display.setTextSlides({verses});'.format(verses=json_verses))
def load_images(self, images):
"""
Set images in the display
"""
for image in images:
if not image['path'].startswith('file://'):
image['path'] = 'file://' + image['path']
json_images = json.dumps(images)
self.run_javascript('Display.setImageSlides({images});'.format(images=json_images))
def load_video(self, video):
"""
Load video in the display
"""
if not video['path'].startswith('file://'):
video['path'] = 'file://' + video['path']
json_video = json.dumps(video)
self.run_javascript('Display.setVideo({video});'.format(video=json_video))
def play_video(self):
"""
Play the currently loaded video
"""
self.run_javascript('Display.playVideo();')
def pause_video(self):
"""
Pause the currently playing video
"""
self.run_javascript('Display.pauseVideo();')
def stop_video(self):
"""
Stop the currently playing video
"""
self.run_javascript('Display.stopVideo();')
def set_video_playback_rate(self, rate):
"""
Set the playback rate of the current video.
The rate can be any valid float, with 0.0 being stopped, 1.0 being normal speed,
over 1.0 is faster, under 1.0 is slower, and negative is backwards.
:param rate: A float indicating the playback rate.
"""
self.run_javascript('Display.setPlaybackRate({rate});'.format(rate=rate))
def set_video_volume(self, level):
"""
Set the volume of the current video.
The volume should be an int from 0 to 100, where 0 is no sound and 100 is maximum volume. Any
values outside this range will raise a ``ValueError``.
:param level: A number between 0 and 100
"""
if level < 0 or level > 100:
raise ValueError('Volume should be from 0 to 100, was "{}"'.format(level))
self.run_javascript('Display.setVideoVolume({level});'.format(level=level))
def toggle_video_mute(self):
"""
Toggle the mute of the current video
"""
self.run_javascript('Display.toggleVideoMute();')
def save_screenshot(self, fname=None):
"""
Save a screenshot, either returning it or saving it to file
"""
pixmap = self.grab()
if fname:
ext = os.path.splitext(fname)[-1][1:]
pixmap.save(fname, ext)
else:
return pixmap
def set_theme(self, theme):
"""
Set the theme of the display
"""
# If background is transparent and this is not a display, inject checkerboard background image instead
if theme.background_type == 'transparent' and not self.is_display:
theme_copy = copy.deepcopy(theme)
theme_copy.background_type = 'image'
theme_copy.background_filename = CHECKERBOARD_PATH
exported_theme = theme_copy.export_theme()
else:
exported_theme = theme.export_theme()
self.run_javascript('Display.setTheme({theme});'.format(theme=exported_theme))
def get_video_types(self):
"""
Get the types of videos playable by the embedded media player
"""
return self.run_javascript('Display.getVideoTypes();', is_sync=True)
def show_display(self):
"""
Show the display
"""
if self.is_display:
# Only make visible on single monitor setup if setting enabled.
if len(ScreenList()) == 1 and not Settings().value('core/display on monitor'):
return
self.run_javascript('Display.show();')
# Check if setting for hiding logo on startup is enabled.
# If it is, display should remain hidden, otherwise logo is shown. (from def setup)
if self.isHidden() and not Settings().value('core/logo hide on startup'):
self.setVisible(True)
self.hide_mode = None
# Trigger actions when display is active again.
if self.is_display:
Registry().execute('live_display_active')
def blank_to_theme(self):
"""
Blank to theme
"""
self.run_javascript('Display.blankToTheme();')
def hide_display(self, mode=HideMode.Screen):
"""
Hide the display by making all layers transparent Store the images so they can be replaced when required
:param mode: How the screen is to be hidden
"""
log.debug('hide_display mode = {mode:d}'.format(mode=mode))
if self.is_display:
# Only make visible on single monitor setup if setting enabled.
if len(ScreenList()) == 1 and not Settings().value('core/display on monitor'):
return
if mode == HideMode.Screen:
self.setVisible(False)
elif mode == HideMode.Blank:
self.run_javascript('Display.blankToBlack();')
else:
self.run_javascript('Display.blankToTheme();')
if mode != HideMode.Screen:
if self.isHidden():
self.setVisible(True)
self.webview.setVisible(True)
self.hide_mode = mode
def set_scale(self, scale):
"""
Set the HTML scale
"""
self.scale = scale
self.run_javascript('Display.setScale({scale});'.format(scale=scale * 100))
def alert(self, text, location):
"""
Set an alert
"""
self.run_javascript('Display.alert({text}, {location});'.format(text=text, location=location))

View File

@ -23,21 +23,15 @@
The :mod:`lib` module contains most of the components and libraries that make
OpenLP work.
"""
import html
import logging
import math
import re
from PyQt5 import QtCore, QtGui, QtWidgets
from openlp.core.common.i18n import translate
from openlp.core.common.path import Path
from openlp.core.lib.formattingtags import FormattingTags
log = logging.getLogger(__name__ + '.__init__')
SLIMCHARS = 'fiíIÍjlĺľrtť.,;/ ()|"\'!:\\'
class ServiceItemContext(object):
"""
@ -84,6 +78,103 @@ class ServiceItemAction(object):
Next = 3
class ItemCapabilities(object):
"""
Provides an enumeration of a service item's capabilities
``CanPreview``
The capability to allow the ServiceManager to add to the preview tab when making the previous item live.
``CanEdit``
The capability to allow the ServiceManager to allow the item to be edited
``CanMaintain``
The capability to allow the ServiceManager to allow the item to be reordered.
``RequiresMedia``
Determines is the service_item needs a Media Player
``CanLoop``
The capability to allow the SlideController to allow the loop processing.
``CanAppend``
The capability to allow the ServiceManager to add leaves to the
item
``NoLineBreaks``
The capability to remove lines breaks in the renderer
``OnLoadUpdate``
The capability to update MediaManager when a service Item is loaded.
``AddIfNewItem``
Not Used
``ProvidesOwnDisplay``
The capability to tell the SlideController the service Item has a different display.
``HasDetailedTitleDisplay``
Being Removed and decommissioned.
``HasVariableStartTime``
The capability to tell the ServiceManager that a change to start time is possible.
``CanSoftBreak``
The capability to tell the renderer that Soft Break is allowed
``CanWordSplit``
The capability to tell the renderer that it can split words is
allowed
``HasBackgroundAudio``
That a audio file is present with the text.
``CanAutoStartForLive``
The capability to ignore the do not play if display blank flag.
``CanEditTitle``
The capability to edit the title of the item
``IsOptical``
Determines is the service_item is based on an optical device
``HasDisplayTitle``
The item contains 'displaytitle' on every frame which should be
preferred over 'title' when displaying the item
``HasNotes``
The item contains 'notes'
``HasThumbnails``
The item has related thumbnails available
``HasMetaData``
The item has Meta Data about item
"""
CanPreview = 1
CanEdit = 2
CanMaintain = 3
RequiresMedia = 4
CanLoop = 5
CanAppend = 6
NoLineBreaks = 7
OnLoadUpdate = 8
AddIfNewItem = 9
ProvidesOwnDisplay = 10
# HasDetailedTitleDisplay = 11
HasVariableStartTime = 12
CanSoftBreak = 13
CanWordSplit = 14
HasBackgroundAudio = 15
CanAutoStartForLive = 16
CanEditTitle = 17
IsOptical = 18
HasDisplayTitle = 19
HasNotes = 20
HasThumbnails = 21
HasMetaData = 22
def get_text_file_string(text_file_path):
"""
Open a file and return its content as a string. If the supplied file path is not a file then the function
@ -288,309 +379,6 @@ def check_item_selected(list_widget, message):
return True
def clean_tags(text, remove_chords=False):
"""
Remove Tags from text for display
:param text: Text to be cleaned
:param remove_chords: Clean ChordPro tags
"""
text = text.replace('<br>', '\n')
text = text.replace('{br}', '\n')
text = text.replace('&nbsp;', ' ')
for tag in FormattingTags.get_html_tags():
text = text.replace(tag['start tag'], '')
text = text.replace(tag['end tag'], '')
# Remove ChordPro tags
if remove_chords:
text = re.sub(r'\[.+?\]', r'', text)
return text
def expand_tags(text, expand_chord_tags=False, for_printing=False):
"""
Expand tags HTML for display
:param text: The text to be expanded.
"""
if expand_chord_tags:
if for_printing:
text = expand_chords_for_printing(text, '{br}')
else:
text = expand_chords(text)
for tag in FormattingTags.get_html_tags():
text = text.replace(tag['start tag'], tag['start html'])
text = text.replace(tag['end tag'], tag['end html'])
return text
def expand_and_align_chords_in_line(match):
"""
Expand the chords in the line and align them using whitespaces.
NOTE: There is equivalent javascript code in chords.js, in the updateSlide function. Make sure to update both!
:param match:
:return: The line with expanded html-chords
"""
whitespaces = ''
chordlen = 0
taillen = 0
# The match could be "[G]sweet the " from a line like "A[D]mazing [D7]grace! How [G]sweet the [D]sound!"
# The actual chord, would be "G" in match "[G]sweet the "
chord = match.group(1)
# The tailing word of the chord, would be "sweet" in match "[G]sweet the "
tail = match.group(2)
# The remainder of the line, until line end or next chord. Would be " the " in match "[G]sweet the "
remainder = match.group(3)
# Line end if found, else None
end = match.group(4)
# Based on char width calculate width of chord
for chord_char in chord:
if chord_char not in SLIMCHARS:
chordlen += 2
else:
chordlen += 1
# Based on char width calculate width of tail
for tail_char in tail:
if tail_char not in SLIMCHARS:
taillen += 2
else:
taillen += 1
# Based on char width calculate width of remainder
for remainder_char in remainder:
if remainder_char not in SLIMCHARS:
taillen += 2
else:
taillen += 1
# If the chord is wider than the tail+remainder and the line goes on, some padding is needed
if chordlen >= taillen and end is None:
# Decide if the padding should be "_" for drawing out words or spaces
if tail:
if not remainder:
for c in range(math.ceil((chordlen - taillen) / 2) + 2):
whitespaces += '_'
else:
for c in range(chordlen - taillen + 1):
whitespaces += '&nbsp;'
else:
if not remainder:
for c in range(math.floor((chordlen - taillen) / 2)):
whitespaces += '_'
else:
for c in range(chordlen - taillen + 1):
whitespaces += '&nbsp;'
else:
if not tail and remainder and remainder[0] == ' ':
for c in range(chordlen):
whitespaces += '&nbsp;'
if whitespaces:
if '_' in whitespaces:
ws_length = len(whitespaces)
if ws_length == 1:
whitespaces = '&ndash;'
else:
wsl_mod = ws_length // 2
ws_right = ws_left = ' ' * wsl_mod
whitespaces = ws_left + '&ndash;' + ws_right
whitespaces = '<span class="ws">' + whitespaces + '</span>'
return '<span class="chord"><span><strong>' + html.escape(chord) + '</strong></span></span>' + html.escape(tail) + \
whitespaces + html.escape(remainder)
def expand_chords(text):
"""
Expand ChordPro tags
:param text:
"""
text_lines = text.split('{br}')
expanded_text_lines = []
chords_on_prev_line = False
for line in text_lines:
# If a ChordPro is detected in the line, replace it with a html-span tag and wrap the line in a span tag.
if '[' in line and ']' in line:
if chords_on_prev_line:
new_line = '<span class="chordline">'
else:
new_line = '<span class="chordline firstchordline">'
chords_on_prev_line = True
# Matches a chord, a tail, a remainder and a line end. See expand_and_align_chords_in_line() for more info.
new_line += re.sub(r'\[(.*?)\]([\u0080-\uFFFF,\w]*)'
r'([\u0080-\uFFFF,\w,\s,\.,\,,\!,\?,\;,\:,\|,\",\',\-,\_]*)(\Z)?',
expand_and_align_chords_in_line, line)
new_line += '</span>'
expanded_text_lines.append(new_line)
else:
chords_on_prev_line = False
expanded_text_lines.append(html.escape(line))
return '{br}'.join(expanded_text_lines)
def compare_chord_lyric(chord, lyric):
"""
Compare the width of chord and lyrics. If chord width is greater than the lyric width the diff is returned.
:param chord:
:param lyric:
:return:
"""
chordlen = 0
if chord == '&nbsp;':
return 0
chord = re.sub(r'\{.*?\}', r'', chord)
lyric = re.sub(r'\{.*?\}', r'', lyric)
for chord_char in chord:
if chord_char not in SLIMCHARS:
chordlen += 2
else:
chordlen += 1
lyriclen = 0
for lyric_char in lyric:
if lyric_char not in SLIMCHARS:
lyriclen += 2
else:
lyriclen += 1
if chordlen > lyriclen:
return chordlen - lyriclen
else:
return 0
def find_formatting_tags(text, active_formatting_tags):
"""
Look for formatting tags in lyrics and adds/removes them to/from the given list. Returns the update list.
:param text:
:param active_formatting_tags:
:return:
"""
if not re.search(r'\{.*?\}', text):
return active_formatting_tags
word_it = iter(text)
# Loop through lyrics to find any formatting tags
for char in word_it:
if char == '{':
tag = ''
char = next(word_it)
start_tag = True
if char == '/':
start_tag = False
char = next(word_it)
while char != '}':
tag += char
char = next(word_it)
# See if the found tag has an end tag
for formatting_tag in FormattingTags.get_html_tags():
if formatting_tag['start tag'] == '{' + tag + '}':
if formatting_tag['end tag']:
if start_tag:
# prepend the new tag to the list of active formatting tags
active_formatting_tags[:0] = [tag]
else:
# remove the tag from the list
active_formatting_tags.remove(tag)
# Break out of the loop matching the found tag against the tag list.
break
return active_formatting_tags
def expand_chords_for_printing(text, line_split):
"""
Expand ChordPro tags
:param text:
:param line_split:
"""
if not re.search(r'\[.*?\]', text):
return text
text_lines = text.split(line_split)
expanded_text_lines = []
for line in text_lines:
# If a ChordPro is detected in the line, build html tables.
new_line = '<table class="line" width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td>'
active_formatting_tags = []
if re.search(r'\[.*?\]', line):
words = line.split(' ')
in_chord = False
for word in words:
chords = []
lyrics = []
new_line += '<table class="segment" cellpadding="0" cellspacing="0" border="0" align="left">'
# If the word contains a chord, we need to handle it.
if re.search(r'\[.*?\]', word):
chord = ''
lyric = ''
# Loop over each character of the word
for char in word:
if char == '[':
in_chord = True
if lyric != '':
if chord == '':
chord = '&nbsp;'
chords.append(chord)
lyrics.append(lyric)
chord = ''
lyric = ''
elif char == ']' and in_chord:
in_chord = False
elif in_chord:
chord += char
else:
lyric += char
if lyric != '' or chord != '':
if chord == '':
chord = '&nbsp;'
if lyric == '':
lyric = '&nbsp;'
chords.append(chord)
lyrics.append(lyric)
new_chord_line = '<tr class="chordrow">'
new_lyric_line = '</tr><tr>'
for i in range(len(lyrics)):
spacer = compare_chord_lyric(chords[i], lyrics[i])
# Handle formatting tags
start_formatting_tags = ''
if active_formatting_tags:
start_formatting_tags = '{' + '}{'.join(active_formatting_tags) + '}'
# Update list of active formatting tags
active_formatting_tags = find_formatting_tags(lyrics[i], active_formatting_tags)
end_formatting_tags = ''
if active_formatting_tags:
end_formatting_tags = '{/' + '}{/'.join(active_formatting_tags) + '}'
new_chord_line += '<td class="chord">%s</td>' % chords[i]
# Check if this is the last column, if so skip spacing calc and instead insert a single space
if i + 1 == len(lyrics):
new_lyric_line += '<td class="lyrics">{starttags}{lyrics}&nbsp;{endtags}</td>'.format(
starttags=start_formatting_tags, lyrics=lyrics[i], endtags=end_formatting_tags)
else:
spacing = ''
if spacer > 0:
space = '&nbsp;' * int(math.ceil(spacer / 2))
spacing = '<span class="chordspacing">%s-%s</span>' % (space, space)
new_lyric_line += '<td class="lyrics">{starttags}{lyrics}{spacing}{endtags}</td>'.format(
starttags=start_formatting_tags, lyrics=lyrics[i], spacing=spacing,
endtags=end_formatting_tags)
new_line += new_chord_line + new_lyric_line + '</tr>'
else:
start_formatting_tags = ''
if active_formatting_tags:
start_formatting_tags = '{' + '}{'.join(active_formatting_tags) + '}'
active_formatting_tags = find_formatting_tags(word, active_formatting_tags)
end_formatting_tags = ''
if active_formatting_tags:
end_formatting_tags = '{/' + '}{/'.join(active_formatting_tags) + '}'
new_line += '<tr class="chordrow"><td class="chord">&nbsp;</td></tr><tr><td class="lyrics">' \
'{starttags}{lyrics}&nbsp;{endtags}</td></tr>'.format(
starttags=start_formatting_tags, lyrics=word, endtags=end_formatting_tags)
new_line += '</table>'
else:
new_line += line
new_line += '</td></tr></table>'
expanded_text_lines.append(new_line)
# the {br} tag used to split lines is not inserted again since the style of the line-tables makes them redundant.
return ''.join(expanded_text_lines)
def create_separated_list(string_list):
"""
Returns a string that represents a join of a list of strings with a localized separator.

View File

@ -31,10 +31,10 @@ from urllib.parse import quote_plus as urlquote
from alembic.migration import MigrationContext
from alembic.operations import Operations
from sqlalchemy import Table, MetaData, Column, UnicodeText, types, create_engine
from sqlalchemy import Column, MetaData, Table, UnicodeText, create_engine, types
from sqlalchemy.engine.url import make_url
from sqlalchemy.exc import SQLAlchemyError, InvalidRequestError, DBAPIError, OperationalError, ProgrammingError
from sqlalchemy.orm import scoped_session, sessionmaker, mapper
from sqlalchemy.exc import DBAPIError, InvalidRequestError, OperationalError, ProgrammingError, SQLAlchemyError
from sqlalchemy.orm import mapper, scoped_session, sessionmaker
from sqlalchemy.pool import NullPool
from openlp.core.common import delete_file
@ -44,6 +44,7 @@ from openlp.core.common.json import OpenLPJsonDecoder, OpenLPJsonEncoder
from openlp.core.common.settings import Settings
from openlp.core.lib.ui import critical_error_message_box
log = logging.getLogger(__name__)

View File

@ -1,828 +0,0 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
###############################################################################
# OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- #
# Copyright (c) 2008-2018 OpenLP Developers #
# --------------------------------------------------------------------------- #
# This program is free software; you can redistribute it and/or modify it #
# under the terms of the GNU General Public License as published by the Free #
# Software Foundation; version 2 of the License. #
# #
# This program is distributed in the hope that it will be useful, but WITHOUT #
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
# more details. #
# #
# You should have received a copy of the GNU General Public License along #
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
r"""
This module is responsible for generating the HTML for :class:`~openlp.core.ui.maindisplay`. The ``build_html`` function
is the function which has to be called from outside. The generated and returned HTML will look similar to this::
<!DOCTYPE html>
<html>
<head>
<title>OpenLP Display</title>
<style>
*{
margin: 0;
padding: 0;
border: 0;
overflow: hidden;
-webkit-user-select: none;
}
body {
background-color: #000000;
}
.size {
position: absolute;
left: 0px;
top: 0px;
width: 100%;
height: 100%;
}
#black {
z-index: 8;
background-color: black;
display: none;
}
#bgimage {
z-index: 1;
}
#image {
z-index: 2;
}
#videobackboard {
z-index:3;
background-color: #000000;
}
#video {
background-color: #000000;
z-index:4;
}
#flash {
z-index:5;
}
#alert {
position: absolute;
left: 0px;
top: 0px;
z-index: 10;
width: 100%;
vertical-align: bottom;
font-family: DejaVu Sans;
font-size: 40pt;
color: #ffffff;
background-color: #660000;
word-wrap: break-word;
}
#footer {
position: absolute;
z-index: 6;
left: 10px;
bottom: 0px;
width: 1580px;
font-family: Nimbus Sans L;
font-size: 12pt;
color: #FFFFFF;
text-align: left;
white-space: nowrap;
}
/* lyric css */
.lyricstable {
z-index: 5;
position: absolute;
display: table;
left: 10px; top: 0px;
}
.lyricscell {
display: table-cell;
word-wrap: break-word;
-webkit-transition: opacity 0.4s ease;
white-space:pre-wrap; word-wrap: break-word; text-align: left; vertical-align: top; font-family: Nimbus
Sans L; font-size: 40pt; color: #FFFFFF; line-height: 100%; margin: 0;padding: 0; padding-bottom: 0;
padding-left: 4px; width: 1580px; height: 810px;
}
.lyricsmain {
-webkit-text-stroke: 0.125em #000000; -webkit-text-fill-color: #FFFFFF; text-shadow: #000000 5px 5px;
}
sup {
font-size: 0.6em;
vertical-align: top;
position: relative;
top: -0.3em;
}
/* Chords css */
.chordline {
line-height: 1.0em;
}
.chordline span.chord span {
position: relative;
}
.chordline span.chord span strong {
position: absolute;
top: -0.8em;
left: 0;
font-size: 75%;
font-weight: normal;
line-height: normal;
display: none;
}
.firstchordline {
line-height: 1.0em;
}
</style>
<script>
var timer = null;
var transition = false;
function show_video(state, path, volume, loop, variable_value){
// Sometimes video.currentTime stops slightly short of video.duration and video.ended is intermittent!
var video = document.getElementById('video');
if(volume != null){
video.volume = volume;
}
switch(state){
case 'load':
video.src = 'file:///' + path;
if(loop == true) {
video.loop = true;
}
video.load();
break;
case 'play':
video.play();
break;
case 'pause':
video.pause();
break;
case 'stop':
show_video('pause');
video.currentTime = 0;
break;
case 'close':
show_video('stop');
video.src = '';
break;
case 'length':
return video.duration;
case 'current_time':
return video.currentTime;
case 'seek':
video.currentTime = variable_value;
break;
case 'isEnded':
return video.ended;
case 'setVisible':
video.style.visibility = variable_value;
break;
case 'setBackBoard':
var back = document.getElementById('videobackboard');
back.style.visibility = variable_value;
break;
}
}
function getFlashMovieObject(movieName)
{
if (window.document[movieName]){
return window.document[movieName];
}
if (document.embeds && document.embeds[movieName]){
return document.embeds[movieName];
}
}
function show_flash(state, path, volume, variable_value){
var text = document.getElementById('flash');
var flashMovie = getFlashMovieObject("OpenLPFlashMovie");
var src = "src = 'file:///" + path + "'";
var view_parm = " wmode='opaque'" + " width='100%%'" + " height='100%%'";
var swf_parm = " name='OpenLPFlashMovie'" + " autostart='true' loop='false' play='true'" +
" hidden='false' swliveconnect='true' allowscriptaccess='always'" + " volume='" + volume + "'";
switch(state){
case 'load':
text.innerHTML = "<embed " + src + view_parm + swf_parm + "/>";
flashMovie = getFlashMovieObject("OpenLPFlashMovie");
flashMovie.Play();
break;
case 'play':
flashMovie.Play();
break;
case 'pause':
flashMovie.StopPlay();
break;
case 'stop':
flashMovie.StopPlay();
tempHtml = text.innerHTML;
text.innerHTML = '';
text.innerHTML = tempHtml;
break;
case 'close':
flashMovie.StopPlay();
text.innerHTML = '';
break;
case 'length':
return flashMovie.TotalFrames();
case 'current_time':
return flashMovie.CurrentFrame();
case 'seek':
// flashMovie.GotoFrame(variable_value);
break;
case 'isEnded':
//TODO check flash end
return false;
case 'setVisible':
text.style.visibility = variable_value;
break;
}
}
function show_alert(alerttext, position){
var text = document.getElementById('alert');
text.innerHTML = alerttext;
if(alerttext == '') {
text.style.visibility = 'hidden';
return 0;
}
if(position == ''){
position = getComputedStyle(text, '').verticalAlign;
}
switch(position)
{
case 'top':
text.style.top = '0px';
break;
case 'middle':
text.style.top = ((window.innerHeight - text.clientHeight) / 2)
+ 'px';
break;
case 'bottom':
text.style.top = (window.innerHeight - text.clientHeight)
+ 'px';
break;
}
text.style.visibility = 'visible';
return text.clientHeight;
}
function update_css(align, font, size, color, bgcolor){
var text = document.getElementById('alert');
text.style.fontSize = size + "pt";
text.style.fontFamily = font;
text.style.color = color;
text.style.backgroundColor = bgcolor;
switch(align)
{
case 'top':
text.style.top = '0px';
break;
case 'middle':
text.style.top = ((window.innerHeight - text.clientHeight) / 2)
+ 'px';
break;
case 'bottom':
text.style.top = (window.innerHeight - text.clientHeight)
+ 'px';
break;
}
}
function show_image(src){
var img = document.getElementById('image');
img.src = src;
if(src == '')
img.style.display = 'none';
else
img.style.display = 'block';
}
function show_blank(state){
var black = 'none';
var lyrics = '';
switch(state){
case 'theme':
lyrics = 'hidden';
break;
case 'black':
black = 'block';
break;
case 'desktop':
break;
}
document.getElementById('black').style.display = black;
document.getElementById('lyricsmain').style.visibility = lyrics;
document.getElementById('image').style.visibility = lyrics;
document.getElementById('footer').style.visibility = lyrics;
}
function show_footer(footertext){
document.getElementById('footer').innerHTML = footertext;
}
function show_text(new_text){
var match = /-webkit-text-fill-color:[^;"]+/gi;
if(timer != null)
clearTimeout(timer);
/*
QtWebkit bug with outlines and justify causing outline alignment
problems. (Bug 859950) Surround each word with a <span> to workaround,
but only in this scenario.
*/
var txt = document.getElementById('lyricsmain');
if(window.getComputedStyle(txt).textAlign == 'justify'){
if(window.getComputedStyle(txt).webkitTextStrokeWidth != '0px'){
new_text = new_text.replace(/(\s|&nbsp;)+(?![^<]*>)/g,
function(match) {
return '</span>' + match + '<span>';
});
new_text = '<span>' + new_text + '</span>';
}
}
text_fade('lyricsmain', new_text);
}
function text_fade(id, new_text){
/*
Show the text.
*/
var text = document.getElementById(id);
if(text == null) return;
if(!transition){
text.innerHTML = new_text;
return;
}
// Fade text out. 0.1 to minimize the time "nothing" is shown on the screen.
text.style.opacity = '0.1';
// Fade new text in after the old text has finished fading out.
timer = window.setTimeout(function(){_show_text(text, new_text)}, 400);
}
function _show_text(text, new_text) {
/*
Helper function to show the new_text delayed.
*/
text.innerHTML = new_text;
text.style.opacity = '1';
// Wait until the text is completely visible. We want to save the timer id, to be able to call
// clearTimeout(timer) when the text has changed before finishing fading.
timer = window.setTimeout(function(){timer = null;}, 400);
}
function show_text_completed(){
return (timer == null);
}
</script>
</head>
<body>
<img id="bgimage" class="size" style="display:none;" />
<img id="image" class="size" style="display:none;" />
<div id="videobackboard" class="size" style="visibility:hidden"></div>
<video id="video" class="size" style="visibility:hidden" autobuffer preload></video>
<div id="flash" class="size" style="visibility:hidden"></div>
<div id="alert" style="visibility:hidden"></div>
<div class="lyricstable"><div id="lyricsmain" style="opacity:1" class="lyricscell lyricsmain"></div></div>
<div id="footer" class="footer"></div>
<div id="black" class="size"></div>
</body>
</html>
"""
import logging
from string import Template
from PyQt5 import QtWebKit
from openlp.core.common.settings import Settings
from openlp.core.lib.theme import BackgroundType, BackgroundGradientType, VerticalType, HorizontalType
log = logging.getLogger(__name__)
HTML_SRC = Template(r"""
<!DOCTYPE html>
<html>
<head>
<title>OpenLP Display</title>
<style>
*{
margin: 0;
padding: 0;
border: 0;
overflow: hidden;
-webkit-user-select: none;
}
body {
${bg_css};
}
.size {
position: absolute;
left: 0px;
top: 0px;
width: 100%;
height: 100%;
}
#black {
z-index: 8;
background-color: black;
display: none;
}
#bgimage {
z-index: 1;
}
#image {
z-index: 2;
}
${css_additions}
#footer {
position: absolute;
z-index: 6;
${footer_css}
}
/* lyric css */${lyrics_css}
sup {
font-size: 0.6em;
vertical-align: top;
position: relative;
top: -0.3em;
}
/* Chords css */${chords_css}
</style>
<script>
var timer = null;
var transition = ${transitions};
${js_additions}
function show_image(src){
var img = document.getElementById('image');
img.src = src;
if(src == '')
img.style.display = 'none';
else
img.style.display = 'block';
}
function show_blank(state){
var black = 'none';
var lyrics = '';
switch(state){
case 'theme':
lyrics = 'hidden';
break;
case 'black':
black = 'block';
break;
case 'desktop':
break;
}
document.getElementById('black').style.display = black;
document.getElementById('lyricsmain').style.visibility = lyrics;
document.getElementById('image').style.visibility = lyrics;
document.getElementById('footer').style.visibility = lyrics;
}
function show_footer(footertext){
document.getElementById('footer').innerHTML = footertext;
}
function show_text(new_text){
var match = /-webkit-text-fill-color:[^;\"]+/gi;
if(timer != null)
clearTimeout(timer);
/*
QtWebkit bug with outlines and justify causing outline alignment
problems. (Bug 859950) Surround each word with a <span> to workaround,
but only in this scenario.
*/
var txt = document.getElementById('lyricsmain');
if(window.getComputedStyle(txt).textAlign == 'justify'){
if(window.getComputedStyle(txt).webkitTextStrokeWidth != '0px'){
new_text = new_text.replace(/(\s|&nbsp;)+(?![^<]*>)/g,
function(match) {
return '</span>' + match + '<span>';
});
new_text = '<span>' + new_text + '</span>';
}
}
text_fade('lyricsmain', new_text);
}
function text_fade(id, new_text){
/*
Show the text.
*/
var text = document.getElementById(id);
if(text == null) return;
if(!transition){
text.innerHTML = new_text;
return;
}
// Fade text out. 0.1 to minimize the time "nothing" is shown on the screen.
text.style.opacity = '0.1';
// Fade new text in after the old text has finished fading out.
timer = window.setTimeout(function(){_show_text(text, new_text)}, 400);
}
function _show_text(text, new_text) {
/*
Helper function to show the new_text delayed.
*/
text.innerHTML = new_text;
text.style.opacity = '1';
// Wait until the text is completely visible. We want to save the timer id, to be able to call
// clearTimeout(timer) when the text has changed before finishing fading.
timer = window.setTimeout(function(){timer = null;}, 400);
}
function show_text_completed(){
return (timer == null);
}
</script>
</head>
<body>
<img id="bgimage" class="size" ${bg_image} />
<img id="image" class="size" ${image} />
${html_additions}
<div class="lyricstable"><div id="lyricsmain" style="opacity:1" class="lyricscell lyricsmain"></div></div>
<div id="footer" class="footer"></div>
<div id="black" class="size"></div>
</body>
</html>
""")
LYRICS_SRC = Template("""
.lyricstable {
z-index: 5;
position: absolute;
display: table;
${stable}
}
.lyricscell {
display: table-cell;
word-wrap: break-word;
-webkit-transition: opacity 0.4s ease;
${lyrics}
}
.lyricsmain {
${main}
}
""")
FOOTER_SRC = Template("""
left: ${left}px;
bottom: ${bottom}px;
width: ${width}px;
font-family: ${family};
font-size: ${size}pt;
color: ${color};
text-align: left;
white-space: ${space};
""")
LYRICS_FORMAT_SRC = Template("""
${justify}word-wrap: break-word;
text-align: ${align};
vertical-align: ${valign};
font-family: ${font};
font-size: ${size}pt;
color: ${color};
line-height: ${line}%;
margin: 0;
padding: 0;
padding-bottom: ${bottom};
padding-left: ${left}px;
width: ${width}px;
height: ${height}px;${font_style}${font_weight}
""")
CHORDS_FORMAT = Template("""
.chordline {
line-height: ${chord_line_height};
}
.chordline span.chord span {
position: relative;
}
.chordline span.chord span strong {
position: absolute;
top: -0.8em;
left: 0;
font-size: 75%;
font-weight: normal;
line-height: normal;
display: ${chords_display};
}
.firstchordline {
line-height: ${first_chord_line_height};
}
.ws {
display: ${chords_display};
white-space: pre-wrap;
}""")
def build_html(item, screen, is_live, background, image=None, plugins=None):
"""
Build the full web paged structure for display
:param item: Service Item to be displayed
:param screen: Current display information
:param is_live: Item is going live, rather than preview/theme building
:param background: Theme background image - bytes
:param image: Image media item - bytes
:param plugins: The List of available plugins
"""
width = screen['size'].width()
height = screen['size'].height()
theme_data = item.theme_data
# Image generated and poked in
if background:
bgimage_src = 'src="data:image/png;base64,{image}"'.format(image=background)
elif item.bg_image_bytes:
bgimage_src = 'src="data:image/png;base64,{image}"'.format(image=item.bg_image_bytes)
else:
bgimage_src = 'style="display:none;"'
if image:
image_src = 'src="data:image/png;base64,{image}"'.format(image=image)
else:
image_src = 'style="display:none;"'
css_additions = ''
js_additions = ''
html_additions = ''
if plugins:
for plugin in plugins:
css_additions += plugin.get_display_css()
js_additions += plugin.get_display_javascript()
html_additions += plugin.get_display_html()
return HTML_SRC.substitute(bg_css=build_background_css(item, width),
css_additions=css_additions,
footer_css=build_footer_css(item, height),
lyrics_css=build_lyrics_css(item),
transitions='true' if (theme_data and
theme_data.display_slide_transition and
is_live) else 'false',
js_additions=js_additions,
bg_image=bgimage_src,
image=image_src,
html_additions=html_additions,
chords_css=build_chords_css())
def webkit_version():
"""
Return the Webkit version in use. Note method added relatively recently, so return 0 if prior to this
"""
try:
webkit_ver = float(QtWebKit.qWebKitVersion())
log.debug('Webkit version = {version}'.format(version=webkit_ver))
except AttributeError:
webkit_ver = 0.0
return webkit_ver
def build_background_css(item, width):
"""
Build the background css
:param item: Service Item containing theme and location information
:param width:
"""
width = int(width) // 2
theme = item.theme_data
background = 'background-color: black'
if theme:
if theme.background_type == BackgroundType.to_string(BackgroundType.Transparent):
background = ''
elif theme.background_type == BackgroundType.to_string(BackgroundType.Solid):
background = 'background-color: {theme}'.format(theme=theme.background_color)
else:
if theme.background_direction == BackgroundGradientType.to_string(BackgroundGradientType.Horizontal):
background = 'background: -webkit-gradient(linear, left top, left bottom, from({start}), to({end})) ' \
'fixed'.format(start=theme.background_start_color, end=theme.background_end_color)
elif theme.background_direction == BackgroundGradientType.to_string(BackgroundGradientType.LeftTop):
background = 'background: -webkit-gradient(linear, left top, right bottom, from({start}), to({end})) ' \
'fixed'.format(start=theme.background_start_color, end=theme.background_end_color)
elif theme.background_direction == BackgroundGradientType.to_string(BackgroundGradientType.LeftBottom):
background = 'background: -webkit-gradient(linear, left bottom, right top, from({start}), to({end})) ' \
'fixed'.format(start=theme.background_start_color, end=theme.background_end_color)
elif theme.background_direction == BackgroundGradientType.to_string(BackgroundGradientType.Vertical):
background = 'background: -webkit-gradient(linear, left top, right top, from({start}), to({end})) ' \
'fixed'.format(start=theme.background_start_color, end=theme.background_end_color)
else:
background = 'background: -webkit-gradient(radial, {width} 50%, 100, {width} 50%, {width}, ' \
'from({start}), to({end})) fixed'.format(width=width,
start=theme.background_start_color,
end=theme.background_end_color)
return background
def build_lyrics_css(item):
"""
Build the lyrics display css
:param item: Service Item containing theme and location information
"""
theme_data = item.theme_data
lyricstable = ''
lyrics = ''
lyricsmain = ''
if theme_data and item.main:
lyricstable = 'left: {left}px; top: {top}px;'.format(left=item.main.x(), top=item.main.y())
lyrics = build_lyrics_format_css(theme_data, item.main.width(), item.main.height())
lyricsmain += build_lyrics_outline_css(theme_data)
if theme_data.font_main_shadow:
lyricsmain += ' text-shadow: {theme} {shadow}px ' \
'{shadow}px;'.format(theme=theme_data.font_main_shadow_color,
shadow=theme_data.font_main_shadow_size)
return LYRICS_SRC.substitute(stable=lyricstable, lyrics=lyrics, main=lyricsmain)
def build_lyrics_outline_css(theme_data):
"""
Build the css which controls the theme outline. Also used by renderer for splitting verses
:param theme_data: Object containing theme information
"""
if theme_data.font_main_outline:
size = float(theme_data.font_main_outline_size) / 16
fill_color = theme_data.font_main_color
outline_color = theme_data.font_main_outline_color
return ' -webkit-text-stroke: {size}em {color}; -webkit-text-fill-color: {fill}; '.format(size=size,
color=outline_color,
fill=fill_color)
return ''
def build_lyrics_format_css(theme_data, width, height):
"""
Build the css which controls the theme format. Also used by renderer for splitting verses
:param theme_data: Object containing theme information
:param width: Width of the lyrics block
:param height: Height of the lyrics block
"""
align = HorizontalType.Names[theme_data.display_horizontal_align]
valign = VerticalType.Names[theme_data.display_vertical_align]
left_margin = (int(theme_data.font_main_outline_size) * 2) if theme_data.font_main_outline else 0
# fix tag incompatibilities
justify = '' if (theme_data.display_horizontal_align == HorizontalType.Justify) else ' white-space: pre-wrap;\n'
padding_bottom = '0.5em' if (theme_data.display_vertical_align == VerticalType.Bottom) else '0'
return LYRICS_FORMAT_SRC.substitute(justify=justify,
align=align,
valign=valign,
font=theme_data.font_main_name,
size=theme_data.font_main_size,
color=theme_data.font_main_color,
line='{line:d}'.format(line=100 + int(theme_data.font_main_line_adjustment)),
bottom=padding_bottom,
left=left_margin,
width=width,
height=height,
font_style='\n font-style: italic;' if theme_data.font_main_italics else '',
font_weight='\n font-weight: bold;' if theme_data.font_main_bold else '')
def build_footer_css(item, height):
"""
Build the display of the item footer
:param item: Service Item to be processed.
:param height:
"""
theme = item.theme_data
if not theme or not item.footer:
return ''
bottom = height - int(item.footer.y()) - int(item.footer.height())
whitespace = 'normal' if Settings().value('themes/wrap footer') else 'nowrap'
return FOOTER_SRC.substitute(left=item.footer.x(), bottom=bottom, width=item.footer.width(),
family=theme.font_footer_name, size=theme.font_footer_size,
color=theme.font_footer_color, space=whitespace)
def build_chords_css():
if Settings().value('songs/enable chords') and Settings().value('songs/mainview chords'):
chord_line_height = '2.0em'
chords_display = 'inline'
first_chord_line_height = '2.1em'
else:
chord_line_height = '1.0em'
chords_display = 'none'
first_chord_line_height = '1.0em'
return CHORDS_FORMAT.substitute(chord_line_height=chord_line_height, chords_display=chords_display,
first_chord_line_height=first_chord_line_height)

View File

@ -34,9 +34,10 @@ from PyQt5 import QtCore
from openlp.core.common.registry import Registry
from openlp.core.common.settings import Settings
from openlp.core.display.screens import ScreenList
from openlp.core.lib import resize_image, image_to_byte
from openlp.core.lib import image_to_byte, resize_image
from openlp.core.threading import ThreadWorker, run_thread
log = logging.getLogger(__name__)
@ -184,8 +185,8 @@ class ImageManager(QtCore.QObject):
super(ImageManager, self).__init__()
Registry().register('image_manager', self)
current_screen = ScreenList().current
self.width = current_screen['size'].width()
self.height = current_screen['size'].height()
self.width = current_screen.display_geometry.width()
self.height = current_screen.display_geometry.height()
self._cache = {}
self._conversion_queue = PriorityQueue()
self.stop_manager = False
@ -197,8 +198,8 @@ class ImageManager(QtCore.QObject):
"""
log.debug('update_display')
current_screen = ScreenList().current
self.width = current_screen['size'].width()
self.height = current_screen['size'].height()
self.width = current_screen.display_geometry.width()
self.height = current_screen.display_geometry.height()
# Mark the images as dirty for a rebuild by setting the image and byte stream to None.
for image in list(self._cache.values()):
self._reset_image(image)

View File

@ -28,7 +28,6 @@ import re
from PyQt5 import QtCore, QtWidgets
from openlp.core.common.i18n import UiStrings, translate
from openlp.core.ui.icons import UiIcons
from openlp.core.common.mixins import RegistryProperties
from openlp.core.common.path import path_to_str, str_to_path
from openlp.core.common.registry import Registry
@ -37,11 +36,13 @@ from openlp.core.lib import ServiceItemContext
from openlp.core.lib.plugin import StringContent
from openlp.core.lib.serviceitem import ServiceItem
from openlp.core.lib.ui import create_widget_action, critical_error_message_box
from openlp.core.ui.icons import UiIcons
from openlp.core.widgets.dialogs import FileDialog
from openlp.core.widgets.edits import SearchEdit
from openlp.core.widgets.toolbar import OpenLPToolbar
from openlp.core.widgets.views import ListWidgetWithDnD
log = logging.getLogger(__name__)
@ -108,8 +109,8 @@ class MediaManagerItem(QtWidgets.QWidget, RegistryProperties):
self.page_layout.setSpacing(0)
self.page_layout.setContentsMargins(0, 0, 0, 0)
self.required_icons()
self.setupUi()
self.retranslateUi()
self.setup_ui()
self.retranslate_ui()
self.auto_select_id = -1
def setup_item(self):
@ -133,7 +134,7 @@ class MediaManagerItem(QtWidgets.QWidget, RegistryProperties):
self.can_make_live = True
self.can_add_to_service = True
def retranslateUi(self):
def retranslate_ui(self):
"""
This method is called automatically to provide OpenLP with the opportunity to translate the ``MediaManagerItem``
to another language.
@ -148,7 +149,7 @@ class MediaManagerItem(QtWidgets.QWidget, RegistryProperties):
self.toolbar = OpenLPToolbar(self)
self.page_layout.addWidget(self.toolbar)
def setupUi(self):
def setup_ui(self):
"""
This method sets up the interface on the button. Plugin developers use this to add and create toolbars, and the
rest of the interface of the media manager item.

View File

@ -30,6 +30,7 @@ from openlp.core.common.registry import Registry, RegistryBase
from openlp.core.common.settings import Settings
from openlp.core.version import get_version
log = logging.getLogger(__name__)

View File

@ -156,6 +156,7 @@ class PluginManager(RegistryBase, LogMixin, RegistryProperties):
Loop through all the plugins and give them an opportunity to initialise themselves.
"""
uninitialised_plugins = []
for plugin in State().list_plugins():
if plugin:
self.log_info('initialising plugins {plugin} in a {state} state'.format(plugin=plugin.name,
@ -168,6 +169,7 @@ class PluginManager(RegistryBase, LogMixin, RegistryProperties):
uninitialised_plugins.append(plugin.name.title())
self.log_exception('Unable to initialise plugin {plugin}'.format(plugin=plugin.name))
display_text = ''
if uninitialised_plugins:
display_text = translate('OpenLP.PluginManager', 'Unable to initialise the following plugins:') + \
'\n\n'.join(uninitialised_plugins) + '\n\n'

View File

@ -24,11 +24,11 @@ The :mod:`serviceitem` provides the service item functionality including the
type and capability of an item.
"""
import datetime
import html
import logging
import ntpath
import os
import uuid
from copy import deepcopy
from PyQt5 import QtGui
@ -36,11 +36,13 @@ from openlp.core.state import State
from openlp.core.common import md5_hash
from openlp.core.common.applocation import AppLocation
from openlp.core.common.i18n import translate
from openlp.core.ui.icons import UiIcons
from openlp.core.common.mixins import RegistryProperties
from openlp.core.common.path import Path
from openlp.core.common.settings import Settings
from openlp.core.lib import ImageSource, clean_tags, expand_tags, expand_chords
from openlp.core.display.render import remove_tags, render_tags
from openlp.core.lib import ItemCapabilities
from openlp.core.ui.icons import UiIcons
log = logging.getLogger(__name__)
@ -54,103 +56,6 @@ class ServiceItemType(object):
Command = 3
class ItemCapabilities(object):
"""
Provides an enumeration of a service item's capabilities
``CanPreview``
The capability to allow the ServiceManager to add to the preview tab when making the previous item live.
``CanEdit``
The capability to allow the ServiceManager to allow the item to be edited
``CanMaintain``
The capability to allow the ServiceManager to allow the item to be reordered.
``RequiresMedia``
Determines is the service_item needs a Media Player
``CanLoop``
The capability to allow the SlideController to allow the loop processing.
``CanAppend``
The capability to allow the ServiceManager to add leaves to the
item
``NoLineBreaks``
The capability to remove lines breaks in the renderer
``OnLoadUpdate``
The capability to update MediaManager when a service Item is loaded.
``AddIfNewItem``
Not Used
``ProvidesOwnDisplay``
The capability to tell the SlideController the service Item has a different display.
``HasDetailedTitleDisplay``
Being Removed and decommissioned.
``HasVariableStartTime``
The capability to tell the ServiceManager that a change to start time is possible.
``CanSoftBreak``
The capability to tell the renderer that Soft Break is allowed
``CanWordSplit``
The capability to tell the renderer that it can split words is
allowed
``HasBackgroundAudio``
That a audio file is present with the text.
``CanAutoStartForLive``
The capability to ignore the do not play if display blank flag.
``CanEditTitle``
The capability to edit the title of the item
``IsOptical``
Determines is the service_item is based on an optical device
``HasDisplayTitle``
The item contains 'displaytitle' on every frame which should be
preferred over 'title' when displaying the item
``HasNotes``
The item contains 'notes'
``HasThumbnails``
The item has related thumbnails available
``HasMetaData``
The item has Meta Data about item
"""
CanPreview = 1
CanEdit = 2
CanMaintain = 3
RequiresMedia = 4
CanLoop = 5
CanAppend = 6
NoLineBreaks = 7
OnLoadUpdate = 8
AddIfNewItem = 9
ProvidesOwnDisplay = 10
# HasDetailedTitleDisplay = 11
HasVariableStartTime = 12
CanSoftBreak = 13
CanWordSplit = 14
HasBackgroundAudio = 15
CanAutoStartForLive = 16
CanEditTitle = 17
IsOptical = 18
HasDisplayTitle = 19
HasNotes = 20
HasThumbnails = 21
HasMetaData = 22
class ServiceItem(RegistryProperties):
"""
The service item is a base class for the plugins to use to interact with
@ -167,7 +72,10 @@ class ServiceItem(RegistryProperties):
"""
if plugin:
self.name = plugin.name
self._rendered_slides = None
self._display_slides = None
self.title = ''
self.slides = []
self.processor = None
self.audit = ''
self.items = []
@ -176,8 +84,6 @@ class ServiceItem(RegistryProperties):
self.foot_text = ''
self.theme = None
self.service_item_type = None
self._raw_frames = []
self._display_frames = []
self.unique_identifier = 0
self.notes = ''
self.from_plugin = False
@ -248,59 +154,58 @@ class ServiceItem(RegistryProperties):
else:
self.icon = UiIcons().clone
def render(self, provides_own_theme_data=False):
def _create_slides(self):
"""
The render method is what generates the frames for the screen and obtains the display information from the
renderer. At this point all slides are built for the given display size.
Create frames for rendering and display
"""
self._rendered_slides = []
self._display_slides = []
:param provides_own_theme_data: This switch disables the usage of the item's theme. However, this is
disabled by default. If this is used, it has to be taken care, that
the renderer knows the correct theme data. However, this is needed
for the theme manager.
"""
log.debug('Render called')
self._display_frames = []
self.bg_image_bytes = None
if not provides_own_theme_data:
self.renderer.set_item_theme(self.theme)
self.theme_data, self.main, self.footer = self.renderer.pre_render()
if self.service_item_type == ServiceItemType.Text:
expand_chord_tags = hasattr(self, 'name') and self.name == 'songs' and Settings().value(
'songs/enable chords')
log.debug('Formatting slides: {title}'.format(title=self.title))
# Save rendered pages to this dict. In the case that a slide is used twice we can use the pages saved to
# the dict instead of rendering them again.
previous_pages = {}
for slide in self._raw_frames:
verse_tag = slide['verseTag']
if verse_tag in previous_pages and previous_pages[verse_tag][0] == slide['raw_slide']:
pages = previous_pages[verse_tag][1]
else:
pages = self.renderer.format_slide(slide['raw_slide'], self)
previous_pages[verse_tag] = (slide['raw_slide'], pages)
for page in pages:
page = page.replace('<br>', '{br}')
html_data = expand_tags(page.rstrip(), expand_chord_tags)
new_frame = {
'title': clean_tags(page),
'text': clean_tags(page.rstrip(), expand_chord_tags),
'chords_text': expand_chords(clean_tags(page.rstrip(), False)),
'html': html_data.replace('&amp;nbsp;', '&nbsp;'),
'printing_html': expand_tags(html.escape(page.rstrip()), expand_chord_tags, True),
'verseTag': verse_tag,
}
self._display_frames.append(new_frame)
elif self.service_item_type == ServiceItemType.Image or self.service_item_type == ServiceItemType.Command:
pass
else:
log.error('Invalid value renderer: {item}'.format(item=self.service_item_type))
self.title = clean_tags(self.title)
# The footer should never be None, but to be compatible with a few
# nightly builds between 1.9.4 and 1.9.5, we have to correct this to
# avoid tracebacks.
if self.raw_footer is None:
self.raw_footer = []
# Save rendered pages to this dict. In the case that a slide is used twice we can use the pages saved to
# the dict instead of rendering them again.
previous_pages = {}
index = 0
self.foot_text = '<br>'.join([_f for _f in self.raw_footer if _f])
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:
rendered_slide = {
'title': raw_slide['title'],
'text': render_tags(page),
'verse': index,
'footer': self.foot_text,
}
self._rendered_slides.append(rendered_slide)
display_slide = {
'title': raw_slide['title'],
'text': remove_tags(page),
'verse': verse_tag,
}
self._display_slides.append(display_slide)
index += 1
@property
def rendered_slides(self):
"""
Render the frames and return them
"""
if not self._rendered_slides:
self._create_slides()
return self._rendered_slides
@property
def display_slides(self):
"""
Render the frames and return them
"""
if not self._display_slides:
self._create_slides()
return self._display_slides
def add_from_image(self, path, title, background=None, thumbnail=None):
"""
@ -308,31 +213,34 @@ class ServiceItem(RegistryProperties):
:param path: The directory in which the image file is located.
:param title: A title for the slide in the service item.
:param background:
:param background: The background colour
:param thumbnail: Optional alternative thumbnail, used for remote thumbnails.
"""
if background:
self.image_border = background
self.service_item_type = ServiceItemType.Image
if not thumbnail:
self._raw_frames.append({'title': title, 'path': path})
else:
self._raw_frames.append({'title': title, 'path': path, 'image': thumbnail})
self.image_manager.add_image(path, ImageSource.ImagePlugin, self.image_border)
slide = {'title': title, 'path': path}
if thumbnail:
slide['thumbnail'] = thumbnail
self.slides.append(slide)
# self.image_manager.add_image(path, ImageSource.ImagePlugin, self.image_border)
self._new_item()
def add_from_text(self, raw_slide, verse_tag=None):
def add_from_text(self, text, verse_tag=None):
"""
Add a text slide to the service item.
:param raw_slide: The raw text of the slide.
:param text: The raw text of the slide.
:param verse_tag:
"""
if verse_tag:
verse_tag = verse_tag.upper()
else:
# For items that don't have a verse tag, autoincrement the slide numbers
verse_tag = str(len(self.slides))
self.service_item_type = ServiceItemType.Text
title = raw_slide[:30].split('\n')[0]
self._raw_frames.append({'title': title, 'raw_slide': raw_slide, 'verseTag': verse_tag})
title = text[:30].split('\n')[0]
self.slides.append({'title': title, 'text': text, 'verse': verse_tag})
self._new_item()
def add_from_command(self, path, file_name, image, display_title=None, notes=None):
@ -349,17 +257,17 @@ class ServiceItem(RegistryProperties):
# If the item should have a display title but this frame doesn't have one, we make one up
if self.is_capable(ItemCapabilities.HasDisplayTitle) and not display_title:
display_title = translate('OpenLP.ServiceItem',
'[slide {frame:d}]').format(frame=len(self._raw_frames) + 1)
'[slide {frame:d}]').format(frame=len(self.slides) + 1)
# Update image path to match servicemanager location if file was loaded from service
if image and not self.has_original_files and self.name == 'presentations':
file_location = os.path.join(path, file_name)
file_location_hash = md5_hash(file_location.encode('utf-8'))
image = os.path.join(str(AppLocation.get_section_data_path(self.name)), 'thumbnails',
file_location_hash, ntpath.basename(image))
self._raw_frames.append({'title': file_name, 'image': image, 'path': path,
'display_title': display_title, 'notes': notes})
if self.is_capable(ItemCapabilities.HasThumbnails):
self.image_manager.add_image(image, ImageSource.CommandPlugins, '#000000')
self.slides.append({'title': file_name, 'image': image, 'path': path, 'display_title': display_title,
'notes': notes, 'thumbnail': image})
# if self.is_capable(ItemCapabilities.HasThumbnails):
# self.image_manager.add_image(image, ImageSource.CommandPlugins, '#000000')
self._new_item()
def get_service_repr(self, lite_save):
@ -394,15 +302,19 @@ class ServiceItem(RegistryProperties):
}
service_data = []
if self.service_item_type == ServiceItemType.Text:
service_data = [slide for slide in self._raw_frames]
for slide in self.slides:
data_slide = deepcopy(slide)
data_slide['raw_slide'] = data_slide.pop('text')
data_slide['verseTag'] = data_slide.pop('verse')
service_data.append(data_slide)
elif self.service_item_type == ServiceItemType.Image:
if lite_save:
for slide in self._raw_frames:
for slide in self.slides:
service_data.append({'title': slide['title'], 'path': slide['path']})
else:
service_data = [slide['title'] for slide in self._raw_frames]
service_data = [slide['title'] for slide in self.slides]
elif self.service_item_type == ServiceItemType.Command:
for slide in self._raw_frames:
for slide in self.slides:
service_data.append({'title': slide['title'], 'image': slide['image'], 'path': slide['path'],
'display_title': slide['display_title'], 'notes': slide['notes']})
return {'header': service_header, 'data': service_data}
@ -454,7 +366,8 @@ class ServiceItem(RegistryProperties):
self.theme_overwritten = header.get('theme_overwritten', False)
if self.service_item_type == ServiceItemType.Text:
for slide in service_item['serviceitem']['data']:
self._raw_frames.append(slide)
self.add_from_text(slide['raw_slide'], slide['verseTag'])
self._create_slides()
elif self.service_item_type == ServiceItemType.Image:
settings_section = service_item['serviceitem']['header']['name']
background = QtGui.QColor(Settings().value(settings_section + '/background color'))
@ -478,7 +391,7 @@ class ServiceItem(RegistryProperties):
self.add_from_command(path, text_image['title'], text_image['image'],
text_image.get('display_title', ''), text_image.get('notes', ''))
else:
self.add_from_command(text_image['path'], text_image['title'], text_image['image'])
self.add_from_command(Path(text_image['path']), text_image['title'], text_image['image'])
self._new_item()
def get_display_title(self):
@ -489,10 +402,10 @@ class ServiceItem(RegistryProperties):
or self.is_capable(ItemCapabilities.CanEditTitle):
return self.title
else:
if len(self._raw_frames) > 1:
if len(self.slides) > 1:
return self.title
else:
return self._raw_frames[0]['title']
return self.slides[0]['title']
def merge(self, other):
"""
@ -508,7 +421,6 @@ class ServiceItem(RegistryProperties):
if other.theme is not None:
self.theme = other.theme
self._new_item()
self.render()
if self.is_capable(ItemCapabilities.HasBackgroundAudio):
log.debug(self.background_audio)
@ -578,9 +490,9 @@ class ServiceItem(RegistryProperties):
Returns the frames for the ServiceItem
"""
if self.service_item_type == ServiceItemType.Text:
return self._display_frames
return self.display_slides
else:
return self._raw_frames
return self.slides
def get_rendered_frame(self, row):
"""
@ -589,18 +501,19 @@ 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.display_frames[row]['html'].split('\n')[0]
return self.rendered_slides[row]['text']
elif self.service_item_type == ServiceItemType.Image:
return self._raw_frames[row]['path']
return self.slides[row]['path']
else:
return self._raw_frames[row]['image']
return self.slides[row]['image']
def get_frame_title(self, row=0):
"""
Returns the title of the raw frame
"""
try:
return self._raw_frames[row]['title']
return self.get_frames()[row]['title']
except IndexError:
return ''
@ -610,7 +523,7 @@ class ServiceItem(RegistryProperties):
"""
if not frame:
try:
frame = self._raw_frames[row]
frame = self.slides[row]
except IndexError:
return ''
if self.is_image() or self.is_capable(ItemCapabilities.IsOptical):
@ -627,8 +540,8 @@ class ServiceItem(RegistryProperties):
"""
Remove the specified frame from the item
"""
if frame in self._raw_frames:
self._raw_frames.remove(frame)
if frame in self.slides:
self.slides.remove(frame)
def get_media_time(self):
"""
@ -662,7 +575,6 @@ class ServiceItem(RegistryProperties):
self.theme_overwritten = (theme is None)
self.theme = theme
self._new_item()
self.render()
def remove_invalid_frames(self, invalid_paths=None):
"""
@ -677,29 +589,29 @@ class ServiceItem(RegistryProperties):
"""
Returns if there are any frames in the service item
"""
return not bool(self._raw_frames)
return not bool(self.slides)
def validate_item(self, suffix_list=None):
"""
Validates a service item to make sure it is valid
"""
self.is_valid = True
for frame in self._raw_frames:
if self.is_image() and not os.path.exists(frame['path']):
for slide in self.slides:
if self.is_image() and not os.path.exists(slide['path']):
self.is_valid = False
break
elif self.is_command():
if self.is_capable(ItemCapabilities.IsOptical) and State().check_preconditions('media'):
if not os.path.exists(frame['title']):
if not os.path.exists(slide['title']):
self.is_valid = False
break
else:
file_name = os.path.join(frame['path'], frame['title'])
file_name = os.path.join(slide['path'], slide['title'])
if not os.path.exists(file_name):
self.is_valid = False
break
if suffix_list and not self.is_text():
file_suffix = frame['title'].split('.')[-1]
file_suffix = slide['title'].split('.')[-1]
if file_suffix.lower() not in suffix_list:
self.is_valid = False
break

View File

@ -54,12 +54,12 @@ class SettingsTab(QtWidgets.QWidget, RegistryProperties):
"""
Run some initial setup. This method is separate from __init__ in order to mock it out in tests.
"""
self.setupUi()
self.retranslateUi()
self.setup_ui()
self.retranslate_ui()
self.initialise()
self.load()
def setupUi(self):
def setup_ui(self):
"""
Setup the tab's interface.
"""
@ -90,7 +90,7 @@ class SettingsTab(QtWidgets.QWidget, RegistryProperties):
left_width = max(left_width, self.left_column.minimumSizeHint().width())
self.left_column.setFixedWidth(left_width)
def retranslateUi(self):
def retranslate_ui(self):
"""
Setup the interface translation strings.
"""

View File

@ -31,7 +31,8 @@ from openlp.core.common import de_hump
from openlp.core.common.applocation import AppLocation
from openlp.core.common.json import OpenLPJsonDecoder, OpenLPJsonEncoder
from openlp.core.display.screens import ScreenList
from openlp.core.lib import str_to_bool, get_text_file_string
from openlp.core.lib import get_text_file_string, str_to_bool
log = logging.getLogger(__name__)
@ -202,13 +203,13 @@ class Theme(object):
Set the header and footer size into the current primary screen.
10 px on each side is removed to allow for a border.
"""
current_screen = ScreenList().current
current_screen_geometry = ScreenList().current.display_geometry
self.font_main_y = 0
self.font_main_width = current_screen['size'].width() - 20
self.font_main_height = current_screen['size'].height() * 9 / 10
self.font_footer_width = current_screen['size'].width() - 20
self.font_footer_y = current_screen['size'].height() * 9 / 10
self.font_footer_height = current_screen['size'].height() / 10
self.font_main_width = current_screen_geometry.width() - 20
self.font_main_height = current_screen_geometry.height() * 9 / 10
self.font_footer_width = current_screen_geometry.width() - 20
self.font_footer_y = current_screen_geometry.height() * 9 / 10
self.font_footer_height = current_screen_geometry.height() / 10
def load_theme(self, theme, theme_path=None):
"""

View File

@ -33,6 +33,7 @@ from openlp.core.common.registry import Registry
from openlp.core.lib import build_icon
from openlp.core.ui.icons import UiIcons
log = logging.getLogger(__name__)

View File

@ -26,7 +26,7 @@ The :mod:`~openlp.core.loader` module provides a bootstrap for the singleton cla
from openlp.core.state import State
from openlp.core.ui.media.mediacontroller import MediaController
from openlp.core.lib.pluginmanager import PluginManager
from openlp.core.display.renderer import Renderer
from openlp.core.display.render import Renderer
from openlp.core.lib.imagemanager import ImageManager
from openlp.core.ui.slidecontroller import LiveController, PreviewController

View File

@ -26,6 +26,7 @@ import logging
from openlp.core.common.i18n import translate
log = logging.getLogger(__name__)
log.debug('projector_constants loaded')

View File

@ -35,16 +35,19 @@ The Projector table keeps track of entries for controlled projectors.
"""
import logging
log = logging.getLogger(__name__)
log.debug('projector.lib.db module loaded')
from sqlalchemy import Column, ForeignKey, Integer, MetaData, String, and_
from sqlalchemy.ext.declarative import declarative_base, declared_attr
from sqlalchemy.orm import relationship
from openlp.core.lib.db import Manager, init_db, init_url
from openlp.core.projectors.constants import PJLINK_DEFAULT_CODES
from openlp.core.projectors import upgrade
from openlp.core.projectors.constants import PJLINK_DEFAULT_CODES
log = logging.getLogger(__name__)
log.debug('projector.lib.db module loaded')
Base = declarative_base(MetaData())

View File

@ -29,9 +29,10 @@ from PyQt5 import QtCore, QtWidgets
from openlp.core.common import verify_ip_address
from openlp.core.common.i18n import translate
from openlp.core.ui.icons import UiIcons
from openlp.core.projectors.constants import PJLINK_PORT
from openlp.core.projectors.db import Projector
from openlp.core.ui.icons import UiIcons
log = logging.getLogger(__name__)
log.debug('editform loaded')
@ -42,7 +43,7 @@ class Ui_ProjectorEditForm(object):
The :class:`~openlp.core.lib.ui.projector.editform.Ui_ProjectorEditForm` class defines
the user interface for the ProjectorEditForm dialog.
"""
def setupUi(self, edit_projector_dialog):
def setup_ui(self, edit_projector_dialog):
"""
Create the interface layout.
"""
@ -108,7 +109,7 @@ class Ui_ProjectorEditForm(object):
QtWidgets.QDialogButtonBox.Cancel)
self.dialog_layout.addWidget(self.button_box, 8, 0, 1, 2)
def retranslateUi(self, edit_projector_dialog):
def retranslate_ui(self, edit_projector_dialog):
if self.new_projector:
title = translate('OpenLP.ProjectorEditForm', 'Add New Projector')
self.projector.port = PJLINK_PORT
@ -150,7 +151,7 @@ class ProjectorEditForm(QtWidgets.QDialog, Ui_ProjectorEditForm):
super(ProjectorEditForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint |
QtCore.Qt.WindowCloseButtonHint)
self.projectordb = projectordb
self.setupUi(self)
self.setup_ui(self)
self.button_box.accepted.connect(self.accept_me)
self.button_box.helpRequested.connect(self.help_me)
self.button_box.rejected.connect(self.cancel_me)
@ -169,7 +170,7 @@ class ProjectorEditForm(QtWidgets.QDialog, Ui_ProjectorEditForm):
self.ip_text_label.setVisible(True)
# Since it's already defined, IP address is unchangeable, so focus on port number
self.port_text.setFocus()
self.retranslateUi(self)
self.retranslate_ui(self)
reply = QtWidgets.QDialog.exec(self)
return reply

View File

@ -30,22 +30,23 @@ import logging
from PyQt5 import QtCore, QtGui, QtWidgets
from openlp.core.common.i18n import translate
from openlp.core.ui.icons import UiIcons
from openlp.core.common.mixins import LogMixin, RegistryProperties
from openlp.core.common.registry import Registry, RegistryBase
from openlp.core.common.settings import Settings
from openlp.core.lib.ui import create_widget_action
from openlp.core.projectors import DialogSourceStyle
from openlp.core.projectors.constants import E_AUTHENTICATION, E_ERROR, E_NETWORK, E_NOT_CONNECTED, \
E_SOCKET_TIMEOUT, E_UNKNOWN_SOCKET_ERROR, S_CONNECTED, S_CONNECTING, S_COOLDOWN, S_INITIALIZE, \
S_NOT_CONNECTED, S_OFF, S_ON, S_STANDBY, S_WARMUP, STATUS_CODE, STATUS_MSG, QSOCKET_STATE
from openlp.core.projectors.constants import E_AUTHENTICATION, E_ERROR, E_NETWORK, E_NOT_CONNECTED, E_SOCKET_TIMEOUT,\
E_UNKNOWN_SOCKET_ERROR, QSOCKET_STATE, S_CONNECTED, S_CONNECTING, S_COOLDOWN, S_INITIALIZE, S_NOT_CONNECTED, S_OFF,\
S_ON, S_STANDBY, S_WARMUP, STATUS_CODE, STATUS_MSG
from openlp.core.projectors.db import ProjectorDB
from openlp.core.projectors.editform import ProjectorEditForm
from openlp.core.projectors.pjlink import PJLink, PJLinkUDP
from openlp.core.projectors.sourceselectform import SourceSelectTabs, SourceSelectSingle
from openlp.core.projectors.sourceselectform import SourceSelectSingle, SourceSelectTabs
from openlp.core.ui.icons import UiIcons
from openlp.core.widgets.toolbar import OpenLPToolbar
log = logging.getLogger(__name__)
log.debug('projectormanager loaded')

View File

@ -55,11 +55,12 @@ from PyQt5 import QtCore, QtNetwork
from openlp.core.common import qmd5_hash
from openlp.core.common.i18n import translate
from openlp.core.common.settings import Settings
from openlp.core.projectors.constants import CONNECTION_ERRORS, PJLINK_CLASS, PJLINK_DEFAULT_CODES, PJLINK_ERRORS, \
PJLINK_ERST_DATA, PJLINK_ERST_STATUS, PJLINK_MAX_PACKET, PJLINK_PREFIX, PJLINK_PORT, PJLINK_POWR_STATUS, \
PJLINK_SUFFIX, PJLINK_VALID_CMD, PROJECTOR_STATE, STATUS_CODE, STATUS_MSG, QSOCKET_STATE, \
E_AUTHENTICATION, E_CONNECTION_REFUSED, E_GENERAL, E_NETWORK, E_NOT_CONNECTED, E_SOCKET_TIMEOUT, \
S_CONNECTED, S_CONNECTING, S_NOT_CONNECTED, S_OFF, S_OK, S_ON, S_STANDBY
from openlp.core.projectors.constants import CONNECTION_ERRORS, E_AUTHENTICATION, E_CONNECTION_REFUSED, E_GENERAL, \
E_NETWORK, E_NOT_CONNECTED, E_SOCKET_TIMEOUT, PJLINK_CLASS, PJLINK_DEFAULT_CODES, PJLINK_ERRORS, PJLINK_ERST_DATA, \
PJLINK_ERST_STATUS, PJLINK_MAX_PACKET, PJLINK_PORT, PJLINK_POWR_STATUS, PJLINK_PREFIX, PJLINK_SUFFIX, \
PJLINK_VALID_CMD, PROJECTOR_STATE, QSOCKET_STATE, S_CONNECTED, S_CONNECTING, S_NOT_CONNECTED, S_OFF, S_OK, S_ON, \
S_STANDBY, STATUS_CODE, STATUS_MSG
log = logging.getLogger(__name__)
log.debug('pjlink loaded')

View File

@ -31,9 +31,10 @@ from PyQt5 import QtCore, QtWidgets
from openlp.core.common import is_macosx
from openlp.core.common.i18n import translate
from openlp.core.lib import build_icon
from openlp.core.projectors.constants import PJLINK_DEFAULT_SOURCES, PJLINK_DEFAULT_CODES
from openlp.core.projectors.constants import PJLINK_DEFAULT_CODES, PJLINK_DEFAULT_SOURCES
from openlp.core.projectors.db import ProjectorSource
log = logging.getLogger(__name__)

View File

@ -30,8 +30,9 @@ from openlp.core.common.i18n import UiStrings, translate
from openlp.core.common.registry import Registry
from openlp.core.common.settings import Settings
from openlp.core.lib.settingstab import SettingsTab
from openlp.core.ui.icons import UiIcons
from openlp.core.projectors import DialogSourceStyle
from openlp.core.ui.icons import UiIcons
log = logging.getLogger(__name__)
log.debug('projectortab module loaded')
@ -54,12 +55,12 @@ class ProjectorTab(SettingsTab):
Registry().register_function('udp_broadcast_add', self.add_udp_listener)
Registry().register_function('udp_broadcast_remove', self.remove_udp_listener)
def setupUi(self):
def setup_ui(self):
"""
Setup the UI
"""
self.setObjectName('ProjectorTab')
super(ProjectorTab, self).setupUi()
super(ProjectorTab, self).setup_ui()
self.connect_box = QtWidgets.QGroupBox(self.left_column)
self.connect_box.setObjectName('connect_box')
self.connect_box_layout = QtWidgets.QFormLayout(self.connect_box)
@ -103,7 +104,7 @@ class ProjectorTab(SettingsTab):
self.connect_on_linkup.setObjectName('connect_on_linkup')
self.connect_box_layout.addRow(self.connect_on_linkup)
def retranslateUi(self):
def retranslate_ui(self):
"""
Translate the UI on the fly
"""

View File

@ -25,11 +25,12 @@ backend for the projector setup.
"""
import logging
from sqlalchemy import Table, Column, types
from sqlalchemy import Column, Table, types
from sqlalchemy.sql.expression import null
from openlp.core.lib.db import get_upgrade_op
log = logging.getLogger(__name__)
# Initial projector DB was unversioned

View File

@ -21,8 +21,8 @@
###############################################################################
from PyQt5 import QtCore, QtNetwork
from openlp.core.common.registry import Registry
from openlp.core.common.mixins import LogMixin
from openlp.core.common.registry import Registry
class Server(QtCore.QObject, LogMixin):

View File

@ -28,6 +28,7 @@ from PyQt5 import QtCore, QtWidgets
from openlp.core.common.i18n import translate
from openlp.core.version import get_version
from .aboutdialog import UiAboutDialog

View File

@ -32,12 +32,13 @@ from openlp.core.common.applocation import AppLocation
from openlp.core.common.i18n import UiStrings, format_time, translate
from openlp.core.common.settings import Settings
from openlp.core.lib.settingstab import SettingsTab
from openlp.core.ui.style import HAS_DARK_STYLE
from openlp.core.ui.icons import UiIcons
from openlp.core.ui.style import HAS_DARK_STYLE
from openlp.core.widgets.edits import PathEdit
from openlp.core.widgets.enums import PathEditType
from openlp.core.widgets.widgets import ProxyWidget
log = logging.getLogger(__name__)
@ -59,12 +60,12 @@ class AdvancedTab(SettingsTab):
advanced_translated = translate('OpenLP.AdvancedTab', 'Advanced')
super(AdvancedTab, self).__init__(parent, 'Advanced', advanced_translated)
def setupUi(self):
def setup_ui(self):
"""
Configure the UI elements for the tab.
"""
self.setObjectName('AdvancedTab')
super(AdvancedTab, self).setupUi()
super(AdvancedTab, self).setup_ui()
self.ui_group_box = QtWidgets.QGroupBox(self.left_column)
self.ui_group_box.setObjectName('ui_group_box')
self.ui_layout = QtWidgets.QFormLayout(self.ui_group_box)
@ -241,7 +242,7 @@ class AdvancedTab(SettingsTab):
self.next_item_radio_button.clicked.connect(self.on_next_item_button_clicked)
self.search_as_type_check_box.stateChanged.connect(self.on_search_as_type_check_box_changed)
def retranslateUi(self):
def retranslate_ui(self):
"""
Setup the interface translation strings.
"""

View File

@ -26,15 +26,15 @@ The GUI widgets of the exception dialog.
from PyQt5 import QtWidgets
from openlp.core.common.i18n import translate
from openlp.core.ui.icons import UiIcons
from openlp.core.lib.ui import create_button, create_button_box
from openlp.core.ui.icons import UiIcons
class Ui_ExceptionDialog(object):
"""
The GUI widgets of the exception dialog.
"""
def setupUi(self, exception_dialog):
def setup_ui(self, exception_dialog):
"""
Set up the UI.
"""
@ -84,10 +84,10 @@ class Ui_ExceptionDialog(object):
[self.send_report_button, self.save_report_button, self.attach_tile_button])
self.exception_layout.addWidget(self.button_box)
self.retranslateUi(exception_dialog)
self.retranslate_ui(exception_dialog)
self.description_text_edit.textChanged.connect(self.on_description_updated)
def retranslateUi(self, exception_dialog):
def retranslate_ui(self, exception_dialog):
"""
Translate the widgets on the fly.
"""

View File

@ -27,48 +27,15 @@ import os
import platform
import re
import bs4
import sqlalchemy
from PyQt5 import Qt, QtCore, QtGui, QtWebKit, QtWidgets
from lxml import etree
try:
import migrate
MIGRATE_VERSION = getattr(migrate, '__version__', '< 0.7')
except ImportError:
MIGRATE_VERSION = '-'
try:
import chardet
CHARDET_VERSION = chardet.__version__
except ImportError:
CHARDET_VERSION = '-'
try:
import enchant
ENCHANT_VERSION = enchant.__version__
except ImportError:
ENCHANT_VERSION = '-'
try:
import mako
MAKO_VERSION = mako.__version__
except ImportError:
MAKO_VERSION = '-'
try:
WEBKIT_VERSION = QtWebKit.qWebKitVersion()
except AttributeError:
WEBKIT_VERSION = '-'
try:
from openlp.core.ui.media.vlcplayer import VERSION
VLC_VERSION = VERSION
except ImportError:
VLC_VERSION = '-'
from PyQt5 import QtCore, QtGui, QtWidgets
from openlp.core.common import is_linux
from openlp.core.common.i18n import UiStrings, translate
from openlp.core.common.mixins import RegistryProperties
from openlp.core.common.settings import Settings
from openlp.core.ui.exceptiondialog import Ui_ExceptionDialog
from openlp.core.version import get_library_versions, get_version
from openlp.core.widgets.dialogs import FileDialog
from openlp.core.version import get_version
log = logging.getLogger(__name__)
@ -83,7 +50,7 @@ class ExceptionForm(QtWidgets.QDialog, Ui_ExceptionDialog, RegistryProperties):
Constructor.
"""
super(ExceptionForm, self).__init__(None, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint)
self.setupUi(self)
self.setup_ui(self)
self.settings_section = 'crashreport'
self.report_text = '**OpenLP Bug Report**\n' \
'Version: {version}\n\n' \
@ -109,15 +76,9 @@ class ExceptionForm(QtWidgets.QDialog, Ui_ExceptionDialog, RegistryProperties):
description = self.description_text_edit.toPlainText()
traceback = self.exception_text_edit.toPlainText()
system = translate('OpenLP.ExceptionForm', 'Platform: {platform}\n').format(platform=platform.platform())
libraries = ('Python: {python}\nQt5: {qt5}\nPyQt5: {pyqt5}\nQtWebkit: {qtwebkit}\nSQLAlchemy: {sqalchemy}\n'
'SQLAlchemy Migrate: {migrate}\nBeautifulSoup: {soup}\nlxml: {etree}\nChardet: {chardet}\n'
'PyEnchant: {enchant}\nMako: {mako}\npyUNO bridge: {uno}\n'
'VLC: {vlc}\n').format(python=platform.python_version(), qt5=Qt.qVersion(),
pyqt5=Qt.PYQT_VERSION_STR, qtwebkit=WEBKIT_VERSION,
sqalchemy=sqlalchemy.__version__, migrate=MIGRATE_VERSION,
soup=bs4.__version__, etree=etree.__version__, chardet=CHARDET_VERSION,
enchant=ENCHANT_VERSION, mako=MAKO_VERSION,
uno=self._pyuno_import(), vlc=VLC_VERSION)
library_versions = get_library_versions()
library_versions['PyUNO'] = self._get_pyuno_version()
libraries = '\n'.join(['{}: {}'.format(library, version) for library, version in library_versions.items()])
if is_linux():
if os.environ.get('KDE_FULL_SESSION') == 'true':
@ -215,7 +176,7 @@ class ExceptionForm(QtWidgets.QDialog, Ui_ExceptionDialog, RegistryProperties):
self.save_report_button.setEnabled(state)
self.send_report_button.setEnabled(state)
def _pyuno_import(self):
def _get_pyuno_version(self):
"""
Added here to define only when the form is actioned. The uno interface spits out lots of exception messages
if the import is at a file level. If uno import is changed this could be reverted.

View File

@ -33,7 +33,7 @@ class Ui_FileRenameDialog(object):
"""
The UI widgets for the rename dialog
"""
def setupUi(self, file_rename_dialog):
def setup_ui(self, file_rename_dialog):
"""
Set up the UI
"""
@ -51,10 +51,10 @@ class Ui_FileRenameDialog(object):
self.dialog_layout.addWidget(self.file_name_edit, 0, 1)
self.button_box = create_button_box(file_rename_dialog, 'button_box', ['cancel', 'ok'])
self.dialog_layout.addWidget(self.button_box, 1, 0, 1, 2)
self.retranslateUi(file_rename_dialog)
self.retranslate_ui(file_rename_dialog)
self.setMaximumHeight(self.sizeHint().height())
def retranslateUi(self, file_rename_dialog):
def retranslate_ui(self, file_rename_dialog):
"""
Translate the UI on the fly.
"""

View File

@ -46,7 +46,7 @@ class FileRenameForm(QtWidgets.QDialog, Ui_FileRenameDialog, RegistryProperties)
"""
Set up the class. This method is mocked out by the tests.
"""
self.setupUi(self)
self.setup_ui(self)
def exec(self, copy=False):
"""

View File

@ -34,7 +34,7 @@ from PyQt5 import QtCore, QtWidgets
from openlp.core.common import clean_button_text, trace_error_handler
from openlp.core.common.applocation import AppLocation
from openlp.core.common.httputils import get_web_page, get_url_file_size, download_file
from openlp.core.common.httputils import download_file, get_url_file_size, get_web_page
from openlp.core.common.i18n import translate
from openlp.core.common.mixins import RegistryProperties
from openlp.core.common.path import Path, create_paths
@ -43,8 +43,9 @@ from openlp.core.common.settings import Settings
from openlp.core.lib import build_icon
from openlp.core.lib.plugin import PluginStatus
from openlp.core.lib.ui import critical_error_message_box
from openlp.core.threading import ThreadWorker, run_thread, get_thread_worker, is_thread_finished
from openlp.core.ui.firsttimewizard import UiFirstTimeWizard, FirstTimePage
from openlp.core.threading import ThreadWorker, get_thread_worker, is_thread_finished, run_thread
from openlp.core.ui.firsttimewizard import FirstTimePage, UiFirstTimeWizard
log = logging.getLogger(__name__)
@ -114,13 +115,13 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties):
"""
Returns the id of the next FirstTimePage to go to based on enabled plugins
"""
if FirstTimePage.Welcome < self.currentId() < FirstTimePage.Songs and self.songs_check_box.isChecked():
if FirstTimePage.ScreenConfig < self.currentId() < FirstTimePage.Songs and self.songs_check_box.isChecked():
# If the songs plugin is enabled then go to the songs page
return FirstTimePage.Songs
elif FirstTimePage.Welcome < self.currentId() < FirstTimePage.Bibles and self.bible_check_box.isChecked():
elif FirstTimePage.ScreenConfig < self.currentId() < FirstTimePage.Bibles and self.bible_check_box.isChecked():
# Otherwise, if the Bibles plugin is enabled then go to the Bibles page
return FirstTimePage.Bibles
elif FirstTimePage.Welcome < self.currentId() < FirstTimePage.Themes:
elif FirstTimePage.ScreenConfig < self.currentId() < FirstTimePage.Themes:
# Otherwise, if the current page is somewhere between the Welcome and the Themes pages, go to the themes
return FirstTimePage.Themes
else:
@ -152,7 +153,7 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties):
self._build_theme_screenshots()
self.application.set_normal_cursor()
self.theme_screenshot_threads = []
return FirstTimePage.Defaults
return self.get_next_page_id()
else:
return self.get_next_page_id()
@ -174,6 +175,8 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties):
self.theme_screenshot_threads = []
self.has_run_wizard = False
self.themes_list_widget.itemChanged.connect(self.on_theme_selected)
def _download_index(self):
"""
Download the configuration file and kick off the theme screenshot download threads
@ -203,7 +206,6 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties):
except (NoSectionError, NoOptionError, MissingSectionHeaderError):
log.debug('A problem occurred while parsing the downloaded config file')
trace_error_handler(log)
self.update_screen_list_combo()
self.application.process_events()
self.downloading = translate('OpenLP.FirstTimeWizard', 'Downloading {name}...')
if self.has_run_wizard:
@ -272,21 +274,21 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties):
self.no_internet_finish_button.clicked.connect(self.on_no_internet_finish_button_clicked)
self.no_internet_cancel_button.clicked.connect(self.on_no_internet_cancel_button_clicked)
self.currentIdChanged.connect(self.on_current_id_changed)
Registry().register_function('config_screen_changed', self.update_screen_list_combo)
Registry().register_function('config_screen_changed', self.screen_selection_widget.load)
self.no_internet_finish_button.setVisible(False)
self.no_internet_cancel_button.setVisible(False)
# Check if this is a re-run of the wizard.
self.has_run_wizard = Settings().value('core/has run wizard')
create_paths(Path(gettempdir(), 'openlp'))
def update_screen_list_combo(self):
"""
The user changed screen resolution or enabled/disabled more screens, so
we need to update the combo box.
"""
self.display_combo_box.clear()
self.display_combo_box.addItems(self.screens.get_screen_list())
self.display_combo_box.setCurrentIndex(self.display_combo_box.count() - 1)
self.theme_combo_box.clear()
if self.has_run_wizard:
# Add any existing themes to list.
for theme in self.theme_manager.get_themes():
self.theme_combo_box.addItem(theme)
default_theme = Settings().value('themes/global theme')
# Pre-select the current default theme.
index = self.theme_combo_box.findText(default_theme)
self.theme_combo_box.setCurrentIndex(index)
def on_current_id_changed(self, page_id):
"""
@ -310,22 +312,6 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties):
self.back_button.setVisible(False)
self.next_button.setVisible(True)
self.next()
elif page_id == FirstTimePage.Defaults:
self.theme_combo_box.clear()
for index in range(self.themes_list_widget.count()):
item = self.themes_list_widget.item(index)
if item.checkState() == QtCore.Qt.Checked:
self.theme_combo_box.addItem(item.text())
if self.has_run_wizard:
# Add any existing themes to list.
for theme in self.theme_manager.get_themes():
index = self.theme_combo_box.findText(theme)
if index == -1:
self.theme_combo_box.addItem(theme)
default_theme = Settings().value('themes/global theme')
# Pre-select the current default theme.
index = self.theme_combo_box.findText(default_theme)
self.theme_combo_box.setCurrentIndex(index)
elif page_id == FirstTimePage.NoInternet:
self.back_button.setVisible(False)
self.next_button.setVisible(False)
@ -367,10 +353,31 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties):
:param title: The title of the theme
:param filename: The filename of the theme
"""
self.themes_list_widget.blockSignals(True)
item = QtWidgets.QListWidgetItem(title, self.themes_list_widget)
item.setData(QtCore.Qt.UserRole, (filename, sha256))
item.setCheckState(QtCore.Qt.Unchecked)
item.setFlags(item.flags() | QtCore.Qt.ItemIsUserCheckable)
self.themes_list_widget.blockSignals(False)
def on_theme_selected(self, item):
"""
Add or remove a de/selected sample theme from the theme_combo_box
:param QtWidgets.QListWidgetItem item: The item that has been de/selected
:rtype: None
"""
theme_name = item.text()
if self.theme_manager and theme_name in self.theme_manager.get_themes():
return True
if item.checkState() == QtCore.Qt.Checked:
self.theme_combo_box.addItem(theme_name)
return True
else:
index = self.theme_combo_box.findText(theme_name)
if index != -1:
self.theme_combo_box.removeItem(index)
return True
def on_no_internet_finish_button_clicked(self):
"""
@ -535,11 +542,7 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties):
translate('OpenLP.FirstTimeWizard', 'There was a connection problem while '
'downloading, so further downloads will be skipped. Try to re-run '
'the First Time Wizard later.'))
# Set Default Display
if self.display_combo_box.currentIndex() != -1:
Settings().setValue('core/monitor', self.display_combo_box.currentIndex())
self.screens.set_current_display(self.display_combo_box.currentIndex())
# Set Global Theme
self.screen_selection_widget.save()
if self.theme_combo_box.currentIndex() != -1:
Settings().setValue('themes/global theme', self.theme_combo_box.currentText())

View File

@ -33,7 +33,7 @@ class Ui_FirstTimeLanguageDialog(object):
"""
The UI widgets of the language selection dialog.
"""
def setupUi(self, language_dialog):
def setup_ui(self, language_dialog):
"""
Set up the UI.
"""
@ -59,10 +59,10 @@ class Ui_FirstTimeLanguageDialog(object):
self.dialog_layout.addLayout(self.language_layout)
self.button_box = create_button_box(language_dialog, 'button_box', ['cancel', 'ok'])
self.dialog_layout.addWidget(self.button_box)
self.retranslateUi(language_dialog)
self.retranslate_ui(language_dialog)
self.setMaximumHeight(self.sizeHint().height())
def retranslateUi(self, language_dialog):
def retranslate_ui(self, language_dialog):
"""
Translate the UI on the fly.
"""

View File

@ -26,6 +26,7 @@ from PyQt5 import QtCore, QtWidgets
from openlp.core.common.i18n import LanguageManager
from openlp.core.lib.ui import create_action
from .firsttimelanguagedialog import Ui_FirstTimeLanguageDialog
@ -39,7 +40,7 @@ class FirstTimeLanguageForm(QtWidgets.QDialog, Ui_FirstTimeLanguageDialog):
"""
super(FirstTimeLanguageForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint |
QtCore.Qt.WindowTitleHint | QtCore.Qt.WindowCloseButtonHint)
self.setupUi(self)
self.setup_ui(self)
self.qm_list = LanguageManager.get_qm_list()
self.language_combo_box.addItem('Autodetect')
self.language_combo_box.addItems(sorted(self.qm_list.keys()))

View File

@ -24,25 +24,28 @@ The UI widgets for the first time wizard.
"""
from PyQt5 import QtCore, QtGui, QtWidgets
from openlp.core.common import is_macosx, clean_button_text
from openlp.core.common import clean_button_text, is_macosx
from openlp.core.common.i18n import translate
from openlp.core.common.settings import Settings
from openlp.core.lib.ui import add_welcome_page
from openlp.core.ui.icons import UiIcons
from openlp.core.display.screens import ScreenList
from openlp.core.widgets.widgets import ScreenSelectionWidget
class FirstTimePage(object):
"""
An enumeration class with each of the pages of the wizard.
"""
Welcome = 0
Download = 1
NoInternet = 2
Plugins = 3
Songs = 4
Bibles = 5
Themes = 6
Defaults = 7
ScreenConfig = 1
Download = 2
NoInternet = 3
Plugins = 4
Songs = 5
Bibles = 6
Themes = 7
Progress = 8
@ -76,6 +79,15 @@ class UiFirstTimeWizard(object):
self.next_button = self.button(QtWidgets.QWizard.NextButton)
self.back_button = self.button(QtWidgets.QWizard.BackButton)
add_welcome_page(first_time_wizard, ':/wizards/wizard_firsttime.bmp')
# The screen config page
self.screen_page = QtWidgets.QWizardPage()
self.screen_page.setObjectName('defaults_page')
self.screen_page_layout = QtWidgets.QFormLayout(self.screen_page)
self.screen_selection_widget = ScreenSelectionWidget(self, ScreenList())
self.screen_selection_widget.use_simple_view()
self.screen_selection_widget.load()
self.screen_page_layout.addRow(self.screen_selection_widget)
first_time_wizard.setPage(FirstTimePage.ScreenConfig, self.screen_page)
# The download page
self.download_page = QtWidgets.QWizardPage()
self.download_page.setObjectName('download_page')
@ -175,29 +187,16 @@ class UiFirstTimeWizard(object):
self.themes_list_widget.setWrapping(False)
self.themes_list_widget.setObjectName('themes_list_widget')
self.themes_layout.addWidget(self.themes_list_widget)
first_time_wizard.setPage(FirstTimePage.Themes, self.themes_page)
# the default settings page
self.defaults_page = QtWidgets.QWizardPage()
self.defaults_page.setObjectName('defaults_page')
self.defaults_layout = QtWidgets.QFormLayout(self.defaults_page)
self.defaults_layout.setContentsMargins(50, 20, 50, 20)
self.defaults_layout.setObjectName('defaults_layout')
self.display_label = QtWidgets.QLabel(self.defaults_page)
self.display_label.setObjectName('display_label')
self.display_combo_box = QtWidgets.QComboBox(self.defaults_page)
self.display_combo_box.setEditable(False)
self.display_combo_box.setInsertPolicy(QtWidgets.QComboBox.NoInsert)
self.display_combo_box.setObjectName('display_combo_box')
self.defaults_layout.addRow(self.display_label, self.display_combo_box)
self.theme_label = QtWidgets.QLabel(self.defaults_page)
self.theme_label.setObjectName('theme_label')
self.theme_combo_box = QtWidgets.QComboBox(self.defaults_page)
self.default_theme_layout = QtWidgets.QHBoxLayout()
self.theme_label = QtWidgets.QLabel(self.themes_page)
self.default_theme_layout.addWidget(self.theme_label)
self.theme_combo_box = QtWidgets.QComboBox(self.themes_page)
self.theme_combo_box.setEditable(False)
self.theme_combo_box.setInsertPolicy(QtWidgets.QComboBox.NoInsert)
self.theme_combo_box.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents)
self.theme_combo_box.setObjectName('theme_combo_box')
self.defaults_layout.addRow(self.theme_label, self.theme_combo_box)
first_time_wizard.setPage(FirstTimePage.Defaults, self.defaults_page)
self.default_theme_layout.addWidget(self.theme_combo_box)
self.themes_layout.addLayout(self.default_theme_layout)
first_time_wizard.setPage(FirstTimePage.Themes, self.themes_page)
# Progress page
self.progress_page = QtWidgets.QWizardPage()
self.progress_page.setObjectName('progress_page')
@ -235,6 +234,9 @@ class UiFirstTimeWizard(object):
self.plugin_page.setTitle(translate('OpenLP.FirstTimeWizard', 'Select parts of the program you wish to use'))
self.plugin_page.setSubTitle(translate('OpenLP.FirstTimeWizard',
'You can also change these settings after the Wizard.'))
self.screen_page.setTitle(translate('OpenLP.FirstTimeWizard', 'Displays'))
self.screen_page.setSubTitle(translate('OpenLP.FirstTimeWizard',
'Choose the main display screen for OpenLP.'))
self.songs_check_box.setText(translate('OpenLP.FirstTimeWizard', 'Songs'))
self.custom_check_box.setText(translate('OpenLP.FirstTimeWizard',
'Custom Slides Easier to manage than songs and they have their own'
@ -271,10 +273,6 @@ class UiFirstTimeWizard(object):
self.bibles_page.setSubTitle(translate('OpenLP.FirstTimeWizard', 'Select and download free Bibles.'))
self.themes_page.setTitle(translate('OpenLP.FirstTimeWizard', 'Sample Themes'))
self.themes_page.setSubTitle(translate('OpenLP.FirstTimeWizard', 'Select and download sample themes.'))
self.defaults_page.setTitle(translate('OpenLP.FirstTimeWizard', 'Default Settings'))
self.defaults_page.setSubTitle(translate('OpenLP.FirstTimeWizard',
'Set up default settings to be used by OpenLP.'))
self.display_label.setText(translate('OpenLP.FirstTimeWizard', 'Default output display:'))
self.theme_label.setText(translate('OpenLP.FirstTimeWizard', 'Select default theme:'))
self.progress_page.setTitle(translate('OpenLP.FirstTimeWizard', 'Downloading and Configuring'))
self.progress_page.setSubTitle(translate('OpenLP.FirstTimeWizard', 'Please wait while resources are downloaded '

View File

@ -25,15 +25,15 @@ The UI widgets for the formatting tags window.
from PyQt5 import QtCore, QtWidgets
from openlp.core.common.i18n import UiStrings, translate
from openlp.core.ui.icons import UiIcons
from openlp.core.lib.ui import create_button_box
from openlp.core.ui.icons import UiIcons
class Ui_FormattingTagDialog(object):
"""
The UI widgets for the formatting tags window.
"""
def setupUi(self, formatting_tag_dialog):
def setup_ui(self, formatting_tag_dialog):
"""
Set up the UI
"""
@ -103,9 +103,9 @@ class Ui_FormattingTagDialog(object):
self.restore_button.setIcon(UiIcons().undo)
self.restore_button.setObjectName('restore_button')
self.list_data_grid_layout.addWidget(self.button_box)
self.retranslateUi(formatting_tag_dialog)
self.retranslate_ui(formatting_tag_dialog)
def retranslateUi(self, formatting_tag_dialog):
def retranslate_ui(self, formatting_tag_dialog):
"""
Translate the UI on the fly
"""

View File

@ -52,7 +52,7 @@ class FormattingTagForm(QtWidgets.QDialog, Ui_FormattingTagDialog, FormattingTag
"""
super(FormattingTagForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint |
QtCore.Qt.WindowCloseButtonHint)
self.setupUi(self)
self.setup_ui(self)
self._setup()
def _setup(self):

View File

@ -24,18 +24,18 @@ The general tab of the configuration dialog.
"""
import logging
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5 import QtGui, QtWidgets
from openlp.core.common import get_images_filter
from openlp.core.common.i18n import UiStrings, translate
from openlp.core.common.path import Path
from openlp.core.common.registry import Registry
from openlp.core.common.settings import Settings
from openlp.core.display.screens import ScreenList
from openlp.core.lib.settingstab import SettingsTab
from openlp.core.widgets.buttons import ColorButton
from openlp.core.widgets.edits import PathEdit
log = logging.getLogger(__name__)
@ -54,68 +54,13 @@ class GeneralTab(SettingsTab):
general_translated = translate('OpenLP.GeneralTab', 'General')
super(GeneralTab, self).__init__(parent, 'Core', general_translated)
def setupUi(self):
def setup_ui(self):
"""
Create the user interface for the general settings tab
"""
self.setObjectName('GeneralTab')
super(GeneralTab, self).setupUi()
super(GeneralTab, self).setup_ui()
self.tab_layout.setStretch(1, 1)
# Monitors
self.monitor_group_box = QtWidgets.QGroupBox(self.left_column)
self.monitor_group_box.setObjectName('monitor_group_box')
self.monitor_layout = QtWidgets.QGridLayout(self.monitor_group_box)
self.monitor_layout.setObjectName('monitor_layout')
self.monitor_radio_button = QtWidgets.QRadioButton(self.monitor_group_box)
self.monitor_radio_button.setObjectName('monitor_radio_button')
self.monitor_layout.addWidget(self.monitor_radio_button, 0, 0, 1, 5)
self.monitor_combo_box = QtWidgets.QComboBox(self.monitor_group_box)
self.monitor_combo_box.setObjectName('monitor_combo_box')
self.monitor_layout.addWidget(self.monitor_combo_box, 1, 1, 1, 4)
# Display Position
self.override_radio_button = QtWidgets.QRadioButton(self.monitor_group_box)
self.override_radio_button.setObjectName('override_radio_button')
self.monitor_layout.addWidget(self.override_radio_button, 2, 0, 1, 5)
# Custom position
self.custom_x_label = QtWidgets.QLabel(self.monitor_group_box)
self.custom_x_label.setObjectName('custom_x_label')
self.monitor_layout.addWidget(self.custom_x_label, 3, 1)
self.custom_X_value_edit = QtWidgets.QSpinBox(self.monitor_group_box)
self.custom_X_value_edit.setObjectName('custom_X_value_edit')
self.custom_X_value_edit.setRange(-9999, 9999)
self.monitor_layout.addWidget(self.custom_X_value_edit, 4, 1)
self.custom_y_label = QtWidgets.QLabel(self.monitor_group_box)
self.custom_y_label.setObjectName('custom_y_label')
self.monitor_layout.addWidget(self.custom_y_label, 3, 2)
self.custom_Y_value_edit = QtWidgets.QSpinBox(self.monitor_group_box)
self.custom_Y_value_edit.setObjectName('custom_Y_value_edit')
self.custom_Y_value_edit.setRange(-9999, 9999)
self.monitor_layout.addWidget(self.custom_Y_value_edit, 4, 2)
self.custom_width_label = QtWidgets.QLabel(self.monitor_group_box)
self.custom_width_label.setObjectName('custom_width_label')
self.monitor_layout.addWidget(self.custom_width_label, 3, 3)
self.custom_width_value_edit = QtWidgets.QSpinBox(self.monitor_group_box)
self.custom_width_value_edit.setObjectName('custom_width_value_edit')
self.custom_width_value_edit.setRange(1, 9999)
self.monitor_layout.addWidget(self.custom_width_value_edit, 4, 3)
self.custom_height_label = QtWidgets.QLabel(self.monitor_group_box)
self.custom_height_label.setObjectName('custom_height_label')
self.monitor_layout.addWidget(self.custom_height_label, 3, 4)
self.custom_height_value_edit = QtWidgets.QSpinBox(self.monitor_group_box)
self.custom_height_value_edit.setObjectName('custom_height_value_edit')
self.custom_height_value_edit.setRange(1, 9999)
self.monitor_layout.addWidget(self.custom_height_value_edit, 4, 4)
self.display_on_monitor_check = QtWidgets.QCheckBox(self.monitor_group_box)
self.display_on_monitor_check.setObjectName('monitor_combo_box')
self.monitor_layout.addWidget(self.display_on_monitor_check, 5, 0, 1, 5)
# Set up the stretchiness of each column, so that the first column
# less stretchy (and therefore smaller) than the others
self.monitor_layout.setColumnStretch(0, 1)
self.monitor_layout.setColumnStretch(1, 3)
self.monitor_layout.setColumnStretch(2, 3)
self.monitor_layout.setColumnStretch(3, 3)
self.monitor_layout.setColumnStretch(4, 3)
self.left_layout.addWidget(self.monitor_group_box)
# CCLI Details
self.ccli_group_box = QtWidgets.QGroupBox(self.left_column)
self.ccli_group_box.setObjectName('ccli_group_box')
@ -216,29 +161,17 @@ class GeneralTab(SettingsTab):
self.settings_layout.addRow(self.timeout_label, self.timeout_spin_box)
self.right_layout.addWidget(self.settings_group_box)
self.right_layout.addStretch()
# Signals and slots
self.override_radio_button.toggled.connect(self.on_override_radio_button_pressed)
self.custom_height_value_edit.valueChanged.connect(self.on_display_changed)
self.custom_width_value_edit.valueChanged.connect(self.on_display_changed)
self.custom_Y_value_edit.valueChanged.connect(self.on_display_changed)
self.custom_X_value_edit.valueChanged.connect(self.on_display_changed)
self.monitor_combo_box.currentIndexChanged.connect(self.on_display_changed)
# Reload the tab, as the screen resolution/count may have changed.
Registry().register_function('config_screen_changed', self.load)
# Remove for now
self.username_label.setVisible(False)
self.username_edit.setVisible(False)
self.password_label.setVisible(False)
self.password_edit.setVisible(False)
def retranslateUi(self):
def retranslate_ui(self):
"""
Translate the general settings tab to the currently selected language
"""
self.tab_title_visible = translate('OpenLP.GeneralTab', 'General')
self.monitor_group_box.setTitle(translate('OpenLP.GeneralTab', 'Monitors'))
self.monitor_radio_button.setText(translate('OpenLP.GeneralTab', 'Select monitor for output display:'))
self.display_on_monitor_check.setText(translate('OpenLP.GeneralTab', 'Display if a single screen'))
self.startup_group_box.setTitle(translate('OpenLP.GeneralTab', 'Application Startup'))
self.warning_check_box.setText(translate('OpenLP.GeneralTab', 'Show blank screen warning'))
self.auto_open_check_box.setText(translate('OpenLP.GeneralTab', 'Automatically open the previous service file'))
@ -263,12 +196,6 @@ class GeneralTab(SettingsTab):
self.number_label.setText(UiStrings().CCLINumberLabel)
self.username_label.setText(translate('OpenLP.GeneralTab', 'SongSelect username:'))
self.password_label.setText(translate('OpenLP.GeneralTab', 'SongSelect password:'))
# Moved from display tab
self.override_radio_button.setText(translate('OpenLP.GeneralTab', 'Override display position:'))
self.custom_x_label.setText(translate('OpenLP.GeneralTab', 'X'))
self.custom_y_label.setText(translate('OpenLP.GeneralTab', 'Y'))
self.custom_height_label.setText(translate('OpenLP.GeneralTab', 'Height'))
self.custom_width_label.setText(translate('OpenLP.GeneralTab', 'Width'))
self.audio_group_box.setTitle(translate('OpenLP.GeneralTab', 'Background Audio'))
self.start_paused_check_box.setText(translate('OpenLP.GeneralTab', 'Start background audio paused'))
self.repeat_list_check_box.setText(translate('OpenLP.GeneralTab', 'Repeat track list'))
@ -282,17 +209,12 @@ class GeneralTab(SettingsTab):
"""
settings = Settings()
settings.beginGroup(self.settings_section)
self.monitor_combo_box.clear()
self.monitor_combo_box.addItems(self.screens.get_screen_list())
monitor_number = settings.value('monitor')
self.monitor_combo_box.setCurrentIndex(monitor_number)
self.number_edit.setText(settings.value('ccli number'))
self.username_edit.setText(settings.value('songselect username'))
self.password_edit.setText(settings.value('songselect password'))
self.save_check_service_check_box.setChecked(settings.value('save prompt'))
self.auto_unblank_check_box.setChecked(settings.value('auto unblank'))
self.click_live_slide_to_unblank_check_box.setChecked(settings.value('click live slide to unblank'))
self.display_on_monitor_check.setChecked(self.screens.display)
self.warning_check_box.setChecked(settings.value('blank warning'))
self.auto_open_check_box.setChecked(settings.value('auto open'))
self.show_splash_check_box.setChecked(settings.value('show splash'))
@ -303,21 +225,9 @@ class GeneralTab(SettingsTab):
self.check_for_updates_check_box.setChecked(settings.value('update check'))
self.auto_preview_check_box.setChecked(settings.value('auto preview'))
self.timeout_spin_box.setValue(settings.value('loop delay'))
self.monitor_radio_button.setChecked(not settings.value('override position',))
self.override_radio_button.setChecked(settings.value('override position'))
self.custom_X_value_edit.setValue(settings.value('x position'))
self.custom_Y_value_edit.setValue(settings.value('y position'))
self.custom_height_value_edit.setValue(settings.value('height'))
self.custom_width_value_edit.setValue(settings.value('width'))
self.start_paused_check_box.setChecked(settings.value('audio start paused'))
self.repeat_list_check_box.setChecked(settings.value('audio repeat list'))
settings.endGroup()
self.monitor_combo_box.setDisabled(self.override_radio_button.isChecked())
self.custom_X_value_edit.setEnabled(self.override_radio_button.isChecked())
self.custom_Y_value_edit.setEnabled(self.override_radio_button.isChecked())
self.custom_height_value_edit.setEnabled(self.override_radio_button.isChecked())
self.custom_width_value_edit.setEnabled(self.override_radio_button.isChecked())
self.display_changed = False
def save(self):
"""
@ -325,8 +235,6 @@ class GeneralTab(SettingsTab):
"""
settings = Settings()
settings.beginGroup(self.settings_section)
settings.setValue('monitor', self.monitor_combo_box.currentIndex())
settings.setValue('display on monitor', self.display_on_monitor_check.isChecked())
settings.setValue('blank warning', self.warning_check_box.isChecked())
settings.setValue('auto open', self.auto_open_check_box.isChecked())
settings.setValue('show splash', self.show_splash_check_box.isChecked())
@ -342,60 +250,16 @@ class GeneralTab(SettingsTab):
settings.setValue('ccli number', self.number_edit.displayText())
settings.setValue('songselect username', self.username_edit.displayText())
settings.setValue('songselect password', self.password_edit.displayText())
settings.setValue('x position', self.custom_X_value_edit.value())
settings.setValue('y position', self.custom_Y_value_edit.value())
settings.setValue('height', self.custom_height_value_edit.value())
settings.setValue('width', self.custom_width_value_edit.value())
settings.setValue('override position', self.override_radio_button.isChecked())
settings.setValue('audio start paused', self.start_paused_check_box.isChecked())
settings.setValue('audio repeat list', self.repeat_list_check_box.isChecked())
settings.endGroup()
# On save update the screens as well
self.post_set_up(True)
self.post_set_up()
def post_set_up(self, postUpdate=False):
def post_set_up(self):
"""
Apply settings after settings tab has loaded and most of the system so must be delayed
Apply settings after the tab has loaded
"""
self.settings_form.register_post_process('slidecontroller_live_spin_delay')
# Do not continue on start up.
if not postUpdate:
return
self.screens.set_current_display(self.monitor_combo_box.currentIndex())
self.screens.display = self.display_on_monitor_check.isChecked()
self.screens.override['size'] = QtCore.QRect(
self.custom_X_value_edit.value(),
self.custom_Y_value_edit.value(),
self.custom_width_value_edit.value(),
self.custom_height_value_edit.value())
self.screens.override['number'] = self.screens.which_screen(self.screens.override['size'])
self.screens.override['primary'] = (self.screens.desktop.primaryScreen() == self.screens.override['number'])
if self.override_radio_button.isChecked():
self.screens.set_override_display()
else:
self.screens.reset_current_display()
if self.display_changed:
self.settings_form.register_post_process('config_screen_changed')
self.display_changed = False
def on_override_radio_button_pressed(self, checked):
"""
Toggle screen state depending on check box state.
:param checked: The state of the check box (boolean).
"""
self.monitor_combo_box.setDisabled(checked)
self.custom_X_value_edit.setEnabled(checked)
self.custom_Y_value_edit.setEnabled(checked)
self.custom_height_value_edit.setEnabled(checked)
self.custom_width_value_edit.setEnabled(checked)
self.display_changed = True
def on_display_changed(self):
"""
Called when the width, height, x position or y position has changed.
"""
self.display_changed = True
def on_logo_background_color_changed(self, color):
"""

View File

@ -23,13 +23,14 @@
The :mod:`languages` module provides a list of icons.
"""
import logging
import qtawesome as qta
import qtawesome as qta
from PyQt5 import QtGui, QtWidgets
from openlp.core.common.applocation import AppLocation
from openlp.core.lib import build_icon
log = logging.getLogger(__name__)

View File

@ -1,602 +0,0 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
###############################################################################
# OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- #
# Copyright (c) 2008-2018 OpenLP Developers #
# --------------------------------------------------------------------------- #
# This program is free software; you can redistribute it and/or modify it #
# under the terms of the GNU General Public License as published by the Free #
# Software Foundation; version 2 of the License. #
# #
# This program is distributed in the hope that it will be useful, but WITHOUT #
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
# more details. #
# #
# You should have received a copy of the GNU General Public License along #
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
"""
The :mod:`maindisplay` module provides the functionality to display screens and play multimedia within OpenLP.
Some of the code for this form is based on the examples at:
* `http://www.steveheffernan.com/html5-video-player/demo-video-player.html`_
* `http://html5demos.com/two-videos`_
"""
import html
import logging
from PyQt5 import QtCore, QtWidgets, QtWebKit, QtWebKitWidgets, QtGui
from openlp.core.common import is_macosx, is_win
from openlp.core.common.applocation import AppLocation
from openlp.core.common.i18n import translate
from openlp.core.common.mixins import LogMixin, RegistryProperties
from openlp.core.common.path import path_to_str
from openlp.core.common.registry import Registry
from openlp.core.common.settings import Settings
from openlp.core.display.screens import ScreenList
from openlp.core.lib import ImageSource, expand_tags, image_to_byte
from openlp.core.lib.htmlbuilder import build_html
from openlp.core.lib.serviceitem import ServiceItem
from openlp.core.lib.theme import BackgroundType
from openlp.core.ui import HideMode, AlertLocation, DisplayControllerType
from openlp.core.ui.icons import UiIcons
if is_macosx():
from ctypes import pythonapi, c_void_p, c_char_p, py_object
from sip import voidptr
from objc import objc_object
from AppKit import NSMainMenuWindowLevel, NSWindowCollectionBehaviorManaged
log = logging.getLogger(__name__)
OPAQUE_STYLESHEET = """
QWidget {
border: 0px;
margin: 0px;
padding: 0px;
}
QGraphicsView {}
"""
TRANSPARENT_STYLESHEET = """
QWidget {
border: 0px;
margin: 0px;
padding: 0px;
}
QGraphicsView {
background: transparent;
border: 0px;
}
"""
class Display(QtWidgets.QGraphicsView):
"""
This is a general display screen class. Here the general display settings will done. It will be used as
specialized classes by Main Display and Preview display.
"""
def __init__(self, parent):
"""
Constructor
"""
self.is_live = False
if hasattr(parent, 'is_live') and parent.is_live:
self.is_live = True
if self.is_live:
self.parent = lambda: parent
super(Display, self).__init__()
self.controller = parent
self.screen = {}
def setup(self):
"""
Set up and build the screen base
"""
self.setGeometry(self.screen['size'])
self.web_view = QtWebKitWidgets.QWebView(self)
self.web_view.setGeometry(0, 0, self.screen['size'].width(), self.screen['size'].height())
self.web_view.settings().setAttribute(QtWebKit.QWebSettings.PluginsEnabled, True)
palette = self.web_view.palette()
palette.setBrush(QtGui.QPalette.Base, QtCore.Qt.transparent)
self.web_view.page().setPalette(palette)
self.web_view.setAttribute(QtCore.Qt.WA_OpaquePaintEvent, False)
self.page = self.web_view.page()
self.frame = self.page.mainFrame()
if self.is_live and log.getEffectiveLevel() == logging.DEBUG:
self.web_view.settings().setAttribute(QtWebKit.QWebSettings.DeveloperExtrasEnabled, True)
self.web_view.loadFinished.connect(self.is_web_loaded)
self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.frame.setScrollBarPolicy(QtCore.Qt.Vertical, QtCore.Qt.ScrollBarAlwaysOff)
self.frame.setScrollBarPolicy(QtCore.Qt.Horizontal, QtCore.Qt.ScrollBarAlwaysOff)
def resizeEvent(self, event):
"""
React to resizing of this display
:param event: The event to be handled
"""
if hasattr(self, 'web_view'):
self.web_view.setGeometry(0, 0, self.width(), self.height())
def is_web_loaded(self, field=None):
"""
Called by webView event to show display is fully loaded
"""
self.web_loaded = True
class MainDisplay(Display, LogMixin, RegistryProperties):
"""
This is the display screen as a specialized class from the Display class
"""
def __init__(self, parent):
"""
Constructor
"""
super(MainDisplay, self).__init__(parent)
self.screens = ScreenList()
self.rebuild_css = False
self.hide_mode = None
self.override = {}
self.retranslateUi()
self.media_object = None
self.first_time = True
self.web_loaded = True
self.setStyleSheet(OPAQUE_STYLESHEET)
window_flags = QtCore.Qt.FramelessWindowHint | QtCore.Qt.Tool | QtCore.Qt.WindowStaysOnTopHint
if Settings().value('advanced/x11 bypass wm'):
window_flags |= QtCore.Qt.X11BypassWindowManagerHint
# TODO: The following combination of window_flags works correctly
# on Mac OS X. For next OpenLP version we should test it on other
# platforms. For OpenLP 2.0 keep it only for OS X to not cause any
# regressions on other platforms.
if is_macosx():
window_flags = QtCore.Qt.FramelessWindowHint | QtCore.Qt.Window | QtCore.Qt.NoDropShadowWindowHint
self.setWindowFlags(window_flags)
self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
self.set_transparency(False)
if is_macosx():
if self.is_live:
# Get a pointer to the underlying NSView
try:
nsview_pointer = self.winId().ascapsule()
except Exception:
nsview_pointer = voidptr(self.winId()).ascapsule()
# Set PyCapsule name so pyobjc will accept it
pythonapi.PyCapsule_SetName.restype = c_void_p
pythonapi.PyCapsule_SetName.argtypes = [py_object, c_char_p]
pythonapi.PyCapsule_SetName(nsview_pointer, c_char_p(b"objc.__object__"))
# Covert the NSView pointer into a pyobjc NSView object
self.pyobjc_nsview = objc_object(cobject=nsview_pointer)
# Set the window level so that the MainDisplay is above the menu bar and dock
self.pyobjc_nsview.window().setLevel_(NSMainMenuWindowLevel + 2)
# Set the collection behavior so the window is visible when Mission Control is activated
self.pyobjc_nsview.window().setCollectionBehavior_(NSWindowCollectionBehaviorManaged)
if self.screens.current['primary']:
# Connect focusWindowChanged signal so we can change the window level when the display is not in
# focus on the primary screen
self.application.focusWindowChanged.connect(self.change_window_level)
if self.is_live:
Registry().register_function('live_display_hide', self.hide_display)
Registry().register_function('live_display_show', self.show_display)
Registry().register_function('update_display_css', self.css_changed)
self.close_display = False
def closeEvent(self, event):
"""
Catch the close event, and check that the close event is triggered by OpenLP closing the display.
On Windows this event can be triggered by pressing ALT+F4, which we want to ignore.
:param event: The triggered event
"""
if self.close_display:
super().closeEvent(event)
else:
event.ignore()
def close(self):
"""
Remove registered function on close.
"""
if self.is_live:
if is_macosx():
# Block signals so signal we are disconnecting can't get called while we disconnect it
self.blockSignals(True)
if self.screens.current['primary']:
self.application.focusWindowChanged.disconnect()
self.blockSignals(False)
Registry().remove_function('live_display_hide', self.hide_display)
Registry().remove_function('live_display_show', self.show_display)
Registry().remove_function('update_display_css', self.css_changed)
self.close_display = True
super().close()
def set_transparency(self, enabled):
"""
Set the transparency of the window
:param enabled: Is transparency enabled
"""
if enabled:
self.setAutoFillBackground(False)
self.setStyleSheet(TRANSPARENT_STYLESHEET)
else:
self.setAttribute(QtCore.Qt.WA_NoSystemBackground, False)
self.setStyleSheet(OPAQUE_STYLESHEET)
self.setAttribute(QtCore.Qt.WA_TranslucentBackground, enabled)
self.repaint()
def css_changed(self):
"""
We need to rebuild the CSS on the live display.
"""
for plugin in self.plugin_manager.plugins:
plugin.refresh_css(self.frame)
def retranslateUi(self):
"""
Setup the interface translation strings.
"""
self.setWindowTitle(translate('OpenLP.MainDisplay', 'OpenLP Display'))
def setup(self):
"""
Set up and build the output screen
"""
self.log_debug('Start MainDisplay setup (live = {islive})'.format(islive=self.is_live))
self.screen = self.screens.current
self.setVisible(False)
Display.setup(self)
if self.is_live:
# Build the initial frame.
background_color = QtGui.QColor()
background_color.setNamedColor(Settings().value('core/logo background color'))
if not background_color.isValid():
background_color = QtCore.Qt.white
image_file = path_to_str(Settings().value('core/logo file'))
splash_image = QtGui.QImage(image_file)
self.initial_fame = QtGui.QImage(
self.screen['size'].width(),
self.screen['size'].height(),
QtGui.QImage.Format_ARGB32_Premultiplied)
painter_image = QtGui.QPainter()
painter_image.begin(self.initial_fame)
painter_image.fillRect(self.initial_fame.rect(), background_color)
painter_image.drawImage(
(self.screen['size'].width() - splash_image.width()) // 2,
(self.screen['size'].height() - splash_image.height()) // 2,
splash_image)
service_item = ServiceItem()
service_item.bg_image_bytes = image_to_byte(self.initial_fame)
self.web_view.setHtml(build_html(service_item, self.screen, self.is_live, None,
plugins=self.plugin_manager.plugins))
self._hide_mouse()
def text(self, slide, animate=True):
"""
Add the slide text from slideController
:param slide: The slide text to be displayed
:param animate: Perform transitions if applicable when setting the text
"""
# Wait for the webview to update before displaying text.
while not self.web_loaded:
self.application.process_events()
self.setGeometry(self.screen['size'])
if animate:
# NOTE: Verify this works with ''.format()
_text = slide.replace('\\', '\\\\').replace('\"', '\\\"')
self.frame.evaluateJavaScript('show_text("{text}")'.format(text=_text))
else:
# This exists for https://bugs.launchpad.net/openlp/+bug/1016843
# For unknown reasons if evaluateJavaScript is called
# from the themewizard, then it causes a crash on
# Windows if there are many items in the service to re-render.
# Setting the div elements direct seems to solve the issue
self.frame.findFirstElement("#lyricsmain").setInnerXml(slide)
def alert(self, text, location):
"""
Display an alert.
:param text: The text to be displayed.
:param location: Where on the screen is the text to be displayed
"""
# First we convert <>& marks to html variants, then apply
# formattingtags, finally we double all backslashes for JavaScript.
text_prepared = expand_tags(html.escape(text)).replace('\\', '\\\\').replace('\"', '\\\"')
if self.height() != self.screen['size'].height() or not self.isVisible():
shrink = True
js = 'show_alert("{text}", "{top}")'.format(text=text_prepared, top='top')
else:
shrink = False
js = 'show_alert("{text}", "")'.format(text=text_prepared)
height = self.frame.evaluateJavaScript(js)
if shrink:
if text:
alert_height = int(height)
self.resize(self.width(), alert_height)
self.setVisible(True)
if location == AlertLocation.Middle:
self.move(self.screen['size'].left(), (self.screen['size'].height() - alert_height) // 2)
elif location == AlertLocation.Bottom:
self.move(self.screen['size'].left(), self.screen['size'].height() - alert_height)
else:
self.setVisible(False)
self.setGeometry(self.screen['size'])
# Workaround for bug #1531319, should not be needed with PyQt 5.6.
if is_win():
self.shake_web_view()
def direct_image(self, path, background):
"""
API for replacement backgrounds so Images are added directly to cache.
:param path: Path to Image
:param background: The background color
"""
self.image_manager.add_image(path, ImageSource.ImagePlugin, background)
if not hasattr(self, 'service_item'):
return False
self.override['image'] = path
self.override['theme'] = path_to_str(self.service_item.theme_data.background_filename)
self.image(path)
# Update the preview frame.
if self.is_live:
self.live_controller.update_preview()
return True
def image(self, path):
"""
Add an image as the background. The image has already been added to the
cache.
:param path: The path to the image to be displayed. **Note**, the path is only passed to identify the image.
If the image has changed it has to be re-added to the image manager.
"""
image = self.image_manager.get_image_bytes(path, ImageSource.ImagePlugin)
self.controller.media_controller.media_reset(self.controller)
self.display_image(image)
def display_image(self, image):
"""
Display an image, as is.
:param image: The image to be displayed
"""
self.setGeometry(self.screen['size'])
if image:
js = 'show_image("data:image/png;base64,{image}");'.format(image=image)
else:
js = 'show_image("");'
self.frame.evaluateJavaScript(js)
def reset_image(self):
"""
Reset the background image to the service item image. Used after the image plugin has changed the background.
"""
if hasattr(self, 'service_item'):
self.display_image(self.service_item.bg_image_bytes)
else:
self.display_image(None)
# Update the preview frame.
if self.is_live:
self.live_controller.update_preview()
# clear the cache
self.override = {}
def preview(self):
"""
Generates a preview of the image displayed.
:rtype: QtGui.QPixmap
"""
was_visible = self.isVisible()
self.application.process_events()
# We must have a service item to preview.
if self.is_live and hasattr(self, 'service_item'):
# Wait for the fade to finish before geting the preview.
# Important otherwise preview will have incorrect text if at all!
if self.service_item.theme_data and self.service_item.theme_data.display_slide_transition:
# Workaround for bug #1531319, should not be needed with PyQt 5.6.
if is_win():
fade_shake_timer = QtCore.QTimer(self)
fade_shake_timer.setInterval(25)
fade_shake_timer.timeout.connect(self.shake_web_view)
fade_shake_timer.start()
while not self.frame.evaluateJavaScript('show_text_completed()'):
self.application.process_events()
# Workaround for bug #1531319, should not be needed with PyQt 5.6.
if is_win():
fade_shake_timer.stop()
# Wait for the webview to update before getting the preview.
# Important otherwise first preview will miss the background !
while not self.web_loaded:
self.application.process_events()
# if was hidden keep it hidden
if self.is_live:
if self.hide_mode:
self.hide_display(self.hide_mode)
# Only continue if the visibility wasn't changed during method call.
elif was_visible == self.isVisible():
# Single screen active
if self.screens.display_count == 1:
# Only make visible if setting enabled.
if Settings().value('core/display on monitor'):
self.setVisible(True)
else:
self.setVisible(True)
# Workaround for bug #1531319, should not be needed with PyQt 5.6.
if is_win():
self.shake_web_view()
return self.grab()
def build_html(self, service_item, image_path=''):
"""
Store the service_item and build the new HTML from it. Add the HTML to the display
:param service_item: The Service item to be used
:param image_path: Where the image resides.
"""
self.web_loaded = False
self.initial_fame = None
self.service_item = service_item
background = None
# We have an image override so keep the image till the theme changes.
if self.override:
# We have an video override so allow it to be stopped.
if 'video' in self.override:
Registry().execute('video_background_replaced')
self.override = {}
# We have a different theme.
elif self.override['theme'] != path_to_str(service_item.theme_data.background_filename):
Registry().execute('live_theme_changed')
self.override = {}
else:
# replace the background
background = self.image_manager.get_image_bytes(self.override['image'], ImageSource.ImagePlugin)
self.set_transparency(self.service_item.theme_data.background_type ==
BackgroundType.to_string(BackgroundType.Transparent))
image_bytes = None
if self.service_item.theme_data.background_type == 'image':
if self.service_item.theme_data.background_filename:
self.service_item.bg_image_bytes = self.image_manager.get_image_bytes(
path_to_str(self.service_item.theme_data.background_filename), ImageSource.Theme)
if image_path:
image_bytes = self.image_manager.get_image_bytes(image_path, ImageSource.ImagePlugin)
created_html = build_html(self.service_item, self.screen, self.is_live, background, image_bytes,
plugins=self.plugin_manager.plugins)
self.web_view.setHtml(created_html)
if service_item.foot_text:
self.footer(service_item.foot_text)
# if was hidden keep it hidden
if self.hide_mode and self.is_live and not service_item.is_media():
if Settings().value('core/auto unblank'):
Registry().execute('slidecontroller_live_unblank')
else:
self.hide_display(self.hide_mode)
if self.service_item.theme_data.background_type == 'video' and self.is_live:
if self.service_item.theme_data.background_filename:
service_item = ServiceItem()
service_item.title = 'webkit'
service_item.processor = 'webkit'
path = str(AppLocation.get_section_data_path('themes') / self.service_item.theme_data.theme_name)
service_item.add_from_command(path,
path_to_str(self.service_item.theme_data.background_filename),
UiIcons().media)
self.media_controller.video(DisplayControllerType.Live, service_item, video_behind_text=True)
self._hide_mouse()
def footer(self, text):
"""
Display the Footer
:param text: footer text to be displayed
"""
js = 'show_footer(\'' + text.replace('\\', '\\\\').replace('\'', '\\\'') + '\')'
self.frame.evaluateJavaScript(js)
def hide_display(self, mode=HideMode.Screen):
"""
Hide the display by making all layers transparent Store the images so they can be replaced when required
:param mode: How the screen is to be hidden
"""
self.log_debug('hide_display mode = {mode:d}'.format(mode=mode))
if self.screens.display_count == 1:
# Only make visible if setting enabled.
if not Settings().value('core/display on monitor'):
return
if mode == HideMode.Screen:
self.frame.evaluateJavaScript('show_blank("desktop");')
self.setVisible(False)
elif mode == HideMode.Blank or self.initial_fame:
self.frame.evaluateJavaScript('show_blank("black");')
else:
self.frame.evaluateJavaScript('show_blank("theme");')
if mode != HideMode.Screen:
if self.isHidden():
self.setVisible(True)
self.web_view.setVisible(True)
# Workaround for bug #1531319, should not be needed with PyQt 5.6.
if is_win():
self.shake_web_view()
self.hide_mode = mode
def show_display(self):
"""
Show the stored layers so the screen reappears as it was originally.
Make the stored images None to release memory.
"""
if self.screens.display_count == 1:
# Only make visible if setting enabled.
if not Settings().value('core/display on monitor'):
return
self.frame.evaluateJavaScript('show_blank("show");')
# Check if setting for hiding logo on startup is enabled.
# If it is, display should remain hidden, otherwise logo is shown. (from def setup)
if self.isHidden() and not Settings().value('core/logo hide on startup'):
self.setVisible(True)
self.hide_mode = None
# Trigger actions when display is active again.
if self.is_live:
Registry().execute('live_display_active')
# Workaround for bug #1531319, should not be needed with PyQt 5.6.
if is_win():
self.shake_web_view()
def _hide_mouse(self):
"""
Hide mouse cursor when moved over display.
"""
if Settings().value('advanced/hide mouse'):
self.setCursor(QtCore.Qt.BlankCursor)
self.frame.evaluateJavaScript('document.body.style.cursor = "none"')
else:
self.setCursor(QtCore.Qt.ArrowCursor)
self.frame.evaluateJavaScript('document.body.style.cursor = "auto"')
def change_window_level(self, window):
"""
Changes the display window level on Mac OS X so that the main window can be brought into focus but still allow
the main display to be above the menu bar and dock when it in focus.
:param window: Window from our application that focus changed to or None if outside our application
"""
if is_macosx():
if window:
# Get different window ids' as int's
try:
window_id = window.winId().__int__()
main_window_id = self.main_window.winId().__int__()
self_id = self.winId().__int__()
except Exception:
return
# If the passed window has the same id as our window make sure the display has the proper level and
# collection behavior.
if window_id == self_id:
self.pyobjc_nsview.window().setLevel_(NSMainMenuWindowLevel + 2)
self.pyobjc_nsview.window().setCollectionBehavior_(NSWindowCollectionBehaviorManaged)
# Else set the displays window level back to normal since we are trying to focus a window other than
# the display.
else:
self.pyobjc_nsview.window().setLevel_(0)
self.pyobjc_nsview.window().setCollectionBehavior_(NSWindowCollectionBehaviorManaged)
# If we are trying to focus the main window raise it now to complete the focus change.
if window_id == main_window_id:
self.main_window.raise_()
def shake_web_view(self):
"""
Resizes the web_view a bit to force an update. Workaround for bug #1531319, should not be needed with PyQt 5.6.
"""
self.web_view.setGeometry(0, 0, self.width(), self.height() - 1)
self.web_view.setGeometry(0, 0, self.width(), self.height())

View File

@ -33,11 +33,10 @@ 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.common import is_win, is_macosx, add_actions
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
from openlp.core.common.i18n import LanguageManager, UiStrings, translate
from openlp.core.ui.icons import UiIcons
from openlp.core.common.mixins import LogMixin, RegistryProperties
from openlp.core.common.path import Path, copyfile, create_paths
from openlp.core.common.registry import Registry
@ -46,26 +45,27 @@ from openlp.core.display.screens import ScreenList
from openlp.core.lib.plugin import PluginStatus
from openlp.core.lib.ui import create_action
from openlp.core.projectors.manager import ProjectorManager
from openlp.core.ui.shortcutlistform import ShortcutListForm
from openlp.core.ui.formattingtagform import FormattingTagForm
from openlp.core.ui.thememanager import ThemeManager
from openlp.core.ui.servicemanager import ServiceManager
from openlp.core.ui.aboutform import AboutForm
from openlp.core.ui.pluginform import PluginForm
from openlp.core.ui.settingsform import SettingsForm
from openlp.core.ui.firsttimeform import FirstTimeForm
from openlp.core.ui.formattingtagform import FormattingTagForm
from openlp.core.ui.icons import UiIcons
from openlp.core.ui.pluginform import PluginForm
from openlp.core.ui.printserviceform import PrintServiceForm
from openlp.core.ui.servicemanager import ServiceManager
from openlp.core.ui.settingsform import SettingsForm
from openlp.core.ui.shortcutlistform import ShortcutListForm
from openlp.core.ui.style import PROGRESSBAR_STYLE, get_library_stylesheet
from openlp.core.ui.thememanager import ThemeManager
from openlp.core.version import get_version
from openlp.core.widgets.dialogs import FileDialog
from openlp.core.widgets.docks import OpenLPDockWidget, MediaDockManager
from openlp.core.widgets.docks import MediaDockManager, OpenLPDockWidget
class Ui_MainWindow(object):
"""
This is the UI part of the main window.
"""
def setupUi(self, main_window):
def setup_ui(self, main_window):
"""
Set up the user interface
"""
@ -340,7 +340,7 @@ class Ui_MainWindow(object):
self.tools_menu.menuAction(), self.settings_menu.menuAction(), self.help_menu.menuAction()))
add_actions(self, [self.search_shortcut_action])
# Initialise the translation
self.retranslateUi(main_window)
self.retranslate_ui(main_window)
self.media_tool_box.setCurrentIndex(0)
# Connect up some signals and slots
self.file_menu.aboutToShow.connect(self.update_recent_files_menu)
@ -351,7 +351,7 @@ class Ui_MainWindow(object):
self.set_lock_panel(panel_locked)
self.settings_imported = False
def retranslateUi(self, main_window):
def retranslate_ui(self, main_window):
"""
Set up the translation system
"""
@ -500,10 +500,10 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, LogMixin, RegistryPropert
self.formatting_tag_form = FormattingTagForm(self)
self.shortcut_form = ShortcutListForm(self)
# Set up the interface
self.setupUi(self)
self.setup_ui(self)
# Define the media Dock Manager
self.media_dock_manager = MediaDockManager(self.media_tool_box)
# Load settings after setupUi so default UI sizes are overwritten
# Load settings after setup_ui so default UI sizes are overwritten
# Once settings are loaded update the menu with the recent files.
self.update_recent_files_menu()
self.plugin_form = PluginForm(self)
@ -577,8 +577,8 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, LogMixin, RegistryPropert
"""
process the bootstrap post setup request
"""
self.preview_controller.panel.setVisible(Settings().value('user interface/preview panel'))
self.live_controller.panel.setVisible(Settings().value('user interface/live panel'))
# self.preview_controller.panel.setVisible(Settings().value('user interface/preview panel'))
# self.live_controller.panel.setVisible(Settings().value('user interface/live panel'))
self.load_settings()
self.restore_current_media_manager_item()
Registry().execute('theme_update_global')
@ -629,8 +629,8 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, LogMixin, RegistryPropert
Show the main form, as well as the display form
"""
QtWidgets.QWidget.show(self)
if self.live_controller.display.isVisible():
self.live_controller.display.setFocus()
# if self.live_controller.display.isVisible():
# self.live_controller.display.setFocus()
self.activateWindow()
if self.arguments:
self.open_cmd_line_files(self.arguments)
@ -813,13 +813,6 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, LogMixin, RegistryPropert
"""
self.settings_form.exec()
def paintEvent(self, event):
"""
We need to make sure, that the SlidePreview's size is correct.
"""
self.preview_controller.preview_size_changed()
self.live_controller.preview_size_changed()
def on_settings_shortcuts_item_clicked(self):
"""
Show the shortcuts dialog
@ -999,7 +992,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, LogMixin, RegistryPropert
"""
self.application.set_busy_cursor()
self.image_manager.update_display()
self.renderer.update_display()
# self.renderer.update_display()
self.preview_controller.screen_size_changed()
self.live_controller.screen_size_changed()
self.setFocus()
@ -1070,7 +1063,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, LogMixin, RegistryPropert
# Close down the display
if self.live_controller.display:
self.live_controller.display.close()
self.live_controller.display = None
# self.live_controller.display = None
# Clean temporary files used by services
self.service_manager_contents.clean_up()
if is_win():

View File

@ -28,6 +28,7 @@ from openlp.core.api.http import requires_auth
from openlp.core.api.http.endpoint import Endpoint
from openlp.core.common.registry import Registry
log = logging.getLogger(__name__)
media_endpoint = Endpoint('media')

View File

@ -50,6 +50,7 @@ from openlp.core.ui.media.endpoint import media_endpoint
from openlp.core.ui.media.vlcplayer import VlcPlayer, get_vlc
from openlp.core.widgets.toolbar import OpenLPToolbar
log = logging.getLogger(__name__)
TICK_TIME = 200
@ -381,7 +382,7 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
log.debug('video mediatype: ' + str(controller.media_info.media_type))
# dont care about actual theme, set a black background
if controller.is_live and not controller.media_info.is_background:
display.frame.evaluateJavaScript('show_video("setBackBoard", null, null,"visible");')
display.frame.runJavaScript('show_video("setBackBoard", null, null,"visible");')
# now start playing - Preview is autoplay!
autoplay = False
# Preview requested
@ -451,7 +452,7 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
# When called from mediaitem display is None
if display is None:
display = controller.preview_display
self.vlc_player.load(display)
self.vlc_player.load(display, filename)
self.resize(display, self.vlc_player)
self.current_media_players[controller.controller_type] = self.vlc_player
if audio_track == -1 and subtitle_track == -1:
@ -533,7 +534,7 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
self.media_volume(controller, controller.media_info.volume)
if first_time:
if not controller.media_info.is_background:
display.frame.evaluateJavaScript('show_blank("desktop");')
display.frame.runJavaScript('show_blank("desktop");')
self.current_media_players[controller.controller_type].set_visible(display, True)
controller.mediabar.actions['playbackPlay'].setVisible(False)
controller.mediabar.actions['playbackPause'].setVisible(True)
@ -653,7 +654,7 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
display = self._define_display(controller)
if controller.controller_type in self.current_media_players:
if not looping_background:
display.frame.evaluateJavaScript('show_blank("black");')
display.frame.runJavaScript('show_blank("black");')
self.current_media_players[controller.controller_type].stop(display)
self.current_media_players[controller.controller_type].set_visible(display, False)
controller.seek_slider.setSliderPosition(0)
@ -724,7 +725,7 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
display.override = {}
self.current_media_players[controller.controller_type].reset(display)
self.current_media_players[controller.controller_type].set_visible(display, False)
display.frame.evaluateJavaScript('show_video("setBackBoard", null, null, "hidden");')
display.frame.runJavaScript('show_video("setBackBoard", null, null, "hidden");')
del self.current_media_players[controller.controller_type]
def media_hide(self, msg):

File diff suppressed because it is too large Load Diff

View File

@ -32,12 +32,13 @@ from distutils.version import LooseVersion
from PyQt5 import QtWidgets
from openlp.core.common import is_win, is_macosx, is_linux
from openlp.core.common import is_linux, is_macosx, is_win
from openlp.core.common.i18n import translate
from openlp.core.common.settings import Settings
from openlp.core.ui.media import MediaState, MediaType
from openlp.core.ui.media.mediaplayer import MediaPlayer
log = logging.getLogger(__name__)
# Audio and video extensions copied from 'include/vlc_interface.h' from vlc 2.2.0 source

View File

@ -33,7 +33,7 @@ class Ui_PluginViewDialog(object):
"""
The UI of the plugin view dialog
"""
def setupUi(self, plugin_view_dialog):
def setup_ui(self, plugin_view_dialog):
"""
Set up the UI
"""
@ -66,9 +66,9 @@ class Ui_PluginViewDialog(object):
self.plugin_layout.addLayout(self.list_layout)
self.button_box = create_button_box(plugin_view_dialog, 'button_box', ['ok'])
self.plugin_layout.addWidget(self.button_box)
self.retranslateUi(plugin_view_dialog)
self.retranslate_ui(plugin_view_dialog)
def retranslateUi(self, plugin_view_dialog):
def retranslate_ui(self, plugin_view_dialog):
"""
Translate the UI on the fly
"""

View File

@ -32,6 +32,7 @@ from openlp.core.common.mixins import RegistryProperties
from openlp.core.lib.plugin import PluginStatus
from openlp.core.ui.plugindialog import Ui_PluginViewDialog
log = logging.getLogger(__name__)
@ -47,7 +48,7 @@ class PluginForm(QtWidgets.QDialog, Ui_PluginViewDialog, RegistryProperties):
QtCore.Qt.WindowCloseButtonHint)
self.active_plugin = None
self.programmatic_change = False
self.setupUi(self)
self.setup_ui(self)
self.load()
self._clear_details()
# Right, now let's put some signals and slots together!

View File

@ -22,7 +22,7 @@
"""
The UI widgets of the print service dialog.
"""
from PyQt5 import QtCore, QtWidgets, QtPrintSupport
from PyQt5 import QtCore, QtPrintSupport, QtWidgets
from openlp.core.common.i18n import UiStrings, translate
from openlp.core.ui.icons import UiIcons
@ -45,7 +45,7 @@ class Ui_PrintServiceDialog(object):
"""
The UI of the print service dialog
"""
def setupUi(self, print_service_dialog):
def setup_ui(self, print_service_dialog):
"""
Set up the UI
"""
@ -127,10 +127,10 @@ class Ui_PrintServiceDialog(object):
self.options_group_box.setLayout(self.group_layout)
self.options_layout.addWidget(self.options_group_box)
self.retranslateUi(print_service_dialog)
self.retranslate_ui(print_service_dialog)
self.options_button.toggled.connect(self.toggle_options)
def retranslateUi(self, print_service_dialog):
def retranslate_ui(self, print_service_dialog):
"""
Translate the UI on the fly
"""

View File

@ -26,7 +26,7 @@ import datetime
import html
import lxml.html
from PyQt5 import QtCore, QtGui, QtWidgets, QtPrintSupport
from PyQt5 import QtCore, QtGui, QtPrintSupport, QtWidgets
from openlp.core.common.applocation import AppLocation
from openlp.core.common.i18n import UiStrings, translate
@ -36,6 +36,7 @@ from openlp.core.common.settings import Settings
from openlp.core.lib import get_text_file_string, image_to_byte
from openlp.core.ui.printservicedialog import Ui_PrintServiceDialog, ZoomSize
DEFAULT_CSS = """/*
Edit this file to customize the service order print. Note, that not all CSS
properties are supported. See:
@ -133,7 +134,7 @@ class PrintServiceForm(QtWidgets.QDialog, Ui_PrintServiceDialog, RegistryPropert
self.print_dialog = QtPrintSupport.QPrintDialog(self.printer, self)
self.document = QtGui.QTextDocument()
self.zoom = 0
self.setupUi(self)
self.setup_ui(self)
# Load the settings for the dialog.
settings = Settings()
settings.beginGroup('advanced')

View File

@ -0,0 +1,97 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
###############################################################################
# OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- #
# Copyright (c) 2008-2018 OpenLP Developers #
# --------------------------------------------------------------------------- #
# This program is free software; you can redistribute it and/or modify it #
# under the terms of the GNU General Public License as published by the Free #
# Software Foundation; version 2 of the License. #
# #
# This program is distributed in the hope that it will be useful, but WITHOUT #
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
# more details. #
# #
# You should have received a copy of the GNU General Public License along #
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
"""
The screen settings tab in the configuration dialog
"""
from PyQt5 import QtWidgets
from openlp.core.common.i18n import translate
from openlp.core.common.settings import Settings
from openlp.core.display.screens import ScreenList
from openlp.core.lib.settingstab import SettingsTab
from openlp.core.common.registry import Registry
from openlp.core.ui.icons import UiIcons
from openlp.core.widgets.widgets import ScreenSelectionWidget
class ScreensTab(SettingsTab):
"""
ScreensTab is the screen settings tab in the configuration dialog
"""
def __init__(self, parent):
"""
Initialise the screen settings tab
"""
self.icon_path = UiIcons().settings
screens_translated = translate('OpenLP.ScreensTab', 'Screens')
super(ScreensTab, self).__init__(parent, 'Screens', screens_translated)
self.settings_section = 'core'
def setup_ui(self):
"""
Set up the user interface elements
"""
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)
self.generic_group_box.setObjectName('generic_group_box')
self.generic_group_layout = QtWidgets.QVBoxLayout(self.generic_group_box)
self.display_on_monitor_check = QtWidgets.QCheckBox(self.generic_group_box)
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'))
def resizeEvent(self, event=None):
"""
Override resizeEvent() to adjust the position of the identify_button.
NB: Don't call SettingsTab's resizeEvent() because we're not using its widgets.
"""
QtWidgets.QWidget.resizeEvent(self, event)
def load(self):
"""
Load the settings to populate the tab
"""
Settings().beginGroup(self.settings_section)
self.screen_selection_widget.load()
# Load generic settings
self.display_on_monitor_check.setChecked(Settings().value('core/display on monitor'))
def save(self):
self.screen_selection_widget.save()
Settings().setValue('core/display on monitor', self.display_on_monitor_check.isChecked())
# On save update the screens as well
self.settings_form.register_post_process('config_screen_changed')

View File

@ -25,7 +25,7 @@ The UI widgets for the service item edit dialog
from PyQt5 import QtWidgets
from openlp.core.common.i18n import translate
from openlp.core.lib.ui import create_button_box, create_button
from openlp.core.lib.ui import create_button, create_button_box
from openlp.core.ui.icons import UiIcons
@ -33,7 +33,7 @@ class Ui_ServiceItemEditDialog(object):
"""
The UI widgets for the service item edit dialog
"""
def setupUi(self, serviceItemEditDialog):
def setup_ui(self, serviceItemEditDialog):
"""
Set up the UI
"""
@ -62,9 +62,9 @@ class Ui_ServiceItemEditDialog(object):
self.dialog_layout.addLayout(self.button_layout, 0, 1)
self.button_box = create_button_box(serviceItemEditDialog, 'button_box', ['cancel', 'save'])
self.dialog_layout.addWidget(self.button_box, 1, 0, 1, 2)
self.retranslateUi(serviceItemEditDialog)
self.retranslate_ui(serviceItemEditDialog)
def retranslateUi(self, serviceItemEditDialog):
def retranslate_ui(self, serviceItemEditDialog):
"""
Translate the UI on the fly
"""

View File

@ -39,7 +39,7 @@ class ServiceItemEditForm(QtWidgets.QDialog, Ui_ServiceItemEditDialog, RegistryP
"""
super(ServiceItemEditForm, self).__init__(Registry().get('main_window'), QtCore.Qt.WindowSystemMenuHint |
QtCore.Qt.WindowTitleHint | QtCore.Qt.WindowCloseButtonHint)
self.setupUi(self)
self.setup_ui(self)
self.item_list = []
self.list_widget.currentRowChanged.connect(self.on_current_row_changed)
@ -51,7 +51,7 @@ class ServiceItemEditForm(QtWidgets.QDialog, Ui_ServiceItemEditDialog, RegistryP
self.item_list = []
if self.item.is_image():
self.data = True
self.item_list.extend(self.item._raw_frames)
self.item_list.extend(self.item.slides)
self.load_data()
self.list_widget.setCurrentItem(self.list_widget.currentItem())
@ -60,7 +60,7 @@ class ServiceItemEditForm(QtWidgets.QDialog, Ui_ServiceItemEditDialog, RegistryP
Get the modified service item.
"""
if self.data:
self.item._raw_frames = []
self.item.slides = []
if self.item.is_image():
for item in self.item_list:
self.item.add_from_image(item['path'], item['title'])

View File

@ -37,17 +37,17 @@ from openlp.core.common import ThemeLevel, delete_file
from openlp.core.common.actions import ActionList, CategoryOrder
from openlp.core.common.applocation import AppLocation
from openlp.core.common.i18n import UiStrings, format_time, translate
from openlp.core.ui.icons import UiIcons
from openlp.core.common.json import OpenLPJsonDecoder, OpenLPJsonEncoder
from openlp.core.common.mixins import LogMixin, RegistryProperties
from openlp.core.common.path import Path, str_to_path
from openlp.core.common.registry import Registry, RegistryBase
from openlp.core.common.settings import Settings
from openlp.core.lib import build_icon
from openlp.core.lib.plugin import PluginStatus
from openlp.core.lib.serviceitem import ServiceItem, ItemCapabilities
from openlp.core.lib.exceptions import ValidationError
from openlp.core.lib.ui import critical_error_message_box, create_widget_action, find_and_set_in_combo_box
from openlp.core.lib.plugin import PluginStatus
from openlp.core.lib.serviceitem import ItemCapabilities, ServiceItem
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.serviceitemeditform import ServiceItemEditForm
from openlp.core.ui.servicenoteform import ServiceNoteForm
from openlp.core.ui.starttimeform import StartTimeForm
@ -730,7 +730,9 @@ 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():
self.renderer.set_service_theme(theme)
# TODO: Use a local display widget
# self.preview_display.set_theme(get_theme_from_name(theme))
pass
else:
if self._save_lite:
service_item.set_from_service(item)
@ -1166,7 +1168,7 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
# Repaint the screen
self.service_manager_list.clear()
self.service_manager_list.clearSelection()
for item_count, item in enumerate(self.service_items):
for item_index, item in enumerate(self.service_items):
service_item_from_item = item['service_item']
tree_widget_item = QtWidgets.QTreeWidgetItem(self.service_manager_list)
if service_item_from_item.is_valid:
@ -1215,17 +1217,17 @@ 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 count, frame in enumerate(service_item_from_item.get_frames()):
for slide_index, slide in enumerate(service_item_from_item.slides):
child = QtWidgets.QTreeWidgetItem(tree_widget_item)
# prefer to use a display_title
if service_item_from_item.is_capable(ItemCapabilities.HasDisplayTitle):
text = frame['display_title'].replace('\n', ' ')
text = slide['display_title'].replace('\n', ' ')
else:
text = frame['title'].replace('\n', ' ')
text = slide['title'].replace('\n', ' ')
child.setText(0, text[:40])
child.setData(0, QtCore.Qt.UserRole, count)
if service_item == item_count:
if item['expanded'] and service_item_child == count:
child.setData(0, QtCore.Qt.UserRole, slide_index)
if service_item == item_index:
if item['expanded'] and service_item_child == slide_index:
self.service_manager_list.setCurrentItem(child)
elif service_item_child == -1:
self.service_manager_list.setCurrentItem(tree_widget_item)
@ -1248,7 +1250,8 @@ 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()
self.renderer.set_service_theme(self.service_theme)
# 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)
@ -1340,7 +1343,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()
# nothing selected for dnd
if self.drop_position == -1:
if isinstance(item, list):
@ -1589,7 +1592,8 @@ 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)
self.renderer.set_service_theme(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

@ -41,8 +41,8 @@ class ServiceNoteForm(QtWidgets.QDialog, RegistryProperties):
"""
super(ServiceNoteForm, self).__init__(Registry().get('main_window'), QtCore.Qt.WindowSystemMenuHint |
QtCore.Qt.WindowTitleHint | QtCore.Qt.WindowCloseButtonHint)
self.setupUi()
self.retranslateUi()
self.setup_ui()
self.retranslate_ui()
def exec(self):
"""
@ -51,7 +51,7 @@ class ServiceNoteForm(QtWidgets.QDialog, RegistryProperties):
self.text_edit.setFocus()
return QtWidgets.QDialog.exec(self)
def setupUi(self):
def setup_ui(self):
"""
Set up the UI of the dialog
"""
@ -66,7 +66,7 @@ class ServiceNoteForm(QtWidgets.QDialog, RegistryProperties):
self.button_box = create_button_box(self, 'button_box', ['cancel', 'save'])
self.dialog_layout.addWidget(self.button_box)
def retranslateUi(self):
def retranslate_ui(self):
"""
Translate the UI on the fly
"""

View File

@ -33,7 +33,7 @@ class Ui_SettingsDialog(object):
"""
The UI widgets of the settings dialog.
"""
def setupUi(self, settings_dialog):
def setup_ui(self, settings_dialog):
"""
Set up the UI
"""
@ -54,9 +54,9 @@ class Ui_SettingsDialog(object):
self.dialog_layout.addLayout(self.stacked_layout, 0, 1, 1, 1)
self.button_box = create_button_box(settings_dialog, 'button_box', ['cancel', 'ok'])
self.dialog_layout.addWidget(self.button_box, 1, 1, 1, 1)
self.retranslateUi(settings_dialog)
self.retranslate_ui(settings_dialog)
def retranslateUi(self, settings_dialog):
def retranslate_ui(self, settings_dialog):
"""
Translate the UI on the fly
"""

View File

@ -34,10 +34,12 @@ from openlp.core.lib import build_icon
from openlp.core.projectors.tab import ProjectorTab
from openlp.core.ui.advancedtab import AdvancedTab
from openlp.core.ui.generaltab import GeneralTab
from openlp.core.ui.screenstab import ScreensTab
from openlp.core.ui.themestab import ThemesTab
from openlp.core.ui.media.mediatab import MediaTab
from openlp.core.ui.settingsdialog import Ui_SettingsDialog
log = logging.getLogger(__name__)
@ -54,7 +56,7 @@ class SettingsForm(QtWidgets.QDialog, Ui_SettingsDialog, RegistryProperties):
super(SettingsForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint |
QtCore.Qt.WindowCloseButtonHint)
self.processes = []
self.setupUi(self)
self.setup_ui(self)
self.setting_list_widget.currentRowChanged.connect(self.list_item_changed)
self.general_tab = None
self.themes_tab = None
@ -74,6 +76,8 @@ class SettingsForm(QtWidgets.QDialog, Ui_SettingsDialog, RegistryProperties):
# take at 0 and the rest shuffle up.
self.stacked_layout.takeAt(0)
self.insert_tab(self.general_tab)
self.insert_tab(self.advanced_tab)
self.insert_tab(self.screens_tab)
self.insert_tab(self.themes_tab)
self.insert_tab(self.advanced_tab)
self.insert_tab(self.player_tab)

View File

@ -67,7 +67,7 @@ class Ui_ShortcutListDialog(object):
"""
The UI widgets for the shortcut dialog.
"""
def setupUi(self, shortcutListDialog):
def setup_ui(self, shortcutListDialog):
"""
Set up the UI
"""
@ -130,9 +130,9 @@ class Ui_ShortcutListDialog(object):
self.button_box = create_button_box(shortcutListDialog, 'button_box', ['cancel', 'ok', 'defaults'])
self.button_box.setOrientation(QtCore.Qt.Horizontal)
self.shortcut_list_layout.addWidget(self.button_box)
self.retranslateUi(shortcutListDialog)
self.retranslate_ui(shortcutListDialog)
def retranslateUi(self, shortcutListDialog):
def retranslate_ui(self, shortcutListDialog):
"""
Translate the UI on the fly
"""

View File

@ -33,6 +33,7 @@ from openlp.core.common.mixins import RegistryProperties
from openlp.core.common.settings import Settings
from openlp.core.ui.shortcutlistdialog import Ui_ShortcutListDialog
REMOVE_AMPERSAND = re.compile(r'&{1}')
log = logging.getLogger(__name__)
@ -49,7 +50,7 @@ class ShortcutListForm(QtWidgets.QDialog, Ui_ShortcutListDialog, RegistryPropert
"""
super(ShortcutListForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint |
QtCore.Qt.WindowCloseButtonHint)
self.setupUi(self)
self.setup_ui(self)
self.changed_actions = {}
self.action_list = ActionList.get_instance()
self.dialog_was_shown = False

View File

@ -32,19 +32,21 @@ from openlp.core.common import SlideLimits
from openlp.core.common.actions import ActionList, CategoryOrder
from openlp.core.common.i18n import UiStrings, translate
from openlp.core.common.mixins import LogMixin, RegistryProperties
from openlp.core.common.path import Path
from openlp.core.common.registry import Registry, RegistryBase
from openlp.core.common.settings import Settings
from openlp.core.display.screens import ScreenList
from openlp.core.lib import ImageSource, ServiceItemAction
from openlp.core.lib.htmlbuilder import build_html
from openlp.core.lib.serviceitem import ServiceItem, ItemCapabilities
from openlp.core.display.window import DisplayWindow
from openlp.core.lib import ServiceItemAction, image_to_byte
from openlp.core.lib.serviceitem import ItemCapabilities
from openlp.core.lib.ui import create_action
from openlp.core.ui import HideMode, DisplayControllerType
from openlp.core.ui.maindisplay import MainDisplay, Display
from openlp.core.ui import DisplayControllerType, HideMode
from openlp.core.ui.icons import UiIcons
from openlp.core.widgets.layouts import AspectRatioLayout
from openlp.core.widgets.toolbar import OpenLPToolbar
from openlp.core.widgets.views import ListPreviewWidget
# Threshold which has to be trespassed to toggle.
HIDE_MENU_THRESHOLD = 27
@ -68,34 +70,6 @@ NON_TEXT_MENU = [
]
class DisplayController(QtWidgets.QWidget):
"""
Controller is a general display controller widget.
"""
def __init__(self, *args, **kwargs):
"""
Set up the general Controller.
"""
super().__init__(*args, **kwargs)
self.is_live = False
self.display = None
self.controller_type = None
Registry().set_flag('has doubleclick added item to service', True)
Registry().set_flag('replace service manager item', False)
def send_to_plugins(self, *args):
"""
This is the generic function to send signal for control widgets, created from within other plugins
This function is needed to catch the current controller
:param args: Arguments to send to the plugins
"""
sender = self.sender().objectName() if self.sender().objectName() else self.sender().text()
controller = self
Registry().execute('{text}'.format(text=sender), [controller, args])
class InfoLabel(QtWidgets.QLabel):
"""
InfoLabel is a subclassed QLabel. Created to provide the ablilty to add a ellipsis if the text is cut off. Original
@ -124,7 +98,7 @@ class InfoLabel(QtWidgets.QLabel):
super().setText(text)
class SlideController(DisplayController, LogMixin, RegistryProperties):
class SlideController(QtWidgets.QWidget, LogMixin, RegistryProperties):
"""
SlideController is the slide controller widget. This widget is what the
user uses to control the displaying of verses/slides/etc on the screen.
@ -135,21 +109,46 @@ class SlideController(DisplayController, LogMixin, RegistryProperties):
Set up the Slide Controller.
"""
super().__init__(*args, **kwargs)
self.is_live = False
self.controller_type = None
self.displays = []
self.screens = ScreenList()
Registry().set_flag('has doubleclick added item to service', True)
Registry().set_flag('replace service manager item', False)
def post_set_up(self):
"""
Call by bootstrap functions
"""
self.initialise()
self.setup_displays()
self.screen_size_changed()
def setup_displays(self):
"""
Set up the display
"""
if not self.is_live:
return
if self.displays:
# Delete any existing displays
del self.displays[:]
for screen in self.screens:
if screen.is_display:
display = DisplayWindow(self, screen)
self.displays.append(display)
# display.media_watcher.progress.connect(self.on_audio_time_remaining)
@property
def display(self):
return self.displays[0] if self.displays else None
def initialise(self):
"""
Initialise the UI elements of the controller
"""
self.screens = ScreenList()
try:
self.ratio = self.screens.current['size'].width() / self.screens.current['size'].height()
self.ratio = self.screens.current.display_geometry.width() / self.screens.current.display_geometry.height()
except ZeroDivisionError:
self.ratio = 1
self.process_queue_lock = Lock()
@ -338,30 +337,15 @@ class SlideController(DisplayController, LogMixin, RegistryProperties):
self.preview_frame.setFrameShape(QtWidgets.QFrame.StyledPanel)
self.preview_frame.setFrameShadow(QtWidgets.QFrame.Sunken)
self.preview_frame.setObjectName('preview_frame')
self.grid = QtWidgets.QGridLayout(self.preview_frame)
self.grid.setContentsMargins(8, 8, 8, 8)
self.grid.setObjectName('grid')
self.slide_layout = QtWidgets.QVBoxLayout()
self.slide_layout = AspectRatioLayout(self.preview_frame, self.ratio)
self.slide_layout.margin = 8
self.slide_layout.setSpacing(0)
self.slide_layout.setContentsMargins(0, 0, 0, 0)
self.slide_layout.setObjectName('SlideLayout')
self.preview_display = Display(self)
self.slide_layout.insertWidget(0, self.preview_display)
self.preview_display.hide()
# Set up the preview display
self.preview_display = DisplayWindow(self)
self.slide_layout.addWidget(self.preview_display)
self.slide_layout.resize.connect(self.on_preview_resize)
# Actual preview screen
self.slide_preview = QtWidgets.QLabel(self)
size_policy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
size_policy.setHorizontalStretch(0)
size_policy.setVerticalStretch(0)
size_policy.setHeightForWidth(self.slide_preview.sizePolicy().hasHeightForWidth())
self.slide_preview.setSizePolicy(size_policy)
self.slide_preview.setFrameShape(QtWidgets.QFrame.Box)
self.slide_preview.setFrameShadow(QtWidgets.QFrame.Plain)
self.slide_preview.setLineWidth(1)
self.slide_preview.setScaledContents(True)
self.slide_preview.setObjectName('slide_preview')
self.slide_layout.insertWidget(0, self.slide_preview)
self.grid.addLayout(self.slide_layout, 0, 0, 1, 1)
if self.is_live:
self.current_shortcut = ''
self.shortcut_timer = QtCore.QTimer()
@ -412,13 +396,13 @@ class SlideController(DisplayController, LogMixin, RegistryProperties):
Registry().register_function('slidecontroller_update_slide_limits', self.update_slide_limits)
getattr(self, 'slidecontroller_{text}_set'.format(text=self.type_prefix)).connect(self.on_slide_selected_index)
getattr(self, 'slidecontroller_{text}_next'.format(text=self.type_prefix)).connect(self.on_slide_selected_next)
# NOTE: {t} used to keep line length < maxline
# NOTE: {} used to keep line length < maxline
getattr(self,
'slidecontroller_{t}_previous'.format(t=self.type_prefix)).connect(self.on_slide_selected_previous)
'slidecontroller_{}_previous'.format(self.type_prefix)).connect(self.on_slide_selected_previous)
if self.is_live:
getattr(self, 'mediacontroller_live_play').connect(self.media_controller.on_media_play)
getattr(self, 'mediacontroller_live_pause').connect(self.media_controller.on_media_pause)
getattr(self, 'mediacontroller_live_stop').connect(self.media_controller.on_media_stop)
self.mediacontroller_live_play.connect(self.media_controller.on_media_play)
self.mediacontroller_live_pause.connect(self.media_controller.on_media_pause)
self.mediacontroller_live_stop.connect(self.media_controller.on_media_stop)
def _slide_shortcut_activated(self):
"""
@ -484,6 +468,17 @@ class SlideController(DisplayController, LogMixin, RegistryProperties):
# Reset the shortcut.
self.current_shortcut = ''
def send_to_plugins(self, *args):
"""
This is the generic function to send signal for control widgets, created from within other plugins
This function is needed to catch the current controller
:param args: Arguments to send to the plugins
"""
sender = self.sender().objectName() if self.sender().objectName() else self.sender().text()
controller = self
Registry().execute('{text}'.format(text=sender), [controller, args])
def set_live_hot_keys(self, parent=None):
"""
Set the live hotkeys
@ -554,30 +549,14 @@ class SlideController(DisplayController, LogMixin, RegistryProperties):
"""
Settings dialog has changed the screen size of adjust output and screen previews.
"""
# rebuild display as screen size changed
if self.display:
self.display.close()
self.display = MainDisplay(self)
self.display.setup()
if self.is_live:
self.__add_actions_to_widget(self.display)
# if self.display.audio_player:
# self.display.audio_player.position_changed.connect(self.on_audio_time_remaining)
if self.is_live and self.displays:
for display in self.displays:
display.resize(self.screens.current.display_geometry.size())
# if self.is_live:
# self.__add_actions_to_widget(self.display)
# The SlidePreview's ratio.
try:
self.ratio = self.screens.current['size'].width() / self.screens.current['size'].height()
except ZeroDivisionError:
self.ratio = 1
self.media_controller.setup_display(self.display, False)
self.preview_size_changed()
self.preview_widget.screen_size_changed(self.ratio)
self.preview_display.setup()
service_item = ServiceItem()
self.preview_display.web_view.setHtml(build_html(service_item, self.preview_display.screen, None, self.is_live,
plugins=self.plugin_manager.plugins))
self.media_controller.setup_display(self.preview_display, True)
if self.service_item:
self.refresh_service_item()
# TODO: Need to basically update everything
def __add_actions_to_widget(self, widget):
"""
@ -598,28 +577,6 @@ class SlideController(DisplayController, LogMixin, RegistryProperties):
self.theme_screen,
self.blank_screen])
def preview_size_changed(self):
"""
Takes care of the SlidePreview's size. Is called when one of the the splitters is moved or when the screen
size is changed. Note, that this method is (also) called frequently from the mainwindow *paintEvent*.
"""
if self.ratio < self.preview_frame.width() / self.preview_frame.height():
# We have to take the height as limit.
max_height = self.preview_frame.height() - self.grid.contentsMargins().top() * 2
self.slide_preview.setFixedSize(QtCore.QSize(max_height * self.ratio, max_height))
self.preview_display.setFixedSize(QtCore.QSize(max_height * self.ratio, max_height))
self.preview_display.screen = {'size': self.preview_display.geometry()}
else:
# We have to take the width as limit.
max_width = self.preview_frame.width() - self.grid.contentsMargins().top() * 2
self.slide_preview.setFixedSize(QtCore.QSize(max_width, max_width / self.ratio))
self.preview_display.setFixedSize(QtCore.QSize(max_width, max_width / self.ratio))
self.preview_display.screen = {'size': self.preview_display.geometry()}
# Only update controller layout if width has actually changed
if self.controller_width != self.controller.width():
self.controller_width = self.controller.width()
self.on_controller_size_changed(self.controller_width)
def on_controller_size_changed(self, width):
"""
Change layout of display control buttons on controller size change
@ -654,16 +611,6 @@ class SlideController(DisplayController, LogMixin, RegistryProperties):
else:
self.toolbar.set_widget_visible(NON_TEXT_MENU, visible)
def on_song_bar_handler(self):
"""
Some song handler
"""
request = self.sender().text()
slide_no = self.slide_list[request]
width = self.main_window.control_splitter.sizes()[self.split]
self.preview_widget.replace_service_item(self.service_item, width, slide_no)
self.slide_selected()
def receive_spin_delay(self):
"""
Adjusts the value of the ``delay_spin_box`` to the given one.
@ -711,7 +658,7 @@ class SlideController(DisplayController, LogMixin, RegistryProperties):
if (Settings().value(self.main_window.songs_settings_section + '/display songbar') and
not self.song_menu.menu().isEmpty()):
self.toolbar.set_widget_visible('song_menu', True)
if item.is_capable(ItemCapabilities.CanLoop) and len(item.get_frames()) > 1:
if item.is_capable(ItemCapabilities.CanLoop) and len(item.slides) > 1:
self.toolbar.set_widget_visible(LOOP_LIST)
if item.is_media() or item.is_capable(ItemCapabilities.HasBackgroundAudio):
self.mediabar.show()
@ -763,7 +710,6 @@ class SlideController(DisplayController, LogMixin, RegistryProperties):
:param item: The current service item
"""
item.render()
slide_no = 0
if self.song_edit:
slide_no = self.selected_row
@ -821,12 +767,23 @@ class SlideController(DisplayController, LogMixin, RegistryProperties):
old_item = self.service_item
# rest to allow the remote pick up verse 1 if large imaged
self.selected_row = 0
self.preview_display.go_to_slide(0)
# take a copy not a link to the servicemanager copy.
self.service_item = copy.copy(service_item)
if self.service_item.is_command():
Registry().execute(
'{text}_start'.format(text=service_item.name.lower()),
[self.service_item, self.is_live, self.hide_mode(), slide_no])
else:
# Get theme
theme_name = service_item.theme if service_item.theme else Registry().get('theme_manager').global_theme
theme_data = Registry().get('theme_manager').get_theme_data(theme_name)
# Set theme for preview
self.preview_display.set_theme(theme_data)
# Set theme for displays
for display in self.displays:
display.set_theme(theme_data)
# Reset blanking if needed
if old_item and self.is_live and (old_item.is_capable(ItemCapabilities.ProvidesOwnDisplay) or
self.service_item.is_capable(ItemCapabilities.ProvidesOwnDisplay)):
@ -841,35 +798,43 @@ class SlideController(DisplayController, LogMixin, RegistryProperties):
self.on_media_start(service_item)
row = 0
width = self.main_window.control_splitter.sizes()[self.split]
for frame_number, frame in enumerate(self.service_item.get_frames()):
if self.service_item.is_text():
if frame['verseTag']:
if self.service_item.is_text():
self.preview_display.load_verses(service_item.rendered_slides)
for display in self.displays:
display.load_verses(service_item.rendered_slides)
for slide_index, slide in enumerate(self.service_item.display_slides):
if not slide['verse'].isdigit():
# These tags are already translated.
verse_def = frame['verseTag']
verse_def = slide['verse']
verse_def = '{def1}{def2}'.format(def1=verse_def[0], def2=verse_def[1:])
two_line_def = '{def1}\n{def2}'.format(def1=verse_def[0], def2=verse_def[1:])
row = two_line_def
if verse_def not in self.slide_list:
self.slide_list[verse_def] = frame_number
self.slide_list[verse_def] = slide_index
if self.is_live:
self.song_menu.menu().addAction(verse_def, self.on_song_bar_handler)
else:
row += 1
self.slide_list[str(row)] = row - 1
else:
else:
if service_item.is_image():
self.preview_display.load_images(service_item.slides)
for display in self.displays:
display.load_images(service_item.slides)
for slide_index, slide in enumerate(self.service_item.slides):
row += 1
self.slide_list[str(row)] = row - 1
# If current slide set background to image
if not self.service_item.is_command() and frame_number == slide_no:
self.service_item.bg_image_bytes = \
self.image_manager.get_image_bytes(frame['path'], ImageSource.ImagePlugin)
# if not self.service_item.is_command() and slide_index == slide_no:
# self.service_item.bg_image_bytes = \
# self.image_manager.get_image_bytes(slide['filename'], ImageSource.ImagePlugin)
self.preview_widget.replace_service_item(self.service_item, width, slide_no)
self.enable_tool_bar(self.service_item)
# Pass to display for viewing.
# Postpone image build, we need to do this later to avoid the theme
# flashing on the screen
if not self.service_item.is_image():
self.display.build_html(self.service_item)
# if not self.service_item.is_image():
# self.display.build_html(self.service_item)
if self.service_item.is_media():
self.on_media_start(self.service_item)
self.slide_selected(True)
@ -906,23 +871,47 @@ class SlideController(DisplayController, LogMixin, RegistryProperties):
self.preview_widget.change_slide(index)
self.slide_selected()
def on_song_bar_handler(self):
"""
Some song handler
"""
request = self.sender().text()
slide_no = self.slide_list[request]
width = self.main_window.control_splitter.sizes()[self.split]
self.preview_widget.replace_service_item(self.service_item, width, slide_no)
self.slide_selected()
def on_preview_resize(self, size):
"""
Set the preview display's zoom factor based on the size relative to the display size
"""
display_with = 0
for screen in self.screens:
if screen.is_display:
display_with = screen.display_geometry.width()
if display_with == 0:
ratio = 0.25
else:
ratio = float(size.width()) / display_with
self.preview_display.set_scale(ratio)
def main_display_set_background(self):
"""
Allow the main display to blank the main display at startup time
"""
display_type = Settings().value(self.main_window.general_settings_section + '/screen blank')
if self.screens.which_screen(self.window()) != self.screens.which_screen(self.display):
# Order done to handle initial conversion
if display_type == 'themed':
self.on_theme_display(True)
elif display_type == 'hidden':
self.on_hide_display(True)
elif display_type == 'blanked':
self.on_blank_display(True)
else:
Registry().execute('live_display_show')
else:
self.on_hide_display_enable()
# display_type = Settings().value(self.main_window.general_settings_section + '/screen blank')
# if self.screens.which_screen(self.window()) != self.screens.which_screen(self.display):
# # Order done to handle initial conversion
# if display_type == 'themed':
# self.on_theme_display(True)
# elif display_type == 'hidden':
# self.on_hide_display(True)
# elif display_type == 'blanked':
# self.on_blank_display(True)
# else:
# Registry().execute('live_display_show')
# else:
# self.on_hide_display_enable()
def on_slide_blank(self):
"""
@ -1087,7 +1076,7 @@ class SlideController(DisplayController, LogMixin, RegistryProperties):
if not start:
Registry().execute('slidecontroller_live_unblank')
row = self.preview_widget.current_slide_number()
old_selected_row = self.selected_row
# old_selected_row = self.selected_row
self.selected_row = 0
if -1 < row < self.preview_widget.slide_count():
if self.service_item.is_command():
@ -1095,20 +1084,26 @@ class SlideController(DisplayController, LogMixin, RegistryProperties):
Registry().execute('{text}_slide'.format(text=self.service_item.name.lower()),
[self.service_item, self.is_live, row])
else:
to_display = self.service_item.get_rendered_frame(row)
# to_display = self.service_item.get_rendered_frame(row)
if self.service_item.is_text():
self.display.text(to_display, row != old_selected_row)
for display in self.displays:
display.go_to_slide(row)
# self.display.text(to_display, row != old_selected_row)
else:
if start:
self.display.build_html(self.service_item, to_display)
for display in self.displays:
display.load_images(self.service_item.slides)
# self.display.build_html(self.service_item, to_display)
else:
self.display.image(to_display)
for display in self.displays:
display.go_to_slide(row)
# self.display.image(to_display)
# reset the store used to display first image
self.service_item.bg_image_bytes = None
self.selected_row = row
self.update_preview()
self.preview_widget.change_slide(row)
self.display.setFocus()
# TODO: self.display.setFocus()
# Release lock
self.slide_selected_lock.release()
@ -1126,7 +1121,7 @@ class SlideController(DisplayController, LogMixin, RegistryProperties):
"""
This updates the preview frame, for example after changing a slide or using *Blank to Theme*.
"""
self.log_debug('update_preview {text} '.format(text=self.screens.current['primary']))
self.log_debug('update_preview {text} '.format(text=self.screens.current))
if self.service_item and self.service_item.is_capable(ItemCapabilities.ProvidesOwnDisplay):
if self.is_live:
# If live, grab screen-cap of main display now
@ -1135,18 +1130,17 @@ class SlideController(DisplayController, LogMixin, RegistryProperties):
QtCore.QTimer.singleShot(2500, self.grab_maindisplay)
else:
# If not live, use the slide's thumbnail/icon instead
image_path = self.service_item.get_rendered_frame(self.selected_row)
if self.service_item.is_capable(ItemCapabilities.HasThumbnails):
image = self.image_manager.get_image(image_path, ImageSource.CommandPlugins)
self.slide_image = QtGui.QPixmap.fromImage(image)
else:
self.slide_image = QtGui.QPixmap(image_path)
self.slide_image.setDevicePixelRatio(self.main_window.devicePixelRatio())
self.slide_preview.setPixmap(self.slide_image)
image_path = Path(self.service_item.get_rendered_frame(self.selected_row))
# if self.service_item.is_capable(ItemCapabilities.HasThumbnails):
# image = self.image_manager.get_image(image_path, ImageSource.CommandPlugins)
# self.slide_image = QtGui.QPixmap.fromImage(image)
# else:
# self.slide_image = QtGui.QPixmap(image_path)
# self.slide_image.setDevicePixelRatio(self.main_window.devicePixelRatio())
# self.slide_preview.setPixmap(self.slide_image)
self.preview_display.set_single_image('#000', image_path)
else:
self.slide_image = self.display.preview()
self.slide_image.setDevicePixelRatio(self.main_window.devicePixelRatio())
self.slide_preview.setPixmap(self.slide_image)
self.preview_display.go_to_slide(self.selected_row)
self.slide_count += 1
def grab_maindisplay(self):
@ -1155,11 +1149,13 @@ class SlideController(DisplayController, LogMixin, RegistryProperties):
"""
win_id = QtWidgets.QApplication.desktop().winId()
screen = QtWidgets.QApplication.primaryScreen()
rect = self.screens.current['size']
rect = ScreenList().current.display_geometry
win_image = screen.grabWindow(win_id, rect.x(), rect.y(), rect.width(), rect.height())
win_image.setDevicePixelRatio(self.slide_preview.devicePixelRatio())
self.slide_preview.setPixmap(win_image)
win_image.setDevicePixelRatio(self.preview_display.devicePixelRatio())
# self.slide_preview.setPixmap(win_image)
self.slide_image = win_image
base64_image = image_to_byte(win_image, True)
self.preview_display.set_single_image_data('#000', base64_image)
def on_slide_selected_next_action(self, checked):
"""
@ -1408,7 +1404,6 @@ class SlideController(DisplayController, LogMixin, RegistryProperties):
self.media_controller.video(self.controller_type, item, self.hide_mode())
if not self.is_live:
self.preview_display.show()
self.slide_preview.hide()
def on_media_close(self):
"""
@ -1416,7 +1411,6 @@ class SlideController(DisplayController, LogMixin, RegistryProperties):
"""
self.media_controller.media_reset(self)
self.preview_display.hide()
self.slide_preview.show()
def _reset_blank(self, no_theme):
"""
@ -1491,7 +1485,7 @@ class PreviewController(RegistryBase, SlideController):
"""
Set up the base Controller as a preview.
"""
self.__registry_name = 'preview_slidecontroller'
self.__registry_name = 'preview_controller'
super().__init__(*args, **kwargs)
self.split = 0
self.type_prefix = 'preview'
@ -1520,6 +1514,7 @@ class LiveController(RegistryBase, SlideController):
"""
Set up the base Controller as a live.
"""
self.__registry_name = 'live_controller'
super().__init__(*args, **kwargs)
self.is_live = True
self.split = 1

View File

@ -35,9 +35,9 @@ class SplashScreen(QtWidgets.QSplashScreen):
Constructor
"""
super(SplashScreen, self).__init__()
self.setupUi()
self.setup_ui()
def setupUi(self):
def setup_ui(self):
"""
Set up the UI
"""

View File

@ -33,7 +33,7 @@ class Ui_StartTimeDialog(object):
"""
The UI widgets for the time dialog
"""
def setupUi(self, StartTimeDialog):
def setup_ui(self, StartTimeDialog):
"""
Set up the UI
"""
@ -107,10 +107,10 @@ class Ui_StartTimeDialog(object):
self.dialog_layout.addWidget(self.second_spin_box, 3, 1, 1, 1)
self.button_box = create_button_box(StartTimeDialog, 'button_box', ['cancel', 'ok'])
self.dialog_layout.addWidget(self.button_box, 5, 2, 1, 2)
self.retranslateUi(StartTimeDialog)
self.retranslate_ui(StartTimeDialog)
self.setMaximumHeight(self.sizeHint().height())
def retranslateUi(self, StartTimeDialog):
def retranslate_ui(self, StartTimeDialog):
"""
Update the translations on the fly
"""

View File

@ -41,7 +41,7 @@ class StartTimeForm(QtWidgets.QDialog, Ui_StartTimeDialog, RegistryProperties):
"""
super(StartTimeForm, self).__init__(Registry().get('main_window'), QtCore.Qt.WindowSystemMenuHint |
QtCore.Qt.WindowTitleHint | QtCore.Qt.WindowCloseButtonHint)
self.setupUi(self)
self.setup_ui(self)
def exec(self):
"""

View File

@ -28,6 +28,7 @@ from openlp.core.common import is_win
from openlp.core.common.registry import Registry
from openlp.core.common.settings import Settings
try:
import qdarkstyle
HAS_DARK_STYLE = True

View File

@ -30,11 +30,13 @@ from openlp.core.common import get_images_filter, is_not_image_file
from openlp.core.common.i18n import UiStrings, translate
from openlp.core.common.mixins import RegistryProperties
from openlp.core.common.registry import Registry
from openlp.core.lib.theme import BackgroundType, BackgroundGradientType
from openlp.core.lib.theme import BackgroundGradientType, BackgroundType
from openlp.core.lib.ui import critical_error_message_box
from openlp.core.ui.themelayoutform import ThemeLayoutForm
# TODO: Fix this. Use a "get_video_extensions" method which uses the current media player
from openlp.core.ui.media.vlcplayer import VIDEO_EXT
from .themewizard import Ui_ThemeWizard
from openlp.core.ui.themelayoutform import ThemeLayoutForm
from openlp.core.ui.themewizard import Ui_ThemeWizard
log = logging.getLogger(__name__)
@ -59,7 +61,7 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties):
"""
Set up the class. This method is mocked out by the tests.
"""
self.setupUi(self)
self.setup_ui(self)
self.registerFields()
self.update_theme_allowed = True
self.temp_background_filename = None
@ -217,15 +219,19 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties):
Generate layout preview and display the form.
"""
self.update_theme()
width = self.renderer.width
height = self.renderer.height
width = self.renderer.width()
height = self.renderer.height()
pixmap = QtGui.QPixmap(width, height)
pixmap.fill(QtCore.Qt.white)
paint = QtGui.QPainter(pixmap)
paint.setPen(QtGui.QPen(QtCore.Qt.blue, 2))
paint.drawRect(self.renderer.get_main_rectangle(self.theme))
main_rect = QtCore.QRect(self.theme.font_main_x, self.theme.font_main_y,
self.theme.font_main_width - 1, self.theme.font_main_height - 1)
paint.drawRect(main_rect)
paint.setPen(QtGui.QPen(QtCore.Qt.red, 2))
paint.drawRect(self.renderer.get_footer_rectangle(self.theme))
footer_rect = QtCore.QRect(self.theme.font_footer_x, self.theme.font_footer_y,
self.theme.font_footer_width - 1, self.theme.font_footer_height - 1)
paint.drawRect(footer_rect)
paint.end()
self.theme_layout_form.exec(pixmap)

View File

@ -33,7 +33,7 @@ class Ui_ThemeLayoutDialog(object):
"""
The layout of the theme
"""
def setupUi(self, themeLayoutDialog):
def setup_ui(self, themeLayoutDialog):
"""
Set up the UI
"""
@ -62,9 +62,9 @@ class Ui_ThemeLayoutDialog(object):
self.preview_layout.addWidget(self.footer_colour_label)
self.button_box = create_button_box(themeLayoutDialog, 'button_box', ['ok'])
self.preview_layout.addWidget(self.button_box)
self.retranslateUi(themeLayoutDialog)
self.retranslate_ui(themeLayoutDialog)
def retranslateUi(self, themeLayoutDialog):
def retranslate_ui(self, themeLayoutDialog):
"""
Translate the UI on the fly
"""

View File

@ -36,7 +36,7 @@ class ThemeLayoutForm(QtWidgets.QDialog, Ui_ThemeLayoutDialog):
Constructor
"""
super(ThemeLayoutForm, self).__init__(parent)
self.setupUi(self)
self.setup_ui(self)
def exec(self, image):
"""

View File

@ -24,24 +24,24 @@ The Theme Manager manages adding, deleteing and modifying of themes.
"""
import os
import zipfile
from xml.etree.ElementTree import ElementTree, XML
from xml.etree.ElementTree import XML, ElementTree
from PyQt5 import QtCore, QtGui, QtWidgets
from openlp.core.common import delete_file
from openlp.core.common.applocation import AppLocation
from openlp.core.common.i18n import UiStrings, translate, get_locale_key
from openlp.core.ui.icons import UiIcons
from openlp.core.common.i18n import UiStrings, get_locale_key, translate
from openlp.core.common.mixins import LogMixin, RegistryProperties
from openlp.core.common.path import Path, copyfile, create_paths, path_to_str
from openlp.core.common.registry import Registry, RegistryBase
from openlp.core.common.settings import Settings
from openlp.core.lib import ImageSource, get_text_file_string, build_icon, \
check_item_selected, create_thumb, validate_thumb
from openlp.core.lib import ImageSource, build_icon, check_item_selected, create_thumb, get_text_file_string, \
validate_thumb
from openlp.core.lib.exceptions import ValidationError
from openlp.core.lib.theme import Theme, BackgroundType
from openlp.core.lib.ui import critical_error_message_box, create_widget_action
from openlp.core.lib.theme import BackgroundType, Theme
from openlp.core.lib.ui import create_widget_action, critical_error_message_box
from openlp.core.ui.filerenameform import FileRenameForm
from openlp.core.ui.icons import UiIcons
from openlp.core.ui.themeform import ThemeForm
from openlp.core.widgets.dialogs import FileDialog
from openlp.core.widgets.toolbar import OpenLPToolbar
@ -295,7 +295,7 @@ class ThemeManager(QtWidgets.QWidget, RegistryBase, Ui_ThemeManager, LogMixin, R
for plugin in self.plugin_manager.plugins:
if plugin.uses_theme(old_theme_name):
plugin.rename_theme(old_theme_name, new_theme_name)
self.renderer.update_theme(new_theme_name, old_theme_name)
self.renderer.set_theme(self.get_theme_data(new_theme_name))
self.load_themes()
def on_copy_theme(self, field=None):
@ -347,7 +347,7 @@ class ThemeManager(QtWidgets.QWidget, RegistryBase, Ui_ThemeManager, LogMixin, R
self.theme_form.theme = theme
self.theme_form.exec(True)
self.old_background_image_path = None
self.renderer.update_theme(theme.theme_name)
self.renderer.set_theme(theme)
self.load_themes()
def on_delete_theme(self, field=None):
@ -363,7 +363,7 @@ class ThemeManager(QtWidgets.QWidget, RegistryBase, Ui_ThemeManager, LogMixin, R
row = self.theme_list_widget.row(item)
self.theme_list_widget.takeItem(row)
self.delete_theme(theme)
self.renderer.update_theme(theme, only_delete=True)
self.renderer.set_theme(item.data(QtCore.Qt.UserRole))
# As we do not reload the themes, push out the change. Reload the
# list as the internal lists and events need to be triggered.
self._push_themes()
@ -669,7 +669,7 @@ class ThemeManager(QtWidgets.QWidget, RegistryBase, Ui_ThemeManager, LogMixin, R
create_paths(theme_dir)
theme_path = theme_dir / '{file_name}.json'.format(file_name=name)
try:
theme_path.write_text(theme_pretty)
theme_path.write_text(theme_pretty)
except OSError:
self.log_exception('Saving theme to file failed')
if image_source_path and image_destination_path:

View File

@ -45,12 +45,12 @@ class ThemesTab(SettingsTab):
theme_translated = translate('OpenLP.ThemesTab', 'Themes')
super(ThemesTab, self).__init__(parent, 'Themes', theme_translated)
def setupUi(self):
def setup_ui(self):
"""
Set up the UI
"""
self.setObjectName('ThemesTab')
super(ThemesTab, self).setupUi()
super(ThemesTab, self).setup_ui()
self.global_group_box = QtWidgets.QGroupBox(self.left_column)
self.global_group_box.setObjectName('global_group_box')
self.global_group_box_layout = QtWidgets.QVBoxLayout(self.global_group_box)
@ -109,7 +109,7 @@ class ThemesTab(SettingsTab):
self.default_combo_box.activated.connect(self.on_default_combo_box_changed)
Registry().register_function('theme_update_list', self.update_theme_list)
def retranslateUi(self):
def retranslate_ui(self):
"""
Translate the UI on the fly
"""
@ -188,7 +188,7 @@ class ThemesTab(SettingsTab):
Set the global default theme
"""
self.global_theme = self.default_combo_box.currentText()
self.renderer.set_global_theme()
# self.renderer.set_global_theme()
self._preview_global_theme()
def update_theme_list(self, theme_list):
@ -204,7 +204,7 @@ class ThemesTab(SettingsTab):
self.default_combo_box.clear()
self.default_combo_box.addItems(theme_list)
find_and_set_in_combo_box(self.default_combo_box, self.global_theme)
self.renderer.set_global_theme()
# self.renderer.set_global_theme()
self.renderer.set_theme_level(self.theme_level)
if self.global_theme is not '':
self._preview_global_theme()

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