forked from openlp/openlp
Head
This commit is contained in:
commit
c0d328dc06
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
})
|
||||
}
|
16
nose2.cfg
16
nose2.cfg
|
@ -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
|
||||
|
|
|
@ -1 +1 @@
|
|||
2.9.0
|
||||
2.5.dev2856
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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__)
|
||||
|
||||
|
||||
|
|
|
@ -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__)
|
||||
|
|
|
@ -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:'))
|
||||
|
|
|
@ -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__)
|
||||
|
||||
|
||||
|
|
|
@ -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']:
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -27,6 +27,7 @@ from copy import deepcopy
|
|||
|
||||
import sqlalchemy
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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__)
|
||||
|
||||
|
||||
|
|
|
@ -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']
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 |
|
@ -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>
|
|
@ -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
|
@ -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);
|
||||
}
|
||||
}));
|
|
@ -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(' ', ' ')
|
||||
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 += ' '
|
||||
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 += ' '
|
||||
else:
|
||||
if not tail and remainder and remainder[0] == ' ':
|
||||
for c in range(chord_length):
|
||||
whitespaces += ' '
|
||||
if whitespaces:
|
||||
if '_' in whitespaces:
|
||||
ws_length = len(whitespaces)
|
||||
if ws_length == 1:
|
||||
whitespaces = '–'
|
||||
else:
|
||||
wsl_mod = ws_length // 2
|
||||
ws_right = ws_left = ' ' * wsl_mod
|
||||
whitespaces = ws_left + '–' + ws_right
|
||||
whitespaces = 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 == ' ':
|
||||
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 = ' '
|
||||
chords.append(chord)
|
||||
lyrics.append(lyric)
|
||||
chord = ''
|
||||
lyric = ''
|
||||
elif char == ']' and in_chord:
|
||||
in_chord = False
|
||||
elif in_chord:
|
||||
chord += char
|
||||
else:
|
||||
lyric += char
|
||||
if lyric != '' or chord != '':
|
||||
if chord == '':
|
||||
chord = ' '
|
||||
if lyric == '':
|
||||
lyric = ' '
|
||||
chords.append(chord)
|
||||
lyrics.append(lyric)
|
||||
new_chord_line = '<tr class="chordrow">'
|
||||
new_lyric_line = '</tr><tr>'
|
||||
for i in range(len(lyrics)):
|
||||
spacer = compare_chord_lyric_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} {endtags}</td>'.format(
|
||||
starttags=start_formatting_tags, lyrics=lyrics[i], endtags=end_formatting_tags)
|
||||
else:
|
||||
spacing = ''
|
||||
if spacer > 0:
|
||||
space = ' ' * int(math.ceil(spacer / 2))
|
||||
spacing = '<span class="chordspacing">%s-%s</span>' % (space, space)
|
||||
new_lyric_line += '<td class="lyrics">{starttags}{lyrics}{spacing}{endtags}</td>'.format(
|
||||
starttags=start_formatting_tags, lyrics=lyrics[i], spacing=spacing,
|
||||
endtags=end_formatting_tags)
|
||||
new_line += new_chord_line + new_lyric_line + '</tr>'
|
||||
else:
|
||||
start_formatting_tags = ''
|
||||
if active_formatting_tags:
|
||||
start_formatting_tags = '{' + '}{'.join(active_formatting_tags) + '}'
|
||||
active_formatting_tags = find_formatting_tags(word, active_formatting_tags)
|
||||
end_formatting_tags = ''
|
||||
if active_formatting_tags:
|
||||
end_formatting_tags = '{/' + '}{/'.join(active_formatting_tags) + '}'
|
||||
new_line += '<tr class="chordrow"><td class="chord"> </td></tr><tr><td class="lyrics">' \
|
||||
'{starttags}{lyrics} {endtags}</td></tr>'.format(
|
||||
starttags=start_formatting_tags, lyrics=word, endtags=end_formatting_tags)
|
||||
new_line += '</table>'
|
||||
else:
|
||||
new_line += line
|
||||
new_line += '</td></tr></table>'
|
||||
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
|
|
@ -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)
|
|
@ -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')
|
||||
|
|
|
@ -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)
|
|
@ -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))
|
|
@ -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(' ', ' ')
|
||||
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 += ' '
|
||||
else:
|
||||
if not remainder:
|
||||
for c in range(math.floor((chordlen - taillen) / 2)):
|
||||
whitespaces += '_'
|
||||
else:
|
||||
for c in range(chordlen - taillen + 1):
|
||||
whitespaces += ' '
|
||||
else:
|
||||
if not tail and remainder and remainder[0] == ' ':
|
||||
for c in range(chordlen):
|
||||
whitespaces += ' '
|
||||
if whitespaces:
|
||||
if '_' in whitespaces:
|
||||
ws_length = len(whitespaces)
|
||||
if ws_length == 1:
|
||||
whitespaces = '–'
|
||||
else:
|
||||
wsl_mod = ws_length // 2
|
||||
ws_right = ws_left = ' ' * wsl_mod
|
||||
whitespaces = ws_left + '–' + ws_right
|
||||
whitespaces = '<span class="ws">' + whitespaces + '</span>'
|
||||
return '<span class="chord"><span><strong>' + html.escape(chord) + '</strong></span></span>' + html.escape(tail) + \
|
||||
whitespaces + html.escape(remainder)
|
||||
|
||||
|
||||
def expand_chords(text):
|
||||
"""
|
||||
Expand ChordPro tags
|
||||
|
||||
:param text:
|
||||
"""
|
||||
text_lines = text.split('{br}')
|
||||
expanded_text_lines = []
|
||||
chords_on_prev_line = False
|
||||
for line in text_lines:
|
||||
# If a ChordPro is detected in the line, replace it with a html-span tag and wrap the line in a span tag.
|
||||
if '[' in line and ']' in line:
|
||||
if chords_on_prev_line:
|
||||
new_line = '<span class="chordline">'
|
||||
else:
|
||||
new_line = '<span class="chordline firstchordline">'
|
||||
chords_on_prev_line = True
|
||||
# Matches a chord, a tail, a remainder and a line end. See expand_and_align_chords_in_line() for more info.
|
||||
new_line += re.sub(r'\[(.*?)\]([\u0080-\uFFFF,\w]*)'
|
||||
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 == ' ':
|
||||
return 0
|
||||
chord = re.sub(r'\{.*?\}', r'', chord)
|
||||
lyric = re.sub(r'\{.*?\}', r'', lyric)
|
||||
for chord_char in chord:
|
||||
if chord_char not in SLIMCHARS:
|
||||
chordlen += 2
|
||||
else:
|
||||
chordlen += 1
|
||||
lyriclen = 0
|
||||
for lyric_char in lyric:
|
||||
if lyric_char not in SLIMCHARS:
|
||||
lyriclen += 2
|
||||
else:
|
||||
lyriclen += 1
|
||||
if chordlen > lyriclen:
|
||||
return chordlen - lyriclen
|
||||
else:
|
||||
return 0
|
||||
|
||||
|
||||
def find_formatting_tags(text, active_formatting_tags):
|
||||
"""
|
||||
Look for formatting tags in lyrics and adds/removes them to/from the given list. Returns the update list.
|
||||
|
||||
:param text:
|
||||
:param active_formatting_tags:
|
||||
:return:
|
||||
"""
|
||||
if not re.search(r'\{.*?\}', text):
|
||||
return active_formatting_tags
|
||||
word_it = iter(text)
|
||||
# Loop through lyrics to find any formatting tags
|
||||
for char in word_it:
|
||||
if char == '{':
|
||||
tag = ''
|
||||
char = next(word_it)
|
||||
start_tag = True
|
||||
if char == '/':
|
||||
start_tag = False
|
||||
char = next(word_it)
|
||||
while char != '}':
|
||||
tag += char
|
||||
char = next(word_it)
|
||||
# See if the found tag has an end tag
|
||||
for formatting_tag in FormattingTags.get_html_tags():
|
||||
if formatting_tag['start tag'] == '{' + tag + '}':
|
||||
if formatting_tag['end tag']:
|
||||
if start_tag:
|
||||
# prepend the new tag to the list of active formatting tags
|
||||
active_formatting_tags[:0] = [tag]
|
||||
else:
|
||||
# remove the tag from the list
|
||||
active_formatting_tags.remove(tag)
|
||||
# Break out of the loop matching the found tag against the tag list.
|
||||
break
|
||||
return active_formatting_tags
|
||||
|
||||
|
||||
def expand_chords_for_printing(text, line_split):
|
||||
"""
|
||||
Expand ChordPro tags
|
||||
|
||||
:param text:
|
||||
:param line_split:
|
||||
"""
|
||||
if not re.search(r'\[.*?\]', text):
|
||||
return text
|
||||
text_lines = text.split(line_split)
|
||||
expanded_text_lines = []
|
||||
for line in text_lines:
|
||||
# If a ChordPro is detected in the line, build html tables.
|
||||
new_line = '<table class="line" width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td>'
|
||||
active_formatting_tags = []
|
||||
if re.search(r'\[.*?\]', line):
|
||||
words = line.split(' ')
|
||||
in_chord = False
|
||||
for word in words:
|
||||
chords = []
|
||||
lyrics = []
|
||||
new_line += '<table class="segment" cellpadding="0" cellspacing="0" border="0" align="left">'
|
||||
# If the word contains a chord, we need to handle it.
|
||||
if re.search(r'\[.*?\]', word):
|
||||
chord = ''
|
||||
lyric = ''
|
||||
# Loop over each character of the word
|
||||
for char in word:
|
||||
if char == '[':
|
||||
in_chord = True
|
||||
if lyric != '':
|
||||
if chord == '':
|
||||
chord = ' '
|
||||
chords.append(chord)
|
||||
lyrics.append(lyric)
|
||||
chord = ''
|
||||
lyric = ''
|
||||
elif char == ']' and in_chord:
|
||||
in_chord = False
|
||||
elif in_chord:
|
||||
chord += char
|
||||
else:
|
||||
lyric += char
|
||||
if lyric != '' or chord != '':
|
||||
if chord == '':
|
||||
chord = ' '
|
||||
if lyric == '':
|
||||
lyric = ' '
|
||||
chords.append(chord)
|
||||
lyrics.append(lyric)
|
||||
new_chord_line = '<tr class="chordrow">'
|
||||
new_lyric_line = '</tr><tr>'
|
||||
for i in range(len(lyrics)):
|
||||
spacer = compare_chord_lyric(chords[i], lyrics[i])
|
||||
# Handle formatting tags
|
||||
start_formatting_tags = ''
|
||||
if active_formatting_tags:
|
||||
start_formatting_tags = '{' + '}{'.join(active_formatting_tags) + '}'
|
||||
# Update list of active formatting tags
|
||||
active_formatting_tags = find_formatting_tags(lyrics[i], active_formatting_tags)
|
||||
end_formatting_tags = ''
|
||||
if active_formatting_tags:
|
||||
end_formatting_tags = '{/' + '}{/'.join(active_formatting_tags) + '}'
|
||||
new_chord_line += '<td class="chord">%s</td>' % chords[i]
|
||||
# Check if this is the last column, if so skip spacing calc and instead insert a single space
|
||||
if i + 1 == len(lyrics):
|
||||
new_lyric_line += '<td class="lyrics">{starttags}{lyrics} {endtags}</td>'.format(
|
||||
starttags=start_formatting_tags, lyrics=lyrics[i], endtags=end_formatting_tags)
|
||||
else:
|
||||
spacing = ''
|
||||
if spacer > 0:
|
||||
space = ' ' * int(math.ceil(spacer / 2))
|
||||
spacing = '<span class="chordspacing">%s-%s</span>' % (space, space)
|
||||
new_lyric_line += '<td class="lyrics">{starttags}{lyrics}{spacing}{endtags}</td>'.format(
|
||||
starttags=start_formatting_tags, lyrics=lyrics[i], spacing=spacing,
|
||||
endtags=end_formatting_tags)
|
||||
new_line += new_chord_line + new_lyric_line + '</tr>'
|
||||
else:
|
||||
start_formatting_tags = ''
|
||||
if active_formatting_tags:
|
||||
start_formatting_tags = '{' + '}{'.join(active_formatting_tags) + '}'
|
||||
active_formatting_tags = find_formatting_tags(word, active_formatting_tags)
|
||||
end_formatting_tags = ''
|
||||
if active_formatting_tags:
|
||||
end_formatting_tags = '{/' + '}{/'.join(active_formatting_tags) + '}'
|
||||
new_line += '<tr class="chordrow"><td class="chord"> </td></tr><tr><td class="lyrics">' \
|
||||
'{starttags}{lyrics} {endtags}</td></tr>'.format(
|
||||
starttags=start_formatting_tags, lyrics=word, endtags=end_formatting_tags)
|
||||
new_line += '</table>'
|
||||
else:
|
||||
new_line += line
|
||||
new_line += '</td></tr></table>'
|
||||
expanded_text_lines.append(new_line)
|
||||
# the {br} tag used to split lines is not inserted again since the style of the line-tables makes them redundant.
|
||||
return ''.join(expanded_text_lines)
|
||||
|
||||
|
||||
def create_separated_list(string_list):
|
||||
"""
|
||||
Returns a string that represents a join of a list of strings with a localized separator.
|
||||
|
|
|
@ -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__)
|
||||
|
||||
|
||||
|
|
|
@ -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| )+(?![^<]*>)/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| )+(?![^<]*>)/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)
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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__)
|
||||
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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('&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
|
||||
|
|
|
@ -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.
|
||||
"""
|
||||
|
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
@ -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__)
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -26,6 +26,7 @@ import logging
|
|||
|
||||
from openlp.core.common.i18n import translate
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
log.debug('projector_constants loaded')
|
||||
|
||||
|
|
|
@ -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())
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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')
|
||||
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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__)
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
"""
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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.
|
||||
"""
|
||||
|
|
|
@ -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.
|
||||
"""
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
"""
|
||||
|
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
@ -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())
|
||||
|
||||
|
|
|
@ -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.
|
||||
"""
|
||||
|
|
|
@ -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()))
|
||||
|
|
|
@ -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 '
|
||||
|
|
|
@ -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
|
||||
"""
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
@ -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__)
|
||||
|
||||
|
||||
|
|
|
@ -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())
|
|
@ -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():
|
||||
|
|
|
@ -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')
|
||||
|
||||
|
|
|
@ -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
|
@ -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
|
||||
|
|
|
@ -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
|
||||
"""
|
||||
|
|
|
@ -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!
|
||||
|
|
|
@ -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
|
||||
"""
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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')
|
|
@ -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
|
||||
"""
|
||||
|
|
|
@ -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'])
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
"""
|
||||
|
|
|
@ -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
|
||||
"""
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
"""
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
"""
|
||||
|
|
|
@ -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
|
||||
"""
|
||||
|
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
"""
|
||||
|
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue