openlp-core team mailing list archive
-
openlp-core team
-
Mailing list archive
-
Message #33346
[Merge] lp:~openlp-dev/openlp/webengine-migrate into lp:openlp
Tomas Groth has proposed merging lp:~openlp-dev/openlp/webengine-migrate into lp:openlp.
Commit message:
Migration from WebKit to Webengine. Also introduced reveal.js for slide rendering, new screen setup dialogs and many other changes.
Requested reviews:
OpenLP Core (openlp-core)
For more details, see:
https://code.launchpad.net/~openlp-dev/openlp/webengine-migrate/+merge/362766
--
The attached diff has been truncated due to its size.
Your team OpenLP Core is requested to review the proposed merge of lp:~openlp-dev/openlp/webengine-migrate into lp:openlp.
=== modified file '.bzrignore'
--- .bzrignore 2018-06-26 17:10:34 +0000
+++ .bzrignore 2019-02-06 07:01:54 +0000
@@ -45,4 +45,11 @@
resources/windows/warnOpenLP.txt
*.ropeproject
tags
+output
+htmlcov
+node_modules
+openlp-test-projectordb.sqlite
+package-lock.json
+.cache
+test
tests.kdev4
=== added file 'karma.conf.js'
--- karma.conf.js 1970-01-01 00:00:00 +0000
+++ karma.conf.js 2019-02-06 07:01:54 +0000
@@ -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
+ }
+ })
+}
=== modified file 'nose2.cfg'
--- nose2.cfg 2017-11-16 05:03:19 +0000
+++ nose2.cfg 2019-02-06 07:01:54 +0000
@@ -1,5 +1,5 @@
[unittest]
-verbose = True
+verbose = true
plugins = nose2.plugins.mp
[log-capture]
@@ -9,19 +9,19 @@
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
=== modified file 'openlp/.version'
--- openlp/.version 2019-01-11 20:23:24 +0000
+++ openlp/.version 2019-02-06 07:01:54 +0000
@@ -1,1 +1,1 @@
-2.9.0
+2.5.dev2856
\ No newline at end of file
=== modified file 'openlp/core/api/deploy.py'
--- openlp/core/api/deploy.py 2018-01-04 07:00:55 +0000
+++ openlp/core/api/deploy.py 2019-02-06 07:01:54 +0000
@@ -25,7 +25,7 @@
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
=== modified file 'openlp/core/api/endpoint/controller.py'
--- openlp/core/api/endpoint/controller.py 2018-08-25 14:08:19 +0000
+++ openlp/core/api/endpoint/controller.py 2019-02-06 07:01:54 +0000
@@ -34,6 +34,7 @@
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 @@
: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 @@
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)
=== modified file 'openlp/core/api/endpoint/core.py'
--- openlp/core/api/endpoint/core.py 2018-08-25 14:08:19 +0000
+++ openlp/core/api/endpoint/core.py 2019-02-06 07:01:54 +0000
@@ -30,8 +30,8 @@
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'
=== modified file 'openlp/core/api/endpoint/remote.py'
--- openlp/core/api/endpoint/remote.py 2017-12-29 09:15:48 +0000
+++ openlp/core/api/endpoint/remote.py 2019-02-06 07:01:54 +0000
@@ -24,6 +24,7 @@
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')
=== modified file 'openlp/core/api/endpoint/service.py'
--- openlp/core/api/endpoint/service.py 2017-12-29 09:15:48 +0000
+++ openlp/core/api/endpoint/service.py 2019-02-06 07:01:54 +0000
@@ -26,6 +26,7 @@
from openlp.core.api.http.endpoint import Endpoint
from openlp.core.common.registry import Registry
+
log = logging.getLogger(__name__)
service_endpoint = Endpoint('service')
=== modified file 'openlp/core/api/http/server.py'
--- openlp/core/api/http/server.py 2018-01-07 04:36:45 +0000
+++ openlp/core/api/http/server.py 2019-02-06 07:01:54 +0000
@@ -30,22 +30,21 @@
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__)
=== modified file 'openlp/core/api/http/wsgiapp.py'
--- openlp/core/api/http/wsgiapp.py 2018-07-04 20:42:55 +0000
+++ openlp/core/api/http/wsgiapp.py 2019-02-06 07:01:54 +0000
@@ -33,6 +33,7 @@
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__)
=== modified file 'openlp/core/api/tab.py'
--- openlp/core/api/tab.py 2018-08-25 14:08:19 +0000
+++ openlp/core/api/tab.py 2019-02-06 07:01:54 +0000
@@ -31,6 +31,7 @@
from openlp.core.lib.settingstab import SettingsTab
from openlp.core.ui.icons import UiIcons
+
ZERO_URL = '0.0.0.0'
@@ -43,9 +44,9 @@
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 @@
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:'))
=== modified file 'openlp/core/api/websockets.py'
--- openlp/core/api/websockets.py 2018-01-07 17:50:29 +0000
+++ openlp/core/api/websockets.py 2019-02-06 07:01:54 +0000
@@ -35,6 +35,7 @@
from openlp.core.common.settings import Settings
from openlp.core.threading import ThreadWorker, run_thread
+
log = logging.getLogger(__name__)
=== modified file 'openlp/core/app.py'
--- openlp/core/app.py 2018-10-25 16:38:39 +0000
+++ openlp/core/app.py 2019-02-06 07:01:54 +0000
@@ -33,27 +33,28 @@
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 @@
"""
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 @@
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 @@
: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']:
=== modified file 'openlp/core/common/actions.py'
--- openlp/core/common/actions.py 2018-10-13 10:24:01 +0000
+++ openlp/core/common/actions.py 2019-02-06 07:01:54 +0000
@@ -29,6 +29,7 @@
from openlp.core.common.settings import Settings
+
log = logging.getLogger(__name__)
@@ -113,7 +114,6 @@
if item[1] == action:
self.actions.remove(item)
return
- log.warning('Action "{action}" does not exist.'.format(action=action))
class CategoryList(object):
=== modified file 'openlp/core/common/applocation.py'
--- openlp/core/common/applocation.py 2018-10-07 23:34:00 +0000
+++ openlp/core/common/applocation.py 2019-02-06 07:01:54 +0000
@@ -29,10 +29,11 @@
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
=== modified file 'openlp/core/common/db.py'
--- openlp/core/common/db.py 2017-12-29 09:15:48 +0000
+++ openlp/core/common/db.py 2019-02-06 07:01:54 +0000
@@ -27,6 +27,7 @@
import sqlalchemy
+
log = logging.getLogger(__name__)
=== modified file 'openlp/core/common/httputils.py'
--- openlp/core/common/httputils.py 2018-06-08 20:55:20 +0000
+++ openlp/core/common/httputils.py 2019-02-06 07:01:54 +0000
@@ -34,6 +34,7 @@
from openlp.core.common.registry import Registry
from openlp.core.common.settings import ProxyMode, Settings
+
log = logging.getLogger(__name__ + '.__init__')
USER_AGENTS = {
=== modified file 'openlp/core/common/i18n.py'
--- openlp/core/common/i18n.py 2018-10-27 11:05:41 +0000
+++ openlp/core/common/i18n.py 2019-02-06 07:01:54 +0000
@@ -29,10 +29,11 @@
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__)
=== modified file 'openlp/core/common/mixins.py'
--- openlp/core/common/mixins.py 2018-11-18 17:29:47 +0000
+++ openlp/core/common/mixins.py 2019-02-06 07:01:54 +0000
@@ -28,6 +28,7 @@
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']
=== modified file 'openlp/core/common/path.py'
--- openlp/core/common/path.py 2018-08-12 11:14:47 +0000
+++ openlp/core/common/path.py 2019-02-06 07:01:54 +0000
@@ -25,6 +25,7 @@
from openlp.core.common import is_win
+
if is_win():
from pathlib import WindowsPath as PathVariant # pragma: nocover
else:
=== modified file 'openlp/core/common/registry.py'
--- openlp/core/common/registry.py 2018-10-25 16:38:39 +0000
+++ openlp/core/common/registry.py 2019-02-06 07:01:54 +0000
@@ -27,6 +27,7 @@
from openlp.core.common import de_hump, trace_error_handler
+
log = logging.getLogger(__name__)
@@ -143,6 +144,7 @@
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)
=== modified file 'openlp/core/common/settings.py'
--- openlp/core/common/settings.py 2018-10-24 19:35:22 +0000
+++ openlp/core/common/settings.py 2019-02-06 07:01:54 +0000
@@ -1,4 +1,4 @@
-# -*- coding: utf-8 -*-
+ # -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
###############################################################################
@@ -33,7 +33,8 @@
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 @@
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 @@
# circular dependency.
'core/display on monitor': True,
'core/override position': False,
+ 'core/monitor': {},
'core/application version': '0.0',
'images/background color': '#000000',
'media/players': 'system,webkit',
@@ -276,6 +306,8 @@
('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', '', []),
@@ -545,7 +577,7 @@
: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)
@@ -568,8 +600,11 @@
# 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):
@@ -578,6 +613,8 @@
# 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
=== added directory 'openlp/core/display/html'
=== added file 'openlp/core/display/html/black.css'
--- openlp/core/display/html/black.css 1970-01-01 00:00:00 +0000
+++ openlp/core/display/html/black.css 2019-02-06 07:01:54 +0000
@@ -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); }
=== added file 'openlp/core/display/html/checkerboard.png'
Binary files openlp/core/display/html/checkerboard.png 1970-01-01 00:00:00 +0000 and openlp/core/display/html/checkerboard.png 2019-02-06 07:01:54 +0000 differ
=== added file 'openlp/core/display/html/display.html'
--- openlp/core/display/html/display.html 1970-01-01 00:00:00 +0000
+++ openlp/core/display/html/display.html 2019-02-06 07:01:54 +0000
@@ -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>
=== added file 'openlp/core/display/html/display.js'
--- openlp/core/display/html/display.js 1970-01-01 00:00:00 +0000
+++ openlp/core/display/html/display.js 2019-02-06 07:01:54 +0000
@@ -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;
+});
=== added file 'openlp/core/display/html/openlp-splash-screen.png'
Binary files openlp/core/display/html/openlp-splash-screen.png 1970-01-01 00:00:00 +0000 and openlp/core/display/html/openlp-splash-screen.png 2019-02-06 07:01:54 +0000 differ
=== added file 'openlp/core/display/html/reveal.css'
--- openlp/core/display/html/reveal.css 1970-01-01 00:00:00 +0000
+++ openlp/core/display/html/reveal.css 2019-02-06 07:01:54 +0000
@@ -0,0 +1,1387 @@
+/*!
+ * reveal.js
+ * http://lab.hakim.se/reveal-js
+ * MIT licensed
+ *
+ * Copyright (C) 2017 Hakim El Hattab, http://hakim.se
+ */
+/*********************************************
+ * RESET STYLES
+ *********************************************/
+html, body, .reveal div, .reveal span, .reveal applet, .reveal object, .reveal iframe,
+.reveal h1, .reveal h2, .reveal h3, .reveal h4, .reveal h5, .reveal h6, .reveal p, .reveal blockquote, .reveal pre,
+.reveal a, .reveal abbr, .reveal acronym, .reveal address, .reveal big, .reveal cite, .reveal code,
+.reveal del, .reveal dfn, .reveal em, .reveal img, .reveal ins, .reveal kbd, .reveal q, .reveal s, .reveal samp,
+.reveal small, .reveal strike, .reveal strong, .reveal sub, .reveal sup, .reveal tt, .reveal var,
+.reveal b, .reveal u, .reveal center,
+.reveal dl, .reveal dt, .reveal dd, .reveal ol, .reveal ul, .reveal li,
+.reveal fieldset, .reveal form, .reveal label, .reveal legend,
+.reveal table, .reveal caption, .reveal tbody, .reveal tfoot, .reveal thead, .reveal tr, .reveal th, .reveal td,
+.reveal article, .reveal aside, .reveal canvas, .reveal details, .reveal embed,
+.reveal figure, .reveal figcaption, .reveal footer, .reveal header, .reveal hgroup,
+.reveal menu, .reveal nav, .reveal output, .reveal ruby, .reveal section, .reveal summary,
+.reveal time, .reveal mark, .reveal audio, .reveal video {
+ margin: 0;
+ padding: 0;
+ border: 0;
+ font-size: 100%;
+ font: inherit;
+ vertical-align: baseline; }
+
+.reveal article, .reveal aside, .reveal details, .reveal figcaption, .reveal figure,
+.reveal footer, .reveal header, .reveal hgroup, .reveal menu, .reveal nav, .reveal section {
+ display: block; }
+
+/*********************************************
+ * GLOBAL STYLES
+ *********************************************/
+html,
+body {
+ width: 100%;
+ height: 100%;
+ overflow: hidden; }
+
+body {
+ position: relative;
+ line-height: 1;
+ background-color: #fff;
+ color: #000; }
+
+/*********************************************
+ * VIEW FRAGMENTS
+ *********************************************/
+.reveal .slides section .fragment {
+ opacity: 0;
+ visibility: hidden;
+ -webkit-transition: all .2s ease;
+ transition: all .2s ease; }
+ .reveal .slides section .fragment.visible {
+ opacity: 1;
+ visibility: inherit; }
+
+.reveal .slides section .fragment.grow {
+ opacity: 1;
+ visibility: inherit; }
+ .reveal .slides section .fragment.grow.visible {
+ -webkit-transform: scale(1.3);
+ transform: scale(1.3); }
+
+.reveal .slides section .fragment.shrink {
+ opacity: 1;
+ visibility: inherit; }
+ .reveal .slides section .fragment.shrink.visible {
+ -webkit-transform: scale(0.7);
+ transform: scale(0.7); }
+
+.reveal .slides section .fragment.zoom-in {
+ -webkit-transform: scale(0.1);
+ transform: scale(0.1); }
+ .reveal .slides section .fragment.zoom-in.visible {
+ -webkit-transform: none;
+ transform: none; }
+
+.reveal .slides section .fragment.fade-out {
+ opacity: 1;
+ visibility: inherit; }
+ .reveal .slides section .fragment.fade-out.visible {
+ opacity: 0;
+ visibility: hidden; }
+
+.reveal .slides section .fragment.semi-fade-out {
+ opacity: 1;
+ visibility: inherit; }
+ .reveal .slides section .fragment.semi-fade-out.visible {
+ opacity: 0.5;
+ visibility: inherit; }
+
+.reveal .slides section .fragment.strike {
+ opacity: 1;
+ visibility: inherit; }
+ .reveal .slides section .fragment.strike.visible {
+ text-decoration: line-through; }
+
+.reveal .slides section .fragment.fade-up {
+ -webkit-transform: translate(0, 20%);
+ transform: translate(0, 20%); }
+ .reveal .slides section .fragment.fade-up.visible {
+ -webkit-transform: translate(0, 0);
+ transform: translate(0, 0); }
+
+.reveal .slides section .fragment.fade-down {
+ -webkit-transform: translate(0, -20%);
+ transform: translate(0, -20%); }
+ .reveal .slides section .fragment.fade-down.visible {
+ -webkit-transform: translate(0, 0);
+ transform: translate(0, 0); }
+
+.reveal .slides section .fragment.fade-right {
+ -webkit-transform: translate(-20%, 0);
+ transform: translate(-20%, 0); }
+ .reveal .slides section .fragment.fade-right.visible {
+ -webkit-transform: translate(0, 0);
+ transform: translate(0, 0); }
+
+.reveal .slides section .fragment.fade-left {
+ -webkit-transform: translate(20%, 0);
+ transform: translate(20%, 0); }
+ .reveal .slides section .fragment.fade-left.visible {
+ -webkit-transform: translate(0, 0);
+ transform: translate(0, 0); }
+
+.reveal .slides section .fragment.current-visible {
+ opacity: 0;
+ visibility: hidden; }
+ .reveal .slides section .fragment.current-visible.current-fragment {
+ opacity: 1;
+ visibility: inherit; }
+
+.reveal .slides section .fragment.highlight-red,
+.reveal .slides section .fragment.highlight-current-red,
+.reveal .slides section .fragment.highlight-green,
+.reveal .slides section .fragment.highlight-current-green,
+.reveal .slides section .fragment.highlight-blue,
+.reveal .slides section .fragment.highlight-current-blue {
+ opacity: 1;
+ visibility: inherit; }
+
+.reveal .slides section .fragment.highlight-red.visible {
+ color: #ff2c2d; }
+
+.reveal .slides section .fragment.highlight-green.visible {
+ color: #17ff2e; }
+
+.reveal .slides section .fragment.highlight-blue.visible {
+ color: #1b91ff; }
+
+.reveal .slides section .fragment.highlight-current-red.current-fragment {
+ color: #ff2c2d; }
+
+.reveal .slides section .fragment.highlight-current-green.current-fragment {
+ color: #17ff2e; }
+
+.reveal .slides section .fragment.highlight-current-blue.current-fragment {
+ color: #1b91ff; }
+
+/*********************************************
+ * DEFAULT ELEMENT STYLES
+ *********************************************/
+/* Fixes issue in Chrome where italic fonts did not appear when printing to PDF */
+.reveal:after {
+ content: '';
+ font-style: italic; }
+
+.reveal iframe {
+ z-index: 1; }
+
+/** Prevents layering issues in certain browser/transition combinations */
+.reveal a {
+ position: relative; }
+
+.reveal .stretch {
+ max-width: none;
+ max-height: none; }
+
+.reveal pre.stretch code {
+ height: 100%;
+ max-height: 100%;
+ box-sizing: border-box; }
+
+/*********************************************
+ * CONTROLS
+ *********************************************/
+.reveal .controls {
+ display: none;
+ position: fixed;
+ width: 110px;
+ height: 110px;
+ z-index: 30;
+ right: 10px;
+ bottom: 10px;
+ -webkit-user-select: none; }
+
+.reveal .controls button {
+ padding: 0;
+ position: absolute;
+ opacity: 0.05;
+ width: 0;
+ height: 0;
+ background-color: transparent;
+ border: 12px solid transparent;
+ -webkit-transform: scale(0.9999);
+ transform: scale(0.9999);
+ -webkit-transition: all 0.2s ease;
+ transition: all 0.2s ease;
+ -webkit-appearance: none;
+ -webkit-tap-highlight-color: transparent; }
+
+.reveal .controls .enabled {
+ opacity: 0.7;
+ cursor: pointer; }
+
+.reveal .controls .enabled:active {
+ margin-top: 1px; }
+
+.reveal .controls .navigate-left {
+ top: 42px;
+ border-right-width: 22px;
+ border-right-color: #000; }
+
+.reveal .controls .navigate-left.fragmented {
+ opacity: 0.3; }
+
+.reveal .controls .navigate-right {
+ left: 74px;
+ top: 42px;
+ border-left-width: 22px;
+ border-left-color: #000; }
+
+.reveal .controls .navigate-right.fragmented {
+ opacity: 0.3; }
+
+.reveal .controls .navigate-up {
+ left: 42px;
+ border-bottom-width: 22px;
+ border-bottom-color: #000; }
+
+.reveal .controls .navigate-up.fragmented {
+ opacity: 0.3; }
+
+.reveal .controls .navigate-down {
+ left: 42px;
+ top: 74px;
+ border-top-width: 22px;
+ border-top-color: #000; }
+
+.reveal .controls .navigate-down.fragmented {
+ opacity: 0.3; }
+
+/*********************************************
+ * PROGRESS BAR
+ *********************************************/
+.reveal .progress {
+ position: fixed;
+ display: none;
+ height: 3px;
+ width: 100%;
+ bottom: 0;
+ left: 0;
+ z-index: 10;
+ background-color: rgba(0, 0, 0, 0.2); }
+
+.reveal .progress:after {
+ content: '';
+ display: block;
+ position: absolute;
+ height: 20px;
+ width: 100%;
+ top: -20px; }
+
+.reveal .progress span {
+ display: block;
+ height: 100%;
+ width: 0px;
+ background-color: #000;
+ -webkit-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); }
+
+/*********************************************
+ * SLIDE NUMBER
+ *********************************************/
+.reveal .slide-number {
+ position: fixed;
+ display: block;
+ right: 8px;
+ bottom: 8px;
+ z-index: 31;
+ font-family: Helvetica, sans-serif;
+ font-size: 12px;
+ line-height: 1;
+ color: #fff;
+ background-color: rgba(0, 0, 0, 0.4);
+ padding: 5px; }
+
+.reveal .slide-number-delimiter {
+ margin: 0 3px; }
+
+/*********************************************
+ * SLIDES
+ *********************************************/
+.reveal {
+ position: relative;
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+ -ms-touch-action: none;
+ touch-action: none; }
+
+.reveal .slides {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ margin: auto;
+ pointer-events: none;
+ overflow: visible;
+ z-index: 1;
+ text-align: center;
+ -webkit-perspective: 600px;
+ perspective: 600px;
+ -webkit-perspective-origin: 50% 40%;
+ perspective-origin: 50% 40%; }
+
+.reveal .slides > section {
+ -ms-perspective: 600px; }
+
+.reveal .slides > section,
+.reveal .slides > section > section {
+ display: none;
+ position: absolute;
+ width: 100%;
+ padding: 20px 0px;
+ pointer-events: auto;
+ z-index: 10;
+ -webkit-transform-style: flat;
+ transform-style: flat;
+ -webkit-transition: -webkit-transform-origin 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985), -webkit-transform 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985), visibility 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985), opacity 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985);
+ transition: transform-origin 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985), transform 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985), visibility 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985), opacity 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985); }
+
+/* Global transition speed settings */
+.reveal[data-transition-speed="fast"] .slides section {
+ -webkit-transition-duration: 400ms;
+ transition-duration: 400ms; }
+
+.reveal[data-transition-speed="slow"] .slides section {
+ -webkit-transition-duration: 1200ms;
+ transition-duration: 1200ms; }
+
+/* Slide-specific transition speed overrides */
+.reveal .slides section[data-transition-speed="fast"] {
+ -webkit-transition-duration: 400ms;
+ transition-duration: 400ms; }
+
+.reveal .slides section[data-transition-speed="slow"] {
+ -webkit-transition-duration: 1200ms;
+ transition-duration: 1200ms; }
+
+.reveal .slides > section.stack {
+ padding-top: 0;
+ padding-bottom: 0; }
+
+.reveal .slides > section.present,
+.reveal .slides > section > section.present {
+ display: block;
+ z-index: 11;
+ opacity: 1; }
+
+.reveal .slides > section:empty,
+.reveal .slides > section > section:empty,
+.reveal .slides > section[data-background-interactive],
+.reveal .slides > section > section[data-background-interactive] {
+ pointer-events: none; }
+
+.reveal.center,
+.reveal.center .slides,
+.reveal.center .slides section {
+ min-height: 0 !important; }
+
+/* Don't allow interaction with invisible slides */
+.reveal .slides > section.future,
+.reveal .slides > section > section.future,
+.reveal .slides > section.past,
+.reveal .slides > section > section.past {
+ pointer-events: none; }
+
+.reveal.overview .slides > section,
+.reveal.overview .slides > section > section {
+ pointer-events: auto; }
+
+.reveal .slides > section.past,
+.reveal .slides > section.future,
+.reveal .slides > section > section.past,
+.reveal .slides > section > section.future {
+ opacity: 0; }
+
+/*********************************************
+ * Mixins for readability of transitions
+ *********************************************/
+/*********************************************
+ * SLIDE TRANSITION
+ * Aliased 'linear' for backwards compatibility
+ *********************************************/
+.reveal.slide section {
+ -webkit-backface-visibility: hidden;
+ backface-visibility: hidden; }
+
+.reveal .slides > section[data-transition=slide].past,
+.reveal .slides > section[data-transition~=slide-out].past,
+.reveal.slide .slides > section:not([data-transition]).past {
+ -webkit-transform: translate(-150%, 0);
+ transform: translate(-150%, 0); }
+
+.reveal .slides > section[data-transition=slide].future,
+.reveal .slides > section[data-transition~=slide-in].future,
+.reveal.slide .slides > section:not([data-transition]).future {
+ -webkit-transform: translate(150%, 0);
+ transform: translate(150%, 0); }
+
+.reveal .slides > section > section[data-transition=slide].past,
+.reveal .slides > section > section[data-transition~=slide-out].past,
+.reveal.slide .slides > section > section:not([data-transition]).past {
+ -webkit-transform: translate(0, -150%);
+ transform: translate(0, -150%); }
+
+.reveal .slides > section > section[data-transition=slide].future,
+.reveal .slides > section > section[data-transition~=slide-in].future,
+.reveal.slide .slides > section > section:not([data-transition]).future {
+ -webkit-transform: translate(0, 150%);
+ transform: translate(0, 150%); }
+
+.reveal.linear section {
+ -webkit-backface-visibility: hidden;
+ backface-visibility: hidden; }
+
+.reveal .slides > section[data-transition=linear].past,
+.reveal .slides > section[data-transition~=linear-out].past,
+.reveal.linear .slides > section:not([data-transition]).past {
+ -webkit-transform: translate(-150%, 0);
+ transform: translate(-150%, 0); }
+
+.reveal .slides > section[data-transition=linear].future,
+.reveal .slides > section[data-transition~=linear-in].future,
+.reveal.linear .slides > section:not([data-transition]).future {
+ -webkit-transform: translate(150%, 0);
+ transform: translate(150%, 0); }
+
+.reveal .slides > section > section[data-transition=linear].past,
+.reveal .slides > section > section[data-transition~=linear-out].past,
+.reveal.linear .slides > section > section:not([data-transition]).past {
+ -webkit-transform: translate(0, -150%);
+ transform: translate(0, -150%); }
+
+.reveal .slides > section > section[data-transition=linear].future,
+.reveal .slides > section > section[data-transition~=linear-in].future,
+.reveal.linear .slides > section > section:not([data-transition]).future {
+ -webkit-transform: translate(0, 150%);
+ transform: translate(0, 150%); }
+
+/*********************************************
+ * CONVEX TRANSITION
+ * Aliased 'default' for backwards compatibility
+ *********************************************/
+.reveal .slides section[data-transition=default].stack,
+.reveal.default .slides section.stack {
+ -webkit-transform-style: preserve-3d;
+ transform-style: preserve-3d; }
+
+.reveal .slides > section[data-transition=default].past,
+.reveal .slides > section[data-transition~=default-out].past,
+.reveal.default .slides > section:not([data-transition]).past {
+ -webkit-transform: translate3d(-100%, 0, 0) rotateY(-90deg) translate3d(-100%, 0, 0);
+ transform: translate3d(-100%, 0, 0) rotateY(-90deg) translate3d(-100%, 0, 0); }
+
+.reveal .slides > section[data-transition=default].future,
+.reveal .slides > section[data-transition~=default-in].future,
+.reveal.default .slides > section:not([data-transition]).future {
+ -webkit-transform: translate3d(100%, 0, 0) rotateY(90deg) translate3d(100%, 0, 0);
+ transform: translate3d(100%, 0, 0) rotateY(90deg) translate3d(100%, 0, 0); }
+
+.reveal .slides > section > section[data-transition=default].past,
+.reveal .slides > section > section[data-transition~=default-out].past,
+.reveal.default .slides > section > section:not([data-transition]).past {
+ -webkit-transform: translate3d(0, -300px, 0) rotateX(70deg) translate3d(0, -300px, 0);
+ transform: translate3d(0, -300px, 0) rotateX(70deg) translate3d(0, -300px, 0); }
+
+.reveal .slides > section > section[data-transition=default].future,
+.reveal .slides > section > section[data-transition~=default-in].future,
+.reveal.default .slides > section > section:not([data-transition]).future {
+ -webkit-transform: translate3d(0, 300px, 0) rotateX(-70deg) translate3d(0, 300px, 0);
+ transform: translate3d(0, 300px, 0) rotateX(-70deg) translate3d(0, 300px, 0); }
+
+.reveal .slides section[data-transition=convex].stack,
+.reveal.convex .slides section.stack {
+ -webkit-transform-style: preserve-3d;
+ transform-style: preserve-3d; }
+
+.reveal .slides > section[data-transition=convex].past,
+.reveal .slides > section[data-transition~=convex-out].past,
+.reveal.convex .slides > section:not([data-transition]).past {
+ -webkit-transform: translate3d(-100%, 0, 0) rotateY(-90deg) translate3d(-100%, 0, 0);
+ transform: translate3d(-100%, 0, 0) rotateY(-90deg) translate3d(-100%, 0, 0); }
+
+.reveal .slides > section[data-transition=convex].future,
+.reveal .slides > section[data-transition~=convex-in].future,
+.reveal.convex .slides > section:not([data-transition]).future {
+ -webkit-transform: translate3d(100%, 0, 0) rotateY(90deg) translate3d(100%, 0, 0);
+ transform: translate3d(100%, 0, 0) rotateY(90deg) translate3d(100%, 0, 0); }
+
+.reveal .slides > section > section[data-transition=convex].past,
+.reveal .slides > section > section[data-transition~=convex-out].past,
+.reveal.convex .slides > section > section:not([data-transition]).past {
+ -webkit-transform: translate3d(0, -300px, 0) rotateX(70deg) translate3d(0, -300px, 0);
+ transform: translate3d(0, -300px, 0) rotateX(70deg) translate3d(0, -300px, 0); }
+
+.reveal .slides > section > section[data-transition=convex].future,
+.reveal .slides > section > section[data-transition~=convex-in].future,
+.reveal.convex .slides > section > section:not([data-transition]).future {
+ -webkit-transform: translate3d(0, 300px, 0) rotateX(-70deg) translate3d(0, 300px, 0);
+ transform: translate3d(0, 300px, 0) rotateX(-70deg) translate3d(0, 300px, 0); }
+
+/*********************************************
+ * CONCAVE TRANSITION
+ *********************************************/
+.reveal .slides section[data-transition=concave].stack,
+.reveal.concave .slides section.stack {
+ -webkit-transform-style: preserve-3d;
+ transform-style: preserve-3d; }
+
+.reveal .slides > section[data-transition=concave].past,
+.reveal .slides > section[data-transition~=concave-out].past,
+.reveal.concave .slides > section:not([data-transition]).past {
+ -webkit-transform: translate3d(-100%, 0, 0) rotateY(90deg) translate3d(-100%, 0, 0);
+ transform: translate3d(-100%, 0, 0) rotateY(90deg) translate3d(-100%, 0, 0); }
+
+.reveal .slides > section[data-transition=concave].future,
+.reveal .slides > section[data-transition~=concave-in].future,
+.reveal.concave .slides > section:not([data-transition]).future {
+ -webkit-transform: translate3d(100%, 0, 0) rotateY(-90deg) translate3d(100%, 0, 0);
+ transform: translate3d(100%, 0, 0) rotateY(-90deg) translate3d(100%, 0, 0); }
+
+.reveal .slides > section > section[data-transition=concave].past,
+.reveal .slides > section > section[data-transition~=concave-out].past,
+.reveal.concave .slides > section > section:not([data-transition]).past {
+ -webkit-transform: translate3d(0, -80%, 0) rotateX(-70deg) translate3d(0, -80%, 0);
+ transform: translate3d(0, -80%, 0) rotateX(-70deg) translate3d(0, -80%, 0); }
+
+.reveal .slides > section > section[data-transition=concave].future,
+.reveal .slides > section > section[data-transition~=concave-in].future,
+.reveal.concave .slides > section > section:not([data-transition]).future {
+ -webkit-transform: translate3d(0, 80%, 0) rotateX(70deg) translate3d(0, 80%, 0);
+ transform: translate3d(0, 80%, 0) rotateX(70deg) translate3d(0, 80%, 0); }
+
+/*********************************************
+ * ZOOM TRANSITION
+ *********************************************/
+.reveal .slides section[data-transition=zoom],
+.reveal.zoom .slides section:not([data-transition]) {
+ -webkit-transition-timing-function: ease;
+ transition-timing-function: ease; }
+
+.reveal .slides > section[data-transition=zoom].past,
+.reveal .slides > section[data-transition~=zoom-out].past,
+.reveal.zoom .slides > section:not([data-transition]).past {
+ visibility: hidden;
+ -webkit-transform: scale(16);
+ transform: scale(16); }
+
+.reveal .slides > section[data-transition=zoom].future,
+.reveal .slides > section[data-transition~=zoom-in].future,
+.reveal.zoom .slides > section:not([data-transition]).future {
+ visibility: hidden;
+ -webkit-transform: scale(0.2);
+ transform: scale(0.2); }
+
+.reveal .slides > section > section[data-transition=zoom].past,
+.reveal .slides > section > section[data-transition~=zoom-out].past,
+.reveal.zoom .slides > section > section:not([data-transition]).past {
+ -webkit-transform: translate(0, -150%);
+ transform: translate(0, -150%); }
+
+.reveal .slides > section > section[data-transition=zoom].future,
+.reveal .slides > section > section[data-transition~=zoom-in].future,
+.reveal.zoom .slides > section > section:not([data-transition]).future {
+ -webkit-transform: translate(0, 150%);
+ transform: translate(0, 150%); }
+
+/*********************************************
+ * CUBE TRANSITION
+ *
+ * WARNING:
+ * this is deprecated and will be removed in a
+ * future version.
+ *********************************************/
+.reveal.cube .slides {
+ -webkit-perspective: 1300px;
+ perspective: 1300px; }
+
+.reveal.cube .slides section {
+ padding: 30px;
+ min-height: 700px;
+ -webkit-backface-visibility: hidden;
+ backface-visibility: hidden;
+ box-sizing: border-box;
+ -webkit-transform-style: preserve-3d;
+ transform-style: preserve-3d; }
+
+.reveal.center.cube .slides section {
+ min-height: 0; }
+
+.reveal.cube .slides section:not(.stack):before {
+ content: '';
+ position: absolute;
+ display: block;
+ width: 100%;
+ height: 100%;
+ left: 0;
+ top: 0;
+ background: rgba(0, 0, 0, 0.1);
+ border-radius: 4px;
+ -webkit-transform: translateZ(-20px);
+ transform: translateZ(-20px); }
+
+.reveal.cube .slides section:not(.stack):after {
+ content: '';
+ position: absolute;
+ display: block;
+ width: 90%;
+ height: 30px;
+ left: 5%;
+ bottom: 0;
+ background: none;
+ z-index: 1;
+ border-radius: 4px;
+ box-shadow: 0px 95px 25px rgba(0, 0, 0, 0.2);
+ -webkit-transform: translateZ(-90px) rotateX(65deg);
+ transform: translateZ(-90px) rotateX(65deg); }
+
+.reveal.cube .slides > section.stack {
+ padding: 0;
+ background: none; }
+
+.reveal.cube .slides > section.past {
+ -webkit-transform-origin: 100% 0%;
+ transform-origin: 100% 0%;
+ -webkit-transform: translate3d(-100%, 0, 0) rotateY(-90deg);
+ transform: translate3d(-100%, 0, 0) rotateY(-90deg); }
+
+.reveal.cube .slides > section.future {
+ -webkit-transform-origin: 0% 0%;
+ transform-origin: 0% 0%;
+ -webkit-transform: translate3d(100%, 0, 0) rotateY(90deg);
+ transform: translate3d(100%, 0, 0) rotateY(90deg); }
+
+.reveal.cube .slides > section > section.past {
+ -webkit-transform-origin: 0% 100%;
+ transform-origin: 0% 100%;
+ -webkit-transform: translate3d(0, -100%, 0) rotateX(90deg);
+ transform: translate3d(0, -100%, 0) rotateX(90deg); }
+
+.reveal.cube .slides > section > section.future {
+ -webkit-transform-origin: 0% 0%;
+ transform-origin: 0% 0%;
+ -webkit-transform: translate3d(0, 100%, 0) rotateX(-90deg);
+ transform: translate3d(0, 100%, 0) rotateX(-90deg); }
+
+/*********************************************
+ * PAGE TRANSITION
+ *
+ * WARNING:
+ * this is deprecated and will be removed in a
+ * future version.
+ *********************************************/
+.reveal.page .slides {
+ -webkit-perspective-origin: 0% 50%;
+ perspective-origin: 0% 50%;
+ -webkit-perspective: 3000px;
+ perspective: 3000px; }
+
+.reveal.page .slides section {
+ padding: 30px;
+ min-height: 700px;
+ box-sizing: border-box;
+ -webkit-transform-style: preserve-3d;
+ transform-style: preserve-3d; }
+
+.reveal.page .slides section.past {
+ z-index: 12; }
+
+.reveal.page .slides section:not(.stack):before {
+ content: '';
+ position: absolute;
+ display: block;
+ width: 100%;
+ height: 100%;
+ left: 0;
+ top: 0;
+ background: rgba(0, 0, 0, 0.1);
+ -webkit-transform: translateZ(-20px);
+ transform: translateZ(-20px); }
+
+.reveal.page .slides section:not(.stack):after {
+ content: '';
+ position: absolute;
+ display: block;
+ width: 90%;
+ height: 30px;
+ left: 5%;
+ bottom: 0;
+ background: none;
+ z-index: 1;
+ border-radius: 4px;
+ box-shadow: 0px 95px 25px rgba(0, 0, 0, 0.2);
+ -webkit-transform: translateZ(-90px) rotateX(65deg); }
+
+.reveal.page .slides > section.stack {
+ padding: 0;
+ background: none; }
+
+.reveal.page .slides > section.past {
+ -webkit-transform-origin: 0% 0%;
+ transform-origin: 0% 0%;
+ -webkit-transform: translate3d(-40%, 0, 0) rotateY(-80deg);
+ transform: translate3d(-40%, 0, 0) rotateY(-80deg); }
+
+.reveal.page .slides > section.future {
+ -webkit-transform-origin: 100% 0%;
+ transform-origin: 100% 0%;
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0); }
+
+.reveal.page .slides > section > section.past {
+ -webkit-transform-origin: 0% 0%;
+ transform-origin: 0% 0%;
+ -webkit-transform: translate3d(0, -40%, 0) rotateX(80deg);
+ transform: translate3d(0, -40%, 0) rotateX(80deg); }
+
+.reveal.page .slides > section > section.future {
+ -webkit-transform-origin: 0% 100%;
+ transform-origin: 0% 100%;
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0); }
+
+/*********************************************
+ * FADE TRANSITION
+ *********************************************/
+.reveal .slides section[data-transition=fade],
+.reveal.fade .slides section:not([data-transition]),
+.reveal.fade .slides > section > section:not([data-transition]) {
+ -webkit-transform: none;
+ transform: none;
+ -webkit-transition: opacity 0.5s;
+ transition: opacity 0.5s; }
+
+.reveal.fade.overview .slides section,
+.reveal.fade.overview .slides > section > section {
+ -webkit-transition: none;
+ transition: none; }
+
+/*********************************************
+ * NO TRANSITION
+ *********************************************/
+.reveal .slides section[data-transition=none],
+.reveal.none .slides section:not([data-transition]) {
+ -webkit-transform: none;
+ transform: none;
+ -webkit-transition: none;
+ transition: none; }
+
+/*********************************************
+ * PAUSED MODE
+ *********************************************/
+.reveal .pause-overlay {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: black;
+ visibility: hidden;
+ opacity: 0;
+ z-index: 100;
+ -webkit-transition: all 1s ease;
+ transition: all 1s ease; }
+
+.reveal.paused .pause-overlay {
+ visibility: visible;
+ opacity: 1; }
+
+/*********************************************
+ * FALLBACK
+ *********************************************/
+.no-transforms {
+ overflow-y: auto; }
+
+.no-transforms .reveal .slides {
+ position: relative;
+ width: 80%;
+ height: auto !important;
+ top: 0;
+ left: 50%;
+ margin: 0;
+ text-align: center; }
+
+.no-transforms .reveal .controls,
+.no-transforms .reveal .progress {
+ display: none !important; }
+
+.no-transforms .reveal .slides section {
+ display: block !important;
+ opacity: 1 !important;
+ position: relative !important;
+ height: auto;
+ min-height: 0;
+ top: 0;
+ left: -50%;
+ margin: 70px 0;
+ -webkit-transform: none;
+ transform: none; }
+
+.no-transforms .reveal .slides section section {
+ left: 0; }
+
+.reveal .no-transition,
+.reveal .no-transition * {
+ -webkit-transition: none !important;
+ transition: none !important; }
+
+/*********************************************
+ * PER-SLIDE BACKGROUNDS
+ *********************************************/
+.reveal .backgrounds {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ top: 0;
+ left: 0;
+ -webkit-perspective: 600px;
+ perspective: 600px; }
+
+.reveal .slide-background {
+ display: none;
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ opacity: 0;
+ visibility: hidden;
+ overflow: hidden;
+ background-color: transparent;
+ background-position: 50% 50%;
+ background-repeat: no-repeat;
+ background-size: cover;
+ -webkit-transition: all 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985);
+ transition: all 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985); }
+
+.reveal .slide-background.stack {
+ display: block; }
+
+.reveal .slide-background.present {
+ opacity: 1;
+ visibility: visible;
+ z-index: 2; }
+
+.print-pdf .reveal .slide-background {
+ opacity: 1 !important;
+ visibility: visible !important; }
+
+/* Video backgrounds */
+.reveal .slide-background video {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ max-width: none;
+ max-height: none;
+ top: 0;
+ left: 0;
+ -o-object-fit: cover;
+ object-fit: cover; }
+
+.reveal .slide-background[data-background-size="contain"] video {
+ -o-object-fit: contain;
+ object-fit: contain; }
+
+/* Immediate transition style */
+.reveal[data-background-transition=none] > .backgrounds .slide-background,
+.reveal > .backgrounds .slide-background[data-background-transition=none] {
+ -webkit-transition: none;
+ transition: none; }
+
+/* Slide */
+.reveal[data-background-transition=slide] > .backgrounds .slide-background,
+.reveal > .backgrounds .slide-background[data-background-transition=slide] {
+ opacity: 1;
+ -webkit-backface-visibility: hidden;
+ backface-visibility: hidden; }
+
+.reveal[data-background-transition=slide] > .backgrounds .slide-background.past,
+.reveal > .backgrounds .slide-background.past[data-background-transition=slide] {
+ -webkit-transform: translate(-100%, 0);
+ transform: translate(-100%, 0); }
+
+.reveal[data-background-transition=slide] > .backgrounds .slide-background.future,
+.reveal > .backgrounds .slide-background.future[data-background-transition=slide] {
+ -webkit-transform: translate(100%, 0);
+ transform: translate(100%, 0); }
+
+.reveal[data-background-transition=slide] > .backgrounds .slide-background > .slide-background.past,
+.reveal > .backgrounds .slide-background > .slide-background.past[data-background-transition=slide] {
+ -webkit-transform: translate(0, -100%);
+ transform: translate(0, -100%); }
+
+.reveal[data-background-transition=slide] > .backgrounds .slide-background > .slide-background.future,
+.reveal > .backgrounds .slide-background > .slide-background.future[data-background-transition=slide] {
+ -webkit-transform: translate(0, 100%);
+ transform: translate(0, 100%); }
+
+/* Convex */
+.reveal[data-background-transition=convex] > .backgrounds .slide-background.past,
+.reveal > .backgrounds .slide-background.past[data-background-transition=convex] {
+ opacity: 0;
+ -webkit-transform: translate3d(-100%, 0, 0) rotateY(-90deg) translate3d(-100%, 0, 0);
+ transform: translate3d(-100%, 0, 0) rotateY(-90deg) translate3d(-100%, 0, 0); }
+
+.reveal[data-background-transition=convex] > .backgrounds .slide-background.future,
+.reveal > .backgrounds .slide-background.future[data-background-transition=convex] {
+ opacity: 0;
+ -webkit-transform: translate3d(100%, 0, 0) rotateY(90deg) translate3d(100%, 0, 0);
+ transform: translate3d(100%, 0, 0) rotateY(90deg) translate3d(100%, 0, 0); }
+
+.reveal[data-background-transition=convex] > .backgrounds .slide-background > .slide-background.past,
+.reveal > .backgrounds .slide-background > .slide-background.past[data-background-transition=convex] {
+ opacity: 0;
+ -webkit-transform: translate3d(0, -100%, 0) rotateX(90deg) translate3d(0, -100%, 0);
+ transform: translate3d(0, -100%, 0) rotateX(90deg) translate3d(0, -100%, 0); }
+
+.reveal[data-background-transition=convex] > .backgrounds .slide-background > .slide-background.future,
+.reveal > .backgrounds .slide-background > .slide-background.future[data-background-transition=convex] {
+ opacity: 0;
+ -webkit-transform: translate3d(0, 100%, 0) rotateX(-90deg) translate3d(0, 100%, 0);
+ transform: translate3d(0, 100%, 0) rotateX(-90deg) translate3d(0, 100%, 0); }
+
+/* Concave */
+.reveal[data-background-transition=concave] > .backgrounds .slide-background.past,
+.reveal > .backgrounds .slide-background.past[data-background-transition=concave] {
+ opacity: 0;
+ -webkit-transform: translate3d(-100%, 0, 0) rotateY(90deg) translate3d(-100%, 0, 0);
+ transform: translate3d(-100%, 0, 0) rotateY(90deg) translate3d(-100%, 0, 0); }
+
+.reveal[data-background-transition=concave] > .backgrounds .slide-background.future,
+.reveal > .backgrounds .slide-background.future[data-background-transition=concave] {
+ opacity: 0;
+ -webkit-transform: translate3d(100%, 0, 0) rotateY(-90deg) translate3d(100%, 0, 0);
+ transform: translate3d(100%, 0, 0) rotateY(-90deg) translate3d(100%, 0, 0); }
+
+.reveal[data-background-transition=concave] > .backgrounds .slide-background > .slide-background.past,
+.reveal > .backgrounds .slide-background > .slide-background.past[data-background-transition=concave] {
+ opacity: 0;
+ -webkit-transform: translate3d(0, -100%, 0) rotateX(-90deg) translate3d(0, -100%, 0);
+ transform: translate3d(0, -100%, 0) rotateX(-90deg) translate3d(0, -100%, 0); }
+
+.reveal[data-background-transition=concave] > .backgrounds .slide-background > .slide-background.future,
+.reveal > .backgrounds .slide-background > .slide-background.future[data-background-transition=concave] {
+ opacity: 0;
+ -webkit-transform: translate3d(0, 100%, 0) rotateX(90deg) translate3d(0, 100%, 0);
+ transform: translate3d(0, 100%, 0) rotateX(90deg) translate3d(0, 100%, 0); }
+
+/* Zoom */
+.reveal[data-background-transition=zoom] > .backgrounds .slide-background,
+.reveal > .backgrounds .slide-background[data-background-transition=zoom] {
+ -webkit-transition-timing-function: ease;
+ transition-timing-function: ease; }
+
+.reveal[data-background-transition=zoom] > .backgrounds .slide-background.past,
+.reveal > .backgrounds .slide-background.past[data-background-transition=zoom] {
+ opacity: 0;
+ visibility: hidden;
+ -webkit-transform: scale(16);
+ transform: scale(16); }
+
+.reveal[data-background-transition=zoom] > .backgrounds .slide-background.future,
+.reveal > .backgrounds .slide-background.future[data-background-transition=zoom] {
+ opacity: 0;
+ visibility: hidden;
+ -webkit-transform: scale(0.2);
+ transform: scale(0.2); }
+
+.reveal[data-background-transition=zoom] > .backgrounds .slide-background > .slide-background.past,
+.reveal > .backgrounds .slide-background > .slide-background.past[data-background-transition=zoom] {
+ opacity: 0;
+ visibility: hidden;
+ -webkit-transform: scale(16);
+ transform: scale(16); }
+
+.reveal[data-background-transition=zoom] > .backgrounds .slide-background > .slide-background.future,
+.reveal > .backgrounds .slide-background > .slide-background.future[data-background-transition=zoom] {
+ opacity: 0;
+ visibility: hidden;
+ -webkit-transform: scale(0.2);
+ transform: scale(0.2); }
+
+/* Global transition speed settings */
+.reveal[data-transition-speed="fast"] > .backgrounds .slide-background {
+ -webkit-transition-duration: 400ms;
+ transition-duration: 400ms; }
+
+.reveal[data-transition-speed="slow"] > .backgrounds .slide-background {
+ -webkit-transition-duration: 1200ms;
+ transition-duration: 1200ms; }
+
+/*********************************************
+ * OVERVIEW
+ *********************************************/
+.reveal.overview {
+ -webkit-perspective-origin: 50% 50%;
+ perspective-origin: 50% 50%;
+ -webkit-perspective: 700px;
+ perspective: 700px; }
+ .reveal.overview .slides {
+ -moz-transform-style: preserve-3d; }
+ .reveal.overview .slides section {
+ height: 100%;
+ top: 0 !important;
+ opacity: 1 !important;
+ overflow: hidden;
+ visibility: visible !important;
+ cursor: pointer;
+ box-sizing: border-box; }
+ .reveal.overview .slides section:hover,
+ .reveal.overview .slides section.present {
+ outline: 10px solid rgba(150, 150, 150, 0.4);
+ outline-offset: 10px; }
+ .reveal.overview .slides section .fragment {
+ opacity: 1;
+ -webkit-transition: none;
+ transition: none; }
+ .reveal.overview .slides section:after,
+ .reveal.overview .slides section:before {
+ display: none !important; }
+ .reveal.overview .slides > section.stack {
+ padding: 0;
+ top: 0 !important;
+ background: none;
+ outline: none;
+ overflow: visible; }
+ .reveal.overview .backgrounds {
+ -webkit-perspective: inherit;
+ perspective: inherit;
+ -moz-transform-style: preserve-3d; }
+ .reveal.overview .backgrounds .slide-background {
+ opacity: 1;
+ visibility: visible;
+ outline: 10px solid rgba(150, 150, 150, 0.1);
+ outline-offset: 10px; }
+ .reveal.overview .backgrounds .slide-background.stack {
+ overflow: visible; }
+
+.reveal.overview .slides section,
+.reveal.overview-deactivating .slides section {
+ -webkit-transition: none;
+ transition: none; }
+
+.reveal.overview .backgrounds .slide-background,
+.reveal.overview-deactivating .backgrounds .slide-background {
+ -webkit-transition: none;
+ transition: none; }
+
+/*********************************************
+ * RTL SUPPORT
+ *********************************************/
+.reveal.rtl .slides,
+.reveal.rtl .slides h1,
+.reveal.rtl .slides h2,
+.reveal.rtl .slides h3,
+.reveal.rtl .slides h4,
+.reveal.rtl .slides h5,
+.reveal.rtl .slides h6 {
+ direction: rtl;
+ font-family: sans-serif; }
+
+.reveal.rtl pre,
+.reveal.rtl code {
+ direction: ltr; }
+
+.reveal.rtl ol,
+.reveal.rtl ul {
+ text-align: right; }
+
+.reveal.rtl .progress span {
+ float: right; }
+
+/*********************************************
+ * PARALLAX BACKGROUND
+ *********************************************/
+.reveal.has-parallax-background .backgrounds {
+ -webkit-transition: all 0.8s ease;
+ transition: all 0.8s ease; }
+
+/* Global transition speed settings */
+.reveal.has-parallax-background[data-transition-speed="fast"] .backgrounds {
+ -webkit-transition-duration: 400ms;
+ transition-duration: 400ms; }
+
+.reveal.has-parallax-background[data-transition-speed="slow"] .backgrounds {
+ -webkit-transition-duration: 1200ms;
+ transition-duration: 1200ms; }
+
+/*********************************************
+ * LINK PREVIEW OVERLAY
+ *********************************************/
+.reveal .overlay {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ z-index: 1000;
+ background: rgba(0, 0, 0, 0.9);
+ opacity: 0;
+ visibility: hidden;
+ -webkit-transition: all 0.3s ease;
+ transition: all 0.3s ease; }
+
+.reveal .overlay.visible {
+ opacity: 1;
+ visibility: visible; }
+
+.reveal .overlay .spinner {
+ position: absolute;
+ display: block;
+ top: 50%;
+ left: 50%;
+ width: 32px;
+ height: 32px;
+ margin: -16px 0 0 -16px;
+ z-index: 10;
+ background-image: url(data:image/gif;base64,R0lGODlhIAAgAPMAAJmZmf%2F%2F%2F6%2Bvr8nJybW1tcDAwOjo6Nvb26ioqKOjo7Ozs%2FLy8vz8%2FAAAAAAAAAAAACH%2FC05FVFNDQVBFMi4wAwEAAAAh%2FhpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh%2BQQJCgAAACwAAAAAIAAgAAAE5xDISWlhperN52JLhSSdRgwVo1ICQZRUsiwHpTJT4iowNS8vyW2icCF6k8HMMBkCEDskxTBDAZwuAkkqIfxIQyhBQBFvAQSDITM5VDW6XNE4KagNh6Bgwe60smQUB3d4Rz1ZBApnFASDd0hihh12BkE9kjAJVlycXIg7CQIFA6SlnJ87paqbSKiKoqusnbMdmDC2tXQlkUhziYtyWTxIfy6BE8WJt5YJvpJivxNaGmLHT0VnOgSYf0dZXS7APdpB309RnHOG5gDqXGLDaC457D1zZ%2FV%2FnmOM82XiHRLYKhKP1oZmADdEAAAh%2BQQJCgAAACwAAAAAIAAgAAAE6hDISWlZpOrNp1lGNRSdRpDUolIGw5RUYhhHukqFu8DsrEyqnWThGvAmhVlteBvojpTDDBUEIFwMFBRAmBkSgOrBFZogCASwBDEY%2FCZSg7GSE0gSCjQBMVG023xWBhklAnoEdhQEfyNqMIcKjhRsjEdnezB%2BA4k8gTwJhFuiW4dokXiloUepBAp5qaKpp6%2BHo7aWW54wl7obvEe0kRuoplCGepwSx2jJvqHEmGt6whJpGpfJCHmOoNHKaHx61WiSR92E4lbFoq%2BB6QDtuetcaBPnW6%2BO7wDHpIiK9SaVK5GgV543tzjgGcghAgAh%2BQQJCgAAACwAAAAAIAAgAAAE7hDISSkxpOrN5zFHNWRdhSiVoVLHspRUMoyUakyEe8PTPCATW9A14E0UvuAKMNAZKYUZCiBMuBakSQKG8G2FzUWox2AUtAQFcBKlVQoLgQReZhQlCIJesQXI5B0CBnUMOxMCenoCfTCEWBsJColTMANldx15BGs8B5wlCZ9Po6OJkwmRpnqkqnuSrayqfKmqpLajoiW5HJq7FL1Gr2mMMcKUMIiJgIemy7xZtJsTmsM4xHiKv5KMCXqfyUCJEonXPN2rAOIAmsfB3uPoAK%2B%2BG%2Bw48edZPK%2BM6hLJpQg484enXIdQFSS1u6UhksENEQAAIfkECQoAAAAsAAAAACAAIAAABOcQyEmpGKLqzWcZRVUQnZYg1aBSh2GUVEIQ2aQOE%2BG%2BcD4ntpWkZQj1JIiZIogDFFyHI0UxQwFugMSOFIPJftfVAEoZLBbcLEFhlQiqGp1Vd140AUklUN3eCA51C1EWMzMCezCBBmkxVIVHBWd3HHl9JQOIJSdSnJ0TDKChCwUJjoWMPaGqDKannasMo6WnM562R5YluZRwur0wpgqZE7NKUm%2BFNRPIhjBJxKZteWuIBMN4zRMIVIhffcgojwCF117i4nlLnY5ztRLsnOk%2BaV%2BoJY7V7m76PdkS4trKcdg0Zc0tTcKkRAAAIfkECQoAAAAsAAAAACAAIAAABO4QyEkpKqjqzScpRaVkXZWQEximw1BSCUEIlDohrft6cpKCk5xid5MNJTaAIkekKGQkWyKHkvhKsR7ARmitkAYDYRIbUQRQjWBwJRzChi9CRlBcY1UN4g0%2FVNB0AlcvcAYHRyZPdEQFYV8ccwR5HWxEJ02YmRMLnJ1xCYp0Y5idpQuhopmmC2KgojKasUQDk5BNAwwMOh2RtRq5uQuPZKGIJQIGwAwGf6I0JXMpC8C7kXWDBINFMxS4DKMAWVWAGYsAdNqW5uaRxkSKJOZKaU3tPOBZ4DuK2LATgJhkPJMgTwKCdFjyPHEnKxFCDhEAACH5BAkKAAAALAAAAAAgACAAAATzEMhJaVKp6s2nIkolIJ2WkBShpkVRWqqQrhLSEu9MZJKK9y1ZrqYK9WiClmvoUaF8gIQSNeF1Er4MNFn4SRSDARWroAIETg1iVwuHjYB1kYc1mwruwXKC9gmsJXliGxc%2BXiUCby9ydh1sOSdMkpMTBpaXBzsfhoc5l58Gm5yToAaZhaOUqjkDgCWNHAULCwOLaTmzswadEqggQwgHuQsHIoZCHQMMQgQGubVEcxOPFAcMDAYUA85eWARmfSRQCdcMe0zeP1AAygwLlJtPNAAL19DARdPzBOWSm1brJBi45soRAWQAAkrQIykShQ9wVhHCwCQCACH5BAkKAAAALAAAAAAgACAAAATrEMhJaVKp6s2nIkqFZF2VIBWhUsJaTokqUCoBq%2BE71SRQeyqUToLA7VxF0JDyIQh%2FMVVPMt1ECZlfcjZJ9mIKoaTl1MRIl5o4CUKXOwmyrCInCKqcWtvadL2SYhyASyNDJ0uIiRMDjI0Fd30%2FiI2UA5GSS5UDj2l6NoqgOgN4gksEBgYFf0FDqKgHnyZ9OX8HrgYHdHpcHQULXAS2qKpENRg7eAMLC7kTBaixUYFkKAzWAAnLC7FLVxLWDBLKCwaKTULgEwbLA4hJtOkSBNqITT3xEgfLpBtzE%2FjiuL04RGEBgwWhShRgQExHBAAh%2BQQJCgAAACwAAAAAIAAgAAAE7xDISWlSqerNpyJKhWRdlSAVoVLCWk6JKlAqAavhO9UkUHsqlE6CwO1cRdCQ8iEIfzFVTzLdRAmZX3I2SfZiCqGk5dTESJeaOAlClzsJsqwiJwiqnFrb2nS9kmIcgEsjQydLiIlHehhpejaIjzh9eomSjZR%2BipslWIRLAgMDOR2DOqKogTB9pCUJBagDBXR6XB0EBkIIsaRsGGMMAxoDBgYHTKJiUYEGDAzHC9EACcUGkIgFzgwZ0QsSBcXHiQvOwgDdEwfFs0sDzt4S6BK4xYjkDOzn0unFeBzOBijIm1Dgmg5YFQwsCMjp1oJ8LyIAACH5BAkKAAAALAAAAAAgACAAAATwEMhJaVKp6s2nIkqFZF2VIBWhUsJaTokqUCoBq%2BE71SRQeyqUToLA7VxF0JDyIQh%2FMVVPMt1ECZlfcjZJ9mIKoaTl1MRIl5o4CUKXOwmyrCInCKqcWtvadL2SYhyASyNDJ0uIiUd6GGl6NoiPOH16iZKNlH6KmyWFOggHhEEvAwwMA0N9GBsEC6amhnVcEwavDAazGwIDaH1ipaYLBUTCGgQDA8NdHz0FpqgTBwsLqAbWAAnIA4FWKdMLGdYGEgraigbT0OITBcg5QwPT4xLrROZL6AuQAPUS7bxLpoWidY0JtxLHKhwwMJBTHgPKdEQAACH5BAkKAAAALAAAAAAgACAAAATrEMhJaVKp6s2nIkqFZF2VIBWhUsJaTokqUCoBq%2BE71SRQeyqUToLA7VxF0JDyIQh%2FMVVPMt1ECZlfcjZJ9mIKoaTl1MRIl5o4CUKXOwmyrCInCKqcWtvadL2SYhyASyNDJ0uIiUd6GAULDJCRiXo1CpGXDJOUjY%2BYip9DhToJA4RBLwMLCwVDfRgbBAaqqoZ1XBMHswsHtxtFaH1iqaoGNgAIxRpbFAgfPQSqpbgGBqUD1wBXeCYp1AYZ19JJOYgH1KwA4UBvQwXUBxPqVD9L3sbp2BNk2xvvFPJd%2BMFCN6HAAIKgNggY0KtEBAAh%2BQQJCgAAACwAAAAAIAAgAAAE6BDISWlSqerNpyJKhWRdlSAVoVLCWk6JKlAqAavhO9UkUHsqlE6CwO1cRdCQ8iEIfzFVTzLdRAmZX3I2SfYIDMaAFdTESJeaEDAIMxYFqrOUaNW4E4ObYcCXaiBVEgULe0NJaxxtYksjh2NLkZISgDgJhHthkpU4mW6blRiYmZOlh4JWkDqILwUGBnE6TYEbCgevr0N1gH4At7gHiRpFaLNrrq8HNgAJA70AWxQIH1%2BvsYMDAzZQPC9VCNkDWUhGkuE5PxJNwiUK4UfLzOlD4WvzAHaoG9nxPi5d%2BjYUqfAhhykOFwJWiAAAIfkECQoAAAAsAAAAACAAIAAABPAQyElpUqnqzaciSoVkXVUMFaFSwlpOCcMYlErAavhOMnNLNo8KsZsMZItJEIDIFSkLGQoQTNhIsFehRww2CQLKF0tYGKYSg%2BygsZIuNqJksKgbfgIGepNo2cIUB3V1B3IvNiBYNQaDSTtfhhx0CwVPI0UJe0%2Bbm4g5VgcGoqOcnjmjqDSdnhgEoamcsZuXO1aWQy8KAwOAuTYYGwi7w5h%2BKr0SJ8MFihpNbx%2B4Erq7BYBuzsdiH1jCAzoSfl0rVirNbRXlBBlLX%2BBP0XJLAPGzTkAuAOqb0WT5AH7OcdCm5B8TgRwSRKIHQtaLCwg1RAAAOwAAAAAAAAAAAA%3D%3D);
+ visibility: visible;
+ opacity: 0.6;
+ -webkit-transition: all 0.3s ease;
+ transition: all 0.3s ease; }
+
+.reveal .overlay header {
+ position: absolute;
+ left: 0;
+ top: 0;
+ width: 100%;
+ height: 40px;
+ z-index: 2;
+ border-bottom: 1px solid #222; }
+
+.reveal .overlay header a {
+ display: inline-block;
+ width: 40px;
+ height: 40px;
+ line-height: 36px;
+ padding: 0 10px;
+ float: right;
+ opacity: 0.6;
+ box-sizing: border-box; }
+
+.reveal .overlay header a:hover {
+ opacity: 1; }
+
+.reveal .overlay header a .icon {
+ display: inline-block;
+ width: 20px;
+ height: 20px;
+ background-position: 50% 50%;
+ background-size: 100%;
+ background-repeat: no-repeat; }
+
+.reveal .overlay header a.close .icon {
+ background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAABkklEQVRYR8WX4VHDMAxG6wnoJrABZQPYBCaBTWAD2g1gE5gg6OOsXuxIlr40d81dfrSJ9V4c2VLK7spHuTJ/5wpM07QXuXc5X0opX2tEJcadjHuV80li/FgxTIEK/5QBCICBD6xEhSMGHgQPgBgLiYVAB1dpSqKDawxTohFw4JSEA3clzgIBPCURwE2JucBR7rhPJJv5OpJwDX+SfDjgx1wACQeJG1aChP9K/IMmdZ8DtESV1WyP3Bt4MwM6sj4NMxMYiqUWHQu4KYA/SYkIjOsm3BXYWMKFDwU2khjCQ4ELJUJ4SmClRArOCmSXGuKma0fYD5CbzHxFpCSGAhfAVSSUGDUk2BWZaff2g6GE15BsBQ9nwmpIGDiyHQddwNTMKkbZaf9fajXQca1EX44puJZUsnY0ObGmITE3GVLCbEhQUjGVt146j6oasWN+49Vph2w1pZ5EansNZqKBm1txbU57iRRcZ86RWMDdWtBJUHBHwoQPi1GV+JCbntmvok7iTX4/Up9mgyTc/FJYDTcndgH/AA5A/CHsyEkVAAAAAElFTkSuQmCC); }
+
+.reveal .overlay header a.external .icon {
+ background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAcElEQVRYR+2WSQoAIQwEzf8f7XiOMkUQxUPlGkM3hVmiQfQR9GYnH1SsAQlI4DiBqkCMoNb9y2e90IAEJPAcgdznU9+engMaeJ7Azh5Y1U67gAho4DqBqmB1buAf0MB1AlVBek83ZPkmJMGc1wAR+AAqod/B97TRpQAAAABJRU5ErkJggg==); }
+
+.reveal .overlay .viewport {
+ position: absolute;
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ top: 40px;
+ right: 0;
+ bottom: 0;
+ left: 0; }
+
+.reveal .overlay.overlay-preview .viewport iframe {
+ width: 100%;
+ height: 100%;
+ max-width: 100%;
+ max-height: 100%;
+ border: 0;
+ opacity: 0;
+ visibility: hidden;
+ -webkit-transition: all 0.3s ease;
+ transition: all 0.3s ease; }
+
+.reveal .overlay.overlay-preview.loaded .viewport iframe {
+ opacity: 1;
+ visibility: visible; }
+
+.reveal .overlay.overlay-preview.loaded .viewport-inner {
+ position: absolute;
+ z-index: -1;
+ left: 0;
+ top: 45%;
+ width: 100%;
+ text-align: center;
+ letter-spacing: normal; }
+
+.reveal .overlay.overlay-preview .x-frame-error {
+ opacity: 0;
+ -webkit-transition: opacity 0.3s ease 0.3s;
+ transition: opacity 0.3s ease 0.3s; }
+
+.reveal .overlay.overlay-preview.loaded .x-frame-error {
+ opacity: 1; }
+
+.reveal .overlay.overlay-preview.loaded .spinner {
+ opacity: 0;
+ visibility: hidden;
+ -webkit-transform: scale(0.2);
+ transform: scale(0.2); }
+
+.reveal .overlay.overlay-help .viewport {
+ overflow: auto;
+ color: #fff; }
+
+.reveal .overlay.overlay-help .viewport .viewport-inner {
+ width: 600px;
+ margin: auto;
+ padding: 20px 20px 80px 20px;
+ text-align: center;
+ letter-spacing: normal; }
+
+.reveal .overlay.overlay-help .viewport .viewport-inner .title {
+ font-size: 20px; }
+
+.reveal .overlay.overlay-help .viewport .viewport-inner table {
+ border: 1px solid #fff;
+ border-collapse: collapse;
+ font-size: 16px; }
+
+.reveal .overlay.overlay-help .viewport .viewport-inner table th,
+.reveal .overlay.overlay-help .viewport .viewport-inner table td {
+ width: 200px;
+ padding: 14px;
+ border: 1px solid #fff;
+ vertical-align: middle; }
+
+.reveal .overlay.overlay-help .viewport .viewport-inner table th {
+ padding-top: 20px;
+ padding-bottom: 20px; }
+
+/*********************************************
+ * PLAYBACK COMPONENT
+ *********************************************/
+.reveal .playback {
+ position: fixed;
+ left: 15px;
+ bottom: 20px;
+ z-index: 30;
+ cursor: pointer;
+ -webkit-transition: all 400ms ease;
+ transition: all 400ms ease; }
+
+.reveal.overview .playback {
+ opacity: 0;
+ visibility: hidden; }
+
+/*********************************************
+ * ROLLING LINKS
+ *********************************************/
+.reveal .roll {
+ display: inline-block;
+ line-height: 1.2;
+ overflow: hidden;
+ vertical-align: top;
+ -webkit-perspective: 400px;
+ perspective: 400px;
+ -webkit-perspective-origin: 50% 50%;
+ perspective-origin: 50% 50%; }
+
+.reveal .roll:hover {
+ background: none;
+ text-shadow: none; }
+
+.reveal .roll span {
+ display: block;
+ position: relative;
+ padding: 0 2px;
+ pointer-events: none;
+ -webkit-transition: all 400ms ease;
+ transition: all 400ms ease;
+ -webkit-transform-origin: 50% 0%;
+ transform-origin: 50% 0%;
+ -webkit-transform-style: preserve-3d;
+ transform-style: preserve-3d;
+ -webkit-backface-visibility: hidden;
+ backface-visibility: hidden; }
+
+.reveal .roll:hover span {
+ background: rgba(0, 0, 0, 0.5);
+ -webkit-transform: translate3d(0px, 0px, -45px) rotateX(90deg);
+ transform: translate3d(0px, 0px, -45px) rotateX(90deg); }
+
+.reveal .roll span:after {
+ content: attr(data-title);
+ display: block;
+ position: absolute;
+ left: 0;
+ top: 0;
+ padding: 0 2px;
+ -webkit-backface-visibility: hidden;
+ backface-visibility: hidden;
+ -webkit-transform-origin: 50% 0%;
+ transform-origin: 50% 0%;
+ -webkit-transform: translate3d(0px, 110%, 0px) rotateX(-90deg);
+ transform: translate3d(0px, 110%, 0px) rotateX(-90deg); }
+
+/*********************************************
+ * SPEAKER NOTES
+ *********************************************/
+.reveal aside.notes {
+ display: none; }
+
+.reveal .speaker-notes {
+ display: none;
+ position: absolute;
+ width: 70%;
+ max-height: 15%;
+ left: 15%;
+ bottom: 26px;
+ padding: 10px;
+ z-index: 1;
+ font-size: 18px;
+ line-height: 1.4;
+ color: #fff;
+ background-color: rgba(0, 0, 0, 0.5);
+ overflow: auto;
+ box-sizing: border-box;
+ text-align: left;
+ font-family: Helvetica, sans-serif;
+ -webkit-overflow-scrolling: touch; }
+
+.reveal .speaker-notes.visible:not(:empty) {
+ display: block; }
+
+@media screen and (max-width: 1024px) {
+ .reveal .speaker-notes {
+ font-size: 14px; } }
+
+@media screen and (max-width: 600px) {
+ .reveal .speaker-notes {
+ width: 90%;
+ left: 5%; } }
+
+/*********************************************
+ * ZOOM PLUGIN
+ *********************************************/
+.zoomed .reveal *,
+.zoomed .reveal *:before,
+.zoomed .reveal *:after {
+ -webkit-backface-visibility: visible !important;
+ backface-visibility: visible !important; }
+
+.zoomed .reveal .progress,
+.zoomed .reveal .controls {
+ opacity: 0; }
+
+.zoomed .reveal .roll span {
+ background: none; }
+
+.zoomed .reveal .roll span:after {
+ visibility: hidden; }
=== added file 'openlp/core/display/html/reveal.js'
--- openlp/core/display/html/reveal.js 1970-01-01 00:00:00 +0000
+++ openlp/core/display/html/reveal.js 2019-02-06 07:01:54 +0000
@@ -0,0 +1,5121 @@
+/*!
+ * reveal.js
+ * http://lab.hakim.se/reveal-js
+ * MIT licensed
+ *
+ * Copyright (C) 2017 Hakim El Hattab, http://hakim.se
+ */
+(function( root, factory ) {
+ if( typeof define === 'function' && define.amd ) {
+ // AMD. Register as an anonymous module.
+ define( function() {
+ root.Reveal = factory();
+ return root.Reveal;
+ } );
+ } else if( typeof exports === 'object' ) {
+ // Node. Does not work with strict CommonJS.
+ module.exports = factory();
+ } else {
+ // Browser globals.
+ root.Reveal = factory();
+ }
+}( this, function() {
+
+ 'use strict';
+
+ var Reveal;
+
+ // The reveal.js version
+ var VERSION = '3.5.0';
+
+ var SLIDES_SELECTOR = '.slides section',
+ HORIZONTAL_SLIDES_SELECTOR = '.slides>section',
+ VERTICAL_SLIDES_SELECTOR = '.slides>section.present>section',
+ HOME_SLIDE_SELECTOR = '.slides>section:first-of-type',
+ UA = navigator.userAgent,
+
+ // Configuration defaults, can be overridden at initialization time
+ config = {
+
+ // The "normal" size of the presentation, aspect ratio will be preserved
+ // when the presentation is scaled to fit different resolutions
+ width: 960,
+ height: 700,
+
+ // Factor of the display size that should remain empty around the content
+ margin: 0.04,
+
+ // Bounds for smallest/largest possible scale to apply to content
+ minScale: 0.2,
+ maxScale: 2.0,
+
+ // Display controls in the bottom right corner
+ controls: true,
+
+ // Display a presentation progress bar
+ progress: true,
+
+ // Display the page number of the current slide
+ slideNumber: false,
+
+ // Determine which displays to show the slide number on
+ showSlideNumber: 'all',
+
+ // Push each slide change to the browser history
+ history: false,
+
+ // Enable keyboard shortcuts for navigation
+ keyboard: true,
+
+ // Optional function that blocks keyboard events when retuning false
+ keyboardCondition: null,
+
+ // Enable the slide overview mode
+ overview: true,
+
+ // Vertical centering of slides
+ center: true,
+
+ // Enables touch navigation on devices with touch input
+ touch: true,
+
+ // Loop the presentation
+ loop: false,
+
+ // Change the presentation direction to be RTL
+ rtl: false,
+
+ // Randomizes the order of slides each time the presentation loads
+ shuffle: false,
+
+ // Turns fragments on and off globally
+ fragments: true,
+
+ // Flags if the presentation is running in an embedded mode,
+ // i.e. contained within a limited portion of the screen
+ embedded: false,
+
+ // Flags if we should show a help overlay when the question-mark
+ // key is pressed
+ help: true,
+
+ // Flags if it should be possible to pause the presentation (blackout)
+ pause: true,
+
+ // Flags if speaker notes should be visible to all viewers
+ showNotes: false,
+
+ // Global override for autolaying embedded media (video/audio/iframe)
+ // - null: Media will only autoplay if data-autoplay is present
+ // - true: All media will autoplay, regardless of individual setting
+ // - false: No media will autoplay, regardless of individual setting
+ autoPlayMedia: null,
+
+ // Number of milliseconds between automatically proceeding to the
+ // next slide, disabled when set to 0, this value can be overwritten
+ // by using a data-autoslide attribute on your slides
+ autoSlide: 0,
+
+ // Stop auto-sliding after user input
+ autoSlideStoppable: true,
+
+ // Use this method for navigation when auto-sliding (defaults to navigateNext)
+ autoSlideMethod: null,
+
+ // Enable slide navigation via mouse wheel
+ mouseWheel: false,
+
+ // Apply a 3D roll to links on hover
+ rollingLinks: false,
+
+ // Hides the address bar on mobile devices
+ hideAddressBar: true,
+
+ // Opens links in an iframe preview overlay
+ previewLinks: false,
+
+ // Exposes the reveal.js API through window.postMessage
+ postMessage: true,
+
+ // Dispatches all reveal.js events to the parent window through postMessage
+ postMessageEvents: false,
+
+ // Focuses body when page changes visibility to ensure keyboard shortcuts work
+ focusBodyOnPageVisibilityChange: true,
+
+ // Transition style
+ transition: 'slide', // none/fade/slide/convex/concave/zoom
+
+ // Transition speed
+ transitionSpeed: 'default', // default/fast/slow
+
+ // Transition style for full page slide backgrounds
+ backgroundTransition: 'fade', // none/fade/slide/convex/concave/zoom
+
+ // Parallax background image
+ parallaxBackgroundImage: '', // CSS syntax, e.g. "a.jpg"
+
+ // Parallax background size
+ parallaxBackgroundSize: '', // CSS syntax, e.g. "3000px 2000px"
+
+ // Amount of pixels to move the parallax background per slide step
+ parallaxBackgroundHorizontal: null,
+ parallaxBackgroundVertical: null,
+
+ // The maximum number of pages a single slide can expand onto when printing
+ // to PDF, unlimited by default
+ pdfMaxPagesPerSlide: Number.POSITIVE_INFINITY,
+
+ // Offset used to reduce the height of content within exported PDF pages.
+ // This exists to account for environment differences based on how you
+ // print to PDF. CLI printing options, like phantomjs and wkpdf, can end
+ // on precisely the total height of the document whereas in-browser
+ // printing has to end one pixel before.
+ pdfPageHeightOffset: -1,
+
+ // Number of slides away from the current that are visible
+ viewDistance: 3,
+
+ // The display mode that will be used to show slides
+ display: 'block',
+
+ // Script dependencies to load
+ dependencies: []
+
+ },
+
+ // Flags if Reveal.initialize() has been called
+ initialized = false,
+
+ // Flags if reveal.js is loaded (has dispatched the 'ready' event)
+ loaded = false,
+
+ // Flags if the overview mode is currently active
+ overview = false,
+
+ // Holds the dimensions of our overview slides, including margins
+ overviewSlideWidth = null,
+ overviewSlideHeight = null,
+
+ // The horizontal and vertical index of the currently active slide
+ indexh,
+ indexv,
+
+ // The previous and current slide HTML elements
+ previousSlide,
+ currentSlide,
+
+ previousBackground,
+
+ // Slides may hold a data-state attribute which we pick up and apply
+ // as a class to the body. This list contains the combined state of
+ // all current slides.
+ state = [],
+
+ // The current scale of the presentation (see width/height config)
+ scale = 1,
+
+ // CSS transform that is currently applied to the slides container,
+ // split into two groups
+ slidesTransform = { layout: '', overview: '' },
+
+ // Cached references to DOM elements
+ dom = {},
+
+ // Features supported by the browser, see #checkCapabilities()
+ features = {},
+
+ // Client is a mobile device, see #checkCapabilities()
+ isMobileDevice,
+
+ // Client is a desktop Chrome, see #checkCapabilities()
+ isChrome,
+
+ // Throttles mouse wheel navigation
+ lastMouseWheelStep = 0,
+
+ // Delays updates to the URL due to a Chrome thumbnailer bug
+ writeURLTimeout = 0,
+
+ // Flags if the interaction event listeners are bound
+ eventsAreBound = false,
+
+ // The current auto-slide duration
+ autoSlide = 0,
+
+ // Auto slide properties
+ autoSlidePlayer,
+ autoSlideTimeout = 0,
+ autoSlideStartTime = -1,
+ autoSlidePaused = false,
+
+ // Holds information about the currently ongoing touch input
+ touch = {
+ startX: 0,
+ startY: 0,
+ startSpan: 0,
+ startCount: 0,
+ captured: false,
+ threshold: 40
+ },
+
+ // Holds information about the keyboard shortcuts
+ keyboardShortcuts = {
+ 'N , SPACE': 'Next slide',
+ 'P': 'Previous slide',
+ '← , H': 'Navigate left',
+ '→ , L': 'Navigate right',
+ '↑ , K': 'Navigate up',
+ '↓ , J': 'Navigate down',
+ 'Home': 'First slide',
+ 'End': 'Last slide',
+ 'B , .': 'Pause',
+ 'F': 'Fullscreen',
+ 'ESC, O': 'Slide overview'
+ };
+
+ /**
+ * Starts up the presentation if the client is capable.
+ */
+ function initialize( options ) {
+
+ // Make sure we only initialize once
+ if( initialized === true ) return;
+
+ initialized = true;
+
+ checkCapabilities();
+
+ if( !features.transforms2d && !features.transforms3d ) {
+ document.body.setAttribute( 'class', 'no-transforms' );
+
+ // Since JS won't be running any further, we load all lazy
+ // loading elements upfront
+ var images = toArray( document.getElementsByTagName( 'img' ) ),
+ iframes = toArray( document.getElementsByTagName( 'iframe' ) );
+
+ var lazyLoadable = images.concat( iframes );
+
+ for( var i = 0, len = lazyLoadable.length; i < len; i++ ) {
+ var element = lazyLoadable[i];
+ if( element.getAttribute( 'data-src' ) ) {
+ element.setAttribute( 'src', element.getAttribute( 'data-src' ) );
+ element.removeAttribute( 'data-src' );
+ }
+ }
+
+ // If the browser doesn't support core features we won't be
+ // using JavaScript to control the presentation
+ return;
+ }
+
+ // Cache references to key DOM elements
+ dom.wrapper = document.querySelector( '.reveal' );
+ dom.slides = document.querySelector( '.reveal .slides' );
+
+ // Force a layout when the whole page, incl fonts, has loaded
+ window.addEventListener( 'load', layout, false );
+
+ var query = Reveal.getQueryHash();
+
+ // Do not accept new dependencies via query config to avoid
+ // the potential of malicious script injection
+ if( typeof query['dependencies'] !== 'undefined' ) delete query['dependencies'];
+
+ // Copy options over to our config object
+ extend( config, options );
+ extend( config, query );
+
+ // Hide the address bar in mobile browsers
+ hideAddressBar();
+
+ // Loads the dependencies and continues to #start() once done
+ load();
+
+ }
+
+ /**
+ * Restarts up the presentation if the client is capable.
+ */
+ function reinitialize() {
+ initialized = false;
+ initialize(config);
+ }
+
+ /**
+ * Inspect the client to see what it's capable of, this
+ * should only happens once per runtime.
+ */
+ function checkCapabilities() {
+
+ isMobileDevice = /(iphone|ipod|ipad|android)/gi.test( UA );
+ isChrome = /chrome/i.test( UA ) && !/edge/i.test( UA );
+
+ var testElement = document.createElement( 'div' );
+
+ features.transforms3d = 'WebkitPerspective' in testElement.style ||
+ 'MozPerspective' in testElement.style ||
+ 'msPerspective' in testElement.style ||
+ 'OPerspective' in testElement.style ||
+ 'perspective' in testElement.style;
+
+ features.transforms2d = 'WebkitTransform' in testElement.style ||
+ 'MozTransform' in testElement.style ||
+ 'msTransform' in testElement.style ||
+ 'OTransform' in testElement.style ||
+ 'transform' in testElement.style;
+
+ features.requestAnimationFrameMethod = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame;
+ features.requestAnimationFrame = typeof features.requestAnimationFrameMethod === 'function';
+
+ features.canvas = !!document.createElement( 'canvas' ).getContext;
+
+ // Transitions in the overview are disabled in desktop and
+ // Safari due to lag
+ features.overviewTransitions = !/Version\/[\d\.]+.*Safari/.test( UA );
+
+ // Flags if we should use zoom instead of transform to scale
+ // up slides. Zoom produces crisper results but has a lot of
+ // xbrowser quirks so we only use it in whitelsited browsers.
+ features.zoom = 'zoom' in testElement.style && !isMobileDevice &&
+ ( isChrome || /Version\/[\d\.]+.*Safari/.test( UA ) );
+
+ }
+
+ /**
+ * Loads the dependencies of reveal.js. Dependencies are
+ * defined via the configuration option 'dependencies'
+ * and will be loaded prior to starting/binding reveal.js.
+ * Some dependencies may have an 'async' flag, if so they
+ * will load after reveal.js has been started up.
+ */
+ function load() {
+
+ var scripts = [],
+ scriptsAsync = [],
+ scriptsToPreload = 0;
+
+ // Called once synchronous scripts finish loading
+ function proceed() {
+ if( scriptsAsync.length ) {
+ // Load asynchronous scripts
+ head.js.apply( null, scriptsAsync );
+ }
+
+ start();
+ }
+
+ function loadScript( s ) {
+ head.ready( s.src.match( /([\w\d_\-]*)\.?js$|[^\\\/]*$/i )[0], function() {
+ // Extension may contain callback functions
+ if( typeof s.callback === 'function' ) {
+ s.callback.apply( this );
+ }
+
+ if( --scriptsToPreload === 0 ) {
+ proceed();
+ }
+ });
+ }
+
+ for( var i = 0, len = config.dependencies.length; i < len; i++ ) {
+ var s = config.dependencies[i];
+
+ // Load if there's no condition or the condition is truthy
+ if( !s.condition || s.condition() ) {
+ if( s.async ) {
+ scriptsAsync.push( s.src );
+ }
+ else {
+ scripts.push( s.src );
+ }
+
+ loadScript( s );
+ }
+ }
+
+ if( scripts.length ) {
+ scriptsToPreload = scripts.length;
+
+ // Load synchronous scripts
+ head.js.apply( null, scripts );
+ }
+ else {
+ proceed();
+ }
+
+ }
+
+ /**
+ * Starts up reveal.js by binding input events and navigating
+ * to the current URL deeplink if there is one.
+ */
+ function start() {
+
+ // Make sure we've got all the DOM elements we need
+ setupDOM();
+
+ // Listen to messages posted to this window
+ setupPostMessage();
+
+ // Prevent the slides from being scrolled out of view
+ setupScrollPrevention();
+
+ // Resets all vertical slides so that only the first is visible
+ resetVerticalSlides();
+
+ // Updates the presentation to match the current configuration values
+ configure();
+
+ // Read the initial hash
+ readURL();
+
+ // Update all backgrounds
+ updateBackground( true );
+
+ // Notify listeners that the presentation is ready but use a 1ms
+ // timeout to ensure it's not fired synchronously after #initialize()
+ setTimeout( function() {
+ // Enable transitions now that we're loaded
+ dom.slides.classList.remove( 'no-transition' );
+
+ loaded = true;
+
+ dom.wrapper.classList.add( 'ready' );
+
+ dispatchEvent( 'ready', {
+ 'indexh': indexh,
+ 'indexv': indexv,
+ 'currentSlide': currentSlide
+ } );
+ }, 1 );
+
+ // Special setup and config is required when printing to PDF
+ if( isPrintingPDF() ) {
+ removeEventListeners();
+
+ // The document needs to have loaded for the PDF layout
+ // measurements to be accurate
+ if( document.readyState === 'complete' ) {
+ setupPDF();
+ }
+ else {
+ window.addEventListener( 'load', setupPDF );
+ }
+ }
+
+ }
+
+ /**
+ * Finds and stores references to DOM elements which are
+ * required by the presentation. If a required element is
+ * not found, it is created.
+ */
+ function setupDOM() {
+
+ // Prevent transitions while we're loading
+ dom.slides.classList.add( 'no-transition' );
+
+ // Background element
+ dom.background = createSingletonNode( dom.wrapper, 'div', 'backgrounds', null );
+
+ // Progress bar
+ dom.progress = createSingletonNode( dom.wrapper, 'div', 'progress', '<span></span>' );
+ dom.progressbar = dom.progress.querySelector( 'span' );
+
+ // Arrow controls
+ createSingletonNode( dom.wrapper, 'aside', 'controls',
+ '<button class="navigate-left" aria-label="previous slide"></button>' +
+ '<button class="navigate-right" aria-label="next slide"></button>' +
+ '<button class="navigate-up" aria-label="above slide"></button>' +
+ '<button class="navigate-down" aria-label="below slide"></button>' );
+
+ // Slide number
+ dom.slideNumber = createSingletonNode( dom.wrapper, 'div', 'slide-number', '' );
+
+ // Element containing notes that are visible to the audience
+ dom.speakerNotes = createSingletonNode( dom.wrapper, 'div', 'speaker-notes', null );
+ dom.speakerNotes.setAttribute( 'data-prevent-swipe', '' );
+ dom.speakerNotes.setAttribute( 'tabindex', '0' );
+
+ // Overlay graphic which is displayed during the paused mode
+ createSingletonNode( dom.wrapper, 'div', 'pause-overlay', null );
+
+ // Cache references to elements
+ dom.controls = document.querySelector( '.reveal .controls' );
+
+ dom.wrapper.setAttribute( 'role', 'application' );
+
+ // There can be multiple instances of controls throughout the page
+ dom.controlsLeft = toArray( document.querySelectorAll( '.navigate-left' ) );
+ dom.controlsRight = toArray( document.querySelectorAll( '.navigate-right' ) );
+ dom.controlsUp = toArray( document.querySelectorAll( '.navigate-up' ) );
+ dom.controlsDown = toArray( document.querySelectorAll( '.navigate-down' ) );
+ dom.controlsPrev = toArray( document.querySelectorAll( '.navigate-prev' ) );
+ dom.controlsNext = toArray( document.querySelectorAll( '.navigate-next' ) );
+
+ dom.statusDiv = createStatusDiv();
+ }
+
+ /**
+ * Creates a hidden div with role aria-live to announce the
+ * current slide content. Hide the div off-screen to make it
+ * available only to Assistive Technologies.
+ *
+ * @return {HTMLElement}
+ */
+ function createStatusDiv() {
+
+ var statusDiv = document.getElementById( 'aria-status-div' );
+ if( !statusDiv ) {
+ statusDiv = document.createElement( 'div' );
+ statusDiv.style.position = 'absolute';
+ statusDiv.style.height = '1px';
+ statusDiv.style.width = '1px';
+ statusDiv.style.overflow = 'hidden';
+ statusDiv.style.clip = 'rect( 1px, 1px, 1px, 1px )';
+ statusDiv.setAttribute( 'id', 'aria-status-div' );
+ statusDiv.setAttribute( 'aria-live', 'polite' );
+ statusDiv.setAttribute( 'aria-atomic','true' );
+ dom.wrapper.appendChild( statusDiv );
+ }
+ return statusDiv;
+
+ }
+
+ /**
+ * Converts the given HTML element into a string of text
+ * that can be announced to a screen reader. Hidden
+ * elements are excluded.
+ */
+ function getStatusText( node ) {
+
+ var text = '';
+
+ // Text node
+ if( node.nodeType === 3 ) {
+ text += node.textContent;
+ }
+ // Element node
+ else if( node.nodeType === 1 ) {
+
+ var isAriaHidden = node.getAttribute( 'aria-hidden' );
+ var isDisplayHidden = window.getComputedStyle( node )['display'] === 'none';
+ if( isAriaHidden !== 'true' && !isDisplayHidden ) {
+
+ toArray( node.childNodes ).forEach( function( child ) {
+ text += getStatusText( child );
+ } );
+
+ }
+
+ }
+
+ return text;
+
+ }
+
+ /**
+ * Configures the presentation for printing to a static
+ * PDF.
+ */
+ function setupPDF() {
+
+ var slideSize = getComputedSlideSize( window.innerWidth, window.innerHeight );
+
+ // Dimensions of the PDF pages
+ var pageWidth = Math.floor( slideSize.width * ( 1 + config.margin ) ),
+ pageHeight = Math.floor( slideSize.height * ( 1 + config.margin ) );
+
+ // Dimensions of slides within the pages
+ var slideWidth = slideSize.width,
+ slideHeight = slideSize.height;
+
+ // Let the browser know what page size we want to print
+ injectStyleSheet( '@page{size:'+ pageWidth +'px '+ pageHeight +'px; margin: 0px;}' );
+
+ // Limit the size of certain elements to the dimensions of the slide
+ injectStyleSheet( '.reveal section>img, .reveal section>video, .reveal section>iframe{max-width: '+ slideWidth +'px; max-height:'+ slideHeight +'px}' );
+
+ document.body.classList.add( 'print-pdf' );
+ document.body.style.width = pageWidth + 'px';
+ document.body.style.height = pageHeight + 'px';
+
+ // Make sure stretch elements fit on slide
+ layoutSlideContents( slideWidth, slideHeight );
+
+ // Add each slide's index as attributes on itself, we need these
+ // indices to generate slide numbers below
+ toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ).forEach( function( hslide, h ) {
+ hslide.setAttribute( 'data-index-h', h );
+
+ if( hslide.classList.contains( 'stack' ) ) {
+ toArray( hslide.querySelectorAll( 'section' ) ).forEach( function( vslide, v ) {
+ vslide.setAttribute( 'data-index-h', h );
+ vslide.setAttribute( 'data-index-v', v );
+ } );
+ }
+ } );
+
+ // Slide and slide background layout
+ toArray( dom.wrapper.querySelectorAll( SLIDES_SELECTOR ) ).forEach( function( slide ) {
+
+ // Vertical stacks are not centred since their section
+ // children will be
+ if( slide.classList.contains( 'stack' ) === false ) {
+ // Center the slide inside of the page, giving the slide some margin
+ var left = ( pageWidth - slideWidth ) / 2,
+ top = ( pageHeight - slideHeight ) / 2;
+
+ var contentHeight = slide.scrollHeight;
+ var numberOfPages = Math.max( Math.ceil( contentHeight / pageHeight ), 1 );
+
+ // Adhere to configured pages per slide limit
+ numberOfPages = Math.min( numberOfPages, config.pdfMaxPagesPerSlide );
+
+ // Center slides vertically
+ if( numberOfPages === 1 && config.center || slide.classList.contains( 'center' ) ) {
+ top = Math.max( ( pageHeight - contentHeight ) / 2, 0 );
+ }
+
+ // Wrap the slide in a page element and hide its overflow
+ // so that no page ever flows onto another
+ var page = document.createElement( 'div' );
+ page.className = 'pdf-page';
+ page.style.height = ( ( pageHeight + config.pdfPageHeightOffset ) * numberOfPages ) + 'px';
+ slide.parentNode.insertBefore( page, slide );
+ page.appendChild( slide );
+
+ // Position the slide inside of the page
+ slide.style.left = left + 'px';
+ slide.style.top = top + 'px';
+ slide.style.width = slideWidth + 'px';
+
+ if( slide.slideBackgroundElement ) {
+ page.insertBefore( slide.slideBackgroundElement, slide );
+ }
+
+ // Inject notes if `showNotes` is enabled
+ if( config.showNotes ) {
+
+ // Are there notes for this slide?
+ var notes = getSlideNotes( slide );
+ if( notes ) {
+
+ var notesSpacing = 8;
+ var notesLayout = typeof config.showNotes === 'string' ? config.showNotes : 'inline';
+ var notesElement = document.createElement( 'div' );
+ notesElement.classList.add( 'speaker-notes' );
+ notesElement.classList.add( 'speaker-notes-pdf' );
+ notesElement.setAttribute( 'data-layout', notesLayout );
+ notesElement.innerHTML = notes;
+
+ if( notesLayout === 'separate-page' ) {
+ page.parentNode.insertBefore( notesElement, page.nextSibling );
+ }
+ else {
+ notesElement.style.left = notesSpacing + 'px';
+ notesElement.style.bottom = notesSpacing + 'px';
+ notesElement.style.width = ( pageWidth - notesSpacing*2 ) + 'px';
+ page.appendChild( notesElement );
+ }
+
+ }
+
+ }
+
+ // Inject slide numbers if `slideNumbers` are enabled
+ if( config.slideNumber && /all|print/i.test( config.showSlideNumber ) ) {
+ var slideNumberH = parseInt( slide.getAttribute( 'data-index-h' ), 10 ) + 1,
+ slideNumberV = parseInt( slide.getAttribute( 'data-index-v' ), 10 ) + 1;
+
+ var numberElement = document.createElement( 'div' );
+ numberElement.classList.add( 'slide-number' );
+ numberElement.classList.add( 'slide-number-pdf' );
+ numberElement.innerHTML = formatSlideNumber( slideNumberH, '.', slideNumberV );
+ page.appendChild( numberElement );
+ }
+ }
+
+ } );
+
+ // Show all fragments
+ toArray( dom.wrapper.querySelectorAll( SLIDES_SELECTOR + ' .fragment' ) ).forEach( function( fragment ) {
+ fragment.classList.add( 'visible' );
+ } );
+
+ // Notify subscribers that the PDF layout is good to go
+ dispatchEvent( 'pdf-ready' );
+
+ }
+
+ /**
+ * This is an unfortunate necessity. Some actions – such as
+ * an input field being focused in an iframe or using the
+ * keyboard to expand text selection beyond the bounds of
+ * a slide – can trigger our content to be pushed out of view.
+ * This scrolling can not be prevented by hiding overflow in
+ * CSS (we already do) so we have to resort to repeatedly
+ * checking if the slides have been offset :(
+ */
+ function setupScrollPrevention() {
+
+ setInterval( function() {
+ if( dom.wrapper.scrollTop !== 0 || dom.wrapper.scrollLeft !== 0 ) {
+ dom.wrapper.scrollTop = 0;
+ dom.wrapper.scrollLeft = 0;
+ }
+ }, 1000 );
+
+ }
+
+ /**
+ * Creates an HTML element and returns a reference to it.
+ * If the element already exists the existing instance will
+ * be returned.
+ *
+ * @param {HTMLElement} container
+ * @param {string} tagname
+ * @param {string} classname
+ * @param {string} innerHTML
+ *
+ * @return {HTMLElement}
+ */
+ function createSingletonNode( container, tagname, classname, innerHTML ) {
+
+ // Find all nodes matching the description
+ var nodes = container.querySelectorAll( '.' + classname );
+
+ // Check all matches to find one which is a direct child of
+ // the specified container
+ for( var i = 0; i < nodes.length; i++ ) {
+ var testNode = nodes[i];
+ if( testNode.parentNode === container ) {
+ return testNode;
+ }
+ }
+
+ // If no node was found, create it now
+ var node = document.createElement( tagname );
+ node.classList.add( classname );
+ if( typeof innerHTML === 'string' ) {
+ node.innerHTML = innerHTML;
+ }
+ container.appendChild( node );
+
+ return node;
+
+ }
+
+ /**
+ * Creates the slide background elements and appends them
+ * to the background container. One element is created per
+ * slide no matter if the given slide has visible background.
+ */
+ function createBackgrounds() {
+
+ var printMode = isPrintingPDF();
+
+ // Clear prior backgrounds
+ dom.background.innerHTML = '';
+ dom.background.classList.add( 'no-transition' );
+
+ // Iterate over all horizontal slides
+ toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ).forEach( function( slideh ) {
+
+ var backgroundStack = createBackground( slideh, dom.background );
+
+ // Iterate over all vertical slides
+ toArray( slideh.querySelectorAll( 'section' ) ).forEach( function( slidev ) {
+
+ createBackground( slidev, backgroundStack );
+
+ backgroundStack.classList.add( 'stack' );
+
+ } );
+
+ } );
+
+ // Add parallax background if specified
+ if( config.parallaxBackgroundImage ) {
+
+ dom.background.style.backgroundImage = 'url("' + config.parallaxBackgroundImage + '")';
+ dom.background.style.backgroundSize = config.parallaxBackgroundSize;
+
+ // Make sure the below properties are set on the element - these properties are
+ // needed for proper transitions to be set on the element via CSS. To remove
+ // annoying background slide-in effect when the presentation starts, apply
+ // these properties after short time delay
+ setTimeout( function() {
+ dom.wrapper.classList.add( 'has-parallax-background' );
+ }, 1 );
+
+ }
+ else {
+
+ dom.background.style.backgroundImage = '';
+ dom.wrapper.classList.remove( 'has-parallax-background' );
+
+ }
+
+ }
+
+ /**
+ * Creates a background for the given slide.
+ *
+ * @param {HTMLElement} slide
+ * @param {HTMLElement} container The element that the background
+ * should be appended to
+ * @return {HTMLElement} New background div
+ */
+ function createBackground( slide, container ) {
+
+ var data = {
+ background: slide.getAttribute( 'data-background' ),
+ backgroundSize: slide.getAttribute( 'data-background-size' ),
+ backgroundImage: slide.getAttribute( 'data-background-image' ),
+ backgroundVideo: slide.getAttribute( 'data-background-video' ),
+ backgroundIframe: slide.getAttribute( 'data-background-iframe' ),
+ backgroundColor: slide.getAttribute( 'data-background-color' ),
+ backgroundRepeat: slide.getAttribute( 'data-background-repeat' ),
+ backgroundPosition: slide.getAttribute( 'data-background-position' ),
+ backgroundTransition: slide.getAttribute( 'data-background-transition' )
+ };
+
+ var element = document.createElement( 'div' );
+
+ // Carry over custom classes from the slide to the background
+ element.className = 'slide-background ' + slide.className.replace( /present|past|future/, '' );
+
+ if( data.background ) {
+ // Auto-wrap image urls in url(...)
+ if( /^(http|file|\/\/)/gi.test( data.background ) || /\.(svg|png|jpg|jpeg|gif|bmp)([?#]|$)/gi.test( data.background ) ) {
+ slide.setAttribute( 'data-background-image', data.background );
+ }
+ else {
+ element.style.background = data.background;
+ }
+ }
+
+ // Create a hash for this combination of background settings.
+ // This is used to determine when two slide backgrounds are
+ // the same.
+ if( data.background || data.backgroundColor || data.backgroundImage || data.backgroundVideo || data.backgroundIframe ) {
+ element.setAttribute( 'data-background-hash', data.background +
+ data.backgroundSize +
+ data.backgroundImage +
+ data.backgroundVideo +
+ data.backgroundIframe +
+ data.backgroundColor +
+ data.backgroundRepeat +
+ data.backgroundPosition +
+ data.backgroundTransition );
+ }
+
+ // Additional and optional background properties
+ if( data.backgroundSize ) element.style.backgroundSize = data.backgroundSize;
+ if( data.backgroundSize ) element.setAttribute( 'data-background-size', data.backgroundSize );
+ if( data.backgroundColor ) element.style.backgroundColor = data.backgroundColor;
+ if( data.backgroundRepeat ) element.style.backgroundRepeat = data.backgroundRepeat;
+ if( data.backgroundPosition ) element.style.backgroundPosition = data.backgroundPosition;
+ if( data.backgroundTransition ) element.setAttribute( 'data-background-transition', data.backgroundTransition );
+
+ container.appendChild( element );
+
+ // If backgrounds are being recreated, clear old classes
+ slide.classList.remove( 'has-dark-background' );
+ slide.classList.remove( 'has-light-background' );
+
+ slide.slideBackgroundElement = element;
+
+ // If this slide has a background color, add a class that
+ // signals if it is light or dark. If the slide has no background
+ // color, no class will be set
+ var computedBackgroundStyle = window.getComputedStyle( element );
+ if( computedBackgroundStyle && computedBackgroundStyle.backgroundColor ) {
+ var rgb = colorToRgb( computedBackgroundStyle.backgroundColor );
+
+ // Ignore fully transparent backgrounds. Some browsers return
+ // rgba(0,0,0,0) when reading the computed background color of
+ // an element with no background
+ if( rgb && rgb.a !== 0 ) {
+ if( colorBrightness( computedBackgroundStyle.backgroundColor ) < 128 ) {
+ slide.classList.add( 'has-dark-background' );
+ }
+ else {
+ slide.classList.add( 'has-light-background' );
+ }
+ }
+ }
+
+ return element;
+
+ }
+
+ /**
+ * Registers a listener to postMessage events, this makes it
+ * possible to call all reveal.js API methods from another
+ * window. For example:
+ *
+ * revealWindow.postMessage( JSON.stringify({
+ * method: 'slide',
+ * args: [ 2 ]
+ * }), '*' );
+ */
+ function setupPostMessage() {
+
+ if( config.postMessage ) {
+ window.addEventListener( 'message', function ( event ) {
+ var data = event.data;
+
+ // Make sure we're dealing with JSON
+ if( typeof data === 'string' && data.charAt( 0 ) === '{' && data.charAt( data.length - 1 ) === '}' ) {
+ data = JSON.parse( data );
+
+ // Check if the requested method can be found
+ if( data.method && typeof Reveal[data.method] === 'function' ) {
+ Reveal[data.method].apply( Reveal, data.args );
+ }
+ }
+ }, false );
+ }
+
+ }
+
+ /**
+ * Applies the configuration settings from the config
+ * object. May be called multiple times.
+ *
+ * @param {object} options
+ */
+ function configure( options ) {
+
+ var numberOfSlides = dom.wrapper.querySelectorAll( SLIDES_SELECTOR ).length;
+
+ dom.wrapper.classList.remove( config.transition );
+
+ // New config options may be passed when this method
+ // is invoked through the API after initialization
+ if( typeof options === 'object' ) extend( config, options );
+
+ // Force linear transition based on browser capabilities
+ if( features.transforms3d === false ) config.transition = 'linear';
+
+ dom.wrapper.classList.add( config.transition );
+
+ dom.wrapper.setAttribute( 'data-transition-speed', config.transitionSpeed );
+ dom.wrapper.setAttribute( 'data-background-transition', config.backgroundTransition );
+
+ dom.controls.style.display = config.controls ? 'block' : 'none';
+ dom.progress.style.display = config.progress ? 'block' : 'none';
+
+ if( config.shuffle ) {
+ shuffle();
+ }
+
+ if( config.rtl ) {
+ dom.wrapper.classList.add( 'rtl' );
+ }
+ else {
+ dom.wrapper.classList.remove( 'rtl' );
+ }
+
+ if( config.center ) {
+ dom.wrapper.classList.add( 'center' );
+ }
+ else {
+ dom.wrapper.classList.remove( 'center' );
+ }
+
+ // Exit the paused mode if it was configured off
+ if( config.pause === false ) {
+ resume();
+ }
+
+ if( config.showNotes ) {
+ dom.speakerNotes.classList.add( 'visible' );
+ dom.speakerNotes.setAttribute( 'data-layout', typeof config.showNotes === 'string' ? config.showNotes : 'inline' );
+ }
+ else {
+ dom.speakerNotes.classList.remove( 'visible' );
+ }
+
+ if( config.mouseWheel ) {
+ document.addEventListener( 'DOMMouseScroll', onDocumentMouseScroll, false ); // FF
+ document.addEventListener( 'mousewheel', onDocumentMouseScroll, false );
+ }
+ else {
+ document.removeEventListener( 'DOMMouseScroll', onDocumentMouseScroll, false ); // FF
+ document.removeEventListener( 'mousewheel', onDocumentMouseScroll, false );
+ }
+
+ // Rolling 3D links
+ if( config.rollingLinks ) {
+ enableRollingLinks();
+ }
+ else {
+ disableRollingLinks();
+ }
+
+ // Iframe link previews
+ if( config.previewLinks ) {
+ enablePreviewLinks();
+ disablePreviewLinks( '[data-preview-link=false]' );
+ }
+ else {
+ disablePreviewLinks();
+ enablePreviewLinks( '[data-preview-link]:not([data-preview-link=false])' );
+ }
+
+ // Remove existing auto-slide controls
+ if( autoSlidePlayer ) {
+ autoSlidePlayer.destroy();
+ autoSlidePlayer = null;
+ }
+
+ // Generate auto-slide controls if needed
+ if( numberOfSlides > 1 && config.autoSlide && config.autoSlideStoppable && features.canvas && features.requestAnimationFrame ) {
+ autoSlidePlayer = new Playback( dom.wrapper, function() {
+ return Math.min( Math.max( ( Date.now() - autoSlideStartTime ) / autoSlide, 0 ), 1 );
+ } );
+
+ autoSlidePlayer.on( 'click', onAutoSlidePlayerClick );
+ autoSlidePaused = false;
+ }
+
+ // When fragments are turned off they should be visible
+ if( config.fragments === false ) {
+ toArray( dom.slides.querySelectorAll( '.fragment' ) ).forEach( function( element ) {
+ element.classList.add( 'visible' );
+ element.classList.remove( 'current-fragment' );
+ } );
+ }
+
+ // Slide numbers
+ var slideNumberDisplay = 'none';
+ if( config.slideNumber && !isPrintingPDF() ) {
+ if( config.showSlideNumber === 'all' ) {
+ slideNumberDisplay = 'block';
+ }
+ else if( config.showSlideNumber === 'speaker' && isSpeakerNotes() ) {
+ slideNumberDisplay = 'block';
+ }
+ }
+
+ dom.slideNumber.style.display = slideNumberDisplay;
+
+ sync();
+
+ }
+
+ /**
+ * Binds all event listeners.
+ */
+ function addEventListeners() {
+
+ eventsAreBound = true;
+
+ window.addEventListener( 'hashchange', onWindowHashChange, false );
+ window.addEventListener( 'resize', onWindowResize, false );
+
+ if( config.touch ) {
+ dom.wrapper.addEventListener( 'touchstart', onTouchStart, false );
+ dom.wrapper.addEventListener( 'touchmove', onTouchMove, false );
+ dom.wrapper.addEventListener( 'touchend', onTouchEnd, false );
+
+ // Support pointer-style touch interaction as well
+ if( window.navigator.pointerEnabled ) {
+ // IE 11 uses un-prefixed version of pointer events
+ dom.wrapper.addEventListener( 'pointerdown', onPointerDown, false );
+ dom.wrapper.addEventListener( 'pointermove', onPointerMove, false );
+ dom.wrapper.addEventListener( 'pointerup', onPointerUp, false );
+ }
+ else if( window.navigator.msPointerEnabled ) {
+ // IE 10 uses prefixed version of pointer events
+ dom.wrapper.addEventListener( 'MSPointerDown', onPointerDown, false );
+ dom.wrapper.addEventListener( 'MSPointerMove', onPointerMove, false );
+ dom.wrapper.addEventListener( 'MSPointerUp', onPointerUp, false );
+ }
+ }
+
+ if( config.keyboard ) {
+ document.addEventListener( 'keydown', onDocumentKeyDown, false );
+ document.addEventListener( 'keypress', onDocumentKeyPress, false );
+ }
+
+ if( config.progress && dom.progress ) {
+ dom.progress.addEventListener( 'click', onProgressClicked, false );
+ }
+
+ if( config.focusBodyOnPageVisibilityChange ) {
+ var visibilityChange;
+
+ if( 'hidden' in document ) {
+ visibilityChange = 'visibilitychange';
+ }
+ else if( 'msHidden' in document ) {
+ visibilityChange = 'msvisibilitychange';
+ }
+ else if( 'webkitHidden' in document ) {
+ visibilityChange = 'webkitvisibilitychange';
+ }
+
+ if( visibilityChange ) {
+ document.addEventListener( visibilityChange, onPageVisibilityChange, false );
+ }
+ }
+
+ // Listen to both touch and click events, in case the device
+ // supports both
+ var pointerEvents = [ 'touchstart', 'click' ];
+
+ // Only support touch for Android, fixes double navigations in
+ // stock browser
+ if( UA.match( /android/gi ) ) {
+ pointerEvents = [ 'touchstart' ];
+ }
+
+ pointerEvents.forEach( function( eventName ) {
+ dom.controlsLeft.forEach( function( el ) { el.addEventListener( eventName, onNavigateLeftClicked, false ); } );
+ dom.controlsRight.forEach( function( el ) { el.addEventListener( eventName, onNavigateRightClicked, false ); } );
+ dom.controlsUp.forEach( function( el ) { el.addEventListener( eventName, onNavigateUpClicked, false ); } );
+ dom.controlsDown.forEach( function( el ) { el.addEventListener( eventName, onNavigateDownClicked, false ); } );
+ dom.controlsPrev.forEach( function( el ) { el.addEventListener( eventName, onNavigatePrevClicked, false ); } );
+ dom.controlsNext.forEach( function( el ) { el.addEventListener( eventName, onNavigateNextClicked, false ); } );
+ } );
+
+ }
+
+ /**
+ * Unbinds all event listeners.
+ */
+ function removeEventListeners() {
+
+ eventsAreBound = false;
+
+ document.removeEventListener( 'keydown', onDocumentKeyDown, false );
+ document.removeEventListener( 'keypress', onDocumentKeyPress, false );
+ window.removeEventListener( 'hashchange', onWindowHashChange, false );
+ window.removeEventListener( 'resize', onWindowResize, false );
+
+ dom.wrapper.removeEventListener( 'touchstart', onTouchStart, false );
+ dom.wrapper.removeEventListener( 'touchmove', onTouchMove, false );
+ dom.wrapper.removeEventListener( 'touchend', onTouchEnd, false );
+
+ // IE11
+ if( window.navigator.pointerEnabled ) {
+ dom.wrapper.removeEventListener( 'pointerdown', onPointerDown, false );
+ dom.wrapper.removeEventListener( 'pointermove', onPointerMove, false );
+ dom.wrapper.removeEventListener( 'pointerup', onPointerUp, false );
+ }
+ // IE10
+ else if( window.navigator.msPointerEnabled ) {
+ dom.wrapper.removeEventListener( 'MSPointerDown', onPointerDown, false );
+ dom.wrapper.removeEventListener( 'MSPointerMove', onPointerMove, false );
+ dom.wrapper.removeEventListener( 'MSPointerUp', onPointerUp, false );
+ }
+
+ if ( config.progress && dom.progress ) {
+ dom.progress.removeEventListener( 'click', onProgressClicked, false );
+ }
+
+ [ 'touchstart', 'click' ].forEach( function( eventName ) {
+ dom.controlsLeft.forEach( function( el ) { el.removeEventListener( eventName, onNavigateLeftClicked, false ); } );
+ dom.controlsRight.forEach( function( el ) { el.removeEventListener( eventName, onNavigateRightClicked, false ); } );
+ dom.controlsUp.forEach( function( el ) { el.removeEventListener( eventName, onNavigateUpClicked, false ); } );
+ dom.controlsDown.forEach( function( el ) { el.removeEventListener( eventName, onNavigateDownClicked, false ); } );
+ dom.controlsPrev.forEach( function( el ) { el.removeEventListener( eventName, onNavigatePrevClicked, false ); } );
+ dom.controlsNext.forEach( function( el ) { el.removeEventListener( eventName, onNavigateNextClicked, false ); } );
+ } );
+
+ }
+
+ /**
+ * Extend object a with the properties of object b.
+ * If there's a conflict, object b takes precedence.
+ *
+ * @param {object} a
+ * @param {object} b
+ */
+ function extend( a, b ) {
+
+ for( var i in b ) {
+ a[ i ] = b[ i ];
+ }
+
+ }
+
+ /**
+ * Converts the target object to an array.
+ *
+ * @param {object} o
+ * @return {object[]}
+ */
+ function toArray( o ) {
+
+ return Array.prototype.slice.call( o );
+
+ }
+
+ /**
+ * Utility for deserializing a value.
+ *
+ * @param {*} value
+ * @return {*}
+ */
+ function deserialize( value ) {
+
+ if( typeof value === 'string' ) {
+ if( value === 'null' ) return null;
+ else if( value === 'true' ) return true;
+ else if( value === 'false' ) return false;
+ else if( value.match( /^[\d\.]+$/ ) ) return parseFloat( value );
+ }
+
+ return value;
+
+ }
+
+ /**
+ * Measures the distance in pixels between point a
+ * and point b.
+ *
+ * @param {object} a point with x/y properties
+ * @param {object} b point with x/y properties
+ *
+ * @return {number}
+ */
+ function distanceBetween( a, b ) {
+
+ var dx = a.x - b.x,
+ dy = a.y - b.y;
+
+ return Math.sqrt( dx*dx + dy*dy );
+
+ }
+
+ /**
+ * Applies a CSS transform to the target element.
+ *
+ * @param {HTMLElement} element
+ * @param {string} transform
+ */
+ function transformElement( element, transform ) {
+
+ element.style.WebkitTransform = transform;
+ element.style.MozTransform = transform;
+ element.style.msTransform = transform;
+ element.style.transform = transform;
+
+ }
+
+ /**
+ * Applies CSS transforms to the slides container. The container
+ * is transformed from two separate sources: layout and the overview
+ * mode.
+ *
+ * @param {object} transforms
+ */
+ function transformSlides( transforms ) {
+
+ // Pick up new transforms from arguments
+ if( typeof transforms.layout === 'string' ) slidesTransform.layout = transforms.layout;
+ if( typeof transforms.overview === 'string' ) slidesTransform.overview = transforms.overview;
+
+ // Apply the transforms to the slides container
+ if( slidesTransform.layout ) {
+ transformElement( dom.slides, slidesTransform.layout + ' ' + slidesTransform.overview );
+ }
+ else {
+ transformElement( dom.slides, slidesTransform.overview );
+ }
+
+ }
+
+ /**
+ * Injects the given CSS styles into the DOM.
+ *
+ * @param {string} value
+ */
+ function injectStyleSheet( value ) {
+
+ var tag = document.createElement( 'style' );
+ tag.type = 'text/css';
+ if( tag.styleSheet ) {
+ tag.styleSheet.cssText = value;
+ }
+ else {
+ tag.appendChild( document.createTextNode( value ) );
+ }
+ document.getElementsByTagName( 'head' )[0].appendChild( tag );
+
+ }
+
+ /**
+ * Find the closest parent that matches the given
+ * selector.
+ *
+ * @param {HTMLElement} target The child element
+ * @param {String} selector The CSS selector to match
+ * the parents against
+ *
+ * @return {HTMLElement} The matched parent or null
+ * if no matching parent was found
+ */
+ function closestParent( target, selector ) {
+
+ var parent = target.parentNode;
+
+ while( parent ) {
+
+ // There's some overhead doing this each time, we don't
+ // want to rewrite the element prototype but should still
+ // be enough to feature detect once at startup...
+ var matchesMethod = parent.matches || parent.matchesSelector || parent.msMatchesSelector;
+
+ // If we find a match, we're all set
+ if( matchesMethod && matchesMethod.call( parent, selector ) ) {
+ return parent;
+ }
+
+ // Keep searching
+ parent = parent.parentNode;
+
+ }
+
+ return null;
+
+ }
+
+ /**
+ * Converts various color input formats to an {r:0,g:0,b:0} object.
+ *
+ * @param {string} color The string representation of a color
+ * @example
+ * colorToRgb('#000');
+ * @example
+ * colorToRgb('#000000');
+ * @example
+ * colorToRgb('rgb(0,0,0)');
+ * @example
+ * colorToRgb('rgba(0,0,0)');
+ *
+ * @return {{r: number, g: number, b: number, [a]: number}|null}
+ */
+ function colorToRgb( color ) {
+
+ var hex3 = color.match( /^#([0-9a-f]{3})$/i );
+ if( hex3 && hex3[1] ) {
+ hex3 = hex3[1];
+ return {
+ r: parseInt( hex3.charAt( 0 ), 16 ) * 0x11,
+ g: parseInt( hex3.charAt( 1 ), 16 ) * 0x11,
+ b: parseInt( hex3.charAt( 2 ), 16 ) * 0x11
+ };
+ }
+
+ var hex6 = color.match( /^#([0-9a-f]{6})$/i );
+ if( hex6 && hex6[1] ) {
+ hex6 = hex6[1];
+ return {
+ r: parseInt( hex6.substr( 0, 2 ), 16 ),
+ g: parseInt( hex6.substr( 2, 2 ), 16 ),
+ b: parseInt( hex6.substr( 4, 2 ), 16 )
+ };
+ }
+
+ var rgb = color.match( /^rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/i );
+ if( rgb ) {
+ return {
+ r: parseInt( rgb[1], 10 ),
+ g: parseInt( rgb[2], 10 ),
+ b: parseInt( rgb[3], 10 )
+ };
+ }
+
+ var rgba = color.match( /^rgba\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\,\s*([\d]+|[\d]*.[\d]+)\s*\)$/i );
+ if( rgba ) {
+ return {
+ r: parseInt( rgba[1], 10 ),
+ g: parseInt( rgba[2], 10 ),
+ b: parseInt( rgba[3], 10 ),
+ a: parseFloat( rgba[4] )
+ };
+ }
+
+ return null;
+
+ }
+
+ /**
+ * Calculates brightness on a scale of 0-255.
+ *
+ * @param {string} color See colorToRgb for supported formats.
+ * @see {@link colorToRgb}
+ */
+ function colorBrightness( color ) {
+
+ if( typeof color === 'string' ) color = colorToRgb( color );
+
+ if( color ) {
+ return ( color.r * 299 + color.g * 587 + color.b * 114 ) / 1000;
+ }
+
+ return null;
+
+ }
+
+ /**
+ * Returns the remaining height within the parent of the
+ * target element.
+ *
+ * remaining height = [ configured parent height ] - [ current parent height ]
+ *
+ * @param {HTMLElement} element
+ * @param {number} [height]
+ */
+ function getRemainingHeight( element, height ) {
+
+ height = height || 0;
+
+ if( element ) {
+ var newHeight, oldHeight = element.style.height;
+
+ // Change the .stretch element height to 0 in order find the height of all
+ // the other elements
+ element.style.height = '0px';
+ newHeight = height - element.parentNode.offsetHeight;
+
+ // Restore the old height, just in case
+ element.style.height = oldHeight + 'px';
+
+ return newHeight;
+ }
+
+ return height;
+
+ }
+
+ /**
+ * Checks if this instance is being used to print a PDF.
+ */
+ function isPrintingPDF() {
+
+ return ( /print-pdf/gi ).test( window.location.search );
+
+ }
+
+ /**
+ * Hides the address bar if we're on a mobile device.
+ */
+ function hideAddressBar() {
+
+ if( config.hideAddressBar && isMobileDevice ) {
+ // Events that should trigger the address bar to hide
+ window.addEventListener( 'load', removeAddressBar, false );
+ window.addEventListener( 'orientationchange', removeAddressBar, false );
+ }
+
+ }
+
+ /**
+ * Causes the address bar to hide on mobile devices,
+ * more vertical space ftw.
+ */
+ function removeAddressBar() {
+
+ setTimeout( function() {
+ window.scrollTo( 0, 1 );
+ }, 10 );
+
+ }
+
+ /**
+ * Dispatches an event of the specified type from the
+ * reveal DOM element.
+ */
+ function dispatchEvent( type, args ) {
+
+ var event = document.createEvent( 'HTMLEvents', 1, 2 );
+ event.initEvent( type, true, true );
+ extend( event, args );
+ dom.wrapper.dispatchEvent( event );
+
+ // If we're in an iframe, post each reveal.js event to the
+ // parent window. Used by the notes plugin
+ if( config.postMessageEvents && window.parent !== window.self ) {
+ window.parent.postMessage( JSON.stringify({ namespace: 'reveal', eventName: type, state: getState() }), '*' );
+ }
+
+ }
+
+ /**
+ * Wrap all links in 3D goodness.
+ */
+ function enableRollingLinks() {
+
+ if( features.transforms3d && !( 'msPerspective' in document.body.style ) ) {
+ var anchors = dom.wrapper.querySelectorAll( SLIDES_SELECTOR + ' a' );
+
+ for( var i = 0, len = anchors.length; i < len; i++ ) {
+ var anchor = anchors[i];
+
+ if( anchor.textContent && !anchor.querySelector( '*' ) && ( !anchor.className || !anchor.classList.contains( anchor, 'roll' ) ) ) {
+ var span = document.createElement('span');
+ span.setAttribute('data-title', anchor.text);
+ span.innerHTML = anchor.innerHTML;
+
+ anchor.classList.add( 'roll' );
+ anchor.innerHTML = '';
+ anchor.appendChild(span);
+ }
+ }
+ }
+
+ }
+
+ /**
+ * Unwrap all 3D links.
+ */
+ function disableRollingLinks() {
+
+ var anchors = dom.wrapper.querySelectorAll( SLIDES_SELECTOR + ' a.roll' );
+
+ for( var i = 0, len = anchors.length; i < len; i++ ) {
+ var anchor = anchors[i];
+ var span = anchor.querySelector( 'span' );
+
+ if( span ) {
+ anchor.classList.remove( 'roll' );
+ anchor.innerHTML = span.innerHTML;
+ }
+ }
+
+ }
+
+ /**
+ * Bind preview frame links.
+ *
+ * @param {string} [selector=a] - selector for anchors
+ */
+ function enablePreviewLinks( selector ) {
+
+ var anchors = toArray( document.querySelectorAll( selector ? selector : 'a' ) );
+
+ anchors.forEach( function( element ) {
+ if( /^(http|www)/gi.test( element.getAttribute( 'href' ) ) ) {
+ element.addEventListener( 'click', onPreviewLinkClicked, false );
+ }
+ } );
+
+ }
+
+ /**
+ * Unbind preview frame links.
+ */
+ function disablePreviewLinks( selector ) {
+
+ var anchors = toArray( document.querySelectorAll( selector ? selector : 'a' ) );
+
+ anchors.forEach( function( element ) {
+ if( /^(http|www)/gi.test( element.getAttribute( 'href' ) ) ) {
+ element.removeEventListener( 'click', onPreviewLinkClicked, false );
+ }
+ } );
+
+ }
+
+ /**
+ * Opens a preview window for the target URL.
+ *
+ * @param {string} url - url for preview iframe src
+ */
+ function showPreview( url ) {
+
+ closeOverlay();
+
+ dom.overlay = document.createElement( 'div' );
+ dom.overlay.classList.add( 'overlay' );
+ dom.overlay.classList.add( 'overlay-preview' );
+ dom.wrapper.appendChild( dom.overlay );
+
+ dom.overlay.innerHTML = [
+ '<header>',
+ '<a class="close" href="#"><span class="icon"></span></a>',
+ '<a class="external" href="'+ url +'" target="_blank"><span class="icon"></span></a>',
+ '</header>',
+ '<div class="spinner"></div>',
+ '<div class="viewport">',
+ '<iframe src="'+ url +'"></iframe>',
+ '<small class="viewport-inner">',
+ '<span class="x-frame-error">Unable to load iframe. This is likely due to the site\'s policy (x-frame-options).</span>',
+ '</small>',
+ '</div>'
+ ].join('');
+
+ dom.overlay.querySelector( 'iframe' ).addEventListener( 'load', function( event ) {
+ dom.overlay.classList.add( 'loaded' );
+ }, false );
+
+ dom.overlay.querySelector( '.close' ).addEventListener( 'click', function( event ) {
+ closeOverlay();
+ event.preventDefault();
+ }, false );
+
+ dom.overlay.querySelector( '.external' ).addEventListener( 'click', function( event ) {
+ closeOverlay();
+ }, false );
+
+ setTimeout( function() {
+ dom.overlay.classList.add( 'visible' );
+ }, 1 );
+
+ }
+
+ /**
+ * Open or close help overlay window.
+ *
+ * @param {Boolean} [override] Flag which overrides the
+ * toggle logic and forcibly sets the desired state. True means
+ * help is open, false means it's closed.
+ */
+ function toggleHelp( override ){
+
+ if( typeof override === 'boolean' ) {
+ override ? showHelp() : closeOverlay();
+ }
+ else {
+ if( dom.overlay ) {
+ closeOverlay();
+ }
+ else {
+ showHelp();
+ }
+ }
+ }
+
+ /**
+ * Opens an overlay window with help material.
+ */
+ function showHelp() {
+
+ if( config.help ) {
+
+ closeOverlay();
+
+ dom.overlay = document.createElement( 'div' );
+ dom.overlay.classList.add( 'overlay' );
+ dom.overlay.classList.add( 'overlay-help' );
+ dom.wrapper.appendChild( dom.overlay );
+
+ var html = '<p class="title">Keyboard Shortcuts</p><br/>';
+
+ html += '<table><th>KEY</th><th>ACTION</th>';
+ for( var key in keyboardShortcuts ) {
+ html += '<tr><td>' + key + '</td><td>' + keyboardShortcuts[ key ] + '</td></tr>';
+ }
+
+ html += '</table>';
+
+ dom.overlay.innerHTML = [
+ '<header>',
+ '<a class="close" href="#"><span class="icon"></span></a>',
+ '</header>',
+ '<div class="viewport">',
+ '<div class="viewport-inner">'+ html +'</div>',
+ '</div>'
+ ].join('');
+
+ dom.overlay.querySelector( '.close' ).addEventListener( 'click', function( event ) {
+ closeOverlay();
+ event.preventDefault();
+ }, false );
+
+ setTimeout( function() {
+ dom.overlay.classList.add( 'visible' );
+ }, 1 );
+
+ }
+
+ }
+
+ /**
+ * Closes any currently open overlay.
+ */
+ function closeOverlay() {
+
+ if( dom.overlay ) {
+ dom.overlay.parentNode.removeChild( dom.overlay );
+ dom.overlay = null;
+ }
+
+ }
+
+ /**
+ * Applies JavaScript-controlled layout rules to the
+ * presentation.
+ */
+ function layout() {
+
+ if( dom.wrapper && !isPrintingPDF() ) {
+
+ var size = getComputedSlideSize();
+
+ // Layout the contents of the slides
+ layoutSlideContents( config.width, config.height );
+
+ dom.slides.style.width = size.width + 'px';
+ dom.slides.style.height = size.height + 'px';
+
+ // Determine scale of content to fit within available space
+ scale = Math.min( size.presentationWidth / size.width, size.presentationHeight / size.height );
+
+ // Respect max/min scale settings
+ scale = Math.max( scale, config.minScale );
+ scale = Math.min( scale, config.maxScale );
+
+ // Don't apply any scaling styles if scale is 1
+ if( scale === 1 ) {
+ dom.slides.style.zoom = '';
+ dom.slides.style.left = '';
+ dom.slides.style.top = '';
+ dom.slides.style.bottom = '';
+ dom.slides.style.right = '';
+ transformSlides( { layout: '' } );
+ }
+ else {
+ // Prefer zoom for scaling up so that content remains crisp.
+ // Don't use zoom to scale down since that can lead to shifts
+ // in text layout/line breaks.
+ if( scale > 1 && features.zoom ) {
+ dom.slides.style.zoom = scale;
+ dom.slides.style.left = '';
+ dom.slides.style.top = '';
+ dom.slides.style.bottom = '';
+ dom.slides.style.right = '';
+ transformSlides( { layout: '' } );
+ }
+ // Apply scale transform as a fallback
+ else {
+ dom.slides.style.zoom = '';
+ dom.slides.style.left = '50%';
+ dom.slides.style.top = '50%';
+ dom.slides.style.bottom = 'auto';
+ dom.slides.style.right = 'auto';
+ transformSlides( { layout: 'translate(-50%, -50%) scale('+ scale +')' } );
+ }
+ }
+
+ // Select all slides, vertical and horizontal
+ var slides = toArray( dom.wrapper.querySelectorAll( SLIDES_SELECTOR ) );
+
+ for( var i = 0, len = slides.length; i < len; i++ ) {
+ var slide = slides[ i ];
+
+ // Don't bother updating invisible slides
+ if( slide.style.display === 'none' ) {
+ continue;
+ }
+
+ if( config.center || slide.classList.contains( 'center' ) ) {
+ // Vertical stacks are not centred since their section
+ // children will be
+ if( slide.classList.contains( 'stack' ) ) {
+ slide.style.top = 0;
+ }
+ else {
+ slide.style.top = Math.max( ( size.height - slide.scrollHeight ) / 2, 0 ) + 'px';
+ }
+ }
+ else {
+ slide.style.top = '';
+ }
+
+ }
+
+ updateProgress();
+ updateParallax();
+
+ if( isOverview() ) {
+ updateOverview();
+ }
+
+ }
+
+ }
+
+ /**
+ * Applies layout logic to the contents of all slides in
+ * the presentation.
+ *
+ * @param {string|number} width
+ * @param {string|number} height
+ */
+ function layoutSlideContents( width, height ) {
+
+ // Handle sizing of elements with the 'stretch' class
+ toArray( dom.slides.querySelectorAll( 'section > .stretch' ) ).forEach( function( element ) {
+
+ // Determine how much vertical space we can use
+ var remainingHeight = getRemainingHeight( element, height );
+
+ // Consider the aspect ratio of media elements
+ if( /(img|video)/gi.test( element.nodeName ) ) {
+ var nw = element.naturalWidth || element.videoWidth,
+ nh = element.naturalHeight || element.videoHeight;
+
+ var es = Math.min( width / nw, remainingHeight / nh );
+
+ element.style.width = ( nw * es ) + 'px';
+ element.style.height = ( nh * es ) + 'px';
+
+ }
+ else {
+ element.style.width = width + 'px';
+ element.style.height = remainingHeight + 'px';
+ }
+
+ } );
+
+ }
+
+ /**
+ * Calculates the computed pixel size of our slides. These
+ * values are based on the width and height configuration
+ * options.
+ *
+ * @param {number} [presentationWidth=dom.wrapper.offsetWidth]
+ * @param {number} [presentationHeight=dom.wrapper.offsetHeight]
+ */
+ function getComputedSlideSize( presentationWidth, presentationHeight ) {
+
+ var size = {
+ // Slide size
+ width: config.width,
+ height: config.height,
+
+ // Presentation size
+ presentationWidth: presentationWidth || dom.wrapper.offsetWidth,
+ presentationHeight: presentationHeight || dom.wrapper.offsetHeight
+ };
+
+ // Reduce available space by margin
+ size.presentationWidth -= ( size.presentationWidth * config.margin );
+ size.presentationHeight -= ( size.presentationHeight * config.margin );
+
+ // Slide width may be a percentage of available width
+ if( typeof size.width === 'string' && /%$/.test( size.width ) ) {
+ size.width = parseInt( size.width, 10 ) / 100 * size.presentationWidth;
+ }
+
+ // Slide height may be a percentage of available height
+ if( typeof size.height === 'string' && /%$/.test( size.height ) ) {
+ size.height = parseInt( size.height, 10 ) / 100 * size.presentationHeight;
+ }
+
+ return size;
+
+ }
+
+ /**
+ * Stores the vertical index of a stack so that the same
+ * vertical slide can be selected when navigating to and
+ * from the stack.
+ *
+ * @param {HTMLElement} stack The vertical stack element
+ * @param {string|number} [v=0] Index to memorize
+ */
+ function setPreviousVerticalIndex( stack, v ) {
+
+ if( typeof stack === 'object' && typeof stack.setAttribute === 'function' ) {
+ stack.setAttribute( 'data-previous-indexv', v || 0 );
+ }
+
+ }
+
+ /**
+ * Retrieves the vertical index which was stored using
+ * #setPreviousVerticalIndex() or 0 if no previous index
+ * exists.
+ *
+ * @param {HTMLElement} stack The vertical stack element
+ */
+ function getPreviousVerticalIndex( stack ) {
+
+ if( typeof stack === 'object' && typeof stack.setAttribute === 'function' && stack.classList.contains( 'stack' ) ) {
+ // Prefer manually defined start-indexv
+ var attributeName = stack.hasAttribute( 'data-start-indexv' ) ? 'data-start-indexv' : 'data-previous-indexv';
+
+ return parseInt( stack.getAttribute( attributeName ) || 0, 10 );
+ }
+
+ return 0;
+
+ }
+
+ /**
+ * Displays the overview of slides (quick nav) by scaling
+ * down and arranging all slide elements.
+ */
+ function activateOverview() {
+
+ // Only proceed if enabled in config
+ if( config.overview && !isOverview() ) {
+
+ overview = true;
+
+ dom.wrapper.classList.add( 'overview' );
+ dom.wrapper.classList.remove( 'overview-deactivating' );
+
+ if( features.overviewTransitions ) {
+ setTimeout( function() {
+ dom.wrapper.classList.add( 'overview-animated' );
+ }, 1 );
+ }
+
+ // Don't auto-slide while in overview mode
+ cancelAutoSlide();
+
+ // Move the backgrounds element into the slide container to
+ // that the same scaling is applied
+ dom.slides.appendChild( dom.background );
+
+ // Clicking on an overview slide navigates to it
+ toArray( dom.wrapper.querySelectorAll( SLIDES_SELECTOR ) ).forEach( function( slide ) {
+ if( !slide.classList.contains( 'stack' ) ) {
+ slide.addEventListener( 'click', onOverviewSlideClicked, true );
+ }
+ } );
+
+ // Calculate slide sizes
+ var margin = 70;
+ var slideSize = getComputedSlideSize();
+ overviewSlideWidth = slideSize.width + margin;
+ overviewSlideHeight = slideSize.height + margin;
+
+ // Reverse in RTL mode
+ if( config.rtl ) {
+ overviewSlideWidth = -overviewSlideWidth;
+ }
+
+ updateSlidesVisibility();
+ layoutOverview();
+ updateOverview();
+
+ layout();
+
+ // Notify observers of the overview showing
+ dispatchEvent( 'overviewshown', {
+ 'indexh': indexh,
+ 'indexv': indexv,
+ 'currentSlide': currentSlide
+ } );
+
+ }
+
+ }
+
+ /**
+ * Uses CSS transforms to position all slides in a grid for
+ * display inside of the overview mode.
+ */
+ function layoutOverview() {
+
+ // Layout slides
+ toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ).forEach( function( hslide, h ) {
+ hslide.setAttribute( 'data-index-h', h );
+ transformElement( hslide, 'translate3d(' + ( h * overviewSlideWidth ) + 'px, 0, 0)' );
+
+ if( hslide.classList.contains( 'stack' ) ) {
+
+ toArray( hslide.querySelectorAll( 'section' ) ).forEach( function( vslide, v ) {
+ vslide.setAttribute( 'data-index-h', h );
+ vslide.setAttribute( 'data-index-v', v );
+
+ transformElement( vslide, 'translate3d(0, ' + ( v * overviewSlideHeight ) + 'px, 0)' );
+ } );
+
+ }
+ } );
+
+ // Layout slide backgrounds
+ toArray( dom.background.childNodes ).forEach( function( hbackground, h ) {
+ transformElement( hbackground, 'translate3d(' + ( h * overviewSlideWidth ) + 'px, 0, 0)' );
+
+ toArray( hbackground.querySelectorAll( '.slide-background' ) ).forEach( function( vbackground, v ) {
+ transformElement( vbackground, 'translate3d(0, ' + ( v * overviewSlideHeight ) + 'px, 0)' );
+ } );
+ } );
+
+ }
+
+ /**
+ * Moves the overview viewport to the current slides.
+ * Called each time the current slide changes.
+ */
+ function updateOverview() {
+
+ var vmin = Math.min( window.innerWidth, window.innerHeight );
+ var scale = Math.max( vmin / 5, 150 ) / vmin;
+
+ transformSlides( {
+ overview: [
+ 'scale('+ scale +')',
+ 'translateX('+ ( -indexh * overviewSlideWidth ) +'px)',
+ 'translateY('+ ( -indexv * overviewSlideHeight ) +'px)'
+ ].join( ' ' )
+ } );
+
+ }
+
+ /**
+ * Exits the slide overview and enters the currently
+ * active slide.
+ */
+ function deactivateOverview() {
+
+ // Only proceed if enabled in config
+ if( config.overview ) {
+
+ overview = false;
+
+ dom.wrapper.classList.remove( 'overview' );
+ dom.wrapper.classList.remove( 'overview-animated' );
+
+ // Temporarily add a class so that transitions can do different things
+ // depending on whether they are exiting/entering overview, or just
+ // moving from slide to slide
+ dom.wrapper.classList.add( 'overview-deactivating' );
+
+ setTimeout( function () {
+ dom.wrapper.classList.remove( 'overview-deactivating' );
+ }, 1 );
+
+ // Move the background element back out
+ dom.wrapper.appendChild( dom.background );
+
+ // Clean up changes made to slides
+ toArray( dom.wrapper.querySelectorAll( SLIDES_SELECTOR ) ).forEach( function( slide ) {
+ transformElement( slide, '' );
+
+ slide.removeEventListener( 'click', onOverviewSlideClicked, true );
+ } );
+
+ // Clean up changes made to backgrounds
+ toArray( dom.background.querySelectorAll( '.slide-background' ) ).forEach( function( background ) {
+ transformElement( background, '' );
+ } );
+
+ transformSlides( { overview: '' } );
+
+ slide( indexh, indexv );
+
+ layout();
+
+ cueAutoSlide();
+
+ // Notify observers of the overview hiding
+ dispatchEvent( 'overviewhidden', {
+ 'indexh': indexh,
+ 'indexv': indexv,
+ 'currentSlide': currentSlide
+ } );
+
+ }
+ }
+
+ /**
+ * Toggles the slide overview mode on and off.
+ *
+ * @param {Boolean} [override] Flag which overrides the
+ * toggle logic and forcibly sets the desired state. True means
+ * overview is open, false means it's closed.
+ */
+ function toggleOverview( override ) {
+
+ if( typeof override === 'boolean' ) {
+ override ? activateOverview() : deactivateOverview();
+ }
+ else {
+ isOverview() ? deactivateOverview() : activateOverview();
+ }
+
+ }
+
+ /**
+ * Checks if the overview is currently active.
+ *
+ * @return {Boolean} true if the overview is active,
+ * false otherwise
+ */
+ function isOverview() {
+
+ return overview;
+
+ }
+
+ /**
+ * Checks if the current or specified slide is vertical
+ * (nested within another slide).
+ *
+ * @param {HTMLElement} [slide=currentSlide] The slide to check
+ * orientation of
+ * @return {Boolean}
+ */
+ function isVerticalSlide( slide ) {
+
+ // Prefer slide argument, otherwise use current slide
+ slide = slide ? slide : currentSlide;
+
+ return slide && slide.parentNode && !!slide.parentNode.nodeName.match( /section/i );
+
+ }
+
+ /**
+ * Handling the fullscreen functionality via the fullscreen API
+ *
+ * @see http://fullscreen.spec.whatwg.org/
+ * @see https://developer.mozilla.org/en-US/docs/DOM/Using_fullscreen_mode
+ */
+ function enterFullscreen() {
+
+ var element = document.documentElement;
+
+ // Check which implementation is available
+ var requestMethod = element.requestFullscreen ||
+ element.webkitRequestFullscreen ||
+ element.webkitRequestFullScreen ||
+ element.mozRequestFullScreen ||
+ element.msRequestFullscreen;
+
+ if( requestMethod ) {
+ requestMethod.apply( element );
+ }
+
+ }
+
+ /**
+ * Enters the paused mode which fades everything on screen to
+ * black.
+ */
+ function pause() {
+
+ if( config.pause ) {
+ var wasPaused = dom.wrapper.classList.contains( 'paused' );
+
+ cancelAutoSlide();
+ dom.wrapper.classList.add( 'paused' );
+
+ if( wasPaused === false ) {
+ dispatchEvent( 'paused' );
+ }
+ }
+
+ }
+
+ /**
+ * Exits from the paused mode.
+ */
+ function resume() {
+
+ var wasPaused = dom.wrapper.classList.contains( 'paused' );
+ dom.wrapper.classList.remove( 'paused' );
+
+ cueAutoSlide();
+
+ if( wasPaused ) {
+ dispatchEvent( 'resumed' );
+ }
+
+ }
+
+ /**
+ * Toggles the paused mode on and off.
+ */
+ function togglePause( override ) {
+
+ if( typeof override === 'boolean' ) {
+ override ? pause() : resume();
+ }
+ else {
+ isPaused() ? resume() : pause();
+ }
+
+ }
+
+ /**
+ * Checks if we are currently in the paused mode.
+ *
+ * @return {Boolean}
+ */
+ function isPaused() {
+
+ return dom.wrapper.classList.contains( 'paused' );
+
+ }
+
+ /**
+ * Toggles the auto slide mode on and off.
+ *
+ * @param {Boolean} [override] Flag which sets the desired state.
+ * True means autoplay starts, false means it stops.
+ */
+
+ function toggleAutoSlide( override ) {
+
+ if( typeof override === 'boolean' ) {
+ override ? resumeAutoSlide() : pauseAutoSlide();
+ }
+
+ else {
+ autoSlidePaused ? resumeAutoSlide() : pauseAutoSlide();
+ }
+
+ }
+
+ /**
+ * Checks if the auto slide mode is currently on.
+ *
+ * @return {Boolean}
+ */
+ function isAutoSliding() {
+
+ return !!( autoSlide && !autoSlidePaused );
+
+ }
+
+ /**
+ * Steps from the current point in the presentation to the
+ * slide which matches the specified horizontal and vertical
+ * indices.
+ *
+ * @param {number} [h=indexh] Horizontal index of the target slide
+ * @param {number} [v=indexv] Vertical index of the target slide
+ * @param {number} [f] Index of a fragment within the
+ * target slide to activate
+ * @param {number} [o] Origin for use in multimaster environments
+ */
+ function slide( h, v, f, o ) {
+
+ // Remember where we were at before
+ previousSlide = currentSlide;
+
+ // Query all horizontal slides in the deck
+ var horizontalSlides = dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR );
+
+ // Abort if there are no slides
+ if( horizontalSlides.length === 0 ) return;
+
+ // If no vertical index is specified and the upcoming slide is a
+ // stack, resume at its previous vertical index
+ if( v === undefined && !isOverview() ) {
+ v = getPreviousVerticalIndex( horizontalSlides[ h ] );
+ }
+
+ // If we were on a vertical stack, remember what vertical index
+ // it was on so we can resume at the same position when returning
+ if( previousSlide && previousSlide.parentNode && previousSlide.parentNode.classList.contains( 'stack' ) ) {
+ setPreviousVerticalIndex( previousSlide.parentNode, indexv );
+ }
+
+ // Remember the state before this slide
+ var stateBefore = state.concat();
+
+ // Reset the state array
+ state.length = 0;
+
+ var indexhBefore = indexh || 0,
+ indexvBefore = indexv || 0;
+
+ // Activate and transition to the new slide
+ indexh = updateSlides( HORIZONTAL_SLIDES_SELECTOR, h === undefined ? indexh : h );
+ indexv = updateSlides( VERTICAL_SLIDES_SELECTOR, v === undefined ? indexv : v );
+
+ // Update the visibility of slides now that the indices have changed
+ updateSlidesVisibility();
+
+ layout();
+
+ // Apply the new state
+ stateLoop: for( var i = 0, len = state.length; i < len; i++ ) {
+ // Check if this state existed on the previous slide. If it
+ // did, we will avoid adding it repeatedly
+ for( var j = 0; j < stateBefore.length; j++ ) {
+ if( stateBefore[j] === state[i] ) {
+ stateBefore.splice( j, 1 );
+ continue stateLoop;
+ }
+ }
+
+ document.documentElement.classList.add( state[i] );
+
+ // Dispatch custom event matching the state's name
+ dispatchEvent( state[i] );
+ }
+
+ // Clean up the remains of the previous state
+ while( stateBefore.length ) {
+ document.documentElement.classList.remove( stateBefore.pop() );
+ }
+
+ // Update the overview if it's currently active
+ if( isOverview() ) {
+ updateOverview();
+ }
+
+ // Find the current horizontal slide and any possible vertical slides
+ // within it
+ var currentHorizontalSlide = horizontalSlides[ indexh ],
+ currentVerticalSlides = currentHorizontalSlide.querySelectorAll( 'section' );
+
+ // Store references to the previous and current slides
+ currentSlide = currentVerticalSlides[ indexv ] || currentHorizontalSlide;
+
+ // Show fragment, if specified
+ if( typeof f !== 'undefined' ) {
+ navigateFragment( f );
+ }
+
+ // Dispatch an event if the slide changed
+ var slideChanged = ( indexh !== indexhBefore || indexv !== indexvBefore );
+ if( slideChanged ) {
+ dispatchEvent( 'slidechanged', {
+ 'indexh': indexh,
+ 'indexv': indexv,
+ 'previousSlide': previousSlide,
+ 'currentSlide': currentSlide,
+ 'origin': o
+ } );
+ }
+ else {
+ // Ensure that the previous slide is never the same as the current
+ previousSlide = null;
+ }
+
+ // Solves an edge case where the previous slide maintains the
+ // 'present' class when navigating between adjacent vertical
+ // stacks
+ if( previousSlide ) {
+ previousSlide.classList.remove( 'present' );
+ previousSlide.setAttribute( 'aria-hidden', 'true' );
+
+ // Reset all slides upon navigate to home
+ // Issue: #285
+ if ( dom.wrapper.querySelector( HOME_SLIDE_SELECTOR ).classList.contains( 'present' ) ) {
+ // Launch async task
+ setTimeout( function () {
+ var slides = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR + '.stack') ), i;
+ for( i in slides ) {
+ if( slides[i] ) {
+ // Reset stack
+ setPreviousVerticalIndex( slides[i], 0 );
+ }
+ }
+ }, 0 );
+ }
+ }
+
+ // Handle embedded content
+ if( slideChanged || !previousSlide ) {
+ stopEmbeddedContent( previousSlide );
+ startEmbeddedContent( currentSlide );
+ }
+
+ // Announce the current slide contents, for screen readers
+ dom.statusDiv.textContent = getStatusText( currentSlide );
+
+ updateControls();
+ updateProgress();
+ updateBackground();
+ updateParallax();
+ updateSlideNumber();
+ updateNotes();
+
+ // Update the URL hash
+ writeURL();
+
+ cueAutoSlide();
+
+ }
+
+ /**
+ * Syncs the presentation with the current DOM. Useful
+ * when new slides or control elements are added or when
+ * the configuration has changed.
+ */
+ function sync() {
+
+ // Subscribe to input
+ removeEventListeners();
+ addEventListeners();
+
+ // Force a layout to make sure the current config is accounted for
+ layout();
+
+ // Reflect the current autoSlide value
+ autoSlide = config.autoSlide;
+
+ // Start auto-sliding if it's enabled
+ cueAutoSlide();
+
+ // Re-create the slide backgrounds
+ createBackgrounds();
+
+ // Write the current hash to the URL
+ writeURL();
+
+ sortAllFragments();
+
+ updateControls();
+ updateProgress();
+ updateSlideNumber();
+ updateSlidesVisibility();
+ updateBackground( true );
+ updateNotes();
+
+ formatEmbeddedContent();
+
+ // Start or stop embedded content depending on global config
+ if( config.autoPlayMedia === false ) {
+ stopEmbeddedContent( currentSlide );
+ }
+ else {
+ startEmbeddedContent( currentSlide );
+ }
+
+ if( isOverview() ) {
+ layoutOverview();
+ }
+
+ }
+
+ /**
+ * Resets all vertical slides so that only the first
+ * is visible.
+ */
+ function resetVerticalSlides() {
+
+ var horizontalSlides = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) );
+ horizontalSlides.forEach( function( horizontalSlide ) {
+
+ var verticalSlides = toArray( horizontalSlide.querySelectorAll( 'section' ) );
+ verticalSlides.forEach( function( verticalSlide, y ) {
+
+ if( y > 0 ) {
+ verticalSlide.classList.remove( 'present' );
+ verticalSlide.classList.remove( 'past' );
+ verticalSlide.classList.add( 'future' );
+ verticalSlide.setAttribute( 'aria-hidden', 'true' );
+ }
+
+ } );
+
+ } );
+
+ }
+
+ /**
+ * Sorts and formats all of fragments in the
+ * presentation.
+ */
+ function sortAllFragments() {
+
+ var horizontalSlides = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) );
+ horizontalSlides.forEach( function( horizontalSlide ) {
+
+ var verticalSlides = toArray( horizontalSlide.querySelectorAll( 'section' ) );
+ verticalSlides.forEach( function( verticalSlide, y ) {
+
+ sortFragments( verticalSlide.querySelectorAll( '.fragment' ) );
+
+ } );
+
+ if( verticalSlides.length === 0 ) sortFragments( horizontalSlide.querySelectorAll( '.fragment' ) );
+
+ } );
+
+ }
+
+ /**
+ * Randomly shuffles all slides in the deck.
+ */
+ function shuffle() {
+
+ var slides = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) );
+
+ slides.forEach( function( slide ) {
+
+ // Insert this slide next to another random slide. This may
+ // cause the slide to insert before itself but that's fine.
+ dom.slides.insertBefore( slide, slides[ Math.floor( Math.random() * slides.length ) ] );
+
+ } );
+
+ }
+
+ /**
+ * Updates one dimension of slides by showing the slide
+ * with the specified index.
+ *
+ * @param {string} selector A CSS selector that will fetch
+ * the group of slides we are working with
+ * @param {number} index The index of the slide that should be
+ * shown
+ *
+ * @return {number} The index of the slide that is now shown,
+ * might differ from the passed in index if it was out of
+ * bounds.
+ */
+ function updateSlides( selector, index ) {
+
+ // Select all slides and convert the NodeList result to
+ // an array
+ var slides = toArray( dom.wrapper.querySelectorAll( selector ) ),
+ slidesLength = slides.length;
+
+ var printMode = isPrintingPDF();
+
+ if( slidesLength ) {
+
+ // Should the index loop?
+ if( config.loop ) {
+ index %= slidesLength;
+
+ if( index < 0 ) {
+ index = slidesLength + index;
+ }
+ }
+
+ // Enforce max and minimum index bounds
+ index = Math.max( Math.min( index, slidesLength - 1 ), 0 );
+
+ for( var i = 0; i < slidesLength; i++ ) {
+ var element = slides[i];
+
+ var reverse = config.rtl && !isVerticalSlide( element );
+
+ element.classList.remove( 'past' );
+ element.classList.remove( 'present' );
+ element.classList.remove( 'future' );
+
+ // http://www.w3.org/html/wg/drafts/html/master/editing.html#the-hidden-attribute
+ element.setAttribute( 'hidden', '' );
+ element.setAttribute( 'aria-hidden', 'true' );
+
+ // If this element contains vertical slides
+ if( element.querySelector( 'section' ) ) {
+ element.classList.add( 'stack' );
+ }
+
+ // If we're printing static slides, all slides are "present"
+ if( printMode ) {
+ element.classList.add( 'present' );
+ continue;
+ }
+
+ if( i < index ) {
+ // Any element previous to index is given the 'past' class
+ element.classList.add( reverse ? 'future' : 'past' );
+
+ if( config.fragments ) {
+ var pastFragments = toArray( element.querySelectorAll( '.fragment' ) );
+
+ // Show all fragments on prior slides
+ while( pastFragments.length ) {
+ var pastFragment = pastFragments.pop();
+ pastFragment.classList.add( 'visible' );
+ pastFragment.classList.remove( 'current-fragment' );
+ }
+ }
+ }
+ else if( i > index ) {
+ // Any element subsequent to index is given the 'future' class
+ element.classList.add( reverse ? 'past' : 'future' );
+
+ if( config.fragments ) {
+ var futureFragments = toArray( element.querySelectorAll( '.fragment.visible' ) );
+
+ // No fragments in future slides should be visible ahead of time
+ while( futureFragments.length ) {
+ var futureFragment = futureFragments.pop();
+ futureFragment.classList.remove( 'visible' );
+ futureFragment.classList.remove( 'current-fragment' );
+ }
+ }
+ }
+ }
+
+ // Mark the current slide as present
+ slides[index].classList.add( 'present' );
+ slides[index].removeAttribute( 'hidden' );
+ slides[index].removeAttribute( 'aria-hidden' );
+
+ // If this slide has a state associated with it, add it
+ // onto the current state of the deck
+ var slideState = slides[index].getAttribute( 'data-state' );
+ if( slideState ) {
+ state = state.concat( slideState.split( ' ' ) );
+ }
+
+ }
+ else {
+ // Since there are no slides we can't be anywhere beyond the
+ // zeroth index
+ index = 0;
+ }
+
+ return index;
+
+ }
+
+ /**
+ * Optimization method; hide all slides that are far away
+ * from the present slide.
+ */
+ function updateSlidesVisibility() {
+
+ // Select all slides and convert the NodeList result to
+ // an array
+ var horizontalSlides = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ),
+ horizontalSlidesLength = horizontalSlides.length,
+ distanceX,
+ distanceY;
+
+ if( horizontalSlidesLength && typeof indexh !== 'undefined' ) {
+
+ // The number of steps away from the present slide that will
+ // be visible
+ var viewDistance = isOverview() ? 10 : config.viewDistance;
+
+ // Limit view distance on weaker devices
+ if( isMobileDevice ) {
+ viewDistance = isOverview() ? 6 : 2;
+ }
+
+ // All slides need to be visible when exporting to PDF
+ if( isPrintingPDF() ) {
+ viewDistance = Number.MAX_VALUE;
+ }
+
+ for( var x = 0; x < horizontalSlidesLength; x++ ) {
+ var horizontalSlide = horizontalSlides[x];
+
+ var verticalSlides = toArray( horizontalSlide.querySelectorAll( 'section' ) ),
+ verticalSlidesLength = verticalSlides.length;
+
+ // Determine how far away this slide is from the present
+ distanceX = Math.abs( ( indexh || 0 ) - x ) || 0;
+
+ // If the presentation is looped, distance should measure
+ // 1 between the first and last slides
+ if( config.loop ) {
+ distanceX = Math.abs( ( ( indexh || 0 ) - x ) % ( horizontalSlidesLength - viewDistance ) ) || 0;
+ }
+
+ // Show the horizontal slide if it's within the view distance
+ if( distanceX < viewDistance ) {
+ showSlide( horizontalSlide );
+ }
+ else {
+ hideSlide( horizontalSlide );
+ }
+
+ if( verticalSlidesLength ) {
+
+ var oy = getPreviousVerticalIndex( horizontalSlide );
+
+ for( var y = 0; y < verticalSlidesLength; y++ ) {
+ var verticalSlide = verticalSlides[y];
+
+ distanceY = x === ( indexh || 0 ) ? Math.abs( ( indexv || 0 ) - y ) : Math.abs( y - oy );
+
+ if( distanceX + distanceY < viewDistance ) {
+ showSlide( verticalSlide );
+ }
+ else {
+ hideSlide( verticalSlide );
+ }
+ }
+
+ }
+ }
+
+ }
+
+ }
+
+ /**
+ * Pick up notes from the current slide and display them
+ * to the viewer.
+ *
+ * @see {@link config.showNotes}
+ */
+ function updateNotes() {
+
+ if( config.showNotes && dom.speakerNotes && currentSlide && !isPrintingPDF() ) {
+
+ dom.speakerNotes.innerHTML = getSlideNotes() || '';
+
+ }
+
+ }
+
+ /**
+ * Updates the progress bar to reflect the current slide.
+ */
+ function updateProgress() {
+
+ // Update progress if enabled
+ if( config.progress && dom.progressbar ) {
+
+ dom.progressbar.style.width = getProgress() * dom.wrapper.offsetWidth + 'px';
+
+ }
+
+ }
+
+ /**
+ * Updates the slide number div to reflect the current slide.
+ *
+ * The following slide number formats are available:
+ * "h.v": horizontal . vertical slide number (default)
+ * "h/v": horizontal / vertical slide number
+ * "c": flattened slide number
+ * "c/t": flattened slide number / total slides
+ */
+ function updateSlideNumber() {
+
+ // Update slide number if enabled
+ if( config.slideNumber && dom.slideNumber ) {
+
+ var value = [];
+ var format = 'h.v';
+
+ // Check if a custom number format is available
+ if( typeof config.slideNumber === 'string' ) {
+ format = config.slideNumber;
+ }
+
+ switch( format ) {
+ case 'c':
+ value.push( getSlidePastCount() + 1 );
+ break;
+ case 'c/t':
+ value.push( getSlidePastCount() + 1, '/', getTotalSlides() );
+ break;
+ case 'h/v':
+ value.push( indexh + 1 );
+ if( isVerticalSlide() ) value.push( '/', indexv + 1 );
+ break;
+ default:
+ value.push( indexh + 1 );
+ if( isVerticalSlide() ) value.push( '.', indexv + 1 );
+ }
+
+ dom.slideNumber.innerHTML = formatSlideNumber( value[0], value[1], value[2] );
+ }
+
+ }
+
+ /**
+ * Applies HTML formatting to a slide number before it's
+ * written to the DOM.
+ *
+ * @param {number} a Current slide
+ * @param {string} delimiter Character to separate slide numbers
+ * @param {(number|*)} b Total slides
+ * @return {string} HTML string fragment
+ */
+ function formatSlideNumber( a, delimiter, b ) {
+
+ if( typeof b === 'number' && !isNaN( b ) ) {
+ return '<span class="slide-number-a">'+ a +'</span>' +
+ '<span class="slide-number-delimiter">'+ delimiter +'</span>' +
+ '<span class="slide-number-b">'+ b +'</span>';
+ }
+ else {
+ return '<span class="slide-number-a">'+ a +'</span>';
+ }
+
+ }
+
+ /**
+ * Updates the state of all control/navigation arrows.
+ */
+ function updateControls() {
+
+ var routes = availableRoutes();
+ var fragments = availableFragments();
+
+ // Remove the 'enabled' class from all directions
+ dom.controlsLeft.concat( dom.controlsRight )
+ .concat( dom.controlsUp )
+ .concat( dom.controlsDown )
+ .concat( dom.controlsPrev )
+ .concat( dom.controlsNext ).forEach( function( node ) {
+ node.classList.remove( 'enabled' );
+ node.classList.remove( 'fragmented' );
+
+ // Set 'disabled' attribute on all directions
+ node.setAttribute( 'disabled', 'disabled' );
+ } );
+
+ // Add the 'enabled' class to the available routes; remove 'disabled' attribute to enable buttons
+ if( routes.left ) dom.controlsLeft.forEach( function( el ) { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
+ if( routes.right ) dom.controlsRight.forEach( function( el ) { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
+ if( routes.up ) dom.controlsUp.forEach( function( el ) { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
+ if( routes.down ) dom.controlsDown.forEach( function( el ) { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
+
+ // Prev/next buttons
+ if( routes.left || routes.up ) dom.controlsPrev.forEach( function( el ) { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
+ if( routes.right || routes.down ) dom.controlsNext.forEach( function( el ) { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
+
+ // Highlight fragment directions
+ if( currentSlide ) {
+
+ // Always apply fragment decorator to prev/next buttons
+ if( fragments.prev ) dom.controlsPrev.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
+ if( fragments.next ) dom.controlsNext.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
+
+ // Apply fragment decorators to directional buttons based on
+ // what slide axis they are in
+ if( isVerticalSlide( currentSlide ) ) {
+ if( fragments.prev ) dom.controlsUp.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
+ if( fragments.next ) dom.controlsDown.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
+ }
+ else {
+ if( fragments.prev ) dom.controlsLeft.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
+ if( fragments.next ) dom.controlsRight.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
+ }
+
+ }
+
+ }
+
+ /**
+ * Updates the background elements to reflect the current
+ * slide.
+ *
+ * @param {boolean} includeAll If true, the backgrounds of
+ * all vertical slides (not just the present) will be updated.
+ */
+ function updateBackground( includeAll ) {
+
+ var currentBackground = null;
+
+ // Reverse past/future classes when in RTL mode
+ var horizontalPast = config.rtl ? 'future' : 'past',
+ horizontalFuture = config.rtl ? 'past' : 'future';
+
+ // Update the classes of all backgrounds to match the
+ // states of their slides (past/present/future)
+ toArray( dom.background.childNodes ).forEach( function( backgroundh, h ) {
+
+ backgroundh.classList.remove( 'past' );
+ backgroundh.classList.remove( 'present' );
+ backgroundh.classList.remove( 'future' );
+
+ if( h < indexh ) {
+ backgroundh.classList.add( horizontalPast );
+ }
+ else if ( h > indexh ) {
+ backgroundh.classList.add( horizontalFuture );
+ }
+ else {
+ backgroundh.classList.add( 'present' );
+
+ // Store a reference to the current background element
+ currentBackground = backgroundh;
+ }
+
+ if( includeAll || h === indexh ) {
+ toArray( backgroundh.querySelectorAll( '.slide-background' ) ).forEach( function( backgroundv, v ) {
+
+ backgroundv.classList.remove( 'past' );
+ backgroundv.classList.remove( 'present' );
+ backgroundv.classList.remove( 'future' );
+
+ if( v < indexv ) {
+ backgroundv.classList.add( 'past' );
+ }
+ else if ( v > indexv ) {
+ backgroundv.classList.add( 'future' );
+ }
+ else {
+ backgroundv.classList.add( 'present' );
+
+ // Only if this is the present horizontal and vertical slide
+ if( h === indexh ) currentBackground = backgroundv;
+ }
+
+ } );
+ }
+
+ } );
+
+ // Stop content inside of previous backgrounds
+ if( previousBackground ) {
+
+ stopEmbeddedContent( previousBackground );
+
+ }
+
+ // Start content in the current background
+ if( currentBackground ) {
+
+ startEmbeddedContent( currentBackground );
+
+ var backgroundImageURL = currentBackground.style.backgroundImage || '';
+
+ // Restart GIFs (doesn't work in Firefox)
+ if( /\.gif/i.test( backgroundImageURL ) ) {
+ currentBackground.style.backgroundImage = '';
+ window.getComputedStyle( currentBackground ).opacity;
+ currentBackground.style.backgroundImage = backgroundImageURL;
+ }
+
+ // Don't transition between identical backgrounds. This
+ // prevents unwanted flicker.
+ var previousBackgroundHash = previousBackground ? previousBackground.getAttribute( 'data-background-hash' ) : null;
+ var currentBackgroundHash = currentBackground.getAttribute( 'data-background-hash' );
+ if( currentBackgroundHash && currentBackgroundHash === previousBackgroundHash && currentBackground !== previousBackground ) {
+ dom.background.classList.add( 'no-transition' );
+ }
+
+ previousBackground = currentBackground;
+
+ }
+
+ // If there's a background brightness flag for this slide,
+ // bubble it to the .reveal container
+ if( currentSlide ) {
+ [ 'has-light-background', 'has-dark-background' ].forEach( function( classToBubble ) {
+ if( currentSlide.classList.contains( classToBubble ) ) {
+ dom.wrapper.classList.add( classToBubble );
+ }
+ else {
+ dom.wrapper.classList.remove( classToBubble );
+ }
+ } );
+ }
+
+ // Allow the first background to apply without transition
+ setTimeout( function() {
+ dom.background.classList.remove( 'no-transition' );
+ }, 1 );
+
+ }
+
+ /**
+ * Updates the position of the parallax background based
+ * on the current slide index.
+ */
+ function updateParallax() {
+
+ if( config.parallaxBackgroundImage ) {
+
+ var horizontalSlides = dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ),
+ verticalSlides = dom.wrapper.querySelectorAll( VERTICAL_SLIDES_SELECTOR );
+
+ var backgroundSize = dom.background.style.backgroundSize.split( ' ' ),
+ backgroundWidth, backgroundHeight;
+
+ if( backgroundSize.length === 1 ) {
+ backgroundWidth = backgroundHeight = parseInt( backgroundSize[0], 10 );
+ }
+ else {
+ backgroundWidth = parseInt( backgroundSize[0], 10 );
+ backgroundHeight = parseInt( backgroundSize[1], 10 );
+ }
+
+ var slideWidth = dom.background.offsetWidth,
+ horizontalSlideCount = horizontalSlides.length,
+ horizontalOffsetMultiplier,
+ horizontalOffset;
+
+ if( typeof config.parallaxBackgroundHorizontal === 'number' ) {
+ horizontalOffsetMultiplier = config.parallaxBackgroundHorizontal;
+ }
+ else {
+ horizontalOffsetMultiplier = horizontalSlideCount > 1 ? ( backgroundWidth - slideWidth ) / ( horizontalSlideCount-1 ) : 0;
+ }
+
+ horizontalOffset = horizontalOffsetMultiplier * indexh * -1;
+
+ var slideHeight = dom.background.offsetHeight,
+ verticalSlideCount = verticalSlides.length,
+ verticalOffsetMultiplier,
+ verticalOffset;
+
+ if( typeof config.parallaxBackgroundVertical === 'number' ) {
+ verticalOffsetMultiplier = config.parallaxBackgroundVertical;
+ }
+ else {
+ verticalOffsetMultiplier = ( backgroundHeight - slideHeight ) / ( verticalSlideCount-1 );
+ }
+
+ verticalOffset = verticalSlideCount > 0 ? verticalOffsetMultiplier * indexv : 0;
+
+ dom.background.style.backgroundPosition = horizontalOffset + 'px ' + -verticalOffset + 'px';
+
+ }
+
+ }
+
+ /**
+ * Called when the given slide is within the configured view
+ * distance. Shows the slide element and loads any content
+ * that is set to load lazily (data-src).
+ *
+ * @param {HTMLElement} slide Slide to show
+ */
+ /**
+ * Called when the given slide is within the configured view
+ * distance. Shows the slide element and loads any content
+ * that is set to load lazily (data-src).
+ *
+ * @param {HTMLElement} slide Slide to show
+ */
+ function showSlide( slide ) {
+
+ // Show the slide element
+ slide.style.display = config.display;
+
+ // Media elements with data-src attributes
+ toArray( slide.querySelectorAll( 'img[data-src], video[data-src], audio[data-src]' ) ).forEach( function( element ) {
+ element.setAttribute( 'src', element.getAttribute( 'data-src' ) );
+ element.removeAttribute( 'data-src' );
+ } );
+
+ // Media elements with <source> children
+ toArray( slide.querySelectorAll( 'video, audio' ) ).forEach( function( media ) {
+ var sources = 0;
+
+ toArray( media.querySelectorAll( 'source[data-src]' ) ).forEach( function( source ) {
+ source.setAttribute( 'src', source.getAttribute( 'data-src' ) );
+ source.removeAttribute( 'data-src' );
+ sources += 1;
+ } );
+
+ // If we rewrote sources for this video/audio element, we need
+ // to manually tell it to load from its new origin
+ if( sources > 0 ) {
+ media.load();
+ }
+ } );
+
+
+ // Show the corresponding background element
+ var indices = getIndices( slide );
+ var background = getSlideBackground( indices.h, indices.v );
+ if( background ) {
+ background.style.display = 'block';
+
+ // If the background contains media, load it
+ if( background.hasAttribute( 'data-loaded' ) === false ) {
+ background.setAttribute( 'data-loaded', 'true' );
+
+ var backgroundImage = slide.getAttribute( 'data-background-image' ),
+ backgroundVideo = slide.getAttribute( 'data-background-video' ),
+ backgroundVideoLoop = slide.hasAttribute( 'data-background-video-loop' ),
+ backgroundVideoMuted = slide.hasAttribute( 'data-background-video-muted' ),
+ backgroundIframe = slide.getAttribute( 'data-background-iframe' );
+
+ // Images
+ if( backgroundImage ) {
+ background.style.backgroundImage = 'url('+ backgroundImage +')';
+ }
+ // Videos
+ else if ( backgroundVideo && !isSpeakerNotes() ) {
+ var video = document.createElement( 'video' );
+
+ if( backgroundVideoLoop ) {
+ video.setAttribute( 'loop', '' );
+ }
+
+ if( backgroundVideoMuted ) {
+ video.muted = true;
+ }
+
+ // Inline video playback works (at least in Mobile Safari) as
+ // long as the video is muted and the `playsinline` attribute is
+ // present
+ if( isMobileDevice ) {
+ video.muted = true;
+ video.autoplay = true;
+ video.setAttribute( 'playsinline', '' );
+ }
+
+ // Support comma separated lists of video sources
+ backgroundVideo.split( ',' ).forEach( function( source ) {
+ video.innerHTML += '<source src="'+ source +'">';
+ } );
+
+ background.appendChild( video );
+ }
+ // Iframes
+ else if( backgroundIframe ) {
+ var iframe = document.createElement( 'iframe' );
+ iframe.setAttribute( 'allowfullscreen', '' );
+ iframe.setAttribute( 'mozallowfullscreen', '' );
+ iframe.setAttribute( 'webkitallowfullscreen', '' );
+
+ // Only load autoplaying content when the slide is shown to
+ // avoid having it play in the background
+ if( /autoplay=(1|true|yes)/gi.test( backgroundIframe ) ) {
+ iframe.setAttribute( 'data-src', backgroundIframe );
+ }
+ else {
+ iframe.setAttribute( 'src', backgroundIframe );
+ }
+
+ iframe.style.width = '100%';
+ iframe.style.height = '100%';
+ iframe.style.maxHeight = '100%';
+ iframe.style.maxWidth = '100%';
+
+ background.appendChild( iframe );
+ }
+ }
+
+ }
+
+ }
+
+ /**
+ * Called when the given slide is moved outside of the
+ * configured view distance.
+ *
+ * @param {HTMLElement} slide
+ */
+ function hideSlide( slide ) {
+
+ // Hide the slide element
+ slide.style.display = 'none';
+
+ // Hide the corresponding background element
+ var indices = getIndices( slide );
+ var background = getSlideBackground( indices.h, indices.v );
+ if( background ) {
+ background.style.display = 'none';
+ }
+
+ }
+
+ /**
+ * Determine what available routes there are for navigation.
+ *
+ * @return {{left: boolean, right: boolean, up: boolean, down: boolean}}
+ */
+ function availableRoutes() {
+
+ var horizontalSlides = dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ),
+ verticalSlides = dom.wrapper.querySelectorAll( VERTICAL_SLIDES_SELECTOR );
+
+ var routes = {
+ left: indexh > 0 || config.loop,
+ right: indexh < horizontalSlides.length - 1 || config.loop,
+ up: indexv > 0,
+ down: indexv < verticalSlides.length - 1
+ };
+
+ // reverse horizontal controls for rtl
+ if( config.rtl ) {
+ var left = routes.left;
+ routes.left = routes.right;
+ routes.right = left;
+ }
+
+ return routes;
+
+ }
+
+ /**
+ * Returns an object describing the available fragment
+ * directions.
+ *
+ * @return {{prev: boolean, next: boolean}}
+ */
+ function availableFragments() {
+
+ if( currentSlide && config.fragments ) {
+ var fragments = currentSlide.querySelectorAll( '.fragment' );
+ var hiddenFragments = currentSlide.querySelectorAll( '.fragment:not(.visible)' );
+
+ return {
+ prev: fragments.length - hiddenFragments.length > 0,
+ next: !!hiddenFragments.length
+ };
+ }
+ else {
+ return { prev: false, next: false };
+ }
+
+ }
+
+ /**
+ * Enforces origin-specific format rules for embedded media.
+ */
+ function formatEmbeddedContent() {
+
+ var _appendParamToIframeSource = function( sourceAttribute, sourceURL, param ) {
+ toArray( dom.slides.querySelectorAll( 'iframe['+ sourceAttribute +'*="'+ sourceURL +'"]' ) ).forEach( function( el ) {
+ var src = el.getAttribute( sourceAttribute );
+ if( src && src.indexOf( param ) === -1 ) {
+ el.setAttribute( sourceAttribute, src + ( !/\?/.test( src ) ? '?' : '&' ) + param );
+ }
+ });
+ };
+
+ // YouTube frames must include "?enablejsapi=1"
+ _appendParamToIframeSource( 'src', 'youtube.com/embed/', 'enablejsapi=1' );
+ _appendParamToIframeSource( 'data-src', 'youtube.com/embed/', 'enablejsapi=1' );
+
+ // Vimeo frames must include "?api=1"
+ _appendParamToIframeSource( 'src', 'player.vimeo.com/', 'api=1' );
+ _appendParamToIframeSource( 'data-src', 'player.vimeo.com/', 'api=1' );
+
+ }
+
+ /**
+ * Start playback of any embedded content inside of
+ * the given element.
+ *
+ * @param {HTMLElement} element
+ */
+ function startEmbeddedContent( element ) {
+
+ if( element && !isSpeakerNotes() ) {
+
+ // Restart GIFs
+ toArray( element.querySelectorAll( 'img[src$=".gif"]' ) ).forEach( function( el ) {
+ // Setting the same unchanged source like this was confirmed
+ // to work in Chrome, FF & Safari
+ el.setAttribute( 'src', el.getAttribute( 'src' ) );
+ } );
+
+ // HTML5 media elements
+ toArray( element.querySelectorAll( 'video, audio' ) ).forEach( function( el ) {
+ if( closestParent( el, '.fragment' ) && !closestParent( el, '.fragment.visible' ) ) {
+ return;
+ }
+
+ // Prefer an explicit global autoplay setting
+ var autoplay = config.autoPlayMedia;
+
+ // If no global setting is available, fall back on the element's
+ // own autoplay setting
+ if( typeof autoplay !== 'boolean' ) {
+ autoplay = el.hasAttribute( 'data-autoplay' ) || !!closestParent( el, '.slide-background' );
+ }
+
+ if( autoplay && typeof el.play === 'function' ) {
+
+ if( el.readyState > 1 ) {
+ startEmbeddedMedia( { target: el } );
+ }
+ else {
+ el.removeEventListener( 'loadeddata', startEmbeddedMedia ); // remove first to avoid dupes
+ el.addEventListener( 'loadeddata', startEmbeddedMedia );
+ }
+
+ }
+ } );
+
+ // Normal iframes
+ toArray( element.querySelectorAll( 'iframe[src]' ) ).forEach( function( el ) {
+ if( closestParent( el, '.fragment' ) && !closestParent( el, '.fragment.visible' ) ) {
+ return;
+ }
+
+ startEmbeddedIframe( { target: el } );
+ } );
+
+ // Lazy loading iframes
+ toArray( element.querySelectorAll( 'iframe[data-src]' ) ).forEach( function( el ) {
+ if( closestParent( el, '.fragment' ) && !closestParent( el, '.fragment.visible' ) ) {
+ return;
+ }
+
+ if( el.getAttribute( 'src' ) !== el.getAttribute( 'data-src' ) ) {
+ el.removeEventListener( 'load', startEmbeddedIframe ); // remove first to avoid dupes
+ el.addEventListener( 'load', startEmbeddedIframe );
+ el.setAttribute( 'src', el.getAttribute( 'data-src' ) );
+ }
+ } );
+
+ }
+
+ }
+
+ /**
+ * Starts playing an embedded video/audio element after
+ * it has finished loading.
+ *
+ * @param {object} event
+ */
+ function startEmbeddedMedia( event ) {
+
+ var isAttachedToDOM = !!closestParent( event.target, 'html' ),
+ isVisible = !!closestParent( event.target, '.present' );
+
+ if( isAttachedToDOM && isVisible ) {
+ event.target.currentTime = 0;
+ event.target.play();
+ }
+
+ event.target.removeEventListener( 'loadeddata', startEmbeddedMedia );
+
+ }
+
+ /**
+ * "Starts" the content of an embedded iframe using the
+ * postMessage API.
+ *
+ * @param {object} event
+ */
+ function startEmbeddedIframe( event ) {
+
+ var iframe = event.target;
+
+ if( iframe && iframe.contentWindow ) {
+
+ var isAttachedToDOM = !!closestParent( event.target, 'html' ),
+ isVisible = !!closestParent( event.target, '.present' );
+
+ if( isAttachedToDOM && isVisible ) {
+
+ // Prefer an explicit global autoplay setting
+ var autoplay = config.autoPlayMedia;
+
+ // If no global setting is available, fall back on the element's
+ // own autoplay setting
+ if( typeof autoplay !== 'boolean' ) {
+ autoplay = iframe.hasAttribute( 'data-autoplay' ) || !!closestParent( iframe, '.slide-background' );
+ }
+
+ // YouTube postMessage API
+ if( /youtube\.com\/embed\//.test( iframe.getAttribute( 'src' ) ) && autoplay ) {
+ iframe.contentWindow.postMessage( '{"event":"command","func":"playVideo","args":""}', '*' );
+ }
+ // Vimeo postMessage API
+ else if( /player\.vimeo\.com\//.test( iframe.getAttribute( 'src' ) ) && autoplay ) {
+ iframe.contentWindow.postMessage( '{"method":"play"}', '*' );
+ }
+ // Generic postMessage API
+ else {
+ iframe.contentWindow.postMessage( 'slide:start', '*' );
+ }
+
+ }
+
+ }
+
+ }
+
+ /**
+ * Stop playback of any embedded content inside of
+ * the targeted slide.
+ *
+ * @param {HTMLElement} element
+ */
+ function stopEmbeddedContent( element ) {
+
+ if( element && element.parentNode ) {
+ // HTML5 media elements
+ toArray( element.querySelectorAll( 'video, audio' ) ).forEach( function( el ) {
+ if( !el.hasAttribute( 'data-ignore' ) && typeof el.pause === 'function' ) {
+ el.setAttribute('data-paused-by-reveal', '');
+ el.pause();
+ }
+ } );
+
+ // Generic postMessage API for non-lazy loaded iframes
+ toArray( element.querySelectorAll( 'iframe' ) ).forEach( function( el ) {
+ if( el.contentWindow ) el.contentWindow.postMessage( 'slide:stop', '*' );
+ el.removeEventListener( 'load', startEmbeddedIframe );
+ });
+
+ // YouTube postMessage API
+ toArray( element.querySelectorAll( 'iframe[src*="youtube.com/embed/"]' ) ).forEach( function( el ) {
+ if( !el.hasAttribute( 'data-ignore' ) && el.contentWindow && typeof el.contentWindow.postMessage === 'function' ) {
+ el.contentWindow.postMessage( '{"event":"command","func":"pauseVideo","args":""}', '*' );
+ }
+ });
+
+ // Vimeo postMessage API
+ toArray( element.querySelectorAll( 'iframe[src*="player.vimeo.com/"]' ) ).forEach( function( el ) {
+ if( !el.hasAttribute( 'data-ignore' ) && el.contentWindow && typeof el.contentWindow.postMessage === 'function' ) {
+ el.contentWindow.postMessage( '{"method":"pause"}', '*' );
+ }
+ });
+
+ // Lazy loading iframes
+ toArray( element.querySelectorAll( 'iframe[data-src]' ) ).forEach( function( el ) {
+ // Only removing the src doesn't actually unload the frame
+ // in all browsers (Firefox) so we set it to blank first
+ el.setAttribute( 'src', 'about:blank' );
+ el.removeAttribute( 'src' );
+ } );
+ }
+
+ }
+
+ /**
+ * Returns the number of past slides. This can be used as a global
+ * flattened index for slides.
+ *
+ * @return {number} Past slide count
+ */
+ function getSlidePastCount() {
+
+ var horizontalSlides = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) );
+
+ // The number of past slides
+ var pastCount = 0;
+
+ // Step through all slides and count the past ones
+ mainLoop: for( var i = 0; i < horizontalSlides.length; i++ ) {
+
+ var horizontalSlide = horizontalSlides[i];
+ var verticalSlides = toArray( horizontalSlide.querySelectorAll( 'section' ) );
+
+ for( var j = 0; j < verticalSlides.length; j++ ) {
+
+ // Stop as soon as we arrive at the present
+ if( verticalSlides[j].classList.contains( 'present' ) ) {
+ break mainLoop;
+ }
+
+ pastCount++;
+
+ }
+
+ // Stop as soon as we arrive at the present
+ if( horizontalSlide.classList.contains( 'present' ) ) {
+ break;
+ }
+
+ // Don't count the wrapping section for vertical slides
+ if( horizontalSlide.classList.contains( 'stack' ) === false ) {
+ pastCount++;
+ }
+
+ }
+
+ return pastCount;
+
+ }
+
+ /**
+ * Returns a value ranging from 0-1 that represents
+ * how far into the presentation we have navigated.
+ *
+ * @return {number}
+ */
+ function getProgress() {
+
+ // The number of past and total slides
+ var totalCount = getTotalSlides();
+ var pastCount = getSlidePastCount();
+
+ if( currentSlide ) {
+
+ var allFragments = currentSlide.querySelectorAll( '.fragment' );
+
+ // If there are fragments in the current slide those should be
+ // accounted for in the progress.
+ if( allFragments.length > 0 ) {
+ var visibleFragments = currentSlide.querySelectorAll( '.fragment.visible' );
+
+ // This value represents how big a portion of the slide progress
+ // that is made up by its fragments (0-1)
+ var fragmentWeight = 0.9;
+
+ // Add fragment progress to the past slide count
+ pastCount += ( visibleFragments.length / allFragments.length ) * fragmentWeight;
+ }
+
+ }
+
+ return pastCount / ( totalCount - 1 );
+
+ }
+
+ /**
+ * Checks if this presentation is running inside of the
+ * speaker notes window.
+ *
+ * @return {boolean}
+ */
+ function isSpeakerNotes() {
+
+ return !!window.location.search.match( /receiver/gi );
+
+ }
+
+ /**
+ * Reads the current URL (hash) and navigates accordingly.
+ */
+ function readURL() {
+
+ var hash = window.location.hash;
+
+ // Attempt to parse the hash as either an index or name
+ var bits = hash.slice( 2 ).split( '/' ),
+ name = hash.replace( /#|\//gi, '' );
+
+ // If the first bit is invalid and there is a name we can
+ // assume that this is a named link
+ if( isNaN( parseInt( bits[0], 10 ) ) && name.length ) {
+ var element;
+
+ // Ensure the named link is a valid HTML ID attribute
+ if( /^[a-zA-Z][\w:.-]*$/.test( name ) ) {
+ // Find the slide with the specified ID
+ element = document.getElementById( name );
+ }
+
+ if( element ) {
+ // Find the position of the named slide and navigate to it
+ var indices = Reveal.getIndices( element );
+ slide( indices.h, indices.v );
+ }
+ // If the slide doesn't exist, navigate to the current slide
+ else {
+ slide( indexh || 0, indexv || 0 );
+ }
+ }
+ else {
+ // Read the index components of the hash
+ var h = parseInt( bits[0], 10 ) || 0,
+ v = parseInt( bits[1], 10 ) || 0;
+
+ if( h !== indexh || v !== indexv ) {
+ slide( h, v );
+ }
+ }
+
+ }
+
+ /**
+ * Updates the page URL (hash) to reflect the current
+ * state.
+ *
+ * @param {number} delay The time in ms to wait before
+ * writing the hash
+ */
+ function writeURL( delay ) {
+
+ if( config.history ) {
+
+ // Make sure there's never more than one timeout running
+ clearTimeout( writeURLTimeout );
+
+ // If a delay is specified, timeout this call
+ if( typeof delay === 'number' ) {
+ writeURLTimeout = setTimeout( writeURL, delay );
+ }
+ else if( currentSlide ) {
+ var url = '/';
+
+ // Attempt to create a named link based on the slide's ID
+ var id = currentSlide.getAttribute( 'id' );
+ if( id ) {
+ id = id.replace( /[^a-zA-Z0-9\-\_\:\.]/g, '' );
+ }
+
+ // If the current slide has an ID, use that as a named link
+ if( typeof id === 'string' && id.length ) {
+ url = '/' + id;
+ }
+ // Otherwise use the /h/v index
+ else {
+ if( indexh > 0 || indexv > 0 ) url += indexh;
+ if( indexv > 0 ) url += '/' + indexv;
+ }
+
+ window.location.hash = url;
+ }
+ }
+
+ }
+ /**
+ * Retrieves the h/v location and fragment of the current,
+ * or specified, slide.
+ *
+ * @param {HTMLElement} [slide] If specified, the returned
+ * index will be for this slide rather than the currently
+ * active one
+ *
+ * @return {{h: number, v: number, f: number}}
+ */
+ function getIndices( slide ) {
+
+ // By default, return the current indices
+ var h = indexh,
+ v = indexv,
+ f;
+
+ // If a slide is specified, return the indices of that slide
+ if( slide ) {
+ var isVertical = isVerticalSlide( slide );
+ var slideh = isVertical ? slide.parentNode : slide;
+
+ // Select all horizontal slides
+ var horizontalSlides = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) );
+
+ // Now that we know which the horizontal slide is, get its index
+ h = Math.max( horizontalSlides.indexOf( slideh ), 0 );
+
+ // Assume we're not vertical
+ v = undefined;
+
+ // If this is a vertical slide, grab the vertical index
+ if( isVertical ) {
+ v = Math.max( toArray( slide.parentNode.querySelectorAll( 'section' ) ).indexOf( slide ), 0 );
+ }
+ }
+
+ if( !slide && currentSlide ) {
+ var hasFragments = currentSlide.querySelectorAll( '.fragment' ).length > 0;
+ if( hasFragments ) {
+ var currentFragment = currentSlide.querySelector( '.current-fragment' );
+ if( currentFragment && currentFragment.hasAttribute( 'data-fragment-index' ) ) {
+ f = parseInt( currentFragment.getAttribute( 'data-fragment-index' ), 10 );
+ }
+ else {
+ f = currentSlide.querySelectorAll( '.fragment.visible' ).length - 1;
+ }
+ }
+ }
+
+ return { h: h, v: v, f: f };
+
+ }
+
+ /**
+ * Retrieves all slides in this presentation.
+ */
+ function getSlides() {
+
+ return toArray( dom.wrapper.querySelectorAll( SLIDES_SELECTOR + ':not(.stack)' ));
+
+ }
+
+ /**
+ * Retrieves the total number of slides in this presentation.
+ *
+ * @return {number}
+ */
+ function getTotalSlides() {
+
+ return getSlides().length;
+
+ }
+
+ /**
+ * Returns the slide element matching the specified index.
+ *
+ * @return {HTMLElement}
+ */
+ function getSlide( x, y ) {
+
+ var horizontalSlide = dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR )[ x ];
+ var verticalSlides = horizontalSlide && horizontalSlide.querySelectorAll( 'section' );
+
+ if( verticalSlides && verticalSlides.length && typeof y === 'number' ) {
+ return verticalSlides ? verticalSlides[ y ] : undefined;
+ }
+
+ return horizontalSlide;
+
+ }
+
+ /**
+ * Returns the background element for the given slide.
+ * All slides, even the ones with no background properties
+ * defined, have a background element so as long as the
+ * index is valid an element will be returned.
+ *
+ * @param {number} x Horizontal background index
+ * @param {number} y Vertical background index
+ * @return {(HTMLElement[]|*)}
+ */
+ function getSlideBackground( x, y ) {
+
+ // When printing to PDF the slide backgrounds are nested
+ // inside of the slides
+ if( isPrintingPDF() ) {
+ var slide = getSlide( x, y );
+ if( slide ) {
+ return slide.slideBackgroundElement;
+ }
+
+ return undefined;
+ }
+
+ var horizontalBackground = dom.wrapper.querySelectorAll( '.backgrounds>.slide-background' )[ x ];
+ var verticalBackgrounds = horizontalBackground && horizontalBackground.querySelectorAll( '.slide-background' );
+
+ if( verticalBackgrounds && verticalBackgrounds.length && typeof y === 'number' ) {
+ return verticalBackgrounds ? verticalBackgrounds[ y ] : undefined;
+ }
+
+ return horizontalBackground;
+
+ }
+
+ /**
+ * Retrieves the speaker notes from a slide. Notes can be
+ * defined in two ways:
+ * 1. As a data-notes attribute on the slide <section>
+ * 2. As an <aside class="notes"> inside of the slide
+ *
+ * @param {HTMLElement} [slide=currentSlide]
+ * @return {(string|null)}
+ */
+ function getSlideNotes( slide ) {
+
+ // Default to the current slide
+ slide = slide || currentSlide;
+
+ // Notes can be specified via the data-notes attribute...
+ if( slide.hasAttribute( 'data-notes' ) ) {
+ return slide.getAttribute( 'data-notes' );
+ }
+
+ // ... or using an <aside class="notes"> element
+ var notesElement = slide.querySelector( 'aside.notes' );
+ if( notesElement ) {
+ return notesElement.innerHTML;
+ }
+
+ return null;
+
+ }
+
+ /**
+ * Retrieves the current state of the presentation as
+ * an object. This state can then be restored at any
+ * time.
+ *
+ * @return {{indexh: number, indexv: number, indexf: number, paused: boolean, overview: boolean}}
+ */
+ function getState() {
+
+ var indices = getIndices();
+
+ return {
+ indexh: indices.h,
+ indexv: indices.v,
+ indexf: indices.f,
+ paused: isPaused(),
+ overview: isOverview()
+ };
+
+ }
+
+ /**
+ * Restores the presentation to the given state.
+ *
+ * @param {object} state As generated by getState()
+ * @see {@link getState} generates the parameter `state`
+ */
+ function setState( state ) {
+
+ if( typeof state === 'object' ) {
+ slide( deserialize( state.indexh ), deserialize( state.indexv ), deserialize( state.indexf ) );
+
+ var pausedFlag = deserialize( state.paused ),
+ overviewFlag = deserialize( state.overview );
+
+ if( typeof pausedFlag === 'boolean' && pausedFlag !== isPaused() ) {
+ togglePause( pausedFlag );
+ }
+
+ if( typeof overviewFlag === 'boolean' && overviewFlag !== isOverview() ) {
+ toggleOverview( overviewFlag );
+ }
+ }
+
+ }
+
+ /**
+ * Return a sorted fragments list, ordered by an increasing
+ * "data-fragment-index" attribute.
+ *
+ * Fragments will be revealed in the order that they are returned by
+ * this function, so you can use the index attributes to control the
+ * order of fragment appearance.
+ *
+ * To maintain a sensible default fragment order, fragments are presumed
+ * to be passed in document order. This function adds a "fragment-index"
+ * attribute to each node if such an attribute is not already present,
+ * and sets that attribute to an integer value which is the position of
+ * the fragment within the fragments list.
+ *
+ * @param {object[]|*} fragments
+ * @return {object[]} sorted Sorted array of fragments
+ */
+ function sortFragments( fragments ) {
+
+ fragments = toArray( fragments );
+
+ var ordered = [],
+ unordered = [],
+ sorted = [];
+
+ // Group ordered and unordered elements
+ fragments.forEach( function( fragment, i ) {
+ if( fragment.hasAttribute( 'data-fragment-index' ) ) {
+ var index = parseInt( fragment.getAttribute( 'data-fragment-index' ), 10 );
+
+ if( !ordered[index] ) {
+ ordered[index] = [];
+ }
+
+ ordered[index].push( fragment );
+ }
+ else {
+ unordered.push( [ fragment ] );
+ }
+ } );
+
+ // Append fragments without explicit indices in their
+ // DOM order
+ ordered = ordered.concat( unordered );
+
+ // Manually count the index up per group to ensure there
+ // are no gaps
+ var index = 0;
+
+ // Push all fragments in their sorted order to an array,
+ // this flattens the groups
+ ordered.forEach( function( group ) {
+ group.forEach( function( fragment ) {
+ sorted.push( fragment );
+ fragment.setAttribute( 'data-fragment-index', index );
+ } );
+
+ index ++;
+ } );
+
+ return sorted;
+
+ }
+
+ /**
+ * Navigate to the specified slide fragment.
+ *
+ * @param {?number} index The index of the fragment that
+ * should be shown, -1 means all are invisible
+ * @param {number} offset Integer offset to apply to the
+ * fragment index
+ *
+ * @return {boolean} true if a change was made in any
+ * fragments visibility as part of this call
+ */
+ function navigateFragment( index, offset ) {
+
+ if( currentSlide && config.fragments ) {
+
+ var fragments = sortFragments( currentSlide.querySelectorAll( '.fragment' ) );
+ if( fragments.length ) {
+
+ // If no index is specified, find the current
+ if( typeof index !== 'number' ) {
+ var lastVisibleFragment = sortFragments( currentSlide.querySelectorAll( '.fragment.visible' ) ).pop();
+
+ if( lastVisibleFragment ) {
+ index = parseInt( lastVisibleFragment.getAttribute( 'data-fragment-index' ) || 0, 10 );
+ }
+ else {
+ index = -1;
+ }
+ }
+
+ // If an offset is specified, apply it to the index
+ if( typeof offset === 'number' ) {
+ index += offset;
+ }
+
+ var fragmentsShown = [],
+ fragmentsHidden = [];
+
+ toArray( fragments ).forEach( function( element, i ) {
+
+ if( element.hasAttribute( 'data-fragment-index' ) ) {
+ i = parseInt( element.getAttribute( 'data-fragment-index' ), 10 );
+ }
+
+ // Visible fragments
+ if( i <= index ) {
+ if( !element.classList.contains( 'visible' ) ) fragmentsShown.push( element );
+ element.classList.add( 'visible' );
+ element.classList.remove( 'current-fragment' );
+
+ // Announce the fragments one by one to the Screen Reader
+ dom.statusDiv.textContent = getStatusText( element );
+
+ if( i === index ) {
+ element.classList.add( 'current-fragment' );
+ startEmbeddedContent( element );
+ }
+ }
+ // Hidden fragments
+ else {
+ if( element.classList.contains( 'visible' ) ) fragmentsHidden.push( element );
+ element.classList.remove( 'visible' );
+ element.classList.remove( 'current-fragment' );
+ }
+
+ } );
+
+ if( fragmentsHidden.length ) {
+ dispatchEvent( 'fragmenthidden', { fragment: fragmentsHidden[0], fragments: fragmentsHidden } );
+ }
+
+ if( fragmentsShown.length ) {
+ dispatchEvent( 'fragmentshown', { fragment: fragmentsShown[0], fragments: fragmentsShown } );
+ }
+
+ updateControls();
+ updateProgress();
+
+ return !!( fragmentsShown.length || fragmentsHidden.length );
+
+ }
+
+ }
+
+ return false;
+
+ }
+
+ /**
+ * Navigate to the next slide fragment.
+ *
+ * @return {boolean} true if there was a next fragment,
+ * false otherwise
+ */
+ function nextFragment() {
+
+ return navigateFragment( null, 1 );
+
+ }
+
+ /**
+ * Navigate to the previous slide fragment.
+ *
+ * @return {boolean} true if there was a previous fragment,
+ * false otherwise
+ */
+ function previousFragment() {
+
+ return navigateFragment( null, -1 );
+
+ }
+
+ /**
+ * Cues a new automated slide if enabled in the config.
+ */
+ function cueAutoSlide() {
+
+ cancelAutoSlide();
+
+ if( currentSlide ) {
+
+ var fragment = currentSlide.querySelector( '.current-fragment' );
+
+ // When the slide first appears there is no "current" fragment so
+ // we look for a data-autoslide timing on the first fragment
+ if( !fragment ) fragment = currentSlide.querySelector( '.fragment' );
+
+ var fragmentAutoSlide = fragment ? fragment.getAttribute( 'data-autoslide' ) : null;
+ var parentAutoSlide = currentSlide.parentNode ? currentSlide.parentNode.getAttribute( 'data-autoslide' ) : null;
+ var slideAutoSlide = currentSlide.getAttribute( 'data-autoslide' );
+
+ // Pick value in the following priority order:
+ // 1. Current fragment's data-autoslide
+ // 2. Current slide's data-autoslide
+ // 3. Parent slide's data-autoslide
+ // 4. Global autoSlide setting
+ if( fragmentAutoSlide ) {
+ autoSlide = parseInt( fragmentAutoSlide, 10 );
+ }
+ else if( slideAutoSlide ) {
+ autoSlide = parseInt( slideAutoSlide, 10 );
+ }
+ else if( parentAutoSlide ) {
+ autoSlide = parseInt( parentAutoSlide, 10 );
+ }
+ else {
+ autoSlide = config.autoSlide;
+ }
+
+ // If there are media elements with data-autoplay,
+ // automatically set the autoSlide duration to the
+ // length of that media. Not applicable if the slide
+ // is divided up into fragments.
+ // playbackRate is accounted for in the duration.
+ if( currentSlide.querySelectorAll( '.fragment' ).length === 0 ) {
+ toArray( currentSlide.querySelectorAll( 'video, audio' ) ).forEach( function( el ) {
+ if( el.hasAttribute( 'data-autoplay' ) ) {
+ if( autoSlide && (el.duration * 1000 / el.playbackRate ) > autoSlide ) {
+ autoSlide = ( el.duration * 1000 / el.playbackRate ) + 1000;
+ }
+ }
+ } );
+ }
+
+ // Cue the next auto-slide if:
+ // - There is an autoSlide value
+ // - Auto-sliding isn't paused by the user
+ // - The presentation isn't paused
+ // - The overview isn't active
+ // - The presentation isn't over
+ if( autoSlide && !autoSlidePaused && !isPaused() && !isOverview() && ( !Reveal.isLastSlide() || availableFragments().next || config.loop === true ) ) {
+ autoSlideTimeout = setTimeout( function() {
+ typeof config.autoSlideMethod === 'function' ? config.autoSlideMethod() : navigateNext();
+ cueAutoSlide();
+ }, autoSlide );
+ autoSlideStartTime = Date.now();
+ }
+
+ if( autoSlidePlayer ) {
+ autoSlidePlayer.setPlaying( autoSlideTimeout !== -1 );
+ }
+
+ }
+
+ }
+
+ /**
+ * Cancels any ongoing request to auto-slide.
+ */
+ function cancelAutoSlide() {
+
+ clearTimeout( autoSlideTimeout );
+ autoSlideTimeout = -1;
+
+ }
+
+ function pauseAutoSlide() {
+
+ if( autoSlide && !autoSlidePaused ) {
+ autoSlidePaused = true;
+ dispatchEvent( 'autoslidepaused' );
+ clearTimeout( autoSlideTimeout );
+
+ if( autoSlidePlayer ) {
+ autoSlidePlayer.setPlaying( false );
+ }
+ }
+
+ }
+
+ function resumeAutoSlide() {
+
+ if( autoSlide && autoSlidePaused ) {
+ autoSlidePaused = false;
+ dispatchEvent( 'autoslideresumed' );
+ cueAutoSlide();
+ }
+
+ }
+
+ function navigateLeft() {
+
+ // Reverse for RTL
+ if( config.rtl ) {
+ if( ( isOverview() || nextFragment() === false ) && availableRoutes().left ) {
+ slide( indexh + 1 );
+ }
+ }
+ // Normal navigation
+ else if( ( isOverview() || previousFragment() === false ) && availableRoutes().left ) {
+ slide( indexh - 1 );
+ }
+
+ }
+
+ function navigateRight() {
+
+ // Reverse for RTL
+ if( config.rtl ) {
+ if( ( isOverview() || previousFragment() === false ) && availableRoutes().right ) {
+ slide( indexh - 1 );
+ }
+ }
+ // Normal navigation
+ else if( ( isOverview() || nextFragment() === false ) && availableRoutes().right ) {
+ slide( indexh + 1 );
+ }
+
+ }
+
+ function navigateUp() {
+
+ // Prioritize hiding fragments
+ if( ( isOverview() || previousFragment() === false ) && availableRoutes().up ) {
+ slide( indexh, indexv - 1 );
+ }
+
+ }
+
+ function navigateDown() {
+
+ // Prioritize revealing fragments
+ if( ( isOverview() || nextFragment() === false ) && availableRoutes().down ) {
+ slide( indexh, indexv + 1 );
+ }
+
+ }
+
+ /**
+ * Navigates backwards, prioritized in the following order:
+ * 1) Previous fragment
+ * 2) Previous vertical slide
+ * 3) Previous horizontal slide
+ */
+ function navigatePrev() {
+
+ // Prioritize revealing fragments
+ if( previousFragment() === false ) {
+ if( availableRoutes().up ) {
+ navigateUp();
+ }
+ else {
+ // Fetch the previous horizontal slide, if there is one
+ var previousSlide;
+
+ if( config.rtl ) {
+ previousSlide = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR + '.future' ) ).pop();
+ }
+ else {
+ previousSlide = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR + '.past' ) ).pop();
+ }
+
+ if( previousSlide ) {
+ var v = ( previousSlide.querySelectorAll( 'section' ).length - 1 ) || undefined;
+ var h = indexh - 1;
+ slide( h, v );
+ }
+ }
+ }
+
+ }
+
+ /**
+ * The reverse of #navigatePrev().
+ */
+ function navigateNext() {
+
+ // Prioritize revealing fragments
+ if( nextFragment() === false ) {
+ if( availableRoutes().down ) {
+ navigateDown();
+ }
+ else if( config.rtl ) {
+ navigateLeft();
+ }
+ else {
+ navigateRight();
+ }
+ }
+
+ }
+
+ /**
+ * Checks if the target element prevents the triggering of
+ * swipe navigation.
+ */
+ function isSwipePrevented( target ) {
+
+ while( target && typeof target.hasAttribute === 'function' ) {
+ if( target.hasAttribute( 'data-prevent-swipe' ) ) return true;
+ target = target.parentNode;
+ }
+
+ return false;
+
+ }
+
+
+ // --------------------------------------------------------------------//
+ // ----------------------------- EVENTS -------------------------------//
+ // --------------------------------------------------------------------//
+
+ /**
+ * Called by all event handlers that are based on user
+ * input.
+ *
+ * @param {object} [event]
+ */
+ function onUserInput( event ) {
+
+ if( config.autoSlideStoppable ) {
+ pauseAutoSlide();
+ }
+
+ }
+
+ /**
+ * Handler for the document level 'keypress' event.
+ *
+ * @param {object} event
+ */
+ function onDocumentKeyPress( event ) {
+
+ // Check if the pressed key is question mark
+ if( event.shiftKey && event.charCode === 63 ) {
+ toggleHelp();
+ }
+
+ }
+
+ /**
+ * Handler for the document level 'keydown' event.
+ *
+ * @param {object} event
+ */
+ function onDocumentKeyDown( event ) {
+
+ // If there's a condition specified and it returns false,
+ // ignore this event
+ if( typeof config.keyboardCondition === 'function' && config.keyboardCondition() === false ) {
+ return true;
+ }
+
+ // Remember if auto-sliding was paused so we can toggle it
+ var autoSlideWasPaused = autoSlidePaused;
+
+ onUserInput( event );
+
+ // Check if there's a focused element that could be using
+ // the keyboard
+ var activeElementIsCE = document.activeElement && document.activeElement.contentEditable !== 'inherit';
+ var activeElementIsInput = document.activeElement && document.activeElement.tagName && /input|textarea/i.test( document.activeElement.tagName );
+ var activeElementIsNotes = document.activeElement && document.activeElement.className && /speaker-notes/i.test( document.activeElement.className);
+
+ // Disregard the event if there's a focused element or a
+ // keyboard modifier key is present
+ if( activeElementIsCE || activeElementIsInput || activeElementIsNotes || (event.shiftKey && event.keyCode !== 32) || event.altKey || event.ctrlKey || event.metaKey ) return;
+
+ // While paused only allow resume keyboard events; 'b', 'v', '.'
+ var resumeKeyCodes = [66,86,190,191];
+ var key;
+
+ // Custom key bindings for togglePause should be able to resume
+ if( typeof config.keyboard === 'object' ) {
+ for( key in config.keyboard ) {
+ if( config.keyboard[key] === 'togglePause' ) {
+ resumeKeyCodes.push( parseInt( key, 10 ) );
+ }
+ }
+ }
+
+ if( isPaused() && resumeKeyCodes.indexOf( event.keyCode ) === -1 ) {
+ return false;
+ }
+
+ var triggered = false;
+
+ // 1. User defined key bindings
+ if( typeof config.keyboard === 'object' ) {
+
+ for( key in config.keyboard ) {
+
+ // Check if this binding matches the pressed key
+ if( parseInt( key, 10 ) === event.keyCode ) {
+
+ var value = config.keyboard[ key ];
+
+ // Callback function
+ if( typeof value === 'function' ) {
+ value.apply( null, [ event ] );
+ }
+ // String shortcuts to reveal.js API
+ else if( typeof value === 'string' && typeof Reveal[ value ] === 'function' ) {
+ Reveal[ value ].call();
+ }
+
+ triggered = true;
+
+ }
+
+ }
+
+ }
+
+ // 2. System defined key bindings
+ if( triggered === false ) {
+
+ // Assume true and try to prove false
+ triggered = true;
+
+ switch( event.keyCode ) {
+ // p, page up
+ case 80: case 33: navigatePrev(); break;
+ // n, page down
+ case 78: case 34: navigateNext(); break;
+ // h, left
+ case 72: case 37: navigateLeft(); break;
+ // l, right
+ case 76: case 39: navigateRight(); break;
+ // k, up
+ case 75: case 38: navigateUp(); break;
+ // j, down
+ case 74: case 40: navigateDown(); break;
+ // home
+ case 36: slide( 0 ); break;
+ // end
+ case 35: slide( Number.MAX_VALUE ); break;
+ // space
+ case 32: isOverview() ? deactivateOverview() : event.shiftKey ? navigatePrev() : navigateNext(); break;
+ // return
+ case 13: isOverview() ? deactivateOverview() : triggered = false; break;
+ // two-spot, semicolon, b, v, period, Logitech presenter tools "black screen" button
+ case 58: case 59: case 66: case 86: case 190: case 191: togglePause(); break;
+ // f
+ case 70: enterFullscreen(); break;
+ // a
+ case 65: if ( config.autoSlideStoppable ) toggleAutoSlide( autoSlideWasPaused ); break;
+ default:
+ triggered = false;
+ }
+
+ }
+
+ // If the input resulted in a triggered action we should prevent
+ // the browsers default behavior
+ if( triggered ) {
+ event.preventDefault && event.preventDefault();
+ }
+ // ESC or O key
+ else if ( ( event.keyCode === 27 || event.keyCode === 79 ) && features.transforms3d ) {
+ if( dom.overlay ) {
+ closeOverlay();
+ }
+ else {
+ toggleOverview();
+ }
+
+ event.preventDefault && event.preventDefault();
+ }
+
+ // If auto-sliding is enabled we need to cue up
+ // another timeout
+ cueAutoSlide();
+
+ }
+
+ /**
+ * Handler for the 'touchstart' event, enables support for
+ * swipe and pinch gestures.
+ *
+ * @param {object} event
+ */
+ function onTouchStart( event ) {
+
+ if( isSwipePrevented( event.target ) ) return true;
+
+ touch.startX = event.touches[0].clientX;
+ touch.startY = event.touches[0].clientY;
+ touch.startCount = event.touches.length;
+
+ // If there's two touches we need to memorize the distance
+ // between those two points to detect pinching
+ if( event.touches.length === 2 && config.overview ) {
+ touch.startSpan = distanceBetween( {
+ x: event.touches[1].clientX,
+ y: event.touches[1].clientY
+ }, {
+ x: touch.startX,
+ y: touch.startY
+ } );
+ }
+
+ }
+
+ /**
+ * Handler for the 'touchmove' event.
+ *
+ * @param {object} event
+ */
+ function onTouchMove( event ) {
+
+ if( isSwipePrevented( event.target ) ) return true;
+
+ // Each touch should only trigger one action
+ if( !touch.captured ) {
+ onUserInput( event );
+
+ var currentX = event.touches[0].clientX;
+ var currentY = event.touches[0].clientY;
+
+ // If the touch started with two points and still has
+ // two active touches; test for the pinch gesture
+ if( event.touches.length === 2 && touch.startCount === 2 && config.overview ) {
+
+ // The current distance in pixels between the two touch points
+ var currentSpan = distanceBetween( {
+ x: event.touches[1].clientX,
+ y: event.touches[1].clientY
+ }, {
+ x: touch.startX,
+ y: touch.startY
+ } );
+
+ // If the span is larger than the desire amount we've got
+ // ourselves a pinch
+ if( Math.abs( touch.startSpan - currentSpan ) > touch.threshold ) {
+ touch.captured = true;
+
+ if( currentSpan < touch.startSpan ) {
+ activateOverview();
+ }
+ else {
+ deactivateOverview();
+ }
+ }
+
+ event.preventDefault();
+
+ }
+ // There was only one touch point, look for a swipe
+ else if( event.touches.length === 1 && touch.startCount !== 2 ) {
+
+ var deltaX = currentX - touch.startX,
+ deltaY = currentY - touch.startY;
+
+ if( deltaX > touch.threshold && Math.abs( deltaX ) > Math.abs( deltaY ) ) {
+ touch.captured = true;
+ navigateLeft();
+ }
+ else if( deltaX < -touch.threshold && Math.abs( deltaX ) > Math.abs( deltaY ) ) {
+ touch.captured = true;
+ navigateRight();
+ }
+ else if( deltaY > touch.threshold ) {
+ touch.captured = true;
+ navigateUp();
+ }
+ else if( deltaY < -touch.threshold ) {
+ touch.captured = true;
+ navigateDown();
+ }
+
+ // If we're embedded, only block touch events if they have
+ // triggered an action
+ if( config.embedded ) {
+ if( touch.captured || isVerticalSlide( currentSlide ) ) {
+ event.preventDefault();
+ }
+ }
+ // Not embedded? Block them all to avoid needless tossing
+ // around of the viewport in iOS
+ else {
+ event.preventDefault();
+ }
+
+ }
+ }
+ // There's a bug with swiping on some Android devices unless
+ // the default action is always prevented
+ else if( UA.match( /android/gi ) ) {
+ event.preventDefault();
+ }
+
+ }
+
+ /**
+ * Handler for the 'touchend' event.
+ *
+ * @param {object} event
+ */
+ function onTouchEnd( event ) {
+
+ touch.captured = false;
+
+ }
+
+ /**
+ * Convert pointer down to touch start.
+ *
+ * @param {object} event
+ */
+ function onPointerDown( event ) {
+
+ if( event.pointerType === event.MSPOINTER_TYPE_TOUCH || event.pointerType === "touch" ) {
+ event.touches = [{ clientX: event.clientX, clientY: event.clientY }];
+ onTouchStart( event );
+ }
+
+ }
+
+ /**
+ * Convert pointer move to touch move.
+ *
+ * @param {object} event
+ */
+ function onPointerMove( event ) {
+
+ if( event.pointerType === event.MSPOINTER_TYPE_TOUCH || event.pointerType === "touch" ) {
+ event.touches = [{ clientX: event.clientX, clientY: event.clientY }];
+ onTouchMove( event );
+ }
+
+ }
+
+ /**
+ * Convert pointer up to touch end.
+ *
+ * @param {object} event
+ */
+ function onPointerUp( event ) {
+
+ if( event.pointerType === event.MSPOINTER_TYPE_TOUCH || event.pointerType === "touch" ) {
+ event.touches = [{ clientX: event.clientX, clientY: event.clientY }];
+ onTouchEnd( event );
+ }
+
+ }
+
+ /**
+ * Handles mouse wheel scrolling, throttled to avoid skipping
+ * multiple slides.
+ *
+ * @param {object} event
+ */
+ function onDocumentMouseScroll( event ) {
+
+ if( Date.now() - lastMouseWheelStep > 600 ) {
+
+ lastMouseWheelStep = Date.now();
+
+ var delta = event.detail || -event.wheelDelta;
+ if( delta > 0 ) {
+ navigateNext();
+ }
+ else if( delta < 0 ) {
+ navigatePrev();
+ }
+
+ }
+
+ }
+
+ /**
+ * Clicking on the progress bar results in a navigation to the
+ * closest approximate horizontal slide using this equation:
+ *
+ * ( clickX / presentationWidth ) * numberOfSlides
+ *
+ * @param {object} event
+ */
+ function onProgressClicked( event ) {
+
+ onUserInput( event );
+
+ event.preventDefault();
+
+ var slidesTotal = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ).length;
+ var slideIndex = Math.floor( ( event.clientX / dom.wrapper.offsetWidth ) * slidesTotal );
+
+ if( config.rtl ) {
+ slideIndex = slidesTotal - slideIndex;
+ }
+
+ slide( slideIndex );
+
+ }
+
+ /**
+ * Event handler for navigation control buttons.
+ */
+ function onNavigateLeftClicked( event ) { event.preventDefault(); onUserInput(); navigateLeft(); }
+ function onNavigateRightClicked( event ) { event.preventDefault(); onUserInput(); navigateRight(); }
+ function onNavigateUpClicked( event ) { event.preventDefault(); onUserInput(); navigateUp(); }
+ function onNavigateDownClicked( event ) { event.preventDefault(); onUserInput(); navigateDown(); }
+ function onNavigatePrevClicked( event ) { event.preventDefault(); onUserInput(); navigatePrev(); }
+ function onNavigateNextClicked( event ) { event.preventDefault(); onUserInput(); navigateNext(); }
+
+ /**
+ * Handler for the window level 'hashchange' event.
+ *
+ * @param {object} [event]
+ */
+ function onWindowHashChange( event ) {
+
+ readURL();
+
+ }
+
+ /**
+ * Handler for the window level 'resize' event.
+ *
+ * @param {object} [event]
+ */
+ function onWindowResize( event ) {
+
+ layout();
+
+ }
+
+ /**
+ * Handle for the window level 'visibilitychange' event.
+ *
+ * @param {object} [event]
+ */
+ function onPageVisibilityChange( event ) {
+
+ var isHidden = document.webkitHidden ||
+ document.msHidden ||
+ document.hidden;
+
+ // If, after clicking a link or similar and we're coming back,
+ // focus the document.body to ensure we can use keyboard shortcuts
+ if( isHidden === false && document.activeElement !== document.body ) {
+ // Not all elements support .blur() - SVGs among them.
+ if( typeof document.activeElement.blur === 'function' ) {
+ document.activeElement.blur();
+ }
+ document.body.focus();
+ }
+
+ }
+
+ /**
+ * Invoked when a slide is and we're in the overview.
+ *
+ * @param {object} event
+ */
+ function onOverviewSlideClicked( event ) {
+
+ // TODO There's a bug here where the event listeners are not
+ // removed after deactivating the overview.
+ if( eventsAreBound && isOverview() ) {
+ event.preventDefault();
+
+ var element = event.target;
+
+ while( element && !element.nodeName.match( /section/gi ) ) {
+ element = element.parentNode;
+ }
+
+ if( element && !element.classList.contains( 'disabled' ) ) {
+
+ deactivateOverview();
+
+ if( element.nodeName.match( /section/gi ) ) {
+ var h = parseInt( element.getAttribute( 'data-index-h' ), 10 ),
+ v = parseInt( element.getAttribute( 'data-index-v' ), 10 );
+
+ slide( h, v );
+ }
+
+ }
+ }
+
+ }
+
+ /**
+ * Handles clicks on links that are set to preview in the
+ * iframe overlay.
+ *
+ * @param {object} event
+ */
+ function onPreviewLinkClicked( event ) {
+
+ if( event.currentTarget && event.currentTarget.hasAttribute( 'href' ) ) {
+ var url = event.currentTarget.getAttribute( 'href' );
+ if( url ) {
+ showPreview( url );
+ event.preventDefault();
+ }
+ }
+
+ }
+
+ /**
+ * Handles click on the auto-sliding controls element.
+ *
+ * @param {object} [event]
+ */
+ function onAutoSlidePlayerClick( event ) {
+
+ // Replay
+ if( Reveal.isLastSlide() && config.loop === false ) {
+ slide( 0, 0 );
+ resumeAutoSlide();
+ }
+ // Resume
+ else if( autoSlidePaused ) {
+ resumeAutoSlide();
+ }
+ // Pause
+ else {
+ pauseAutoSlide();
+ }
+
+ }
+
+
+ // --------------------------------------------------------------------//
+ // ------------------------ PLAYBACK COMPONENT ------------------------//
+ // --------------------------------------------------------------------//
+
+
+ /**
+ * Constructor for the playback component, which displays
+ * play/pause/progress controls.
+ *
+ * @param {HTMLElement} container The component will append
+ * itself to this
+ * @param {function} progressCheck A method which will be
+ * called frequently to get the current progress on a range
+ * of 0-1
+ */
+ function Playback( container, progressCheck ) {
+
+ // Cosmetics
+ this.diameter = 100;
+ this.diameter2 = this.diameter/2;
+ this.thickness = 6;
+
+ // Flags if we are currently playing
+ this.playing = false;
+
+ // Current progress on a 0-1 range
+ this.progress = 0;
+
+ // Used to loop the animation smoothly
+ this.progressOffset = 1;
+
+ this.container = container;
+ this.progressCheck = progressCheck;
+
+ this.canvas = document.createElement( 'canvas' );
+ this.canvas.className = 'playback';
+ this.canvas.width = this.diameter;
+ this.canvas.height = this.diameter;
+ this.canvas.style.width = this.diameter2 + 'px';
+ this.canvas.style.height = this.diameter2 + 'px';
+ this.context = this.canvas.getContext( '2d' );
+
+ this.container.appendChild( this.canvas );
+
+ this.render();
+
+ }
+
+ /**
+ * @param value
+ */
+ Playback.prototype.setPlaying = function( value ) {
+
+ var wasPlaying = this.playing;
+
+ this.playing = value;
+
+ // Start repainting if we weren't already
+ if( !wasPlaying && this.playing ) {
+ this.animate();
+ }
+ else {
+ this.render();
+ }
+
+ };
+
+ Playback.prototype.animate = function() {
+
+ var progressBefore = this.progress;
+
+ this.progress = this.progressCheck();
+
+ // When we loop, offset the progress so that it eases
+ // smoothly rather than immediately resetting
+ if( progressBefore > 0.8 && this.progress < 0.2 ) {
+ this.progressOffset = this.progress;
+ }
+
+ this.render();
+
+ if( this.playing ) {
+ features.requestAnimationFrameMethod.call( window, this.animate.bind( this ) );
+ }
+
+ };
+
+ /**
+ * Renders the current progress and playback state.
+ */
+ Playback.prototype.render = function() {
+
+ var progress = this.playing ? this.progress : 0,
+ radius = ( this.diameter2 ) - this.thickness,
+ x = this.diameter2,
+ y = this.diameter2,
+ iconSize = 28;
+
+ // Ease towards 1
+ this.progressOffset += ( 1 - this.progressOffset ) * 0.1;
+
+ var endAngle = ( - Math.PI / 2 ) + ( progress * ( Math.PI * 2 ) );
+ var startAngle = ( - Math.PI / 2 ) + ( this.progressOffset * ( Math.PI * 2 ) );
+
+ this.context.save();
+ this.context.clearRect( 0, 0, this.diameter, this.diameter );
+
+ // Solid background color
+ this.context.beginPath();
+ this.context.arc( x, y, radius + 4, 0, Math.PI * 2, false );
+ this.context.fillStyle = 'rgba( 0, 0, 0, 0.4 )';
+ this.context.fill();
+
+ // Draw progress track
+ this.context.beginPath();
+ this.context.arc( x, y, radius, 0, Math.PI * 2, false );
+ this.context.lineWidth = this.thickness;
+ this.context.strokeStyle = '#666';
+ this.context.stroke();
+
+ if( this.playing ) {
+ // Draw progress on top of track
+ this.context.beginPath();
+ this.context.arc( x, y, radius, startAngle, endAngle, false );
+ this.context.lineWidth = this.thickness;
+ this.context.strokeStyle = '#fff';
+ this.context.stroke();
+ }
+
+ this.context.translate( x - ( iconSize / 2 ), y - ( iconSize / 2 ) );
+
+ // Draw play/pause icons
+ if( this.playing ) {
+ this.context.fillStyle = '#fff';
+ this.context.fillRect( 0, 0, iconSize / 2 - 4, iconSize );
+ this.context.fillRect( iconSize / 2 + 4, 0, iconSize / 2 - 4, iconSize );
+ }
+ else {
+ this.context.beginPath();
+ this.context.translate( 4, 0 );
+ this.context.moveTo( 0, 0 );
+ this.context.lineTo( iconSize - 4, iconSize / 2 );
+ this.context.lineTo( 0, iconSize );
+ this.context.fillStyle = '#fff';
+ this.context.fill();
+ }
+
+ this.context.restore();
+
+ };
+
+ Playback.prototype.on = function( type, listener ) {
+ this.canvas.addEventListener( type, listener, false );
+ };
+
+ Playback.prototype.off = function( type, listener ) {
+ this.canvas.removeEventListener( type, listener, false );
+ };
+
+ Playback.prototype.destroy = function() {
+
+ this.playing = false;
+
+ if( this.canvas.parentNode ) {
+ this.container.removeChild( this.canvas );
+ }
+
+ };
+
+
+ // --------------------------------------------------------------------//
+ // ------------------------------- API --------------------------------//
+ // --------------------------------------------------------------------//
+
+
+ Reveal = {
+ VERSION: VERSION,
+
+ initialize: initialize,
+ reinitialize: reinitialize,
+ configure: configure,
+ sync: sync,
+
+ // Navigation methods
+ slide: slide,
+ left: navigateLeft,
+ right: navigateRight,
+ up: navigateUp,
+ down: navigateDown,
+ prev: navigatePrev,
+ next: navigateNext,
+
+ // Fragment methods
+ navigateFragment: navigateFragment,
+ prevFragment: previousFragment,
+ nextFragment: nextFragment,
+
+ // Deprecated aliases
+ navigateTo: slide,
+ navigateLeft: navigateLeft,
+ navigateRight: navigateRight,
+ navigateUp: navigateUp,
+ navigateDown: navigateDown,
+ navigatePrev: navigatePrev,
+ navigateNext: navigateNext,
+
+ // Forces an update in slide layout
+ layout: layout,
+
+ // Randomizes the order of slides
+ shuffle: shuffle,
+
+ // Returns an object with the available routes as booleans (left/right/top/bottom)
+ availableRoutes: availableRoutes,
+
+ // Returns an object with the available fragments as booleans (prev/next)
+ availableFragments: availableFragments,
+
+ // Toggles a help overlay with keyboard shortcuts
+ toggleHelp: toggleHelp,
+
+ // Toggles the overview mode on/off
+ toggleOverview: toggleOverview,
+
+ // Toggles the "black screen" mode on/off
+ togglePause: togglePause,
+
+ // Toggles the auto slide mode on/off
+ toggleAutoSlide: toggleAutoSlide,
+
+ // State checks
+ isOverview: isOverview,
+ isPaused: isPaused,
+ isAutoSliding: isAutoSliding,
+
+ // Adds or removes all internal event listeners (such as keyboard)
+ addEventListeners: addEventListeners,
+ removeEventListeners: removeEventListeners,
+
+ // Facility for persisting and restoring the presentation state
+ getState: getState,
+ setState: setState,
+
+ // Presentation progress
+ getSlidePastCount: getSlidePastCount,
+
+ // Presentation progress on range of 0-1
+ getProgress: getProgress,
+
+ // Returns the indices of the current, or specified, slide
+ getIndices: getIndices,
+
+ // Returns an Array of all slides
+ getSlides: getSlides,
+
+ // Returns the total number of slides
+ getTotalSlides: getTotalSlides,
+
+ // Returns the slide element at the specified index
+ getSlide: getSlide,
+
+ // Returns the slide background element at the specified index
+ getSlideBackground: getSlideBackground,
+
+ // Returns the speaker notes string for a slide, or null
+ getSlideNotes: getSlideNotes,
+
+ // Returns the previous slide element, may be null
+ getPreviousSlide: function() {
+ return previousSlide;
+ },
+
+ // Returns the current slide element
+ getCurrentSlide: function() {
+ return currentSlide;
+ },
+
+ // Returns the current scale of the presentation content
+ getScale: function() {
+ return scale;
+ },
+
+ // Returns the current configuration object
+ getConfig: function() {
+ return config;
+ },
+
+ // Helper method, retrieves query string as a key/value hash
+ getQueryHash: function() {
+ var query = {};
+
+ location.search.replace( /[A-Z0-9]+?=([\w\.%-]*)/gi, function(a) {
+ query[ a.split( '=' ).shift() ] = a.split( '=' ).pop();
+ } );
+
+ // Basic deserialization
+ for( var i in query ) {
+ var value = query[ i ];
+
+ query[ i ] = deserialize( unescape( value ) );
+ }
+
+ return query;
+ },
+
+ // Returns true if we're currently on the first slide
+ isFirstSlide: function() {
+ return ( indexh === 0 && indexv === 0 );
+ },
+
+ // Returns true if we're currently on the last slide
+ isLastSlide: function() {
+ if( currentSlide ) {
+ // Does this slide has next a sibling?
+ if( currentSlide.nextElementSibling ) return false;
+
+ // If it's vertical, does its parent have a next sibling?
+ if( isVerticalSlide( currentSlide ) && currentSlide.parentNode.nextElementSibling ) return false;
+
+ return true;
+ }
+
+ return false;
+ },
+
+ // Checks if reveal.js has been loaded and is ready for use
+ isReady: function() {
+ return loaded;
+ },
+
+ // Forward event binding to the reveal DOM element
+ addEventListener: function( type, listener, useCapture ) {
+ if( 'addEventListener' in window ) {
+ ( dom.wrapper || document.querySelector( '.reveal' ) ).addEventListener( type, listener, useCapture );
+ }
+ },
+ removeEventListener: function( type, listener, useCapture ) {
+ if( 'addEventListener' in window ) {
+ ( dom.wrapper || document.querySelector( '.reveal' ) ).removeEventListener( type, listener, useCapture );
+ }
+ },
+
+ // Programatically triggers a keyboard event
+ triggerKey: function( keyCode ) {
+ onDocumentKeyDown( { keyCode: keyCode } );
+ },
+
+ // Registers a new shortcut to include in the help overlay
+ registerKeyboardShortcut: function( key, value ) {
+ keyboardShortcuts[key] = value;
+ }
+ };
+
+ return Reveal;
+
+}));
=== added file 'openlp/core/display/html/textFit.js'
--- openlp/core/display/html/textFit.js 1970-01-01 00:00:00 +0000
+++ openlp/core/display/html/textFit.js 2019-02-06 07:01:54 +0000
@@ -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);
+ }
+}));
=== added file 'openlp/core/display/render.py'
--- openlp/core/display/render.py 1970-01-01 00:00:00 +0000
+++ openlp/core/display/render.py 2019-02-06 07:01:54 +0000
@@ -0,0 +1,738 @@
+# -*- 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.lib.formattingtags import FormattingTags
+from openlp.core.common import ThemeLevel
+from openlp.core.common.registry import Registry, RegistryBase
+from openlp.core.common.mixins import LogMixin, RegistryProperties
+from openlp.core.display.screens import ScreenList
+from openlp.core.display.window import DisplayWindow
+from openlp.core.lib import ItemCapabilities
+
+
+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 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
=== removed file 'openlp/core/display/renderer.py'
--- openlp/core/display/renderer.py 2018-08-25 14:08:19 +0000
+++ openlp/core/display/renderer.py 1970-01-01 00:00:00 +0000
@@ -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)
=== modified file 'openlp/core/display/screens.py'
--- openlp/core/display/screens.py 2017-12-29 09:15:48 +0000
+++ openlp/core/display/screens.py 2019-02-06 07:01:54 +0000
@@ -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 @@
"""
log.info('Screen loaded')
__instance__ = None
+ screens = []
def __new__(cls):
"""
@@ -52,6 +160,50 @@
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,203 +213,153 @@
"""
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):
- """
- Called when the resolution of a screen has changed.
-
- ``number``
- 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()
- Registry().execute('config_screen_changed')
- break
-
- def screen_count_changed(self, changed_screen=-1):
- """
- 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')
-
- def get_screen_list(self):
- """
- Returns a list with the screens. This should only be used to display
- available screens to the user::
+ 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.screen_list:
+ for screen in self.screens:
screen_name = '{name} {number:d}'.format(name=translate('OpenLP.ScreenList', 'Screen'),
- number=screen['number'] + 1)
- if screen['primary']:
+ 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 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):
+ 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.screen_list:
- if screen['number'] == number:
+ for screen in self.screens:
+ 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()
+ 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.
+
+ ``number``
+ The number of the screen, which size has changed.
+ """
+ log.info('screen_resolution_changed {number:d}'.format(number=number))
+ 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 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.
+ """
+ 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 on_primary_screen_changed(self):
+ """
+ The primary screen has changed, let's sort it out and then notify everyone
+ """
+ for screen in self.screens:
+ screen.is_primary = self.desktop.primaryScreen() == screen.number
+ Registry().execute('config_screen_changed')
=== added file 'openlp/core/display/webengine.py'
--- openlp/core/display/webengine.py 1970-01-01 00:00:00 +0000
+++ openlp/core/display/webengine.py 2019-02-06 07:01:54 +0000
@@ -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)
=== added file 'openlp/core/display/window.py'
--- openlp/core/display/window.py 1970-01-01 00:00:00 +0000
+++ openlp/core/display/window.py 2019-02-06 07:01:54 +0000
@@ -0,0 +1,404 @@
+# -*- 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:
+ :param Path image_path:
+ :return:
+ """
+ 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))
+
+ 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))
+
+ 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))
=== modified file 'openlp/core/lib/__init__.py'
--- openlp/core/lib/__init__.py 2018-10-24 19:35:22 +0000
+++ openlp/core/lib/__init__.py 2019-02-06 07:01:54 +0000
@@ -23,9 +23,7 @@
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
@@ -36,8 +34,6 @@
log = logging.getLogger(__name__ + '.__init__')
-SLIMCHARS = 'fiíIÍjlĺľrtť.,;/ ()|"\'!:\\'
-
class ServiceItemContext(object):
"""
@@ -84,6 +80,103 @@
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 +381,6 @@
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.
=== modified file 'openlp/core/lib/db.py'
--- openlp/core/lib/db.py 2018-10-27 01:40:20 +0000
+++ openlp/core/lib/db.py 2019-02-06 07:01:54 +0000
@@ -31,10 +31,10 @@
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.settings import Settings
from openlp.core.lib.ui import critical_error_message_box
+
log = logging.getLogger(__name__)
=== modified file 'openlp/core/lib/htmlbuilder.py'
--- openlp/core/lib/htmlbuilder.py 2018-07-04 20:42:55 +0000
+++ openlp/core/lib/htmlbuilder.py 2019-02-06 07:01:54 +0000
@@ -409,10 +409,11 @@
import logging
from string import Template
-from PyQt5 import QtWebKit
+from PyQt5 import QtWebEngine
from openlp.core.common.settings import Settings
-from openlp.core.lib.theme import BackgroundType, BackgroundGradientType, VerticalType, HorizontalType
+from openlp.core.lib.theme import BackgroundGradientType, BackgroundType, HorizontalType, VerticalType
+
log = logging.getLogger(__name__)
@@ -688,11 +689,12 @@
"""
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
+ #try:
+ # webkit_ver = float(QtWebKit.qWebKitVersion())
+ # log.debug('Webkit version = {version}'.format(version=webkit_ver))
+ #except AttributeError:
+ # webkit_ver = 0.0
+ webkit_ver = 0.0
return webkit_ver
=== modified file 'openlp/core/lib/imagemanager.py'
--- openlp/core/lib/imagemanager.py 2018-01-04 06:10:20 +0000
+++ openlp/core/lib/imagemanager.py 2019-02-06 07:01:54 +0000
@@ -34,9 +34,10 @@
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 @@
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 @@
"""
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)
=== modified file 'openlp/core/lib/mediamanageritem.py'
--- openlp/core/lib/mediamanageritem.py 2018-11-04 17:13:56 +0000
+++ openlp/core/lib/mediamanageritem.py 2019-02-06 07:01:54 +0000
@@ -28,7 +28,6 @@
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.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 @@
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 @@
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 @@
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.
=== modified file 'openlp/core/lib/plugin.py'
--- openlp/core/lib/plugin.py 2018-10-23 16:43:52 +0000
+++ openlp/core/lib/plugin.py 2019-02-06 07:01:54 +0000
@@ -30,6 +30,7 @@
from openlp.core.common.settings import Settings
from openlp.core.version import get_version
+
log = logging.getLogger(__name__)
=== modified file 'openlp/core/lib/pluginmanager.py'
--- openlp/core/lib/pluginmanager.py 2019-01-04 20:50:40 +0000
+++ openlp/core/lib/pluginmanager.py 2019-02-06 07:01:54 +0000
@@ -156,6 +156,7 @@
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 @@
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'
=== modified file 'openlp/core/lib/serviceitem.py'
--- openlp/core/lib/serviceitem.py 2018-11-18 17:29:47 +0000
+++ openlp/core/lib/serviceitem.py 2019-02-06 07:01:54 +0000
@@ -24,11 +24,11 @@
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.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 @@
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 @@
"""
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 @@
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 @@
else:
self.icon = UiIcons().clone
- def render(self, provides_own_theme_data=False):
- """
- 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.
+ def _create_slides(self):
+ """
+ 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 @@
: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 @@
# 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 @@
}
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 @@
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 @@
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 @@
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):
"""
@@ -578,9 +491,9 @@
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 +502,19 @@
: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 +524,7 @@
"""
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 +541,8 @@
"""
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):
"""
@@ -677,29 +591,29 @@
"""
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
=== modified file 'openlp/core/lib/settingstab.py'
--- openlp/core/lib/settingstab.py 2017-12-29 09:15:48 +0000
+++ openlp/core/lib/settingstab.py 2019-02-06 07:01:54 +0000
@@ -54,12 +54,12 @@
"""
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 @@
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.
"""
=== modified file 'openlp/core/lib/theme.py'
--- openlp/core/lib/theme.py 2017-12-29 09:15:48 +0000
+++ openlp/core/lib/theme.py 2019-02-06 07:01:54 +0000
@@ -30,8 +30,10 @@
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.common.path import Path, str_to_path
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__)
@@ -197,13 +199,13 @@
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):
"""
=== modified file 'openlp/core/lib/ui.py'
--- openlp/core/lib/ui.py 2018-04-13 20:07:09 +0000
+++ openlp/core/lib/ui.py 2019-02-06 07:01:54 +0000
@@ -33,6 +33,7 @@
from openlp.core.lib import build_icon
from openlp.core.ui.icons import UiIcons
+
log = logging.getLogger(__name__)
=== modified file 'openlp/core/loader.py'
--- openlp/core/loader.py 2018-11-01 20:51:42 +0000
+++ openlp/core/loader.py 2019-02-06 07:01:54 +0000
@@ -26,7 +26,7 @@
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
=== modified file 'openlp/core/projectors/constants.py'
--- openlp/core/projectors/constants.py 2018-04-20 06:04:43 +0000
+++ openlp/core/projectors/constants.py 2019-02-06 07:01:54 +0000
@@ -26,6 +26,7 @@
from openlp.core.common.i18n import translate
+
log = logging.getLogger(__name__)
log.debug('projector_constants loaded')
=== modified file 'openlp/core/projectors/db.py'
--- openlp/core/projectors/db.py 2018-01-13 07:17:15 +0000
+++ openlp/core/projectors/db.py 2019-02-06 07:01:54 +0000
@@ -35,16 +35,20 @@
"""
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 import upgrade
from openlp.core.projectors.constants import PJLINK_DEFAULT_CODES
-from openlp.core.projectors import upgrade
+
+
+log = logging.getLogger(__name__)
+log.debug('projector.lib.db module loaded')
+
+
Base = declarative_base(MetaData())
=== modified file 'openlp/core/projectors/editform.py'
--- openlp/core/projectors/editform.py 2018-10-20 04:33:32 +0000
+++ openlp/core/projectors/editform.py 2019-02-06 07:01:54 +0000
@@ -29,9 +29,10 @@
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 @@
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 @@
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 @@
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 @@
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
=== modified file 'openlp/core/projectors/manager.py'
--- openlp/core/projectors/manager.py 2018-10-27 01:40:20 +0000
+++ openlp/core/projectors/manager.py 2019-02-06 07:01:54 +0000
@@ -30,22 +30,23 @@
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')
=== modified file 'openlp/core/projectors/pjlink.py'
--- openlp/core/projectors/pjlink.py 2018-10-20 04:33:32 +0000
+++ openlp/core/projectors/pjlink.py 2019-02-06 07:01:54 +0000
@@ -55,11 +55,12 @@
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')
=== modified file 'openlp/core/projectors/sourceselectform.py'
--- openlp/core/projectors/sourceselectform.py 2017-12-29 09:15:48 +0000
+++ openlp/core/projectors/sourceselectform.py 2019-02-06 07:01:54 +0000
@@ -31,9 +31,10 @@
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__)
=== modified file 'openlp/core/projectors/tab.py'
--- openlp/core/projectors/tab.py 2018-10-20 04:33:32 +0000
+++ openlp/core/projectors/tab.py 2019-02-06 07:01:54 +0000
@@ -30,8 +30,9 @@
from openlp.core.common.registry import Registry
from openlp.core.common.settings import Settings
from openlp.core.lib.settingstab import SettingsTab
+from openlp.core.projectors import DialogSourceStyle
from openlp.core.ui.icons import UiIcons
-from openlp.core.projectors import DialogSourceStyle
+
log = logging.getLogger(__name__)
log.debug('projectortab module loaded')
@@ -54,12 +55,12 @@
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 @@
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
"""
=== modified file 'openlp/core/projectors/upgrade.py'
--- openlp/core/projectors/upgrade.py 2018-01-13 07:17:15 +0000
+++ openlp/core/projectors/upgrade.py 2019-02-06 07:01:54 +0000
@@ -25,11 +25,12 @@
"""
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
=== modified file 'openlp/core/resources.py'
--- openlp/core/resources.py 2018-08-04 21:25:14 +0000
+++ openlp/core/resources.py 2019-02-06 07:01:54 +0000
@@ -25,6 +25,7 @@
"""
from PyQt5 import QtCore
+
qt_resource_data = b"\
\x00\x00\x03\x2f\
\x89\
@@ -61462,4 +61463,3 @@
Cleanup OpenLP resources when the application shuts down.
"""
QtCore.qUnregisterResourceData(rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data)
-
=== modified file 'openlp/core/server.py'
--- openlp/core/server.py 2018-04-08 17:24:31 +0000
+++ openlp/core/server.py 2019-02-06 07:01:54 +0000
@@ -21,8 +21,8 @@
###############################################################################
from PyQt5 import QtCore, QtNetwork
+from openlp.core.common.mixins import LogMixin
from openlp.core.common.registry import Registry
-from openlp.core.common.mixins import LogMixin
class Server(QtCore.QObject, LogMixin):
=== modified file 'openlp/core/ui/aboutform.py'
--- openlp/core/ui/aboutform.py 2017-12-29 09:15:48 +0000
+++ openlp/core/ui/aboutform.py 2019-02-06 07:01:54 +0000
@@ -28,6 +28,7 @@
from openlp.core.common.i18n import translate
from openlp.core.version import get_version
+
from .aboutdialog import UiAboutDialog
=== modified file 'openlp/core/ui/advancedtab.py'
--- openlp/core/ui/advancedtab.py 2018-08-25 14:08:19 +0000
+++ openlp/core/ui/advancedtab.py 2019-02-06 07:01:54 +0000
@@ -32,12 +32,13 @@
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.icons import UiIcons
from openlp.core.ui.style import HAS_DARK_STYLE
-from openlp.core.ui.icons import UiIcons
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 @@
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 @@
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.
"""
=== modified file 'openlp/core/ui/exceptiondialog.py'
--- openlp/core/ui/exceptiondialog.py 2018-10-27 01:40:20 +0000
+++ openlp/core/ui/exceptiondialog.py 2019-02-06 07:01:54 +0000
@@ -26,15 +26,15 @@
from PyQt5 import QtWidgets
from openlp.core.common.i18n import translate
+from openlp.core.lib.ui import create_button, create_button_box
from openlp.core.ui.icons import UiIcons
-from openlp.core.lib.ui import create_button, create_button_box
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 @@
[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.
"""
=== modified file 'openlp/core/ui/exceptionform.py'
--- openlp/core/ui/exceptionform.py 2018-10-27 10:50:15 +0000
+++ openlp/core/ui/exceptionform.py 2019-02-06 07:01:54 +0000
@@ -27,48 +27,15 @@
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 @@
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 @@
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 @@
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.
=== modified file 'openlp/core/ui/filerenamedialog.py'
--- openlp/core/ui/filerenamedialog.py 2018-04-21 19:57:51 +0000
+++ openlp/core/ui/filerenamedialog.py 2019-02-06 07:01:54 +0000
@@ -33,7 +33,7 @@
"""
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 @@
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.
"""
=== modified file 'openlp/core/ui/filerenameform.py'
--- openlp/core/ui/filerenameform.py 2017-12-29 09:15:48 +0000
+++ openlp/core/ui/filerenameform.py 2019-02-06 07:01:54 +0000
@@ -46,7 +46,7 @@
"""
Set up the class. This method is mocked out by the tests.
"""
- self.setupUi(self)
+ self.setup_ui(self)
def exec(self, copy=False):
"""
=== modified file 'openlp/core/ui/firsttimeform.py'
--- openlp/core/ui/firsttimeform.py 2018-08-25 14:08:19 +0000
+++ openlp/core/ui/firsttimeform.py 2019-02-06 07:01:54 +0000
@@ -34,7 +34,7 @@
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.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__)
@@ -285,7 +286,7 @@
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.addItems(self.screens.get_display_screen_list())
self.display_combo_box.setCurrentIndex(self.display_combo_box.count() - 1)
def on_current_id_changed(self, page_id):
@@ -537,8 +538,9 @@
'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())
+ # No longer need to set this setting directly, the ScreenList object will do it
+ # Settings().setValue('core/monitor', self.display_combo_box.currentIndex())
+ self.screens.set_display_screen(self.display_combo_box.currentIndex(), can_save=True)
# Set Global Theme
if self.theme_combo_box.currentIndex() != -1:
Settings().setValue('themes/global theme', self.theme_combo_box.currentText())
=== modified file 'openlp/core/ui/firsttimelanguagedialog.py'
--- openlp/core/ui/firsttimelanguagedialog.py 2018-04-21 19:57:51 +0000
+++ openlp/core/ui/firsttimelanguagedialog.py 2019-02-06 07:01:54 +0000
@@ -33,7 +33,7 @@
"""
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 @@
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.
"""
=== modified file 'openlp/core/ui/firsttimelanguageform.py'
--- openlp/core/ui/firsttimelanguageform.py 2017-12-29 09:15:48 +0000
+++ openlp/core/ui/firsttimelanguageform.py 2019-02-06 07:01:54 +0000
@@ -26,6 +26,7 @@
from openlp.core.common.i18n import LanguageManager
from openlp.core.lib.ui import create_action
+
from .firsttimelanguagedialog import Ui_FirstTimeLanguageDialog
@@ -39,7 +40,7 @@
"""
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()))
=== modified file 'openlp/core/ui/firsttimewizard.py'
--- openlp/core/ui/firsttimewizard.py 2018-04-21 19:57:51 +0000
+++ openlp/core/ui/firsttimewizard.py 2019-02-06 07:01:54 +0000
@@ -24,7 +24,7 @@
"""
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
=== modified file 'openlp/core/ui/formattingtagdialog.py'
--- openlp/core/ui/formattingtagdialog.py 2018-04-21 19:57:51 +0000
+++ openlp/core/ui/formattingtagdialog.py 2019-02-06 07:01:54 +0000
@@ -25,15 +25,15 @@
from PyQt5 import QtCore, QtWidgets
from openlp.core.common.i18n import UiStrings, translate
+from openlp.core.lib.ui import create_button_box
from openlp.core.ui.icons import UiIcons
-from openlp.core.lib.ui import create_button_box
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 @@
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
"""
=== modified file 'openlp/core/ui/formattingtagform.py'
--- openlp/core/ui/formattingtagform.py 2018-08-25 14:08:19 +0000
+++ openlp/core/ui/formattingtagform.py 2019-02-06 07:01:54 +0000
@@ -52,7 +52,7 @@
"""
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):
=== modified file 'openlp/core/ui/generaltab.py'
--- openlp/core/ui/generaltab.py 2018-08-25 14:08:19 +0000
+++ openlp/core/ui/generaltab.py 2019-02-06 07:01:54 +0000
@@ -24,18 +24,18 @@
"""
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 @@
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 @@
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 @@
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 @@
"""
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 @@
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 @@
"""
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 @@
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):
"""
=== modified file 'openlp/core/ui/icons.py'
--- openlp/core/ui/icons.py 2018-10-27 06:00:51 +0000
+++ openlp/core/ui/icons.py 2019-02-06 07:01:54 +0000
@@ -23,13 +23,14 @@
The :mod:`languages` module provides a list of icons.
"""
import logging
+
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__)
=== removed file 'openlp/core/ui/maindisplay.py'
--- openlp/core/ui/maindisplay.py 2019-01-04 20:11:12 +0000
+++ openlp/core/ui/maindisplay.py 1970-01-01 00:00:00 +0000
@@ -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())
=== modified file 'openlp/core/ui/mainwindow.py'
--- openlp/core/ui/mainwindow.py 2018-12-11 21:20:32 +0000
+++ openlp/core/ui/mainwindow.py 2019-02-06 07:01:54 +0000
@@ -33,11 +33,10 @@
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.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.aboutform import AboutForm
+from openlp.core.ui.firsttimeform import FirstTimeForm
from openlp.core.ui.formattingtagform import FormattingTagForm
-from openlp.core.ui.thememanager import ThemeManager
+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.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.printserviceform import PrintServiceForm
+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 @@
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 @@
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 @@
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 @@
"""
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 @@
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 @@
"""
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 @@
"""
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 @@
# 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():
=== modified file 'openlp/core/ui/media/endpoint.py'
--- openlp/core/ui/media/endpoint.py 2017-12-29 09:15:48 +0000
+++ openlp/core/ui/media/endpoint.py 2019-02-06 07:01:54 +0000
@@ -28,6 +28,7 @@
from openlp.core.api.http.endpoint import Endpoint
from openlp.core.common.registry import Registry
+
log = logging.getLogger(__name__)
media_endpoint = Endpoint('media')
=== modified file 'openlp/core/ui/media/mediacontroller.py'
--- openlp/core/ui/media/mediacontroller.py 2019-01-04 20:11:12 +0000
+++ openlp/core/ui/media/mediacontroller.py 2019-02-06 07:01:54 +0000
@@ -50,6 +50,7 @@
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 @@
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
@@ -533,7 +534,7 @@
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 @@
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 @@
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):
=== modified file 'openlp/core/ui/media/playertab.py'
--- openlp/core/ui/media/playertab.py 2019-01-04 20:11:12 +0000
+++ openlp/core/ui/media/playertab.py 2019-02-06 07:01:54 +0000
@@ -54,18 +54,18 @@
"""
Constructor
"""
- self.media_players = Registry().get('media_controller').media_players
+ # self.media_players = Registry().get('media_controller').media_players
self.saved_used_players = None
self.icon_path = UiIcons().player
player_translated = translate('OpenLP.PlayerTab', 'Players')
super(PlayerTab, self).__init__(parent, 'Players', player_translated)
- def setupUi(self):
+ def setup_ui(self):
"""
Set up the UI
"""
self.setObjectName('MediaTab')
- super(PlayerTab, self).setupUi()
+ super(PlayerTab, self).setup_ui()
self.background_color_group_box = QtWidgets.QGroupBox(self.left_column)
self.background_color_group_box.setObjectName('background_color_group_box')
self.form_layout = QtWidgets.QFormLayout(self.background_color_group_box)
@@ -122,7 +122,7 @@
# Signals and slots
self.background_color_button.colorChanged.connect(self.on_background_color_changed)
- def retranslateUi(self):
+ def retranslate_ui(self):
"""
Translate the UI on the fly
"""
=== modified file 'openlp/core/ui/media/systemplayer.py'
--- openlp/core/ui/media/systemplayer.py 2018-01-04 06:10:20 +0000
+++ openlp/core/ui/media/systemplayer.py 2019-02-06 07:01:54 +0000
@@ -29,9 +29,10 @@
from PyQt5 import QtCore, QtMultimedia, QtMultimediaWidgets
from openlp.core.common.i18n import translate
+from openlp.core.threading import ThreadWorker, is_thread_finished, run_thread
from openlp.core.ui.media import MediaState
from openlp.core.ui.media.mediaplayer import MediaPlayer
-from openlp.core.threading import ThreadWorker, run_thread, is_thread_finished
+
log = logging.getLogger(__name__)
=== modified file 'openlp/core/ui/media/vendor/vlc.py'
--- openlp/core/ui/media/vendor/vlc.py 2018-10-13 10:24:01 +0000
+++ openlp/core/ui/media/vendor/vlc.py 2019-02-06 07:01:54 +0000
@@ -50,12 +50,13 @@
from inspect import getargspec
import logging
+
logger = logging.getLogger(__name__)
__version__ = "3.0.3104"
__libvlc_version__ = "3.0.3"
__generator_version__ = "1.4"
-build_date = "Fri Jul 13 15:18:27 2018 3.0.3"
+build_date = "Fri Jul 13 15:18:27 2018 3.0.3"
# The libvlc doc states that filenames are expected to be in UTF8, do
# not rely on sys.getfilesystemencoding() which will be confused,
@@ -68,6 +69,8 @@
bytes = bytes
basestring = (str, bytes)
PYTHON3 = True
+
+
def str_to_bytes(s):
"""Translate string or bytes to bytes.
"""
@@ -76,6 +79,7 @@
else:
return s
+
def bytes_to_str(b):
"""Translate bytes to string.
"""
@@ -89,6 +93,8 @@
bytes = str
basestring = basestring
PYTHON3 = False
+
+
def str_to_bytes(s):
"""Translate string or bytes to bytes.
"""
@@ -97,6 +103,7 @@
else:
return s
+
def bytes_to_str(b):
"""Translate bytes to unicode string.
"""
@@ -109,6 +116,7 @@
# instanciated.
_internal_guard = object()
+
def find_lib():
dll = None
plugin_path = os.environ.get('PYTHON_VLC_MODULE_PATH', None)
@@ -155,19 +163,19 @@
programfiles = os.environ["ProgramFiles"]
homedir = os.environ["HOMEDRIVE"]
for p in ('{programfiles}\\VideoLan{libname}', '{homedir}:\\VideoLan{libname}',
- '{programfiles}{libname}', '{homedir}:{libname}'):
- p = p.format(homedir = homedir,
- programfiles = programfiles,
- libname = '\\VLC\\' + libname)
+ '{programfiles}{libname}', '{homedir}:{libname}'):
+ p = p.format(homedir=homedir,
+ programfiles=programfiles,
+ libname='\\VLC\\' + libname)
if os.path.exists(p):
plugin_path = os.path.dirname(p)
break
if plugin_path is not None: # try loading
p = os.getcwd()
os.chdir(plugin_path)
- # if chdir failed, this will raise an exception
+ # if chdir failed, this will raise an exception
dll = ctypes.CDLL(libname)
- # restore cwd after dll has been loaded
+ # restore cwd after dll has been loaded
os.chdir(p)
else: # may fail
dll = ctypes.CDLL(libname)
@@ -199,20 +207,24 @@
return (dll, plugin_path)
+
# plugin_path used on win32 and MacOS in override.py
-dll, plugin_path = find_lib()
+dll, plugin_path = find_lib()
+
class VLCException(Exception):
"""Exception raised by libvlc methods.
"""
pass
+
try:
_Ints = (int, long)
except NameError: # no long in Python 3+
- _Ints = int
+ _Ints = int
_Seqs = (list, tuple)
+
# Used for handling *event_manager() methods.
class memoize_parameterless(object):
"""Decorator. Caches a parameterless method's return value each time it is called.
@@ -221,6 +233,7 @@
(not reevaluated).
Adapted from https://wiki.python.org/moin/PythonDecoratorLibrary
"""
+
def __init__(self, func):
self.func = func
self._cache = {}
@@ -238,14 +251,16 @@
return self.func.__doc__
def __get__(self, obj, objtype):
- """Support instance methods.
- """
- return functools.partial(self.__call__, obj)
+ """Support instance methods.
+ """
+ return functools.partial(self.__call__, obj)
+
# Default instance. It is used to instanciate classes directly in the
# OO-wrapper.
_default_instance = None
+
def get_default_instance():
"""Return the default VLC.Instance.
"""
@@ -254,9 +269,11 @@
_default_instance = Instance()
return _default_instance
+
_Cfunctions = {} # from LibVLC __version__
_Globals = globals() # sys.modules[__name__].__dict__
+
def _Cfunction(name, flags, errcheck, *types):
"""(INTERNAL) New ctypes function binding.
"""
@@ -275,6 +292,7 @@
return f
raise NameError('no function %r' % (name,))
+
def _Cobject(cls, ctype):
"""(INTERNAL) New instance from ctypes.
"""
@@ -282,15 +300,18 @@
o._as_parameter_ = ctype
return o
+
def _Constructor(cls, ptr=_internal_guard):
"""(INTERNAL) New wrapper from ctypes.
"""
if ptr == _internal_guard:
- raise VLCException("(INTERNAL) ctypes class. You should get references for this class through methods of the LibVLC API.")
+ raise VLCException(
+ "(INTERNAL) ctypes class. You should get references for this class through methods of the LibVLC API.")
if ptr is None or ptr == 0:
return None
return _Cobject(cls, ctypes.c_void_p(ptr))
+
class _Cstruct(ctypes.Structure):
"""(INTERNAL) Base class for ctypes structures.
"""
@@ -303,9 +324,11 @@
def __repr__(self):
return '%s.%s' % (self.__class__.__module__, self)
+
class _Ctype(object):
"""(INTERNAL) Base class for ctypes.
"""
+
@staticmethod
def from_param(this): # not self
"""(INTERNAL) ctypes parameter conversion method.
@@ -314,9 +337,11 @@
return None
return this._as_parameter_
+
class ListPOINTER(object):
"""Just like a POINTER but accept a list of ctype as an argument.
"""
+
def __init__(self, etype):
self.etype = etype
@@ -326,6 +351,7 @@
else:
return ctypes.POINTER(param)
+
# errcheck functions for some native functions.
def string_result(result, func, arguments):
"""Errcheck function. Returns a string and frees the original pointer.
@@ -340,24 +366,33 @@
return s
return None
+
def class_result(classname):
"""Errcheck function. Returns a function that creates the specified class.
"""
+
def wrap_errcheck(result, func, arguments):
if result is None:
return None
return classname(result)
+
return wrap_errcheck
+
# Wrapper for the opaque struct libvlc_log_t
class Log(ctypes.Structure):
pass
+
+
Log_ptr = ctypes.POINTER(Log)
+
# FILE* ctypes wrapper, copied from
# http://svn.python.org/projects/ctypes/trunk/ctypeslib/ctypeslib/contrib/pythonhdr.py
class FILE(ctypes.Structure):
pass
+
+
FILE_ptr = ctypes.POINTER(FILE)
if PYTHON3:
@@ -370,7 +405,7 @@
ctypes.c_char_p,
ctypes.c_char_p,
ctypes.c_char_p,
- ctypes.c_int ]
+ ctypes.c_int]
PyFile_AsFd = ctypes.pythonapi.PyObject_AsFileDescriptor
PyFile_AsFd.restype = ctypes.c_int
@@ -387,7 +422,8 @@
PyFile_AsFile.restype = FILE_ptr
PyFile_AsFile.argtypes = [ctypes.py_object]
- # Generated enum types #
+
+# Generated enum types #
class _Enum(ctypes.c_uint):
'''(INTERNAL) Base class
@@ -405,12 +441,13 @@
return '.'.join((self.__class__.__module__, self.__str__()))
def __eq__(self, other):
- return ( (isinstance(other, _Enum) and self.value == other.value)
- or (isinstance(other, _Ints) and self.value == other) )
+ return ((isinstance(other, _Enum) and self.value == other.value)
+ or (isinstance(other, _Ints) and self.value == other))
def __ne__(self, other):
return not self.__eq__(other)
+
class LogLevel(_Enum):
'''Logging messages level.
\note future libvlc versions may define new levels.
@@ -421,11 +458,14 @@
3: 'WARNING',
4: 'ERROR',
}
-LogLevel.DEBUG = LogLevel(0)
-LogLevel.ERROR = LogLevel(4)
-LogLevel.NOTICE = LogLevel(2)
+
+
+LogLevel.DEBUG = LogLevel(0)
+LogLevel.ERROR = LogLevel(4)
+LogLevel.NOTICE = LogLevel(2)
LogLevel.WARNING = LogLevel(3)
+
class MediaDiscovererCategory(_Enum):
'''Category of a media discoverer
See libvlc_media_discoverer_list_get().
@@ -436,10 +476,13 @@
2: 'podcasts',
3: 'localdirs',
}
-MediaDiscovererCategory.devices = MediaDiscovererCategory(0)
-MediaDiscovererCategory.lan = MediaDiscovererCategory(1)
+
+
+MediaDiscovererCategory.devices = MediaDiscovererCategory(0)
+MediaDiscovererCategory.lan = MediaDiscovererCategory(1)
MediaDiscovererCategory.localdirs = MediaDiscovererCategory(3)
-MediaDiscovererCategory.podcasts = MediaDiscovererCategory(2)
+MediaDiscovererCategory.podcasts = MediaDiscovererCategory(2)
+
class DialogQuestionType(_Enum):
'''@defgroup libvlc_dialog libvlc dialog
@@ -453,9 +496,12 @@
1: 'WARNING',
2: 'CRITICAL',
}
+
+
DialogQuestionType.CRITICAL = DialogQuestionType(2)
-DialogQuestionType.NORMAL = DialogQuestionType(0)
-DialogQuestionType.WARNING = DialogQuestionType(1)
+DialogQuestionType.NORMAL = DialogQuestionType(0)
+DialogQuestionType.WARNING = DialogQuestionType(1)
+
class EventType(_Enum):
'''Event types.
@@ -526,70 +572,73 @@
1545: 'VlmMediaInstanceStatusEnd',
1546: 'VlmMediaInstanceStatusError',
}
-EventType.MediaDiscovererEnded = EventType(1281)
-EventType.MediaDiscovererStarted = EventType(0x500)
-EventType.MediaDurationChanged = EventType(2)
-EventType.MediaFreed = EventType(4)
-EventType.MediaListEndReached = EventType(516)
-EventType.MediaListItemAdded = EventType(0x200)
-EventType.MediaListItemDeleted = EventType(514)
-EventType.MediaListPlayerNextItemSet = EventType(1025)
-EventType.MediaListPlayerPlayed = EventType(0x400)
-EventType.MediaListPlayerStopped = EventType(1026)
-EventType.MediaListViewItemAdded = EventType(0x300)
-EventType.MediaListViewItemDeleted = EventType(770)
-EventType.MediaListViewWillAddItem = EventType(769)
-EventType.MediaListViewWillDeleteItem = EventType(771)
-EventType.MediaListWillAddItem = EventType(513)
-EventType.MediaListWillDeleteItem = EventType(515)
-EventType.MediaMetaChanged = EventType(0)
-EventType.MediaParsedChanged = EventType(3)
-EventType.MediaPlayerAudioDevice = EventType(284)
-EventType.MediaPlayerAudioVolume = EventType(283)
-EventType.MediaPlayerBackward = EventType(264)
-EventType.MediaPlayerBuffering = EventType(259)
-EventType.MediaPlayerChapterChanged = EventType(285)
-EventType.MediaPlayerCorked = EventType(279)
-EventType.MediaPlayerESAdded = EventType(276)
-EventType.MediaPlayerESDeleted = EventType(277)
-EventType.MediaPlayerESSelected = EventType(278)
-EventType.MediaPlayerEncounteredError = EventType(266)
-EventType.MediaPlayerEndReached = EventType(265)
-EventType.MediaPlayerForward = EventType(263)
-EventType.MediaPlayerLengthChanged = EventType(273)
-EventType.MediaPlayerMediaChanged = EventType(0x100)
-EventType.MediaPlayerMuted = EventType(281)
-EventType.MediaPlayerNothingSpecial = EventType(257)
-EventType.MediaPlayerOpening = EventType(258)
-EventType.MediaPlayerPausableChanged = EventType(270)
-EventType.MediaPlayerPaused = EventType(261)
-EventType.MediaPlayerPlaying = EventType(260)
-EventType.MediaPlayerPositionChanged = EventType(268)
-EventType.MediaPlayerScrambledChanged = EventType(275)
-EventType.MediaPlayerSeekableChanged = EventType(269)
-EventType.MediaPlayerSnapshotTaken = EventType(272)
-EventType.MediaPlayerStopped = EventType(262)
-EventType.MediaPlayerTimeChanged = EventType(267)
-EventType.MediaPlayerTitleChanged = EventType(271)
-EventType.MediaPlayerUncorked = EventType(280)
-EventType.MediaPlayerUnmuted = EventType(282)
-EventType.MediaPlayerVout = EventType(274)
-EventType.MediaStateChanged = EventType(5)
-EventType.MediaSubItemAdded = EventType(1)
-EventType.MediaSubItemTreeAdded = EventType(6)
-EventType.RendererDiscovererItemAdded = EventType(1282)
+
+
+EventType.MediaDiscovererEnded = EventType(1281)
+EventType.MediaDiscovererStarted = EventType(0x500)
+EventType.MediaDurationChanged = EventType(2)
+EventType.MediaFreed = EventType(4)
+EventType.MediaListEndReached = EventType(516)
+EventType.MediaListItemAdded = EventType(0x200)
+EventType.MediaListItemDeleted = EventType(514)
+EventType.MediaListPlayerNextItemSet = EventType(1025)
+EventType.MediaListPlayerPlayed = EventType(0x400)
+EventType.MediaListPlayerStopped = EventType(1026)
+EventType.MediaListViewItemAdded = EventType(0x300)
+EventType.MediaListViewItemDeleted = EventType(770)
+EventType.MediaListViewWillAddItem = EventType(769)
+EventType.MediaListViewWillDeleteItem = EventType(771)
+EventType.MediaListWillAddItem = EventType(513)
+EventType.MediaListWillDeleteItem = EventType(515)
+EventType.MediaMetaChanged = EventType(0)
+EventType.MediaParsedChanged = EventType(3)
+EventType.MediaPlayerAudioDevice = EventType(284)
+EventType.MediaPlayerAudioVolume = EventType(283)
+EventType.MediaPlayerBackward = EventType(264)
+EventType.MediaPlayerBuffering = EventType(259)
+EventType.MediaPlayerChapterChanged = EventType(285)
+EventType.MediaPlayerCorked = EventType(279)
+EventType.MediaPlayerESAdded = EventType(276)
+EventType.MediaPlayerESDeleted = EventType(277)
+EventType.MediaPlayerESSelected = EventType(278)
+EventType.MediaPlayerEncounteredError = EventType(266)
+EventType.MediaPlayerEndReached = EventType(265)
+EventType.MediaPlayerForward = EventType(263)
+EventType.MediaPlayerLengthChanged = EventType(273)
+EventType.MediaPlayerMediaChanged = EventType(0x100)
+EventType.MediaPlayerMuted = EventType(281)
+EventType.MediaPlayerNothingSpecial = EventType(257)
+EventType.MediaPlayerOpening = EventType(258)
+EventType.MediaPlayerPausableChanged = EventType(270)
+EventType.MediaPlayerPaused = EventType(261)
+EventType.MediaPlayerPlaying = EventType(260)
+EventType.MediaPlayerPositionChanged = EventType(268)
+EventType.MediaPlayerScrambledChanged = EventType(275)
+EventType.MediaPlayerSeekableChanged = EventType(269)
+EventType.MediaPlayerSnapshotTaken = EventType(272)
+EventType.MediaPlayerStopped = EventType(262)
+EventType.MediaPlayerTimeChanged = EventType(267)
+EventType.MediaPlayerTitleChanged = EventType(271)
+EventType.MediaPlayerUncorked = EventType(280)
+EventType.MediaPlayerUnmuted = EventType(282)
+EventType.MediaPlayerVout = EventType(274)
+EventType.MediaStateChanged = EventType(5)
+EventType.MediaSubItemAdded = EventType(1)
+EventType.MediaSubItemTreeAdded = EventType(6)
+EventType.RendererDiscovererItemAdded = EventType(1282)
EventType.RendererDiscovererItemDeleted = EventType(1283)
-EventType.VlmMediaAdded = EventType(0x600)
-EventType.VlmMediaChanged = EventType(1538)
-EventType.VlmMediaInstanceStarted = EventType(1539)
-EventType.VlmMediaInstanceStatusEnd = EventType(1545)
-EventType.VlmMediaInstanceStatusError = EventType(1546)
-EventType.VlmMediaInstanceStatusInit = EventType(1541)
+EventType.VlmMediaAdded = EventType(0x600)
+EventType.VlmMediaChanged = EventType(1538)
+EventType.VlmMediaInstanceStarted = EventType(1539)
+EventType.VlmMediaInstanceStatusEnd = EventType(1545)
+EventType.VlmMediaInstanceStatusError = EventType(1546)
+EventType.VlmMediaInstanceStatusInit = EventType(1541)
EventType.VlmMediaInstanceStatusOpening = EventType(1542)
-EventType.VlmMediaInstanceStatusPause = EventType(1544)
+EventType.VlmMediaInstanceStatusPause = EventType(1544)
EventType.VlmMediaInstanceStatusPlaying = EventType(1543)
-EventType.VlmMediaInstanceStopped = EventType(1540)
-EventType.VlmMediaRemoved = EventType(1537)
+EventType.VlmMediaInstanceStopped = EventType(1540)
+EventType.VlmMediaRemoved = EventType(1537)
+
class Meta(_Enum):
'''Meta data types.
@@ -622,32 +671,35 @@
24: 'DiscNumber',
25: 'DiscTotal',
}
-Meta.Actors = Meta(22)
-Meta.Album = Meta(4)
+
+
+Meta.Actors = Meta(22)
+Meta.Album = Meta(4)
Meta.AlbumArtist = Meta(23)
-Meta.Artist = Meta(1)
-Meta.ArtworkURL = Meta(15)
-Meta.Copyright = Meta(3)
-Meta.Date = Meta(8)
+Meta.Artist = Meta(1)
+Meta.ArtworkURL = Meta(15)
+Meta.Copyright = Meta(3)
+Meta.Date = Meta(8)
Meta.Description = Meta(6)
-Meta.Director = Meta(18)
-Meta.DiscNumber = Meta(24)
-Meta.DiscTotal = Meta(25)
-Meta.EncodedBy = Meta(14)
-Meta.Episode = Meta(20)
-Meta.Genre = Meta(2)
-Meta.Language = Meta(11)
-Meta.NowPlaying = Meta(12)
-Meta.Publisher = Meta(13)
-Meta.Rating = Meta(7)
-Meta.Season = Meta(19)
-Meta.Setting = Meta(9)
-Meta.ShowName = Meta(21)
-Meta.Title = Meta(0)
-Meta.TrackID = Meta(16)
+Meta.Director = Meta(18)
+Meta.DiscNumber = Meta(24)
+Meta.DiscTotal = Meta(25)
+Meta.EncodedBy = Meta(14)
+Meta.Episode = Meta(20)
+Meta.Genre = Meta(2)
+Meta.Language = Meta(11)
+Meta.NowPlaying = Meta(12)
+Meta.Publisher = Meta(13)
+Meta.Rating = Meta(7)
+Meta.Season = Meta(19)
+Meta.Setting = Meta(9)
+Meta.ShowName = Meta(21)
+Meta.Title = Meta(0)
+Meta.TrackID = Meta(16)
Meta.TrackNumber = Meta(5)
-Meta.TrackTotal = Meta(17)
-Meta.URL = Meta(10)
+Meta.TrackTotal = Meta(17)
+Meta.URL = Meta(10)
+
class State(_Enum):
'''Note the order of libvlc_state_t enum must match exactly the order of
@@ -667,14 +719,17 @@
6: 'Ended',
7: 'Error',
}
-State.Buffering = State(2)
-State.Ended = State(6)
-State.Error = State(7)
+
+
+State.Buffering = State(2)
+State.Ended = State(6)
+State.Error = State(7)
State.NothingSpecial = State(0)
-State.Opening = State(1)
-State.Paused = State(4)
-State.Playing = State(3)
-State.Stopped = State(5)
+State.Opening = State(1)
+State.Paused = State(4)
+State.Playing = State(3)
+State.Stopped = State(5)
+
class TrackType(_Enum):
'''N/A
@@ -685,10 +740,13 @@
1: 'video',
2: 'text',
}
-TrackType.audio = TrackType(0)
-TrackType.text = TrackType(2)
+
+
+TrackType.audio = TrackType(0)
+TrackType.text = TrackType(2)
TrackType.unknown = TrackType(-1)
-TrackType.video = TrackType(1)
+TrackType.video = TrackType(1)
+
class VideoOrient(_Enum):
'''N/A
@@ -703,14 +761,17 @@
6: 'top',
7: 'bottom',
}
+
+
VideoOrient.bottom = VideoOrient(5)
VideoOrient.bottom = VideoOrient(7)
-VideoOrient.left = VideoOrient(0)
-VideoOrient.left = VideoOrient(2)
-VideoOrient.right = VideoOrient(1)
-VideoOrient.right = VideoOrient(3)
-VideoOrient.top = VideoOrient(4)
-VideoOrient.top = VideoOrient(6)
+VideoOrient.left = VideoOrient(0)
+VideoOrient.left = VideoOrient(2)
+VideoOrient.right = VideoOrient(1)
+VideoOrient.right = VideoOrient(3)
+VideoOrient.top = VideoOrient(4)
+VideoOrient.top = VideoOrient(6)
+
class VideoProjection(_Enum):
'''N/A
@@ -720,9 +781,12 @@
1: 'equirectangular',
0x100: 'standard',
}
+
+
VideoProjection.equirectangular = VideoProjection(1)
-VideoProjection.rectangular = VideoProjection(0)
-VideoProjection.standard = VideoProjection(0x100)
+VideoProjection.rectangular = VideoProjection(0)
+VideoProjection.standard = VideoProjection(0x100)
+
class MediaType(_Enum):
'''Media type
@@ -736,12 +800,15 @@
4: 'stream',
5: 'playlist',
}
+
+
MediaType.directory = MediaType(2)
-MediaType.disc = MediaType(3)
-MediaType.file = MediaType(1)
-MediaType.playlist = MediaType(5)
-MediaType.stream = MediaType(4)
-MediaType.unknown = MediaType(0)
+MediaType.disc = MediaType(3)
+MediaType.file = MediaType(1)
+MediaType.playlist = MediaType(5)
+MediaType.stream = MediaType(4)
+MediaType.unknown = MediaType(0)
+
class MediaParseFlag(_Enum):
'''Parse flags used by libvlc_media_parse_with_options()
@@ -754,11 +821,14 @@
0x4: 'network',
0x8: 'interact',
}
+
+
MediaParseFlag.interact = MediaParseFlag(0x8)
-MediaParseFlag.local = MediaParseFlag(0x0)
-MediaParseFlag.local = MediaParseFlag(0x2)
-MediaParseFlag.network = MediaParseFlag(0x1)
-MediaParseFlag.network = MediaParseFlag(0x4)
+MediaParseFlag.local = MediaParseFlag(0x0)
+MediaParseFlag.local = MediaParseFlag(0x2)
+MediaParseFlag.network = MediaParseFlag(0x1)
+MediaParseFlag.network = MediaParseFlag(0x4)
+
class MediaParsedStatus(_Enum):
'''Parse status used sent by libvlc_media_parse_with_options() or returned by
@@ -772,11 +842,14 @@
3: 'timeout',
4: 'done',
}
-MediaParsedStatus.done = MediaParsedStatus(4)
-MediaParsedStatus.failed = MediaParsedStatus(2)
+
+
+MediaParsedStatus.done = MediaParsedStatus(4)
+MediaParsedStatus.failed = MediaParsedStatus(2)
MediaParsedStatus.skipped = MediaParsedStatus(1)
MediaParsedStatus.timeout = MediaParsedStatus(3)
+
class MediaSlaveType(_Enum):
'''Type of a media slave: subtitle or audio.
'''
@@ -784,9 +857,12 @@
0: 'subtitle',
1: 'audio',
}
-MediaSlaveType.audio = MediaSlaveType(1)
+
+
+MediaSlaveType.audio = MediaSlaveType(1)
MediaSlaveType.subtitle = MediaSlaveType(0)
+
class VideoMarqueeOption(_Enum):
'''Marq options definition.
'''
@@ -802,17 +878,20 @@
8: 'marquee_X',
9: 'marquee_Y',
}
-VideoMarqueeOption.Color = VideoMarqueeOption(2)
-VideoMarqueeOption.Enable = VideoMarqueeOption(0)
-VideoMarqueeOption.Opacity = VideoMarqueeOption(3)
-VideoMarqueeOption.Position = VideoMarqueeOption(4)
-VideoMarqueeOption.Refresh = VideoMarqueeOption(5)
-VideoMarqueeOption.Size = VideoMarqueeOption(6)
-VideoMarqueeOption.Text = VideoMarqueeOption(1)
-VideoMarqueeOption.Timeout = VideoMarqueeOption(7)
+
+
+VideoMarqueeOption.Color = VideoMarqueeOption(2)
+VideoMarqueeOption.Enable = VideoMarqueeOption(0)
+VideoMarqueeOption.Opacity = VideoMarqueeOption(3)
+VideoMarqueeOption.Position = VideoMarqueeOption(4)
+VideoMarqueeOption.Refresh = VideoMarqueeOption(5)
+VideoMarqueeOption.Size = VideoMarqueeOption(6)
+VideoMarqueeOption.Text = VideoMarqueeOption(1)
+VideoMarqueeOption.Timeout = VideoMarqueeOption(7)
VideoMarqueeOption.marquee_X = VideoMarqueeOption(8)
VideoMarqueeOption.marquee_Y = VideoMarqueeOption(9)
+
class NavigateMode(_Enum):
'''Navigation mode.
'''
@@ -824,12 +903,15 @@
4: 'right',
5: 'popup',
}
+
+
NavigateMode.activate = NavigateMode(0)
-NavigateMode.down = NavigateMode(2)
-NavigateMode.left = NavigateMode(3)
-NavigateMode.popup = NavigateMode(5)
-NavigateMode.right = NavigateMode(4)
-NavigateMode.up = NavigateMode(1)
+NavigateMode.down = NavigateMode(2)
+NavigateMode.left = NavigateMode(3)
+NavigateMode.popup = NavigateMode(5)
+NavigateMode.right = NavigateMode(4)
+NavigateMode.up = NavigateMode(1)
+
class Position(_Enum):
'''Enumeration of values used to set position (e.g. of video title).
@@ -846,16 +928,19 @@
7: 'left',
8: 'right',
}
-Position.bottom = Position(6)
-Position.center = Position(0)
+
+
+Position.bottom = Position(6)
+Position.center = Position(0)
Position.disable = Position(-1)
-Position.left = Position(1)
-Position.left = Position(4)
-Position.left = Position(7)
-Position.right = Position(2)
-Position.right = Position(5)
-Position.right = Position(8)
-Position.top = Position(3)
+Position.left = Position(1)
+Position.left = Position(4)
+Position.left = Position(7)
+Position.right = Position(2)
+Position.right = Position(5)
+Position.right = Position(8)
+Position.top = Position(3)
+
class TeletextKey(_Enum):
'''Enumeration of teletext keys than can be passed via
@@ -868,12 +953,15 @@
6422528: 'blue',
6881280: 'index',
}
-TeletextKey.blue = TeletextKey(6422528)
-TeletextKey.green = TeletextKey(6750208)
-TeletextKey.index = TeletextKey(6881280)
-TeletextKey.red = TeletextKey(7471104)
+
+
+TeletextKey.blue = TeletextKey(6422528)
+TeletextKey.green = TeletextKey(6750208)
+TeletextKey.index = TeletextKey(6881280)
+TeletextKey.red = TeletextKey(7471104)
TeletextKey.yellow = TeletextKey(7929856)
+
class VideoLogoOption(_Enum):
'''Option values for libvlc_video_{get,set}_logo_{int,string}.
'''
@@ -887,14 +975,17 @@
6: 'opacity',
7: 'position',
}
-VideoLogoOption.delay = VideoLogoOption(4)
-VideoLogoOption.enable = VideoLogoOption(0)
-VideoLogoOption.file = VideoLogoOption(1)
-VideoLogoOption.logo_x = VideoLogoOption(2)
-VideoLogoOption.logo_y = VideoLogoOption(3)
-VideoLogoOption.opacity = VideoLogoOption(6)
+
+
+VideoLogoOption.delay = VideoLogoOption(4)
+VideoLogoOption.enable = VideoLogoOption(0)
+VideoLogoOption.file = VideoLogoOption(1)
+VideoLogoOption.logo_x = VideoLogoOption(2)
+VideoLogoOption.logo_y = VideoLogoOption(3)
+VideoLogoOption.opacity = VideoLogoOption(6)
VideoLogoOption.position = VideoLogoOption(7)
-VideoLogoOption.repeat = VideoLogoOption(5)
+VideoLogoOption.repeat = VideoLogoOption(5)
+
class VideoAdjustOption(_Enum):
'''Option values for libvlc_video_{get,set}_adjust_{int,float,bool}.
@@ -907,13 +998,16 @@
4: 'Saturation',
5: 'Gamma',
}
+
+
VideoAdjustOption.Brightness = VideoAdjustOption(2)
-VideoAdjustOption.Contrast = VideoAdjustOption(1)
-VideoAdjustOption.Enable = VideoAdjustOption(0)
-VideoAdjustOption.Gamma = VideoAdjustOption(5)
-VideoAdjustOption.Hue = VideoAdjustOption(3)
+VideoAdjustOption.Contrast = VideoAdjustOption(1)
+VideoAdjustOption.Enable = VideoAdjustOption(0)
+VideoAdjustOption.Gamma = VideoAdjustOption(5)
+VideoAdjustOption.Hue = VideoAdjustOption(3)
VideoAdjustOption.Saturation = VideoAdjustOption(4)
+
class AudioOutputDeviceTypes(_Enum):
'''Audio device types.
'''
@@ -928,15 +1022,18 @@
8: '_7_1',
10: 'SPDIF',
}
-AudioOutputDeviceTypes.Error = AudioOutputDeviceTypes(-1)
-AudioOutputDeviceTypes.Mono = AudioOutputDeviceTypes(1)
-AudioOutputDeviceTypes.SPDIF = AudioOutputDeviceTypes(10)
+
+
+AudioOutputDeviceTypes.Error = AudioOutputDeviceTypes(-1)
+AudioOutputDeviceTypes.Mono = AudioOutputDeviceTypes(1)
+AudioOutputDeviceTypes.SPDIF = AudioOutputDeviceTypes(10)
AudioOutputDeviceTypes.Stereo = AudioOutputDeviceTypes(2)
-AudioOutputDeviceTypes._2F2R = AudioOutputDeviceTypes(4)
-AudioOutputDeviceTypes._3F2R = AudioOutputDeviceTypes(5)
-AudioOutputDeviceTypes._5_1 = AudioOutputDeviceTypes(6)
-AudioOutputDeviceTypes._6_1 = AudioOutputDeviceTypes(7)
-AudioOutputDeviceTypes._7_1 = AudioOutputDeviceTypes(8)
+AudioOutputDeviceTypes._2F2R = AudioOutputDeviceTypes(4)
+AudioOutputDeviceTypes._3F2R = AudioOutputDeviceTypes(5)
+AudioOutputDeviceTypes._5_1 = AudioOutputDeviceTypes(6)
+AudioOutputDeviceTypes._6_1 = AudioOutputDeviceTypes(7)
+AudioOutputDeviceTypes._7_1 = AudioOutputDeviceTypes(8)
+
class AudioOutputChannel(_Enum):
'''Audio channels.
@@ -949,12 +1046,15 @@
4: 'Right',
5: 'Dolbys',
}
-AudioOutputChannel.Dolbys = AudioOutputChannel(5)
-AudioOutputChannel.Error = AudioOutputChannel(-1)
-AudioOutputChannel.Left = AudioOutputChannel(3)
+
+
+AudioOutputChannel.Dolbys = AudioOutputChannel(5)
+AudioOutputChannel.Error = AudioOutputChannel(-1)
+AudioOutputChannel.Left = AudioOutputChannel(3)
AudioOutputChannel.RStereo = AudioOutputChannel(2)
-AudioOutputChannel.Right = AudioOutputChannel(4)
-AudioOutputChannel.Stereo = AudioOutputChannel(1)
+AudioOutputChannel.Right = AudioOutputChannel(4)
+AudioOutputChannel.Stereo = AudioOutputChannel(1)
+
class MediaPlayerRole(_Enum):
'''Media player roles.
@@ -973,16 +1073,19 @@
8: 'Accessibility',
9: 'Test',
}
+
+
MediaPlayerRole.Accessibility = MediaPlayerRole(8)
-MediaPlayerRole.Animation = MediaPlayerRole(6)
+MediaPlayerRole.Animation = MediaPlayerRole(6)
MediaPlayerRole.Communication = MediaPlayerRole(3)
-MediaPlayerRole.Game = MediaPlayerRole(4)
-MediaPlayerRole.Music = MediaPlayerRole(1)
-MediaPlayerRole.Notification = MediaPlayerRole(5)
-MediaPlayerRole.Production = MediaPlayerRole(7)
-MediaPlayerRole.Test = MediaPlayerRole(9)
-MediaPlayerRole.Video = MediaPlayerRole(2)
-MediaPlayerRole._None = MediaPlayerRole(0)
+MediaPlayerRole.Game = MediaPlayerRole(4)
+MediaPlayerRole.Music = MediaPlayerRole(1)
+MediaPlayerRole.Notification = MediaPlayerRole(5)
+MediaPlayerRole.Production = MediaPlayerRole(7)
+MediaPlayerRole.Test = MediaPlayerRole(9)
+MediaPlayerRole.Video = MediaPlayerRole(2)
+MediaPlayerRole._None = MediaPlayerRole(0)
+
class PlaybackMode(_Enum):
'''Defines playback modes for playlist.
@@ -992,15 +1095,20 @@
1: 'loop',
2: 'repeat',
}
+
+
PlaybackMode.default = PlaybackMode(0)
-PlaybackMode.loop = PlaybackMode(1)
-PlaybackMode.repeat = PlaybackMode(2)
+PlaybackMode.loop = PlaybackMode(1)
+PlaybackMode.repeat = PlaybackMode(2)
+
class Ca
Follow ups