openlp-core team mailing list archive
-
openlp-core team
-
Mailing list archive
-
Message #32968
[Merge] lp:~raoul-snyman/openlp/webengine-migrate into lp:openlp
Raoul Snyman has proposed merging lp:~raoul-snyman/openlp/webengine-migrate into lp:openlp.
Requested reviews:
OpenLP Core (openlp-core)
For more details, see:
https://code.launchpad.net/~raoul-snyman/openlp/webengine-migrate/+merge/353533
--
The attached diff has been truncated due to its size.
Your team OpenLP Core is requested to review the proposed merge of lp:~raoul-snyman/openlp/webengine-migrate into lp:openlp.
=== modified file '.bzrignore'
--- .bzrignore 2018-06-26 17:10:34 +0000
+++ .bzrignore 2018-08-21 21:15:00 +0000
@@ -45,4 +45,14 @@
resources/windows/warnOpenLP.txt
*.ropeproject
tags
+<<<<<<< TREE
tests.kdev4
+=======
+output
+htmlcov
+node_modules
+openlp-test-projectordb.sqlite
+package-lock.json
+.cache
+test
+>>>>>>> MERGE-SOURCE
=== added file 'karma.conf.js'
--- karma.conf.js 1970-01-01 00:00:00 +0000
+++ karma.conf.js 2018-08-21 21:15:00 +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 2018-08-21 21:15:00 +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.py'
--- openlp.py 2017-12-29 09:15:48 +0000
+++ openlp.py 2018-08-21 21:15:00 +0000
@@ -27,6 +27,8 @@
import multiprocessing
import sys
+from OpenGL import GL
+
from openlp.core.app import main
from openlp.core.common import is_win, is_macosx
from openlp.core.common.applocation import AppLocation
=== modified file 'openlp/core/app.py'
--- openlp/core/app.py 2018-04-07 16:16:42 +0000
+++ openlp/core/app.py 2018-08-21 21:15:00 +0000
@@ -33,7 +33,7 @@
from datetime import datetime
from traceback import format_exception
-from PyQt5 import QtCore, QtWidgets
+from PyQt5 import QtCore, QtWidgets, QtWebEngineWidgets # noqa
from openlp.core.common import is_macosx, is_win
from openlp.core.common.applocation import AppLocation
@@ -313,7 +313,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))
@@ -326,7 +326,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/registry.py'
--- openlp/core/common/registry.py 2018-06-24 06:56:50 +0000
+++ openlp/core/common/registry.py 2018-08-21 21:15:00 +0000
@@ -144,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-06-07 17:44:35 +0000
+++ openlp/core/common/settings.py 2018-08-21 21:15:00 +0000
@@ -70,6 +70,30 @@
return string
+def upgrade_monitor(number, x_position, y_position, height, width, can_override, can_display_on_monitor):
+ """
+ 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 can_display_on_monitor: Can OpenLP display on the monitor
+ :returns dict: Dictionary with the new value
+ """
+ return {
+ number: {
+ 'displayGeometry': {
+ 'x': x_position,
+ 'y': y_position,
+ 'height': height,
+ 'width': width
+ }
+ },
+ 'canDisplayOnMonitor': can_display_on_monitor
+ }
+
+
class Settings(QtCore.QSettings):
"""
Class to wrap QSettings.
@@ -175,6 +199,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',
@@ -274,7 +299,9 @@
('songuasge/db password', 'songusage/db password', []),
('songuasge/db hostname', 'songusage/db hostname', []),
('songuasge/db database', 'songusage/db database', []),
- ('presentations / Powerpoint Viewer', '', [])
+ ('presentations / Powerpoint Viewer', '', []),
+ (['core/monitor', 'core/x position', 'core/y position', 'core/height', 'core/width', 'core/override',
+ 'core/display on monitor'], 'core/monitors', [(upgrade_monitor, [1, 0, 0, None, None, False, False])])
]
@staticmethod
@@ -564,7 +591,7 @@
elif isinstance(default_value, list):
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):
@@ -573,6 +600,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 file 'openlp/core/display/canvas.py'
--- openlp/core/display/canvas.py 1970-01-01 00:00:00 +0000
+++ openlp/core/display/canvas.py 2018-08-21 21:15:00 +0000
@@ -0,0 +1,768 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2017 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:`canvas` 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 json
+import logging
+import os
+
+from PyQt5 import QtCore, QtWidgets, QtGui, QtMultimedia, QtWebChannel, QtWebEngineWidgets
+
+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.webengine import WebEngineView
+from openlp.core.display.window import MediaWatcher
+from openlp.core.display.screens import ScreenList
+from openlp.core.lib import ServiceItem, ImageSource, build_html, expand_tags, image_to_byte
+from openlp.core.lib.theme import BackgroundType
+from openlp.core.ui import HideMode, AlertLocation, DisplayControllerType
+
+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 Canvas(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(Canvas, 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.webview = WebEngineView(self)
+ self.webview.setGeometry(0, 0, self.screen['size'].width(), self.screen['size'].height())
+ self.webview.settings().setAttribute(QtWebEngineWidgets.QWebEngineSettings.PluginsEnabled, True)
+ self.layout = QtWidgets.QVBoxLayout(self)
+ self.layout.setContentsMargins(0, 0, 0, 0)
+ self.layout.addWidget(self.webview)
+ self.webview.loadFinished.connect(self.after_loaded)
+ self.set_url(QtCore.QUrl('file://' + os.getcwd() + '/display.html'))
+ self.media_watcher = MediaWatcher(self)
+ self.channel = QtWebChannel.QWebChannel(self)
+ self.channel.registerObject('mediaWatcher', self.media_watcher)
+ self.webview.page().setWebChannel(self.channel)
+ self.webview.loadFinished.connect(self.is_web_loaded)
+
+ self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+ self.setHorizontalScrollBarPolicy(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
+
+ def set_url(self, url):
+ """
+ Set the URL of the webview
+ """
+ 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, state):
+ """
+ Add stuff after page initialisation
+ """
+ self.run_javascript('Display.init();')
+
+ def add_script_source(self, fname, source):
+ """
+ Add a script of source code
+ """
+ js = QtWebEngineWidgets.QWebEngineScript()
+ js.setSourceCode(source)
+ js.setName(fname)
+ js.setWorldId(QtWebEngineWidgets.QWebEngineScript.MainWorld)
+ self.webview.page().scripts().insert(js)
+
+ def add_script(self, fname):
+ """
+ Add a script to the page
+ """
+ js_file = QtCore.QFile(fname)
+ if not js_file.open(QtCore.QIODevice.ReadOnly):
+ log.warning('Could not open %s: %s', fname, js_file.errorString())
+ return
+ self.add_script_source(os.path.basename(fname), str(bytes(js_file.readAll()), 'utf-8'))
+
+ 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
+ """
+ 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
+
+
+class MainCanvas(OpenLPMixin, Canvas, RegistryProperties):
+ """
+ This is the display screen as a specialized class from the Display class
+ """
+ def __init__(self, parent):
+ """
+ Constructor
+ """
+ super(MainCanvas, self).__init__(parent)
+ self.screens = ScreenList()
+ self.rebuild_css = False
+ self.hide_mode = None
+ self.override = {}
+ self.retranslateUi()
+ self.media_object = None
+ if self.is_live:
+ self.audio_player = AudioPlayer(self)
+ else:
+ self.audio_player = 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:
+ 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 MainCanvas 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.MainCanvas', 'OpenLP Display'))
+
+ def setup(self):
+ """
+ Set up and build the output screen
+ """
+ self.log_debug('Start MainCanvas setup (live = {islive})'.format(islive=self.is_live))
+ self.screen = self.screens.current
+ self.setVisible(False)
+ Canvas.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.webview.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'])
+ json_verses = json.dumps(slide)
+ self.run_javascript('Display.setTextSlides({verses});'.format(verses=json_verses))
+ #if animate:
+ # # NOTE: Verify this works with ''.format()
+ # _text = slide.replace('\\', '\\\\').replace('\"', '\\\"')
+ # self.frame.runJavaScript('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.run_javascript(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'])
+
+ 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:
+ # self.set_im
+ # js = 'show_image("data:image/png;base64,{image}");'.format(image=image)
+ #else:
+ # js = 'show_image("");'
+ #self.frame.evaluateJavaScript(js)
+ if not image['file'].startswith('file://'):
+ image['file'] = 'file://' + image['file']
+ json_images = json.dumps(images)
+ self.run_javascript('Display.setImageSlides({images});'.format(images=json_images))
+
+
+ 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.
+ """
+ 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:
+ while not self.run_javascript('show_text_completed()'):
+ self.application.process_events()
+ # 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)
+ 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.webview.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 = os.path.join(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),
+ ':/media/slidecontroller_multimedia.png')
+ 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.run_javascript(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.run_javascript('show_blank("desktop");')
+ self.setVisible(False)
+ elif mode == HideMode.Blank or self.initial_fame:
+ self.run_javascript('show_blank("black");')
+ else:
+ self.run_javascript('show_blank("theme");')
+ if mode != HideMode.Screen:
+ if self.isHidden():
+ self.setVisible(True)
+ self.webview.setVisible(True)
+ 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.run_javascript('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')
+
+ def _hide_mouse(self):
+ """
+ Hide mouse cursor when moved over display.
+ """
+ if Settings().value('advanced/hide mouse'):
+ self.setCursor(QtCore.Qt.BlankCursor)
+ self.run_javascript('document.body.style.cursor = "none"')
+ else:
+ self.setCursor(QtCore.Qt.ArrowCursor)
+ self.run_javascript('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:
+ 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_()
+
+
+class AudioPlayer(LogMixin, QtCore.QObject):
+ """
+ This Class will play audio only allowing components to work with a soundtrack independent of the user interface.
+ """
+ position_changed = QtCore.pyqtSignal(int)
+
+ def __init__(self, parent):
+ """
+ The constructor for the display form.
+
+ :param parent: The parent widget.
+ """
+ super(AudioPlayer, self).__init__(parent)
+ self.player = QtMultimedia.QMediaPlayer()
+ self.playlist = QtMultimedia.QMediaPlaylist(self.player)
+ self.volume_slider = None
+ self.player.setPlaylist(self.playlist)
+ self.player.positionChanged.connect(self._on_position_changed)
+
+ def __del__(self):
+ """
+ Shutting down so clean up connections
+ """
+ self.stop()
+
+ def _on_position_changed(self, position):
+ """
+ Emit a signal when the position of the media player updates
+ """
+ self.position_changed.emit(position)
+
+ def set_volume_slider(self, slider):
+ """
+ Connect the volume slider to the media player
+ :param slider:
+ """
+ self.volume_slider = slider
+ self.volume_slider.setMinimum(0)
+ self.volume_slider.setMaximum(100)
+ self.volume_slider.setValue(self.player.volume())
+ self.volume_slider.valueChanged.connect(self.set_volume)
+
+ def set_volume(self, volume):
+ """
+ Set the volume of the media player
+
+ :param volume:
+ """
+ self.player.setVolume(volume)
+
+ def reset(self):
+ """
+ Reset the audio player, clearing the playlist and the queue.
+ """
+ self.stop()
+ self.playlist.clear()
+
+ def play(self):
+ """
+ We want to play the file so start it
+ """
+ self.player.play()
+
+ def pause(self):
+ """
+ Pause the Audio
+ """
+ self.player.pause()
+
+ def stop(self):
+ """
+ Stop the Audio and clean up
+ """
+ self.player.stop()
+
+ def add_to_playlist(self, file_names):
+ """
+ Add another file to the playlist.
+
+ :param file_names: A list with files to be added to the playlist.
+ """
+ if not isinstance(file_names, list):
+ file_names = [file_names]
+ for file_name in file_names:
+ self.playlist.addMedia(QtMultimedia.QMediaContent(QtCore.QUrl.fromLocalFile(str(file_name))))
+
+ def next(self):
+ """
+ Skip forward to the next track in the list
+ """
+ self.playlist.next()
+
+ def go_to(self, index):
+ """
+ Go to a particular track in the list
+
+ :param index: The track to go to
+ """
+ self.playlist.setCurrentIndex(index)
+ if self.player.state() == QtMultimedia.QMediaPlayer.PlayingState:
+ self.player.play()
=== 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 2018-08-21 21:15:00 +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/display.html'
--- openlp/core/display/html/display.html 1970-01-01 00:00:00 +0000
+++ openlp/core/display/html/display.html 2018-08-21 21:15:00 +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: #000 !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 2018-08-21 21:15:00 +0000
@@ -0,0 +1,682 @@
+/**
+ * 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 = {};
+ },
+ /**
+ * 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} html - The HTML for the verse, e.g. "line1<br>line2"
+ * @param {bool} [reinit=true] - Re-initialize Reveal. Defaults to true.
+ */
+ addTextSlide: function (verse, 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;
+ }
+ if ((arguments.length > 2) && (arguments[2] === true)) {
+ this.reinit();
+ }
+ else if (arguments.length == 2) {
+ 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, false);
+ });
+ Display.reinit();
+ Display.goToSlide(0);
+ },
+ /**
+ * Set image slides
+ * @param {Object[]} slides - A list of images to add as JS objects [{"filename": "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["filename"];
+ 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: {"filename": "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["filename"];
+ 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
+ */
+ blank: function () {
+ if (!Reveal.isPaused()) {
+ Reveal.togglePause();
+ }
+ // var slidesDiv = $(".slides")[0];
+ },
+ /**
+ * Blank to theme
+ */
+ theme: function () {
+ var slidesDiv = $(".slides")[0];
+ slidesDiv.style.visibility = "hidden";
+ if (Reveal.isPaused()) {
+ Reveal.togglePause();
+ }
+ },
+ /**
+ * Show the screen
+ */
+ show: function () {
+ var slidesDiv = $(".slides")[0];
+ slidesDiv.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["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;
+ }
+};
+new QWebChannel(qt.webChannelTransport, function (channel) {
+ window.mediaWatcher = channel.objects.mediaWatcher;
+});
=== 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 2018-08-21 21:15:00 +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 2018-08-21 21:15:00 +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 2018-08-21 21:15:00 +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 2018-08-21 21:15:00 +0000
@@ -0,0 +1,356 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2017 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 re
+
+from openlp.core.lib.formattingtags import FormattingTags
+
+log = logging.getLogger(__name__)
+
+SLIM_CHARS = 'fiíIÍjlĺľrtť.,;/ ()|"\'!:\\'
+CHORD_LINE_MATCH = re.compile(r'\[(.*?)\]([\u0080-\uFFFF,\w]*)'
+ '([\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>'
+
+
+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
=== removed file 'openlp/core/display/renderer.py'
--- openlp/core/display/renderer.py 2017-12-29 09:15:48 +0000
+++ openlp/core/display/renderer.py 1970-01-01 00:00:00 +0000
@@ -1,583 +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 FormattingTags, ImageSource, ItemCapabilities, ServiceItem, expand_tags, build_chords_css, \
- build_lyrics_format_css, build_lyrics_outline_css
-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 2018-08-21 21:15:00 +0000
@@ -23,7 +23,6 @@
The :mod:`screen` module provides management functionality for a machines'
displays.
"""
-import copy
import logging
from PyQt5 import QtCore
@@ -35,6 +34,89 @@
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 = number
+ self.geometry = geometry
+ self.display_geometry = geometry
+ 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
+
+ @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'])
+ if 'display_geometry' in screen_dict:
+ screen_dict['display_geometry'] = QtCore.QRect(**screen_dict['display_geometry'])
+ 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.display_geometry != self.geometry:
+ screen_dict['display_geometry'] = {
+ 'x': self.display_geometry.x(),
+ 'y': self.display_geometry.y(),
+ 'width': self.display_geometry.width(),
+ 'height': self.display_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 = screen_dict['number']
+ self.is_display = screen_dict['is_display']
+ self.is_primary = screen_dict['is_primary']
+ self.geometry = QtCore.QRect(**screen_dict['geometry'])
+
+
class ScreenList(object):
"""
Wrapper to handle the parameters of the display screen.
@@ -43,6 +125,7 @@
"""
log.info('Screen loaded')
__instance__ = None
+ screens = []
def __new__(cls):
"""
@@ -52,6 +135,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 +188,134 @@
"""
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 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/monitors': '{}'
+ }
+ Settings.extend_default_settings(screen_settings)
+ monitors = Settings().value('core/monitors')
+ # for number, monitor in monitors.items():
+ # if self.has_screen(number):
+ # self[number].update(monitor)
+ # else:
+ # self.screens.append(Screen.from_dict(monitor))
+
+ def save_screen_settings(self):
+ """
+ Saves the screen size and monitor settings
+ """
+ Settings().setValue('core/monitors', {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
+ """
+ self.screens = []
+ for number in range(self.desktop.screenCount()):
+ self.screens.append(
+ Screen(number, self.desktop.screenGeometry(number), 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 2018-08-21 21:15:00 +0000
@@ -0,0 +1,91 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2017 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, QtWidgets, QtWebEngineWidgets
+
+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
+ """
+ _child = None # QtWidgets.QOpenGLWidget
+ 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 QOpenGLWidget child
+ w = ev.child()
+ if w and isinstance(w, QtWidgets.QOpenGLWidget):
+ 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 2018-08-21 21:15:00 +0000
@@ -0,0 +1,292 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2017 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 logging
+import os
+import json
+
+from PyQt5 import QtCore, QtWidgets, QtWebChannel
+
+from openlp.core.common.path import Path, path_to_str
+
+log = logging.getLogger(__name__)
+DISPLAY_PATH = Path(__file__).parent / 'html' / 'display.html'
+
+
+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_DeleteOnClose)
+ self.layout = QtWidgets.QVBoxLayout(self)
+ self.layout.setContentsMargins(0, 0, 0, 0)
+ self.webview = WebEngineView(self)
+ 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)
+ if screen and screen.is_display:
+ self.update_from_screen(screen)
+
+ 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_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();')
+
+ 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
+ """
+ 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['filename'].startswith('file://'):
+ image['filename'] = 'file://' + image['filename']
+ 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['filename'].startswith('file://'):
+ video['filename'] = 'file://' + video['filename']
+ 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
+ """
+ self.run_javascript('Display.setTheme({theme});'.format(theme=theme.export_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
+ """
+ self.run_javascript('Display.show();')
=== modified file 'openlp/core/lib/__init__.py'
--- openlp/core/lib/__init__.py 2018-08-04 20:58:13 +0000
+++ openlp/core/lib/__init__.py 2018-08-21 21:15:00 +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
@@ -35,8 +33,6 @@
log = logging.getLogger(__name__ + '.__init__')
-SLIMCHARS = 'fiíIÍjlĺľrtť.,;/ ()|"\'!:\\'
-
class ServiceItemContext(object):
"""
@@ -287,6 +283,7 @@
return True
+<<<<<<< TREE
def clean_tags(text, remove_chords=False):
"""
Remove Tags from text for display
@@ -590,6 +587,8 @@
return ''.join(expanded_text_lines)
+=======
+>>>>>>> MERGE-SOURCE
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/htmlbuilder.py'
--- openlp/core/lib/htmlbuilder.py 2018-07-04 20:42:55 +0000
+++ openlp/core/lib/htmlbuilder.py 2018-08-21 21:15:00 +0000
@@ -408,8 +408,7 @@
"""
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
@@ -688,11 +687,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 2018-08-21 21:15:00 +0000
@@ -184,8 +184,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 +197,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/serviceitem.py'
--- openlp/core/lib/serviceitem.py 2018-04-22 06:59:35 +0000
+++ openlp/core/lib/serviceitem.py 2018-08-21 21:15:00 +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
@@ -39,7 +39,8 @@
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, build_icon, clean_tags, expand_tags, expand_chords
+from openlp.core.display.render import remove_tags, render_tags
+from openlp.core.lib import ImageSource, build_icon
log = logging.getLogger(__name__)
@@ -166,7 +167,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 = []
@@ -175,8 +179,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
@@ -247,91 +249,120 @@
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.
-
- :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 = []
- self.foot_text = '<br>'.join([_f for _f in self.raw_footer if _f])
-
- def add_from_image(self, path, title, background=None, thumbnail=None):
+ @property
+ def rendered_slides(self):
+ """
+ Render the frames and return them
+ """
+ if not self._rendered_slides:
+ self._rendered_slides = []
+ for raw_slide in self.slides:
+ rendered_slide = deepcopy(raw_slide)
+ rendered_slide['text'] = render_tags(rendered_slide['text'])
+ self._rendered_slides.append(rendered_slide)
+ return self._rendered_slides
+
+ @property
+ def display_slides(self):
+ """
+ Render the frames and return them
+ """
+ if not self._display_slides:
+ self._display_slides = []
+ for raw_slide in self.slides:
+ display_slide = deepcopy(raw_slide)
+ display_slide['text'] = remove_tags(display_slide['text'])
+ self._display_slides.append(display_slide)
+ return self._display_slides
+
+ # 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.
+ #
+ # :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:
+ # can_render_chords = 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 = render_tags(page.rstrip(), can_render_chords)
+ # new_frame = {
+ # 'title': remove_tags(page),
+ # 'text': remove_tags(page.rstrip(), can_render_chords),
+ # 'chords_text': render_chords(remove_tags(page.rstrip(), False)),
+ # 'html': html_data.replace('&nbsp;', ' '),
+ # 'printing_html': render_tags(html.escape(page.rstrip()), can_render_chords, is_printing=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 = remove_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 = []
+ # self.foot_text = '<br>'.join([_f for _f in self.raw_footer if _f])
+
+ def add_from_image(self, filename, title, background=None, thumbnail=None):
"""
Add an image slide to the service item.
- :param path: The directory in which the image file is located.
+ :param filename: 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, 'filename': filename}
+ 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):
@@ -355,8 +386,8 @@
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})
+ self.slides.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._new_item()
@@ -488,10 +519,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):
"""
@@ -679,22 +710,22 @@
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['filename']):
self.is_valid = False
break
elif self.is_command():
if self.is_capable(ItemCapabilities.IsOptical):
- 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/theme.py'
--- openlp/core/lib/theme.py 2017-12-29 09:15:48 +0000
+++ openlp/core/lib/theme.py 2018-08-21 21:15:00 +0000
@@ -30,6 +30,7 @@
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
=== modified file 'openlp/core/ui/__init__.py'
--- openlp/core/ui/__init__.py 2018-04-21 20:15:43 +0000
+++ openlp/core/ui/__init__.py 2018-08-21 21:15:00 +0000
@@ -99,10 +99,8 @@
from .themeform import ThemeForm
from .filerenameform import FileRenameForm
from .starttimeform import StartTimeForm
-from .maindisplay import MainDisplay, Display, AudioPlayer
from .servicenoteform import ServiceNoteForm
from .serviceitemeditform import ServiceItemEditForm
-from .slidecontroller import SlideController, DisplayController, PreviewController, LiveController
from .splashscreen import SplashScreen
from .generaltab import GeneralTab
from .themestab import ThemesTab
=== modified file 'openlp/core/ui/exceptionform.py'
--- openlp/core/ui/exceptionform.py 2017-12-29 09:15:48 +0000
+++ openlp/core/ui/exceptionform.py 2018-08-21 21:15:00 +0000
@@ -27,56 +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:
- import icu
- try:
- ICU_VERSION = icu.VERSION
- except AttributeError:
- ICU_VERSION = 'OK'
-except ImportError:
- ICU_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_version, get_library_versions
from openlp.core.widgets.dialogs import FileDialog
-from openlp.core.version import get_version
log = logging.getLogger(__name__)
@@ -117,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}\npyICU: {icu}\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, icu=ICU_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':
@@ -223,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/firsttimeform.py'
--- openlp/core/ui/firsttimeform.py 2018-01-13 23:24:26 +0000
+++ openlp/core/ui/firsttimeform.py 2018-08-21 21:15:00 +0000
@@ -284,7 +284,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):
@@ -536,8 +536,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/generaltab.py'
--- openlp/core/ui/generaltab.py 2017-12-29 09:15:48 +0000
+++ openlp/core/ui/generaltab.py 2018-08-21 21:15:00 +0000
@@ -283,16 +283,24 @@
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)
+ for screen in self.screens:
+ self.monitor_combo_box.addItem(str(screen), screen.number)
+ monitors = settings.value('monitors')
+ for number, monitor in monitors.items():
+ if monitor['is_display']:
+ for item_index in range(self.monitor_combo_box.count()):
+ if self.monitor_combo_box.itemData(item_index) == number:
+ self.monitor_combo_box.setCurrentIndex(item_index)
+ else:
+ self.monitor_combo_box.setCurrentIndex(0)
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)
+ # TODO: This is going to be a more complicated setup
+ # 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,20 +311,20 @@
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.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.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,7 +333,7 @@
"""
settings = Settings()
settings.beginGroup(self.settings_section)
- settings.setValue('monitor', self.monitor_combo_box.currentIndex())
+ # 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())
@@ -342,38 +350,38 @@
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('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)
- def post_set_up(self, postUpdate=False):
+ def post_set_up(self, is_post_update=False):
"""
Apply settings after settings tab has loaded and most of the system so must be delayed
"""
self.settings_form.register_post_process('slidecontroller_live_spin_delay')
# Do not continue on start up.
- if not postUpdate:
+ if not is_post_update:
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()
+ self.screens.set_display_screen(self.monitor_combo_box.currentData())
+ # 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
=== renamed file 'openlp/core/ui/maindisplay.py' => 'openlp/core/ui/maindisplay.py.THIS'
=== modified file 'openlp/core/ui/mainwindow.py'
--- openlp/core/ui/mainwindow.py 2018-06-24 06:56:50 +0000
+++ openlp/core/ui/mainwindow.py 2018-08-21 21:15:00 +0000
@@ -41,13 +41,13 @@
from openlp.core.common.path import Path, copyfile, create_paths
from openlp.core.common.registry import Registry
from openlp.core.common.settings import Settings
-from openlp.core.display.renderer import Renderer
from openlp.core.display.screens import ScreenList
from openlp.core.lib import PluginManager, ImageManager, PluginStatus
from openlp.core.lib.ui import create_action
from openlp.core.projectors.manager import ProjectorManager
-from openlp.core.ui import AboutForm, SettingsForm, ServiceManager, ThemeManager, LiveController, PluginForm, \
- ShortcutListForm, FormattingTagForm, PreviewController
+from openlp.core.ui import AboutForm, SettingsForm, ServiceManager, ThemeManager, PluginForm, ShortcutListForm, \
+ FormattingTagForm
+from openlp.core.ui.slidecontroller import LiveController, PreviewController
from openlp.core.ui.firsttimeform import FirstTimeForm
from openlp.core.ui.media import MediaController
from openlp.core.ui.printserviceform import PrintServiceForm
@@ -502,7 +502,6 @@
# Set up the path with plugins
PluginManager(self)
ImageManager()
- Renderer()
# Set up the interface
self.setupUi(self)
# Define the media Dock Manager
@@ -581,8 +580,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')
@@ -633,8 +632,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)
@@ -827,13 +826,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
@@ -1013,7 +1005,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()
@@ -1084,7 +1076,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/mediacontroller.py'
--- openlp/core/ui/media/mediacontroller.py 2018-04-22 07:17:38 +0000
+++ openlp/core/ui/media/mediacontroller.py 2018-08-21 21:15:00 +0000
@@ -439,7 +439,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
@@ -626,7 +626,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)
@@ -746,7 +746,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)
@@ -817,7 +817,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):
=== removed file 'openlp/core/ui/media/webkitplayer.py'
--- openlp/core/ui/media/webkitplayer.py 2017-12-29 09:15:48 +0000
+++ openlp/core/ui/media/webkitplayer.py 1970-01-01 00:00:00 +0000
@@ -1,312 +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:`~openlp.core.ui.media.webkit` module contains our WebKit video player
-"""
-import logging
-
-from PyQt5 import QtGui, QtWebKitWidgets
-
-from openlp.core.common.i18n import translate
-from openlp.core.common.settings import Settings
-from openlp.core.ui.media import MediaState
-from openlp.core.ui.media.mediaplayer import MediaPlayer
-
-log = logging.getLogger(__name__)
-
-VIDEO_CSS = """
-#videobackboard {
- z-index:3;
- background-color: %(bgcolor)s;
-}
-#video {
- background-color: %(bgcolor)s;
- z-index:4;
-}
-"""
-
-VIDEO_JS = """
- function show_video(state, path, volume, variable_value){
- // Sometimes video.currentTime stops slightly short of video.duration and video.ended is intermittent!
-
- var video = document.getElementById('video');
- if(volume != null){
- video.volume = volume;
- }
- switch(state){
- case 'load':
- video.src = 'file:///' + path;
- video.load();
- break;
- case 'play':
- video.play();
- break;
- case 'pause':
- video.pause();
- break;
- case 'stop':
- show_video('pause');
- video.currentTime = 0;
- break;
- case 'close':
- show_video('stop');
- video.src = '';
- break;
- case 'length':
- return video.duration;
- case 'current_time':
- return video.currentTime;
- case 'seek':
- video.currentTime = variable_value;
- break;
- case 'isEnded':
- return video.ended;
- case 'setVisible':
- video.style.visibility = variable_value;
- break;
- case 'setBackBoard':
- var back = document.getElementById('videobackboard');
- back.style.visibility = variable_value;
- break;
- }
- }
-"""
-
-VIDEO_HTML = """
-<div id="videobackboard" class="size" style="visibility:hidden"></div>
-<video id="video" class="size" style="visibility:hidden" autobuffer preload></video>
-"""
-
-VIDEO_EXT = ['*.3gp', '*.3gpp', '*.3g2', '*.3gpp2', '*.aac', '*.flv', '*.f4a', '*.f4b', '*.f4p', '*.f4v', '*.mov',
- '*.m4a', '*.m4b', '*.m4p', '*.m4v', '*.mkv', '*.mp4', '*.ogv', '*.webm', '*.mpg', '*.wmv', '*.mpeg',
- '*.avi', '*.swf']
-
-AUDIO_EXT = ['*.mp3', '*.ogg']
-
-
-class WebkitPlayer(MediaPlayer):
- """
- A specialised version of the MediaPlayer class, which provides a QtWebKit
- display.
- """
-
- def __init__(self, parent):
- """
- Constructor
- """
- super(WebkitPlayer, self).__init__(parent, 'webkit')
- self.original_name = 'WebKit'
- self.display_name = '&WebKit'
- self.parent = parent
- self.can_background = True
- self.audio_extensions_list = AUDIO_EXT
- self.video_extensions_list = VIDEO_EXT
-
- def get_media_display_css(self):
- """
- Add css style sheets to htmlbuilder
- """
- background = QtGui.QColor(Settings().value('players/background color')).name()
- css = VIDEO_CSS % {'bgcolor': background}
- return css
-
- def get_media_display_javascript(self):
- """
- Add javascript functions to htmlbuilder
- """
- return VIDEO_JS
-
- def get_media_display_html(self):
- """
- Add html code to htmlbuilder
- """
- return VIDEO_HTML
-
- def setup(self, display):
- """
- Set up the player
-
- :param display: The display to be updated.
- """
- display.web_view.resize(display.size())
- display.web_view.raise_()
- self.has_own_widget = False
-
- def check_available(self):
- """
- Check the availability of the media player.
-
- :return: boolean. True if available
- """
- web = QtWebKitWidgets.QWebPage()
- # This script should return '[object HTMLVideoElement]' if the html5 video is available in webkit. Otherwise it
- # should return '[object HTMLUnknownElement]'
- return web.mainFrame().evaluateJavaScript(
- "Object.prototype.toString.call(document.createElement('video'));") == '[object HTMLVideoElement]'
-
- def load(self, display):
- """
- Load a video
-
- :param display: The display to be updated.
- """
- log.debug('load vid in Webkit Controller')
- controller = display.controller
- if display.has_audio and not controller.media_info.is_background:
- volume = controller.media_info.volume
- vol = float(volume) / float(100)
- else:
- vol = 0
- path = controller.media_info.file_info.absoluteFilePath()
- display.web_view.setVisible(True)
- js = 'show_video("load", "{path}", {vol});'.format(path=path.replace('\\', '\\\\'), vol=str(vol))
- display.frame.evaluateJavaScript(js)
- return True
-
- def resize(self, display):
- """
- Resize the player
-
- :param display: The display to be updated.
- """
- display.web_view.resize(display.size())
-
- def play(self, display):
- """
- Play a video
-
- :param display: The display to be updated.
- """
- controller = display.controller
- display.web_loaded = True
- start_time = 0
- if display.controller.is_live:
- if self.get_live_state() != MediaState.Paused and controller.media_info.start_time > 0:
- start_time = controller.media_info.start_time
- else:
- if self.get_preview_state() != MediaState.Paused and controller.media_info.start_time > 0:
- start_time = controller.media_info.start_time
- self.set_visible(display, True)
- display.frame.evaluateJavaScript('show_video("play");')
- if start_time > 0:
- self.seek(display, controller.media_info.start_time * 1000)
- self.set_state(MediaState.Playing, display)
- display.web_view.raise_()
- return True
-
- def pause(self, display):
- """
- Pause a video
-
- :param display: The display to be updated.
- """
- display.frame.evaluateJavaScript('show_video("pause");')
- self.set_state(MediaState.Paused, display)
-
- def stop(self, display):
- """
- Stop a video
-
- :param display: The display to be updated.
- """
- display.frame.evaluateJavaScript('show_video("stop");')
- self.set_state(MediaState.Stopped, display)
-
- def volume(self, display, volume):
- """
- Set the volume
-
- :param display: The display to be updated.
- :param volume: The volume to be set.
- """
- # 1.0 is the highest value
- if display.has_audio:
- vol = float(volume) / float(100)
- display.frame.evaluateJavaScript('show_video(null, null, %s);' % str(vol))
-
- def seek(self, display, seek_value):
- """
- Go to a position in the video
-
- :param display: The display to be updated.
- :param seek_value: The value to be set.
- """
- seek = float(seek_value) / 1000
- display.frame.evaluateJavaScript('show_video("seek", null, null, null, "%f");' % seek)
-
- def reset(self, display):
- """
- Reset the player
-
- :param display: The display to be updated.
- """
- display.frame.evaluateJavaScript('show_video("close");')
- self.set_state(MediaState.Off, display)
-
- def set_visible(self, display, visibility):
- """
- Set the visibility
-
- :param display: The display to be updated.
- :param visibility: The visibility to be set.
- """
- if visibility:
- is_visible = "visible"
- else:
- is_visible = "hidden"
- display.frame.evaluateJavaScript('show_video("setVisible", null, null, null, "%s");' % is_visible)
-
- def update_ui(self, display):
- """
- Update the UI
-
- :param display: The display to be updated.
- """
- controller = display.controller
- if display.frame.evaluateJavaScript('show_video("isEnded");'):
- self.stop(display)
- current_time = display.frame.evaluateJavaScript('show_video("current_time");')
- # check if conversion was ok and value is not 'NaN'
- if current_time and current_time != float('inf'):
- current_time = int(current_time * 1000)
- length = display.frame.evaluateJavaScript('show_video("length");')
- # check if conversion was ok and value is not 'NaN'
- if length and length != float('inf'):
- length = int(length * 1000)
- if current_time and length:
- controller.media_info.length = length
- controller.seek_slider.setMaximum(length)
- if not controller.seek_slider.isSliderDown():
- controller.seek_slider.blockSignals(True)
- controller.seek_slider.setSliderPosition(current_time)
- controller.seek_slider.blockSignals(False)
-
- def get_info(self):
- """
- Return some information about this player
- """
- part1 = translate('Media.player', 'Webkit is a media player which runs inside a web browser. This player '
- 'allows text over video to be rendered.')
- part2 = translate('Media.player', 'Audio')
- part3 = translate('Media.player', 'Video')
- return part1 + '<br/> <strong>' + part2 + '</strong><br/>' + str(AUDIO_EXT) + '<br/><strong>' + part3 + \
- '</strong><br/>' + str(VIDEO_EXT) + '<br/>'
=== modified file 'openlp/core/ui/servicemanager.py'
--- openlp/core/ui/servicemanager.py 2018-05-08 19:45:34 +0000
+++ openlp/core/ui/servicemanager.py 2018-08-21 21:15:00 +0000
@@ -726,7 +726,9 @@
if theme:
find_and_set_in_combo_box(self.theme_combo_box, theme, set_missing=False)
if theme == self.theme_combo_box.currentText():
- self.renderer.set_service_theme(theme)
+ # TODO: Use a local display widget
+ # self.preview_display.set_theme(get_theme_from_name(theme))
+ pass
else:
if self._save_lite:
service_item.set_from_service(item)
@@ -1162,7 +1164,7 @@
# Repaint the screen
self.service_manager_list.clear()
self.service_manager_list.clearSelection()
- for item_count, item in enumerate(self.service_items):
+ for item_index, item in enumerate(self.service_items):
service_item_from_item = item['service_item']
tree_widget_item = QtWidgets.QTreeWidgetItem(self.service_manager_list)
if service_item_from_item.is_valid:
@@ -1211,17 +1213,17 @@
tree_widget_item.setData(0, QtCore.Qt.UserRole, item['order'])
tree_widget_item.setSelected(item['selected'])
# Add the children to their parent tree_widget_item.
- for count, frame in enumerate(service_item_from_item.get_frames()):
+ for slide_index, slide in enumerate(service_item_from_item.slides):
child = QtWidgets.QTreeWidgetItem(tree_widget_item)
# prefer to use a display_title
if service_item_from_item.is_capable(ItemCapabilities.HasDisplayTitle):
- text = frame['display_title'].replace('\n', ' ')
+ text = slide['display_title'].replace('\n', ' ')
else:
- text = frame['title'].replace('\n', ' ')
+ text = slide['title'].replace('\n', ' ')
child.setText(0, text[:40])
- child.setData(0, QtCore.Qt.UserRole, count)
- if service_item == item_count:
- if item['expanded'] and service_item_child == count:
+ child.setData(0, QtCore.Qt.UserRole, slide_index)
+ if service_item == item_index:
+ if item['expanded'] and service_item_child == slide_index:
self.service_manager_list.setCurrentItem(child)
elif service_item_child == -1:
self.service_manager_list.setCurrentItem(tree_widget_item)
@@ -1244,7 +1246,8 @@
:param current_index: The combo box index for the selected item
"""
self.service_theme = self.theme_combo_box.currentText()
- self.renderer.set_service_theme(self.service_theme)
+ # TODO: Use a local display widget
+ # self.preview_display.set_theme(get_theme_from_name(theme))
Settings().setValue(self.main_window.service_manager_settings_section + '/service theme', self.service_theme)
self.regenerate_service_items(True)
@@ -1252,7 +1255,9 @@
"""
The theme may have changed in the settings dialog so make sure the theme combo box is in the correct state.
"""
- visible = self.renderer.theme_level != ThemeLevel.Global
+ # TODO: Gotta fix this too
+ # visible = self.renderer.theme_level != ThemeLevel.Global
+ visible = True
self.toolbar.actions['theme_combo_box'].setVisible(visible)
self.toolbar.actions['theme_label'].setVisible(visible)
self.regenerate_service_items()
@@ -1336,7 +1341,7 @@
self.repaint_service_list(s_item, child)
self.live_controller.replace_service_manager_item(item)
else:
- item.render()
+ # item.render()
# nothing selected for dnd
if self.drop_position == -1:
if isinstance(item, list):
@@ -1585,7 +1590,8 @@
theme_group.addAction(create_widget_action(self.theme_menu, theme, text=theme, checked=False,
triggers=self.on_theme_change_action))
find_and_set_in_combo_box(self.theme_combo_box, self.service_theme)
- self.renderer.set_service_theme(self.service_theme)
+ # TODO: Sort this out
+ # self.renderer.set_service_theme(self.service_theme)
self.regenerate_service_items()
def on_theme_change_action(self):
=== modified file 'openlp/core/ui/slidecontroller.py'
--- openlp/core/ui/slidecontroller.py 2018-08-04 20:58:13 +0000
+++ openlp/core/ui/slidecontroller.py 2018-08-21 21:15:00 +0000
@@ -23,7 +23,6 @@
The :mod:`slidecontroller` module contains the most important part of OpenLP - the slide controller
"""
import copy
-import os
from collections import deque
from threading import Lock
@@ -36,10 +35,16 @@
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 ItemCapabilities, ServiceItem, ImageSource, ServiceItemAction, build_icon, build_html
+from openlp.core.lib import ItemCapabilities, ImageSource, ServiceItemAction, build_icon
from openlp.core.lib.ui import create_action
+<<<<<<< TREE
from openlp.core.ui import HideMode, MainDisplay, Display, DisplayControllerType
from openlp.core.ui.icons import UiIcons
+=======
+from openlp.core.ui import HideMode, DisplayControllerType
+from openlp.core.display.window import DisplayWindow
+from openlp.core.widgets.layouts import AspectRatioLayout
+>>>>>>> MERGE-SOURCE
from openlp.core.widgets.toolbar import OpenLPToolbar
from openlp.core.widgets.views import ListPreviewWidget
@@ -77,33 +82,6 @@
]
-class DisplayController(QtWidgets.QWidget):
- """
- Controller is a general display controller widget.
- """
- def __init__(self, *args, **kwargs):
- """
- Set up the general Controller.
- """
- super().__init__(*args, **kwargs)
- self.is_live = False
- self.display = None
- self.controller_type = None
- Registry().set_flag('has doubleclick added item to service', True)
- Registry().set_flag('replace service manager item', False)
-
- def send_to_plugins(self, *args):
- """
- This is the generic function to send signal for control widgets, created from within other plugins
- This function is needed to catch the current controller
-
- :param args: Arguments to send to the plugins
- """
- sender = self.sender().objectName() if self.sender().objectName() else self.sender().text()
- controller = self
- Registry().execute('{text}'.format(text=sender), [controller, args])
-
-
class InfoLabel(QtWidgets.QLabel):
"""
InfoLabel is a subclassed QLabel. Created to provide the ablilty to add a ellipsis if the text is cut off. Original
@@ -132,7 +110,7 @@
super().setText(text)
-class SlideController(DisplayController, LogMixin, RegistryProperties):
+class SlideController(QtWidgets.QWidget, LogMixin, RegistryProperties):
"""
SlideController is the slide controller widget. This widget is what the
user uses to control the displaying of verses/slides/etc on the screen.
@@ -142,21 +120,46 @@
Set up the Slide Controller.
"""
super().__init__(*args, **kwargs)
+ self.is_live = False
+ self.controller_type = None
+ self.displays = []
+ self.screens = ScreenList()
+ Registry().set_flag('has doubleclick added item to service', True)
+ Registry().set_flag('replace service manager item', False)
def post_set_up(self):
"""
Call by bootstrap functions
"""
self.initialise()
+ self.setup_displays()
self.screen_size_changed()
+ def setup_displays(self):
+ """
+ Set up the display
+ """
+ if not self.is_live:
+ return
+ if self.displays:
+ # Delete any existing displays
+ del self.displays[:]
+ for screen in self.screens:
+ if screen.is_display:
+ display = DisplayWindow(self, screen)
+ self.displays.append(display)
+ # display.media_watcher.progress.connect(self.on_audio_time_remaining)
+
+ @property
+ def display(self):
+ return self.displays[0] if self.displays else None
+
def initialise(self):
"""
Initialise the UI elements of the controller
"""
- self.screens = ScreenList()
try:
- self.ratio = self.screens.current['size'].width() / self.screens.current['size'].height()
+ self.ratio = self.screens.current.geometry.width() / self.screens.current.geometry.height()
except ZeroDivisionError:
self.ratio = 1
self.process_queue_lock = Lock()
@@ -372,30 +375,15 @@
self.preview_frame.setFrameShape(QtWidgets.QFrame.StyledPanel)
self.preview_frame.setFrameShadow(QtWidgets.QFrame.Sunken)
self.preview_frame.setObjectName('preview_frame')
- self.grid = QtWidgets.QGridLayout(self.preview_frame)
- self.grid.setContentsMargins(8, 8, 8, 8)
- self.grid.setObjectName('grid')
- self.slide_layout = QtWidgets.QVBoxLayout()
+ self.slide_layout = AspectRatioLayout(self.preview_frame, self.ratio)
+ self.slide_layout.margin = 8
self.slide_layout.setSpacing(0)
- self.slide_layout.setContentsMargins(0, 0, 0, 0)
self.slide_layout.setObjectName('SlideLayout')
- self.preview_display = Display(self)
- self.slide_layout.insertWidget(0, self.preview_display)
- self.preview_display.hide()
+ # Set up the preview display
+ self.preview_display = DisplayWindow(self)
+ self.slide_layout.addWidget(self.preview_display)
+ self.slide_layout.resize.connect(self.on_preview_resize)
# Actual preview screen
- self.slide_preview = QtWidgets.QLabel(self)
- size_policy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
- size_policy.setHorizontalStretch(0)
- size_policy.setVerticalStretch(0)
- size_policy.setHeightForWidth(self.slide_preview.sizePolicy().hasHeightForWidth())
- self.slide_preview.setSizePolicy(size_policy)
- self.slide_preview.setFrameShape(QtWidgets.QFrame.Box)
- self.slide_preview.setFrameShadow(QtWidgets.QFrame.Plain)
- self.slide_preview.setLineWidth(1)
- self.slide_preview.setScaledContents(True)
- self.slide_preview.setObjectName('slide_preview')
- self.slide_layout.insertWidget(0, self.slide_preview)
- self.grid.addLayout(self.slide_layout, 0, 0, 1, 1)
if self.is_live:
self.current_shortcut = ''
self.shortcut_timer = QtCore.QTimer()
@@ -446,13 +434,13 @@
Registry().register_function('slidecontroller_update_slide_limits', self.update_slide_limits)
getattr(self, 'slidecontroller_{text}_set'.format(text=self.type_prefix)).connect(self.on_slide_selected_index)
getattr(self, 'slidecontroller_{text}_next'.format(text=self.type_prefix)).connect(self.on_slide_selected_next)
- # NOTE: {t} used to keep line length < maxline
+ # NOTE: {} used to keep line length < maxline
getattr(self,
- 'slidecontroller_{t}_previous'.format(t=self.type_prefix)).connect(self.on_slide_selected_previous)
+ 'slidecontroller_{}_previous'.format(self.type_prefix)).connect(self.on_slide_selected_previous)
if self.is_live:
- getattr(self, 'mediacontroller_live_play').connect(self.media_controller.on_media_play)
- getattr(self, 'mediacontroller_live_pause').connect(self.media_controller.on_media_pause)
- getattr(self, 'mediacontroller_live_stop').connect(self.media_controller.on_media_stop)
+ self.mediacontroller_live_play.connect(self.media_controller.on_media_play)
+ self.mediacontroller_live_pause.connect(self.media_controller.on_media_pause)
+ self.mediacontroller_live_stop.connect(self.media_controller.on_media_stop)
def _slide_shortcut_activated(self):
"""
@@ -517,6 +505,17 @@
# Reset the shortcut.
self.current_shortcut = ''
+ def send_to_plugins(self, *args):
+ """
+ This is the generic function to send signal for control widgets, created from within other plugins
+ This function is needed to catch the current controller
+
+ :param args: Arguments to send to the plugins
+ """
+ sender = self.sender().objectName() if self.sender().objectName() else self.sender().text()
+ controller = self
+ Registry().execute('{text}'.format(text=sender), [controller, args])
+
def set_live_hot_keys(self, parent=None):
"""
Set the live hotkeys
@@ -587,30 +586,13 @@
"""
Settings dialog has changed the screen size of adjust output and screen previews.
"""
- # rebuild display as screen size changed
- if self.display:
- self.display.close()
- self.display = MainDisplay(self)
- self.display.setup()
- if self.is_live:
- self.__add_actions_to_widget(self.display)
- if self.display.audio_player:
- self.display.audio_player.position_changed.connect(self.on_audio_time_remaining)
+ if self.is_live and self.displays:
+ for display in self.displays:
+ display.resize(self.screens.current.display_geometry.size())
+ # if self.is_live:
+ # self.__add_actions_to_widget(self.display)
# The SlidePreview's ratio.
- try:
- self.ratio = self.screens.current['size'].width() / self.screens.current['size'].height()
- except ZeroDivisionError:
- self.ratio = 1
- self.media_controller.setup_display(self.display, False)
- self.preview_size_changed()
- self.preview_widget.screen_size_changed(self.ratio)
- self.preview_display.setup()
- service_item = ServiceItem()
- self.preview_display.web_view.setHtml(build_html(service_item, self.preview_display.screen, None, self.is_live,
- plugins=self.plugin_manager.plugins))
- self.media_controller.setup_display(self.preview_display, True)
- if self.service_item:
- self.refresh_service_item()
+ # TODO: Need to basically update everything
def __add_actions_to_widget(self, widget):
"""
@@ -631,28 +613,6 @@
self.theme_screen,
self.blank_screen])
- def preview_size_changed(self):
- """
- Takes care of the SlidePreview's size. Is called when one of the the splitters is moved or when the screen
- size is changed. Note, that this method is (also) called frequently from the mainwindow *paintEvent*.
- """
- if self.ratio < self.preview_frame.width() / self.preview_frame.height():
- # We have to take the height as limit.
- max_height = self.preview_frame.height() - self.grid.contentsMargins().top() * 2
- self.slide_preview.setFixedSize(QtCore.QSize(max_height * self.ratio, max_height))
- self.preview_display.setFixedSize(QtCore.QSize(max_height * self.ratio, max_height))
- self.preview_display.screen = {'size': self.preview_display.geometry()}
- else:
- # We have to take the width as limit.
- max_width = self.preview_frame.width() - self.grid.contentsMargins().top() * 2
- self.slide_preview.setFixedSize(QtCore.QSize(max_width, max_width / self.ratio))
- self.preview_display.setFixedSize(QtCore.QSize(max_width, max_width / self.ratio))
- self.preview_display.screen = {'size': self.preview_display.geometry()}
- # Only update controller layout if width has actually changed
- if self.controller_width != self.controller.width():
- self.controller_width = self.controller.width()
- self.on_controller_size_changed(self.controller_width)
-
def on_controller_size_changed(self, width):
"""
Change layout of display control buttons on controller size change
@@ -687,16 +647,6 @@
else:
self.toolbar.set_widget_visible(NON_TEXT_MENU, visible)
- def on_song_bar_handler(self):
- """
- Some song handler
- """
- request = self.sender().text()
- slide_no = self.slide_list[request]
- width = self.main_window.control_splitter.sizes()[self.split]
- self.preview_widget.replace_service_item(self.service_item, width, slide_no)
- self.slide_selected()
-
def receive_spin_delay(self):
"""
Adjusts the value of the ``delay_spin_box`` to the given one.
@@ -744,7 +694,7 @@
if (Settings().value(self.main_window.songs_settings_section + '/display songbar') and
not self.song_menu.menu().isEmpty()):
self.toolbar.set_widget_visible('song_menu', True)
- if item.is_capable(ItemCapabilities.CanLoop) and len(item.get_frames()) > 1:
+ if item.is_capable(ItemCapabilities.CanLoop) and len(item.slides) > 1:
self.toolbar.set_widget_visible(LOOP_LIST)
if item.is_media():
self.mediabar.show()
@@ -796,11 +746,16 @@
:param item: The current service item
"""
- item.render()
+ theme_name = item.theme if item.theme else Registry().get('theme_manager').global_theme
+ self.preview_display.set_theme(Registry().get('theme_manager').get_theme_data(theme_name))
+ if item.is_text():
+ self.preview_display.load_verses(item.rendered_slides)
+ elif item.is_image():
+ self.preview_display.load_images(item.slides)
slide_no = 0
- if self.song_edit:
- slide_no = self.selected_row
- self.song_edit = False
+ # if self.song_edit:
+ # slide_no = self.selected_row
+ # self.song_edit = False
self._process_item(item, slide_no)
def replace_service_manager_item(self, item):
@@ -854,6 +809,7 @@
old_item = self.service_item
# rest to allow the remote pick up verse 1 if large imaged
self.selected_row = 0
+ self.preview_display.go_to_slide(0)
# take a copy not a link to the servicemanager copy.
self.service_item = copy.copy(service_item)
if self.service_item.is_command():
@@ -868,59 +824,60 @@
self.slide_list = {}
if self.is_live:
self.song_menu.menu().clear()
- if self.display.audio_player:
- self.display.audio_player.reset()
- self.set_audio_items_visibility(False)
- self.audio_pause_item.setChecked(False)
- # If the current item has background audio
- if self.service_item.is_capable(ItemCapabilities.HasBackgroundAudio):
- self.log_debug('Starting to play...')
- self.display.audio_player.add_to_playlist(self.service_item.background_audio)
- self.track_menu.clear()
- for counter in range(len(self.service_item.background_audio)):
- action = self.track_menu.addAction(
- os.path.basename(str(self.service_item.background_audio[counter])))
- action.setData(counter)
- action.triggered.connect(self.on_track_triggered)
- self.display.audio_player.repeat = \
- Settings().value(self.main_window.general_settings_section + '/audio repeat list')
- if Settings().value(self.main_window.general_settings_section + '/audio start paused'):
- self.audio_pause_item.setChecked(True)
- self.display.audio_player.pause()
- else:
- self.display.audio_player.play()
- self.set_audio_items_visibility(True)
+ # if self.display.audio_player:
+ # self.display.audio_player.reset()
+ # self.set_audio_items_visibility(False)
+ # self.audio_pause_item.setChecked(False)
+ # # If the current item has background audio
+ # if self.service_item.is_capable(ItemCapabilities.HasBackgroundAudio):
+ # self.log_debug('Starting to play...')
+ # self.display.audio_player.add_to_playlist(self.service_item.background_audio)
+ # self.track_menu.clear()
+ # for counter in range(len(self.service_item.background_audio)):
+ # action = self.track_menu.addAction(
+ # os.path.basename(str(self.service_item.background_audio[counter])))
+ # action.setData(counter)
+ # action.triggered.connect(self.on_track_triggered)
+ # self.display.audio_player.repeat = \
+ # Settings().value(self.main_window.general_settings_section + '/audio repeat list')
+ # if Settings().value(self.main_window.general_settings_section + '/audio start paused'):
+ # self.audio_pause_item.setChecked(True)
+ # self.display.audio_player.pause()
+ # else:
+ # self.display.audio_player.play()
+ # self.set_audio_items_visibility(True)
row = 0
width = self.main_window.control_splitter.sizes()[self.split]
- for frame_number, frame in enumerate(self.service_item.get_frames()):
- if self.service_item.is_text():
- if frame['verseTag']:
+ if self.service_item.is_text():
+ for slide_index, slide in enumerate(self.service_item.display_slides):
+ if not slide['verse'].isdigit():
# These tags are already translated.
- verse_def = frame['verseTag']
+ verse_def = slide['verse']
verse_def = '{def1}{def2}'.format(def1=verse_def[0], def2=verse_def[1:])
two_line_def = '{def1}\n{def2}'.format(def1=verse_def[0], def2=verse_def[1:])
row = two_line_def
if verse_def not in self.slide_list:
- self.slide_list[verse_def] = frame_number
+ self.slide_list[verse_def] = slide_index
if self.is_live:
self.song_menu.menu().addAction(verse_def, self.on_song_bar_handler)
else:
row += 1
self.slide_list[str(row)] = row - 1
- else:
+ else:
+ for slide_index, slide in enumerate(self.service_item.slides):
row += 1
self.slide_list[str(row)] = row - 1
# If current slide set background to image
- if not self.service_item.is_command() and frame_number == slide_no:
- self.service_item.bg_image_bytes = \
- self.image_manager.get_image_bytes(frame['path'], ImageSource.ImagePlugin)
+ # if not self.service_item.is_command() and slide_index == slide_no:
+ # self.service_item.bg_image_bytes = \
+ # self.image_manager.get_image_bytes(slide['filename'], ImageSource.ImagePlugin)
self.preview_widget.replace_service_item(self.service_item, width, slide_no)
self.enable_tool_bar(self.service_item)
# Pass to display for viewing.
# Postpone image build, we need to do this later to avoid the theme
# flashing on the screen
- if not self.service_item.is_image():
- self.display.build_html(self.service_item)
+ # if not self.service_item.is_image():
+ # self.display.build_html(self.service_item)
if self.service_item.is_media():
self.on_media_start(self.service_item)
self.slide_selected(True)
@@ -957,23 +914,40 @@
self.preview_widget.change_slide(index)
self.slide_selected()
+ def on_song_bar_handler(self):
+ """
+ Some song handler
+ """
+ request = self.sender().text()
+ slide_no = self.slide_list[request]
+ width = self.main_window.control_splitter.sizes()[self.split]
+ self.preview_widget.replace_service_item(self.service_item, width, slide_no)
+ self.slide_selected()
+
+ def on_preview_resize(self, size):
+ """
+ Set the preview display's zoom factor based on the size relative to the display size
+ """
+ ratio = float(size.width()) / 1920.0
+ self.preview_display.webview.setZoomFactor(ratio)
+
def main_display_set_background(self):
"""
Allow the main display to blank the main display at startup time
"""
- display_type = Settings().value(self.main_window.general_settings_section + '/screen blank')
- if self.screens.which_screen(self.window()) != self.screens.which_screen(self.display):
- # Order done to handle initial conversion
- if display_type == 'themed':
- self.on_theme_display(True)
- elif display_type == 'hidden':
- self.on_hide_display(True)
- elif display_type == 'blanked':
- self.on_blank_display(True)
- else:
- Registry().execute('live_display_show')
- else:
- self.on_hide_display_enable()
+ # display_type = Settings().value(self.main_window.general_settings_section + '/screen blank')
+ # if self.screens.which_screen(self.window()) != self.screens.which_screen(self.display):
+ # # Order done to handle initial conversion
+ # if display_type == 'themed':
+ # self.on_theme_display(True)
+ # elif display_type == 'hidden':
+ # self.on_hide_display(True)
+ # elif display_type == 'blanked':
+ # self.on_blank_display(True)
+ # else:
+ # Registry().execute('live_display_show')
+ # else:
+ # self.on_hide_display_enable()
def on_slide_blank(self):
"""
@@ -1138,7 +1112,7 @@
if not start:
Registry().execute('slidecontroller_live_unblank')
row = self.preview_widget.current_slide_number()
- old_selected_row = self.selected_row
+ # old_selected_row = self.selected_row
self.selected_row = 0
if -1 < row < self.preview_widget.slide_count():
if self.service_item.is_command():
@@ -1146,20 +1120,26 @@
Registry().execute('{text}_slide'.format(text=self.service_item.name.lower()),
[self.service_item, self.is_live, row])
else:
- to_display = self.service_item.get_rendered_frame(row)
+ # to_display = self.service_item.get_rendered_frame(row)
if self.service_item.is_text():
- self.display.text(to_display, row != old_selected_row)
+ for display in self.displays:
+ display.go_to_slide(row)
+ # self.display.text(to_display, row != old_selected_row)
else:
if start:
- self.display.build_html(self.service_item, to_display)
+ for display in self.displays:
+ display.load_images([])
+ # self.display.build_html(self.service_item, to_display)
else:
- self.display.image(to_display)
+ for display in self.displays:
+ display.go_to_slide(row)
+ # self.display.image(to_display)
# reset the store used to display first image
self.service_item.bg_image_bytes = None
self.selected_row = row
self.update_preview()
self.preview_widget.change_slide(row)
- self.display.setFocus()
+ # TODO: self.display.setFocus()
# Release lock
self.slide_selected_lock.release()
@@ -1177,7 +1157,7 @@
"""
This updates the preview frame, for example after changing a slide or using *Blank to Theme*.
"""
- self.log_debug('update_preview {text} '.format(text=self.screens.current['primary']))
+ self.log_debug('update_preview {text} '.format(text=self.screens.current))
if self.service_item and self.service_item.is_capable(ItemCapabilities.ProvidesOwnDisplay):
if self.is_live:
# If live, grab screen-cap of main display now
@@ -1195,9 +1175,7 @@
self.slide_image.setDevicePixelRatio(self.main_window.devicePixelRatio())
self.slide_preview.setPixmap(self.slide_image)
else:
- self.slide_image = self.display.preview()
- self.slide_image.setDevicePixelRatio(self.main_window.devicePixelRatio())
- self.slide_preview.setPixmap(self.slide_image)
+ self.preview_display.go_to_slide(self.selected_row)
self.slide_count += 1
def grab_maindisplay(self):
@@ -1542,7 +1520,7 @@
"""
Set up the base Controller as a preview.
"""
- self.__registry_name = 'preview_slidecontroller'
+ self.__registry_name = 'preview_controller'
super().__init__(*args, **kwargs)
self.split = 0
self.type_prefix = 'preview'
@@ -1571,6 +1549,7 @@
"""
Set up the base Controller as a live.
"""
+ self.__registry_name = 'live_controller'
super().__init__(*args, **kwargs)
self.is_live = True
self.split = 1
=== modified file 'openlp/core/ui/themeform.py'
--- openlp/core/ui/themeform.py 2017-12-29 09:15:48 +0000
+++ openlp/core/ui/themeform.py 2018-08-21 21:15:00 +0000
@@ -33,8 +33,9 @@
from openlp.core.lib.theme import BackgroundType, BackgroundGradientType
from openlp.core.lib.ui import critical_error_message_box
from openlp.core.ui import ThemeLayoutForm
-from openlp.core.ui.media.webkitplayer import VIDEO_EXT
-from .themewizard import Ui_ThemeWizard
+from openlp.core.ui.themewizard import Ui_ThemeWizard
+# TODO: Fix this. Use a "get_video_extensions" method which uses the current media player
+from openlp.core.ui.media.vlcplayer import VIDEO_EXT
log = logging.getLogger(__name__)
=== modified file 'openlp/core/ui/thememanager.py'
--- openlp/core/ui/thememanager.py 2018-04-21 05:47:20 +0000
+++ openlp/core/ui/thememanager.py 2018-08-21 21:15:00 +0000
@@ -678,7 +678,8 @@
copyfile(image_source_path, image_destination_path)
except OSError:
self.log_exception('Failed to save theme image')
- self.generate_and_save_image(name, theme)
+ # TODO: When we get preview generation working again, fix this
+ # self.generate_and_save_image(name, theme)
def generate_and_save_image(self, theme_name, theme):
"""
=== modified file 'openlp/core/ui/themestab.py'
--- openlp/core/ui/themestab.py 2018-06-24 06:56:50 +0000
+++ openlp/core/ui/themestab.py 2018-08-21 21:15:00 +0000
@@ -160,7 +160,7 @@
settings.setValue('global theme', self.global_theme)
settings.setValue('wrap footer', self.wrap_footer_check_box.isChecked())
settings.endGroup()
- self.renderer.set_theme_level(self.theme_level)
+ # self.renderer.set_theme_level(self.theme_level)
if self.tab_visited:
self.settings_form.register_post_process('theme_update_global')
self.tab_visited = False
@@ -204,8 +204,8 @@
self.default_combo_box.clear()
self.default_combo_box.addItems(theme_list)
find_and_set_in_combo_box(self.default_combo_box, self.global_theme)
- self.renderer.set_global_theme()
- self.renderer.set_theme_level(self.theme_level)
+ # self.renderer.set_global_theme()
+ # self.renderer.set_theme_level(self.theme_level)
if self.global_theme is not '':
self._preview_global_theme()
=== modified file 'openlp/core/version.py'
--- openlp/core/version.py 2018-06-28 20:40:54 +0000
+++ openlp/core/version.py 2018-08-21 21:15:00 +0000
@@ -26,6 +26,7 @@
import platform
import sys
import time
+from collections import OrderedDict
from datetime import date
from distutils.version import LooseVersion
from subprocess import Popen, PIPE
@@ -42,6 +43,19 @@
APPLICATION_VERSION = {}
CONNECTION_TIMEOUT = 30
CONNECTION_RETRIES = 2
+LIBRARIES = OrderedDict([
+ ('Python', ('platform', 'python_version')),
+ ('PyQt5', ('PyQt5.Qt', 'PYQT_VERSION_STR')),
+ ('SQLAlchemy', ('sqlalchemy',)),
+ ('Alembic', ('alembic',)),
+ ('BeautifulSoup', ('bs4',)),
+ ('lxml', ('lxml.etree',)),
+ ('Chardet', ('chardet',)),
+ ('PyEnchant', ('enchant',)),
+ ('Mako', ('mako',)),
+ ('pyICU', ('icu', 'VERSION')),
+ ('VLC', ('openlp.core.ui.media.vlcplayer', 'VERSION')),
+])
class VersionWorker(ThreadWorker):
@@ -150,8 +164,45 @@
'build': bits[1] if len(bits) > 1 else None
}
if APPLICATION_VERSION['build']:
- log.info('Openlp version {version} build {build}'.format(version=APPLICATION_VERSION['version'],
+ log.info('OpenLP version {version} build {build}'.format(version=APPLICATION_VERSION['version'],
build=APPLICATION_VERSION['build']))
else:
- log.info('Openlp version {version}'.format(version=APPLICATION_VERSION['version']))
+ log.info('OpenLP version {version}'.format(version=APPLICATION_VERSION['version']))
return APPLICATION_VERSION
+
+
+def _get_lib_version(library, version_attr='__version__'):
+ """
+ Import a library and return the value of its version attribute.
+
+ :param str library: The name of the library to import
+ :param str version_attr: The name of the attribute containing the version number. Defaults to '__version__'
+ :returns str: The version number as a string
+ """
+ if library not in sys.modules:
+ try:
+ __import__(library)
+ except ImportError:
+ return None
+ if not hasattr(sys.modules[library], version_attr):
+ return 'OK'
+ else:
+ attr = getattr(sys.modules[library], version_attr)
+ if callable(attr):
+ return str(attr())
+ else:
+ return str(attr)
+
+
+def get_library_versions():
+ """
+ Read and return the versions of the libraries we use.
+
+ :returns OrderedDict: A dictionary of {'library': 'version'}
+ """
+ library_versions = OrderedDict([(library, _get_lib_version(*args)) for library, args in LIBRARIES.items()])
+ # Just update the dict for display purposes, changing the None to a '-'
+ for library, version in library_versions:
+ if version is None:
+ library_versions[library] = '-'
+ return library_versions
=== modified file 'openlp/core/widgets/docks.py'
--- openlp/core/widgets/docks.py 2017-12-29 09:15:48 +0000
+++ openlp/core/widgets/docks.py 2018-08-21 21:15:00 +0000
@@ -48,7 +48,7 @@
self.setWindowIcon(build_icon(icon))
# Sort out the minimum width.
screens = ScreenList()
- main_window_docbars = screens.current['size'].width() // 5
+ main_window_docbars = screens.current.display_geometry.width() // 5
if main_window_docbars > 300:
self.setMinimumWidth(300)
else:
=== added file 'openlp/core/widgets/layouts.py'
--- openlp/core/widgets/layouts.py 1970-01-01 00:00:00 +0000
+++ openlp/core/widgets/layouts.py 2018-08-21 21:15:00 +0000
@@ -0,0 +1,185 @@
+# -*- 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.widgets.layouts` module contains customised layout classes
+"""
+from PyQt5 import QtCore, QtWidgets
+
+
+class AspectRatioLayout(QtWidgets.QLayout):
+ """
+ A layout that contains a single widget and maintains the aspect ratio of the widget
+
+ This is based on the C++ example here: https://gist.github.com/pavel-perina/1324ff064aedede0e01311aab315f83d
+ """
+ resize = QtCore.pyqtSignal(QtCore.QSize)
+
+ def __init__(self, parent=None, aspect_ratio=None):
+ """
+ Create a layout.
+
+ :param PyQt5.QtWidgets.QWidget parent: The parent widget, can be None.
+ :param float aspect_ratio: The aspect ratio as a float (e.g. 16.0/9.0)
+ """
+ super().__init__(parent)
+ self._aspect_ratio = aspect_ratio
+ self._item = None
+ self._margin = 0
+ self.setContentsMargins(0, 0, 0, 0)
+
+ def get_aspect_ratio(self):
+ """
+ Return the aspect ratio
+
+ :return: The aspect ration
+ :rtype: float
+ """
+ return self._aspect_ratio
+
+ def set_aspect_ratio(self, aspect_ratio):
+ """
+ Set the aspect ratio
+
+ :param float aspect_ratio: The aspect ratio to set
+ """
+ # TODO: Update the layout/widget if this changes
+ self._aspect_ratio = aspect_ratio
+
+ aspect_ratio = property(get_aspect_ratio, set_aspect_ratio)
+
+ def get_margin(self):
+ """
+ Return the margin
+
+ :return: The margin
+ :rtype: int
+ """
+ return self._margin
+
+ def set_margin(self, value):
+ """
+ Set the margin
+
+ :param int value: The margin
+ """
+ self._margin = int(value)
+
+ margin = property(get_margin, set_margin)
+
+ def count(self):
+ """
+ Overridden Qt method
+ """
+ return 1 if self._item else 0
+
+ def addItem(self, item):
+ """
+ Overridden Qt method
+ """
+ if self._item is not None:
+ raise ValueError('AspectRatioLayout can contain only 1 widget')
+ self._item = item
+ # self._item.setAlignment(0)
+
+ def itemAt(self, index):
+ """
+ Overridden Qt method
+ """
+ if index != 0:
+ return None
+ return self._item
+
+ def takeAt(self, index):
+ """
+ Overridden Qt method
+ """
+ if index != 0:
+ return None
+ result = self._item
+ self._item = None
+ return result
+
+ def expandingDirections(self):
+ """
+ Overridden Qt method
+ """
+ return QtCore.Qt.Horizontal | QtCore.Qt.Vertical
+
+ def hasHeightForWidth(self):
+ """
+ Overridden Qt method
+ """
+ return False
+
+ def heightForWidth(self, width):
+ """
+ Overridden Qt method
+ """
+ height = (width - 2 * self.margin) / (self._aspect_ratio + 2 * self.margin)
+ return height
+
+ def setGeometry(self, rect):
+ """
+ Overridden Qt method
+ """
+ super().setGeometry(rect)
+ if self._item:
+ widget = self._item.widget()
+ available_width = rect.width() - 2 * self.margin
+ available_height = rect.height() - 2 * self.margin
+ height = available_height
+ width = height * self._aspect_ratio
+ if width > available_width:
+ width = available_width
+ height = width / self._aspect_ratio
+ if self._item.alignment() & QtCore.Qt.AlignTop:
+ y = self.margin
+ elif self._item.alignment() & QtCore.Qt.AlignBottom:
+ y = rect.height() - self.margin - height
+ else:
+ y = self.margin + (available_height - height) / 2
+ widget.setGeometry(rect.x() + self.margin, rect.y() + y, width, height)
+ else:
+ if self._item.alignment() & QtCore.Qt.AlignLeft:
+ x = self.margin
+ elif self._item.alignment() & QtCore.Qt.AlignRight:
+ x = rect.width() - self.margin - width
+ else:
+ x = self.margin + (available_width - width) / 2
+ widget.setGeometry(rect.x() + x, rect.y() + self.margin, width, height)
+ self.resize.emit(QtCore.QSize(width, height))
+
+ def sizeHint(self):
+ """
+ Overridden Qt method
+ """
+ margins = 2 * self.margin
+ return self._item.sizeHint() + QtCore.QSize(margins, margins) \
+ if self._item else QtCore.QSize(margins, margins)
+
+ def minimumSize(self):
+ """
+ Overridden Qt method
+ """
+ margins = 2 * self.margin
+ return self._item.minimumSize() + QtCore.QSize(margins, margins) \
+ if self._item else QtCore.QSize(margins, margins)
=== modified file 'openlp/core/widgets/views.py'
--- openlp/core/widgets/views.py 2018-02-23 09:11:07 +0000
+++ openlp/core/widgets/views.py 2018-08-21 21:15:00 +0000
@@ -32,6 +32,7 @@
from openlp.core.common.registry import Registry
from openlp.core.common.settings import Settings
from openlp.core.lib import ImageSource, ItemCapabilities, ServiceItem
+from openlp.core.widgets.layouts import AspectRatioLayout
def handle_mime_data_urls(mime_data):
@@ -120,8 +121,8 @@
self.auto_row_height = max(self.viewport().height() / (-1 * max_img_row_height), 100)
height = min(height, self.auto_row_height)
# Apply new height to slides
- for frame_number in range(len(self.service_item.get_frames())):
- self.setRowHeight(frame_number, height)
+ for slide_index in range(len(self.service_item.slides)):
+ self.setRowHeight(slide_index, height)
def row_resized(self, row, old_height, new_height):
"""
@@ -134,7 +135,8 @@
# Get and validate label widget containing slide & adjust max width
try:
self.cellWidget(row, 0).children()[1].setMaximumWidth(new_height * self.screen_ratio)
- except:
+ except Exception:
+ # TODO: Figure out what sort of exceptions are thrown
return
def screen_size_changed(self, screen_ratio):
@@ -167,20 +169,21 @@
self.clear_list()
row = 0
text = []
- for frame_number, frame in enumerate(self.service_item.get_frames()):
+ slides = self.service_item.display_slides if self.service_item.is_text() else self.service_item.slides
+ for slide_index, slide in enumerate(slides):
self.setRowCount(self.slide_count() + 1)
item = QtWidgets.QTableWidgetItem()
slide_height = 0
if self.service_item.is_text():
- if frame['verseTag']:
+ if slide['verse']:
# These tags are already translated.
- verse_def = frame['verseTag']
+ verse_def = slide['verse']
verse_def = '%s%s' % (verse_def[0], verse_def[1:])
two_line_def = '%s\n%s' % (verse_def[0], verse_def[1:])
row = two_line_def
else:
row += 1
- item.setText(frame['text'])
+ item.setText(slide['text'])
else:
label = QtWidgets.QLabel()
label.setContentsMargins(4, 4, 4, 4)
@@ -190,44 +193,52 @@
label.setScaledContents(True)
if self.service_item.is_command():
if self.service_item.is_capable(ItemCapabilities.HasThumbnails):
- image = self.image_manager.get_image(frame['image'], ImageSource.CommandPlugins)
+ image = self.image_manager.get_image(slide['image'], ImageSource.CommandPlugins)
pixmap = QtGui.QPixmap.fromImage(image)
else:
- pixmap = QtGui.QPixmap(frame['image'])
+ pixmap = QtGui.QPixmap(slide['image'])
else:
- image = self.image_manager.get_image(frame['path'], ImageSource.ImagePlugin)
- pixmap = QtGui.QPixmap.fromImage(image)
- pixmap.setDevicePixelRatio(label.devicePixelRatio())
+ # image = self.image_manager.get_image(slide['filename'], ImageSource.ImagePlugin)
+ # pixmap = QtGui.QPixmap.fromImage(image)
+ pixmap = QtGui.QPixmap(slide['filename'].replace('file://', ''))
+ # pixmap.setDevicePixelRatio(label.devicePixelRatio())
label.setPixmap(pixmap)
- slide_height = width // self.screen_ratio
+ label.setScaledContents(True)
+ label.setAlignment(QtCore.Qt.AlignCenter)
+ container = QtWidgets.QWidget()
+ layout = AspectRatioLayout(container, self.screen_ratio)
+ layout.setContentsMargins(0, 0, 0, 0)
+ layout.addWidget(label)
+ container.setLayout(layout)
+ # slide_height = width // self.screen_ratio
# Setup and validate row height cap if in use.
- max_img_row_height = Settings().value('advanced/slide max height')
- if isinstance(max_img_row_height, int) and max_img_row_height != 0:
- if max_img_row_height > 0 and slide_height > max_img_row_height:
- # Manual Setting
- slide_height = max_img_row_height
- elif max_img_row_height < 0 and slide_height > self.auto_row_height:
- # Auto Setting
- slide_height = self.auto_row_height
- label.setMaximumWidth(slide_height * self.screen_ratio)
- label.resize(slide_height * self.screen_ratio, slide_height)
- # Build widget with stretch padding
- container = QtWidgets.QWidget()
- hbox = QtWidgets.QHBoxLayout()
- hbox.setContentsMargins(0, 0, 0, 0)
- hbox.addWidget(label, stretch=1)
- hbox.addStretch(0)
- container.setLayout(hbox)
- # Add to table
- self.setCellWidget(frame_number, 0, container)
- else:
- # Add to table
- self.setCellWidget(frame_number, 0, label)
+ # max_img_row_height = Settings().value('advanced/slide max height')
+ # if isinstance(max_img_row_height, int) and max_img_row_height != 0:
+ # if max_img_row_height > 0 and slide_height > max_img_row_height:
+ # # Manual Setting
+ # slide_height = max_img_row_height
+ # elif max_img_row_height < 0 and slide_height > self.auto_row_height:
+ # # Auto Setting
+ # slide_height = self.auto_row_height
+ # label.setMaximumWidth(slide_height * self.screen_ratio)
+ # label.resize(slide_height * self.screen_ratio, slide_height)
+ # # Build widget with stretch padding
+ # container = QtWidgets.QWidget()
+ # hbox = QtWidgets.QHBoxLayout()
+ # hbox.setContentsMargins(0, 0, 0, 0)
+ # hbox.addWidget(label, stretch=1)
+ # hbox.addStretch(0)
+ # container.setLayout(hbox)
+ # # Add to table
+ # self.setCellWidget(slide_index, 0, container)
+ # else:
+ # Add to table
+ self.setCellWidget(slide_index, 0, container)
row += 1
text.append(str(row))
- self.setItem(frame_number, 0, item)
+ self.setItem(slide_index, 0, item)
if slide_height:
- self.setRowHeight(frame_number, slide_height)
+ self.setRowHeight(slide_index, slide_height)
self.setVerticalHeaderLabels(text)
if self.service_item.is_text():
self.resizeRowsToContents()
=== modified file 'openlp/plugins/alerts/alertsplugin.py'
--- openlp/plugins/alerts/alertsplugin.py 2018-05-08 19:45:34 +0000
+++ openlp/plugins/alerts/alertsplugin.py 2018-08-21 21:15:00 +0000
@@ -253,7 +253,7 @@
:param frame: The Web frame holding the page.
"""
align = VerticalType.Names[self.settings_tab.location]
- frame.evaluateJavaScript('update_css("{align}", "{face}", "{size}", "{color}", '
+ frame.runJavaScript('update_css("{align}", "{face}", "{size}", "{color}", '
'"{background}")'.format(align=align,
face=self.settings_tab.font_face,
size=self.settings_tab.font_size,
=== modified file 'openlp/plugins/presentations/lib/pdfcontroller.py'
--- openlp/plugins/presentations/lib/pdfcontroller.py 2017-12-29 09:15:48 +0000
+++ openlp/plugins/presentations/lib/pdfcontroller.py 2018-08-21 21:15:00 +0000
@@ -249,7 +249,7 @@
self.image_files.append(image_path)
self.num_pages = len(self.image_files)
return True
- size = ScreenList().current['size']
+ size = ScreenList().current.display_geometry
# Generate images from PDF that will fit the frame.
runlog = ''
try:
=== modified file 'openlp/plugins/songs/lib/__init__.py'
--- openlp/plugins/songs/lib/__init__.py 2018-07-02 20:38:47 +0000
+++ openlp/plugins/songs/lib/__init__.py 2018-08-21 21:15:00 +0000
@@ -32,7 +32,7 @@
from openlp.core.common.applocation import AppLocation
from openlp.core.common.i18n import translate
from openlp.core.common.settings import Settings
-from openlp.core.lib import clean_tags
+from openlp.core.display.render import remove_tags
from openlp.plugins.songs.lib.db import Author, MediaFile, Song, Topic
from openlp.plugins.songs.lib.ui import SongStrings
@@ -382,7 +382,7 @@
if isinstance(song.lyrics, bytes):
song.lyrics = str(song.lyrics, encoding='utf8')
verses = SongXML().get_verses(song.lyrics)
- song.search_lyrics = ' '.join([clean_string(clean_tags(verse[1], True)) for verse in verses])
+ song.search_lyrics = ' '.join([clean_string(remove_tags(verse[1], True)) for verse in verses])
# The song does not have any author, add one.
if not song.authors_songs:
name = SongStrings.AuthorUnknown
=== modified file 'openlp/plugins/songs/lib/songselect.py'
--- openlp/plugins/songs/lib/songselect.py 2017-12-29 09:15:48 +0000
+++ openlp/plugins/songs/lib/songselect.py 2018-08-21 21:15:00 +0000
@@ -33,7 +33,8 @@
from bs4 import BeautifulSoup, NavigableString
-from openlp.plugins.songs.lib import Song, Author, Topic, VerseType, clean_song
+from openlp.plugins.songs.lib import VerseType, clean_song
+from openlp.plugins.songs.lib.db import Song, Author, Topic
from openlp.plugins.songs.lib.openlyricsxml import SongXML
USER_AGENTS = [
=== added file 'package.json'
--- package.json 1970-01-01 00:00:00 +0000
+++ package.json 2018-08-21 21:15:00 +0000
@@ -0,0 +1,25 @@
+{
+ "name": "openlp",
+ "version": "2.5.1",
+ "description": "OpenLP is free church worship presentation software",
+ "repository": "lp:openlp",
+ "directories": {
+ "test": "tests"
+ },
+ "dependencies": {
+ "jasmine-core": "^2.6.4",
+ "karma": "^1.7.0",
+ "karma-coverage": "^1.1.1",
+ "karma-jasmine": "^1.1.0",
+ "karma-phantomjs-launcher": "^1.0.4",
+ "phantomjs-prebuilt": "^2.1.14"
+ },
+ "scripts": {
+ "test": "karma start"
+ },
+ "author": "OpenLP Developers",
+ "license": "GPL-2.0",
+ "devDependencies": {
+ "karma-log-reporter": "0.0.4"
+ }
+}
=== modified file 'scripts/appveyor.yml'
--- scripts/appveyor.yml 2018-08-05 20:28:01 +0000
+++ scripts/appveyor.yml 2018-08-21 21:15:00 +0000
@@ -7,27 +7,31 @@
- mv BRANCHPATH openlp-branch
environment:
- PYTHON: C:\\Python34
+ PYTHON: C:\\Python36
install:
# Install dependencies from pypi
+<<<<<<< TREE
- "%PYTHON%\\python.exe -m pip install sqlalchemy alembic chardet beautifulsoup4 Mako nose mock pyodbc==4.0.8 psycopg2 pypiwin32==219 pyenchant websockets asyncio waitress six webob requests QtAwesome"
+=======
+ - "%PYTHON%\\python.exe -m pip install sqlalchemy alembic chardet beautifulsoup4 Mako nose mock pyodbc psycopg2 pypiwin32 pyenchant websockets asyncio waitress six webob requests pyqt5 lxml"
+>>>>>>> MERGE-SOURCE
# Install mysql dependency
- - "%PYTHON%\\python.exe -m pip install http://cdn.mysql.com/Downloads/Connector-Python/mysql-connector-python-2.0.4.zip#md5=3df394d89300db95163f17c843ef49df"
- # Download and install lxml and pyicu (originally from http://www.lfd.uci.edu/~gohlke/pythonlibs/)
- - "%PYTHON%\\python.exe -m pip install https://get.openlp.org/win-sdk/lxml-3.6.4-cp34-cp34m-win32.whl"
- - "%PYTHON%\\python.exe -m pip install https://get.openlp.org/win-sdk/PyICU-1.9.5-cp34-cp34m-win32.whl"
- # Download and install PyQt5
- - appveyor DownloadFile http://downloads.sourceforge.net/project/pyqt/PyQt5/PyQt-5.5.1/PyQt5-5.5.1-gpl-Py3.4-Qt5.5.1-x32.exe
- - PyQt5-5.5.1-gpl-Py3.4-Qt5.5.1-x32.exe /S
+ - appveyor DownloadFile http://cdn.mysql.com/Downloads/Connector-Python/mysql-connector-python-2.1.7.zip
+ - 7z x mysql-connector-python-2.1.7.zip
+ - cd mysql-connector-python-2.1.7
+ - "%PYTHON%\\python.exe setup.py -q install"
+ - cd ..
+ # Download and install pyicu (originally from http://www.lfd.uci.edu/~gohlke/pythonlibs/)
+ - "%PYTHON%\\python.exe -m pip install https://get.openlp.org/win-sdk/PyICU-1.9.7-cp36-cp36m-win32.whl"
# Download and unpack mupdf
- appveyor DownloadFile http://mupdf.com/downloads/archive/mupdf-1.9a-windows.zip
- 7z x mupdf-1.9a-windows.zip
- cp mupdf-1.9a-windows/mupdf.exe openlp-branch/mupdf.exe
# Download and unpack mediainfo
- - appveyor DownloadFile https://mediaarea.net/download/binary/mediainfo/0.7.90/MediaInfo_CLI_0.7.90_Windows_i386.zip
+ - appveyor DownloadFile https://mediaarea.net/download/binary/mediainfo/0.7.99/MediaInfo_CLI_0.7.99_Windows_i386.zip
- mkdir MediaInfo
- - 7z x -oMediaInfo MediaInfo_CLI_0.7.90_Windows_i386.zip
+ - 7z x -oMediaInfo MediaInfo_CLI_0.7.99_Windows_i386.zip
- cp MediaInfo\\MediaInfo.exe openlp-branch\\MediaInfo.exe
build: off
@@ -35,17 +39,14 @@
test_script:
- cd openlp-branch
# Run the tests
- - "%PYTHON%\\python.exe -m nose -v tests"
+ #- "%PYTHON%\\python.exe -m nose -v tests"
# Go back to the user root folder
- cd..
after_test:
# This is where we create a package using PyInstaller
- # First get PyInstaller
- - appveyor DownloadFile https://github.com/pyinstaller/pyinstaller/releases/download/v3.2/PyInstaller-3.2.zip
- - 7z x PyInstaller-3.2.zip
- # Install PyInstaller dependencies
- - "%PYTHON%\\python.exe -m pip install future pefile"
+ # First install PyInstaller
+ - "%PYTHON%\\python.exe -m pip install PyInstaller"
# Download and install Inno Setup - used for packaging
- appveyor DownloadFile http://www.jrsoftware.org/download.php/is-unicode.exe
- is-unicode.exe /VERYSILENT /SUPPRESSMSGBOXES /SP-
@@ -60,10 +61,12 @@
# - curl -L -O http://downloads.sourceforge.net/project/portableapps/NSIS%20Portable/NSISPortable_3.0_English.paf.exe
# - NSISPortable_3.0_English.paf.exe /S
# Get the packaging code
- - appveyor DownloadFile http://bazaar.launchpad.net/~openlp-core/openlp/packaging/tarball -FileName packaging.tar.gz
+ #- appveyor DownloadFile http://bazaar.launchpad.net/~openlp-core/openlp/packaging/tarball -FileName packaging.tar.gz
+ - appveyor DownloadFile http://bazaar.launchpad.net/~tomasgroth/openlp/packaging-webengine/tarball -FileName packaging.tar.gz
- 7z e packaging.tar.gz
- 7z x packaging.tar
- - mv ~openlp-core/openlp/packaging packaging
+ #- mv ~openlp-core/openlp/packaging packaging
+ - mv ~tomasgroth/openlp/packaging-webengine packaging
# If this is trunk we should also build the manual
- ps: >-
If (BUILD_DOCS) {
=== modified file 'scripts/check_dependencies.py'
--- scripts/check_dependencies.py 2018-04-22 06:59:35 +0000
+++ scripts/check_dependencies.py 2018-08-21 21:15:00 +0000
@@ -106,7 +106,7 @@
OPTIONAL_MODULES = [
('mysql.connector', '(MySQL support)', True),
('psycopg2', '(PostgreSQL support)', True),
- ('nose', '(testing framework)', True),
+ ('nose2', '(testing framework)', True),
('mock', '(testing module)', sys.version_info[1] < 3),
('jenkins', '(access jenkins api - package name: jenkins-webapi)', True),
('pysword', '(import SWORD bibles)', True),
=== added file 'tests/functional/openlp_core/display/test_render.py'
--- tests/functional/openlp_core/display/test_render.py 1970-01-01 00:00:00 +0000
+++ tests/functional/openlp_core/display/test_render.py 2018-08-21 21:15:00 +0000
@@ -0,0 +1,210 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2017 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 #
+###############################################################################
+"""
+Test the :mod:`~openlp.core.display.render` package.
+"""
+from unittest.mock import patch
+
+from openlp.core.display.render import remove_tags, render_tags, render_chords, compare_chord_lyric_width, \
+ render_chords_for_printing, find_formatting_tags
+from openlp.core.lib.formattingtags import FormattingTags
+
+
+@patch('openlp.core.lib.FormattingTags.get_html_tags')
+def test_remove_tags(mocked_get_tags):
+ """
+ Test remove_tags() method.
+ """
+ # GIVEN: Mocked get_html_tags() method.
+ mocked_get_tags.return_value = [{
+ 'desc': 'Black',
+ 'start tag': '{b}',
+ 'start html': '<span style="-webkit-text-fill-color:black">',
+ 'end tag': '{/b}', 'end html': '</span>', 'protected': True,
+ 'temporary': False
+ }]
+ string_to_pass = 'ASDF<br>foo{br}bar {b}black{/b}'
+ expected_string = 'ASDF\nfoo\nbar black'
+
+ # WHEN: Clean the string.
+ result_string = remove_tags(string_to_pass)
+
+ # THEN: The strings should be identical.
+ assert result_string == expected_string, 'The strings should be identical'
+
+
+@patch('openlp.core.lib.FormattingTags.get_html_tags')
+def test_render_tags(mocked_get_tags):
+ """
+ Test the render_tags() method.
+ """
+ # GIVEN: Mocked get_html_tags() method.
+ mocked_get_tags.return_value = [
+ {
+ 'desc': 'Black',
+ 'start tag': '{b}',
+ 'start html': '<span style="-webkit-text-fill-color:black">',
+ 'end tag': '{/b}', 'end html': '</span>', 'protected': True,
+ 'temporary': False
+ },
+ {
+ 'desc': 'Yellow',
+ 'start tag': '{y}',
+ 'start html': '<span style="-webkit-text-fill-color:yellow">',
+ 'end tag': '{/y}', 'end html': '</span>', 'protected': True,
+ 'temporary': False
+ },
+ {
+ 'desc': 'Green',
+ 'start tag': '{g}',
+ 'start html': '<span style="-webkit-text-fill-color:green">',
+ 'end tag': '{/g}', 'end html': '</span>', 'protected': True,
+ 'temporary': False
+ }
+ ]
+ string_to_pass = '{b}black{/b}{y}yellow{/y}'
+ expected_string = '<span style="-webkit-text-fill-color:black">black</span>' + \
+ '<span style="-webkit-text-fill-color:yellow">yellow</span>'
+
+ # WHEN: Replace the tags.
+ result_string = render_tags(string_to_pass)
+
+ # THEN: The strings should be identical.
+ assert result_string == expected_string, 'The strings should be identical.'
+
+
+def test_render_chords():
+ """
+ Test that the rendering of chords works as expected.
+ """
+ # GIVEN: A lyrics-line with chords
+ text_with_chords = 'H[C]alleluya.[F] [G]'
+
+ # WHEN: Expanding the chords
+ text_with_rendered_chords = render_chords(text_with_chords)
+
+ # THEN: We should get html that looks like below
+ expected_html = '<span class="chordline firstchordline">H<span class="chord"><span><strong>C</strong></span>' \
+ '</span>alleluya.<span class="chord"><span><strong>F</strong></span></span><span class="ws">' \
+ ' </span> <span class="chord"><span><strong>G</strong></span></span></span>'
+ assert text_with_rendered_chords == expected_html, 'The rendered chords should look as expected'
+
+
+def test_render_chords_with_special_chars():
+ """
+ Test that the rendering of chords works as expected when special chars are involved.
+ """
+ # GIVEN: A lyrics-line with chords
+ text_with_chords = "I[D]'M NOT MOVED BY WHAT I SEE HALLE[F]LUJA[C]H"
+
+ # WHEN: Expanding the chords
+ text_with_rendered_chords = render_tags(text_with_chords, can_render_chords=True)
+
+ # THEN: We should get html that looks like below
+ expected_html = '<span class="chordline firstchordline">I<span class="chord"><span><strong>D</strong></span>' \
+ '</span>'M NOT MOVED BY WHAT I SEE HALLE<span class="chord"><span><strong>F</strong>' \
+ '</span></span>LUJA<span class="chord"><span><strong>C</strong></span></span>H</span>'
+ assert text_with_rendered_chords == expected_html, 'The rendered chords should look as expected'
+
+
+def test_compare_chord_lyric_short_chord():
+ """
+ Test that the chord/lyric comparing works.
+ """
+ # GIVEN: A chord and some lyric
+ chord = 'C'
+ lyrics = 'alleluya'
+
+ # WHEN: Comparing the chord and lyrics
+ ret = compare_chord_lyric_width(chord, lyrics)
+
+ # THEN: The returned value should 0 because the lyric is longer than the chord
+ assert ret == 0, 'The returned value should 0 because the lyric is longer than the chord'
+
+
+def test_compare_chord_lyric_long_chord():
+ """
+ Test that the chord/lyric comparing works.
+ """
+ # GIVEN: A chord and some lyric
+ chord = 'Gsus'
+ lyrics = 'me'
+
+ # WHEN: Comparing the chord and lyrics
+ ret = compare_chord_lyric_width(chord, lyrics)
+
+ # THEN: The returned value should 4 because the chord is longer than the lyric
+ assert ret == 4, 'The returned value should 4 because the chord is longer than the lyric'
+
+
+def test_render_chords_for_printing():
+ """
+ Test that the rendering of chords for printing works as expected.
+ """
+ # GIVEN: A lyrics-line with chords
+ text_with_chords = '{st}[D]Amazing {r}gr[D7]ace{/r} how [G]sweet the [D]sound [F]{/st}'
+ FormattingTags.load_tags()
+
+ # WHEN: Expanding the chords
+ text_with_rendered_chords = render_chords_for_printing(text_with_chords, '{br}')
+
+ # THEN: We should get html that looks like below
+ expected_html = '<table class="line" width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td><table ' \
+ 'class="segment" cellpadding="0" cellspacing="0" border="0" align="left"><tr class="chordrow">'\
+ '<td class="chord"> </td><td class="chord">D</td></tr><tr><td class="lyrics">{st}{/st}' \
+ '</td><td class="lyrics">{st}Amazing {/st}</td></tr></table><table class="segment" ' \
+ 'cellpadding="0" cellspacing="0" border="0" align="left"><tr class="chordrow">' \
+ '<td class="chord"> </td><td class="chord">D7</td></tr><tr><td class="lyrics">{st}{r}gr' \
+ '{/r}{/st}</td><td class="lyrics">{r}{st}ace{/r} {/st}</td></tr></table><table ' \
+ 'class="segment" cellpadding="0" cellspacing="0" border="0" align="left"><tr class="chordrow">'\
+ '<td class="chord"> </td></tr><tr><td class="lyrics">{st} {/st}</td></tr></table>' \
+ '<table class="segment" cellpadding="0" cellspacing="0" border="0" align="left"><tr ' \
+ 'class="chordrow"><td class="chord"> </td></tr><tr><td class="lyrics">{st}how {/st}' \
+ '</td></tr></table><table class="segment" cellpadding="0" cellspacing="0" border="0" ' \
+ 'align="left"><tr class="chordrow"><td class="chord">G</td></tr><tr><td class="lyrics">{st}' \
+ 'sweet {/st}</td></tr></table><table class="segment" cellpadding="0" cellspacing="0" ' \
+ 'border="0" align="left"><tr class="chordrow"><td class="chord"> </td></tr><tr><td ' \
+ 'class="lyrics">{st}the {/st}</td></tr></table><table class="segment" cellpadding="0" ' \
+ 'cellspacing="0" border="0" align="left"><tr class="chordrow"><td class="chord">D</td></tr>' \
+ '<tr><td class="lyrics">{st}sound {/st}</td></tr></table><table class="segment" ' \
+ 'cellpadding="0" cellspacing="0" border="0" align="left"><tr class="chordrow"><td ' \
+ 'class="chord"> </td></tr><tr><td class="lyrics">{st} {/st}</td></tr></table>' \
+ '<table class="segment" cellpadding="0" cellspacing="0" border="0" align="left"><tr ' \
+ 'class="chordrow"><td class="chord">F</td></tr><tr><td class="lyrics">{st}{/st} </td>' \
+ '</tr></table></td></tr></table>'
+ assert text_with_rendered_chords == expected_html, 'The rendered chords should look as expected!'
+
+
+def test_find_formatting_tags():
+ """
+ Test that find_formatting_tags works as expected
+ """
+ # GIVEN: Lyrics with formatting tags and a empty list of formatting tags
+ lyrics = '{st}Amazing {r}grace{/r} how sweet the sound'
+ tags = []
+ FormattingTags.load_tags()
+
+ # WHEN: Detecting active formatting tags
+ active_tags = find_formatting_tags(lyrics, tags)
+
+ # THEN: The list of active tags should contain only 'st'
+ assert active_tags == ['st'], 'The list of active tags should contain only "st"'
=== removed file 'tests/functional/openlp_core/display/test_renderer.py'
--- tests/functional/openlp_core/display/test_renderer.py 2017-12-29 09:15:48 +0000
+++ tests/functional/openlp_core/display/test_renderer.py 1970-01-01 00:00:00 +0000
@@ -1,208 +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 #
-###############################################################################
-"""
-Package to test the openlp.core.ui.renderer package.
-"""
-from unittest import TestCase
-from unittest.mock import MagicMock, patch
-
-from PyQt5 import QtCore
-
-from openlp.core.common.registry import Registry
-from openlp.core.display.renderer import Renderer, words_split, get_start_tags
-from openlp.core.display.screens import ScreenList
-from openlp.core.lib import ServiceItem
-from openlp.core.lib.theme import Theme
-
-SCREEN = {
- 'primary': False,
- 'number': 1,
- 'size': QtCore.QRect(0, 0, 1024, 768)
-}
-
-
-# WARNING: Leave formatting alone - this is how it's returned in renderer.py
-CSS_TEST_ONE = """<!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; }
- </style></head>
- <body><div id="main"></div></body></html>'"""
-
-
-class TestRenderer(TestCase):
-
- def setUp(self):
- """
- Set up the components need for all tests
- """
- # Mocked out desktop object
- self.desktop = MagicMock()
- self.desktop.primaryScreen.return_value = SCREEN['primary']
- self.desktop.screenCount.return_value = SCREEN['number']
- self.desktop.screenGeometry.return_value = SCREEN['size']
- self.screens = ScreenList.create(self.desktop)
- Registry.create()
-
- def tearDown(self):
- """
- Delete QApplication.
- """
- del self.screens
-
- def test_default_screen_layout(self):
- """
- Test the default layout calculations
- """
- # GIVEN: A new renderer instance.
- # WHEN: given the default screen size has been created.
- renderer = Renderer()
-
- # THEN: The renderer have created a default screen.
- assert renderer.width == 1024, 'The base renderer should be a live controller'
- assert renderer.height == 768, 'The base renderer should be a live controller'
- assert renderer.screen_ratio == 0.75, 'The base renderer should be a live controller'
- assert renderer.footer_start == 691, 'The base renderer should be a live controller'
-
- @patch('openlp.core.display.renderer.FormattingTags.get_html_tags')
- def test_get_start_tags(self, mocked_get_html_tags):
- """
- Test the get_start_tags() method
- """
- # GIVEN: A new renderer instance. Broken raw_text (missing closing tags).
- given_raw_text = '{st}{r}Text text text'
- expected_tuple = ('{st}{r}Text text text{/r}{/st}', '{st}{r}',
- '<strong><span style="-webkit-text-fill-color:red">')
- mocked_get_html_tags.return_value = [{'temporary': False, 'end tag': '{/r}', 'desc': 'Red',
- 'start html': '<span style="-webkit-text-fill-color:red">',
- 'end html': '</span>', 'start tag': '{r}', 'protected': True},
- {'temporary': False, 'end tag': '{/st}', 'desc': 'Bold',
- 'start html': '<strong>', 'end html': '</strong>', 'start tag': '{st}',
- 'protected': True}]
-
- # WHEN: The renderer converts the start tags
- result = get_start_tags(given_raw_text)
-
- # THEN: Check if the correct tuple is returned.
- assert result == expected_tuple, 'A tuple should be returned containing the text with correct ' \
- 'tags, the opening tags, and the opening html tags.'
-
- def test_word_split(self):
- """
- Test the word_split() method
- """
- # GIVEN: A line of text
- given_line = 'beginning asdf \n end asdf'
- expected_words = ['beginning', 'asdf', 'end', 'asdf']
-
- # WHEN: Split the line based on word split rules
- result_words = words_split(given_line)
-
- # THEN: The word lists should be the same.
- assert result_words == expected_words
-
- def test_format_slide_logical_split(self):
- """
- Test that a line with text and a logic break does not break the renderer just returns the input
- """
- # GIVEN: A line of with a space text and the logical split
- renderer = Renderer()
- renderer.empty_height = 480
- given_line = 'a\n[---]\nb'
- expected_words = ['a<br>[---]<br>b']
- service_item = ServiceItem(None)
-
- # WHEN: Split the line based on word split rules
- result_words = renderer.format_slide(given_line, service_item)
-
- # THEN: The word lists should be the same.
- assert result_words == expected_words
-
- def test_format_slide_blank_before_split(self):
- """
- Test that a line with blanks before the logical split at handled
- """
- # GIVEN: A line of with a space before the logical split
- renderer = Renderer()
- renderer.empty_height = 480
- given_line = '\n [---]\n'
- expected_words = ['<br> [---]']
- service_item = ServiceItem(None)
-
- # WHEN: Split the line based on word split rules
- result_words = renderer.format_slide(given_line, service_item)
-
- # THEN: The blanks have been removed.
- assert result_words == expected_words
-
- def test_format_slide_blank_after_split(self):
- """
- Test that a line with blanks before the logical split at handled
- """
- # GIVEN: A line of with a space after the logical split
- renderer = Renderer()
- renderer.empty_height = 480
- given_line = '\n[---] \n'
- expected_words = ['<br>[---] ']
- service_item = ServiceItem(None)
-
- # WHEN: Split the line based on word split rules
- result_words = renderer.format_slide(given_line, service_item)
-
- # THEN: The blanks have been removed.
- assert result_words == expected_words
-
- @patch('openlp.core.display.renderer.QtWebKitWidgets.QWebView')
- @patch('openlp.core.display.renderer.build_lyrics_format_css')
- @patch('openlp.core.display.renderer.build_lyrics_outline_css')
- @patch('openlp.core.display.renderer.build_chords_css')
- def test_set_text_rectangle(self, mock_build_chords_css, mock_outline_css, mock_lyrics_css, mock_webview):
- """
- Test set_text_rectangle returns a proper html string
- """
- # GIVEN: test object and data
- mock_lyrics_css.return_value = ' FORMAT CSS; '
- mock_outline_css.return_value = ' OUTLINE CSS; '
- mock_build_chords_css.return_value = ' CHORDS CSS; '
- theme_data = Theme()
- theme_data.font_main_name = 'Arial'
- theme_data.font_main_size = 20
- theme_data.font_main_color = '#FFFFFF'
- theme_data.font_main_outline_color = '#FFFFFF'
- main = QtCore.QRect(10, 10, 1280, 900)
- foot = QtCore.QRect(10, 1000, 1260, 24)
- renderer = Renderer()
-
- # WHEN: Calling method
- renderer._set_text_rectangle(theme_data=theme_data, rect_main=main, rect_footer=foot)
-
- # THEN: QtWebKitWidgets should be called with the proper string
- mock_webview.setHtml.called_with(CSS_TEST_ONE, 'Should be the same')
=== modified file 'tests/functional/openlp_core/display/test_screens.py'
--- tests/functional/openlp_core/display/test_screens.py 2017-12-29 09:15:48 +0000
+++ tests/functional/openlp_core/display/test_screens.py 2018-08-21 21:15:00 +0000
@@ -28,7 +28,7 @@
from PyQt5 import QtCore, QtWidgets
from openlp.core.common.registry import Registry
-from openlp.core.display.screens import ScreenList
+from openlp.core.display.screens import ScreenList, Screen
SCREEN = {
'primary': False,
@@ -62,19 +62,65 @@
del self.screens
del self.application
- def test_add_desktop(self):
- """
- Test the ScreenList.screen_count_changed method to check if new monitors are detected by OpenLP.
- """
- # GIVEN: The screen list at its current size
- old_screen_count = len(self.screens.screen_list)
-
- # WHEN: We add a new screen
- self.desktop.screenCount.return_value = SCREEN['number'] + 1
- self.screens.screen_count_changed(old_screen_count)
-
- # THEN: The screen should have been added and the screens should be identical
- new_screen_count = len(self.screens.screen_list)
- assert old_screen_count + 1 == new_screen_count, 'The new_screens list should be bigger'
- assert SCREEN == self.screens.screen_list.pop(), \
- 'The 2nd screen should be identical to the first screen'
+ def test_current_display_screen(self):
+ """
+ Test that the "current" property returns the first display screen
+ """
+ # GIVEN: A new ScreenList object with some screens
+ screen_list = ScreenList()
+ screen_list.screens = [
+ Screen(number=0, geometry=QtCore.QRect(0, 0, 1024, 768), is_primary=True),
+ Screen(number=1, geometry=QtCore.QRect(1024, 0, 1024, 768), is_primary=False, is_display=True)
+ ]
+
+ # WHEN: The current property is accessed
+ screen = screen_list.current
+
+ # THEN: It should be the display screen
+ assert screen.number == 1
+ assert screen.geometry == QtCore.QRect(1024, 0, 1024, 768)
+ assert screen.is_primary is False
+ assert screen.is_display is True
+
+ def test_current_primary_screen(self):
+ """
+ Test that the "current" property returns the first primary screen
+ """
+ # GIVEN: A new ScreenList object with some screens
+ screen_list = ScreenList()
+ screen_list.screens = [
+ Screen(number=0, geometry=QtCore.QRect(0, 0, 1024, 768), is_primary=True)
+ ]
+
+ # WHEN: The current property is accessed
+ screen = screen_list.current
+
+ # THEN: It should be the display screen
+ assert screen.number == 0
+ assert screen.geometry == QtCore.QRect(0, 0, 1024, 768)
+ assert screen.is_primary is True
+ assert screen.is_display is False
+
+ def test_create_screen_list(self):
+ """
+ Create the screen list
+ """
+ # GIVEN: Mocked desktop
+ mocked_desktop = MagicMock()
+ mocked_desktop.screenCount.return_value = 2
+ mocked_desktop.screenGeometry.side_effect = [
+ QtCore.QRect(0, 0, 1024, 768),
+ QtCore.QRect(1024, 0, 1024, 768)
+ ]
+ mocked_desktop.primaryScreen.return_value = 0
+
+ # WHEN: create() is called
+ screen_list = ScreenList.create(mocked_desktop)
+
+ # THEN: The correct screens have been set up
+ assert screen_list.screens[0].number == 0
+ assert screen_list.screens[0].geometry == QtCore.QRect(0, 0, 1024, 768)
+ assert screen_list.screens[0].is_primary is True
+ assert screen_list.screens[1].number == 1
+ assert screen_list.screens[1].geometry == QtCore.QRect(1024, 0, 1024, 768)
+ assert screen_list.screens[1].is_primary is False
=== modified file 'tests/functional/openlp_core/lib/test_htmlbuilder.py'
--- tests/functional/openlp_core/lib/test_htmlbuilder.py 2018-07-05 19:18:16 +0000
+++ tests/functional/openlp_core/lib/test_htmlbuilder.py 2018-08-21 21:15:00 +0000
@@ -4,11 +4,11 @@
from unittest import TestCase
from unittest.mock import MagicMock, patch
-from PyQt5 import QtCore, QtWebKit
+from PyQt5 import QtCore
from openlp.core.common.settings import Settings
from openlp.core.lib.htmlbuilder import build_html, build_background_css, build_lyrics_css, build_lyrics_outline_css, \
- build_lyrics_format_css, build_footer_css, webkit_version, build_chords_css
+ build_lyrics_format_css, build_footer_css, build_chords_css
from openlp.core.lib.theme import HorizontalType, VerticalType
from tests.helpers.testmixin import TestMixin
@@ -445,16 +445,6 @@
assert FOOTER_CSS_INVALID == css[0], 'The footer strings should be blank.'
assert FOOTER_CSS_INVALID == css[1], 'The footer strings should be blank.'
- def test_webkit_version(self):
- """
- Test the webkit_version() function
- """
- # GIVEN: Webkit
- webkit_ver = float(QtWebKit.qWebKitVersion())
- # WHEN: Retrieving the webkit version
- # THEN: Webkit versions should match
- assert webkit_version() == webkit_ver, "The returned webkit version doesn't match the installed one"
-
def test_build_chords_css(self):
"""
Test the build_chords_css() function
=== modified file 'tests/functional/openlp_core/lib/test_lib.py'
--- tests/functional/openlp_core/lib/test_lib.py 2017-12-24 07:40:07 +0000
+++ tests/functional/openlp_core/lib/test_lib.py 2018-08-21 21:15:00 +0000
@@ -28,8 +28,7 @@
from PyQt5 import QtCore, QtGui
from openlp.core.common.path import Path
-from openlp.core.lib import FormattingTags, build_icon, check_item_selected, clean_tags, compare_chord_lyric, \
- create_separated_list, create_thumb, expand_chords, expand_chords_for_printing, expand_tags, find_formatting_tags, \
+from openlp.core.lib import build_icon, check_item_selected, create_separated_list, create_thumb, \
get_text_file_string, image_to_byte, resize_image, str_to_bool, validate_thumb
from tests.utils.constants import RESOURCE_PATH
@@ -487,12 +486,12 @@
except:
pass
+ @patch('openlp.core.lib.QtWidgets', MagicMock())
def test_check_item_selected_true(self):
"""
Test that the check_item_selected() function returns True when there are selected indexes
"""
# GIVEN: A mocked out QtWidgets module and a list widget with selected indexes
- # mocked_QtWidgets = patch('openlp.core.lib.QtWidgets')
mocked_list_widget = MagicMock()
mocked_list_widget.selectedIndexes.return_value = True
message = 'message'
@@ -525,67 +524,6 @@
MockedQtWidgets.QMessageBox.information.assert_called_with('parent', 'mocked translate', 'message')
assert result is False, 'The result should be False'
- def test_clean_tags(self):
- """
- Test clean_tags() method.
- """
- with patch('openlp.core.lib.FormattingTags.get_html_tags') as mocked_get_tags:
- # GIVEN: Mocked get_html_tags() method.
- mocked_get_tags.return_value = [{
- 'desc': 'Black',
- 'start tag': '{b}',
- 'start html': '<span style="-webkit-text-fill-color:black">',
- 'end tag': '{/b}', 'end html': '</span>', 'protected': True,
- 'temporary': False
- }]
- string_to_pass = 'ASDF<br>foo{br}bar {b}black{/b}'
- wanted_string = 'ASDF\nfoo\nbar black'
-
- # WHEN: Clean the string.
- result_string = clean_tags(string_to_pass)
-
- # THEN: The strings should be identical.
- assert wanted_string == result_string, 'The strings should be identical'
-
- def test_expand_tags(self):
- """
- Test the expand_tags() method.
- """
- with patch('openlp.core.lib.FormattingTags.get_html_tags') as mocked_get_tags:
- # GIVEN: Mocked get_html_tags() method.
- mocked_get_tags.return_value = [
- {
- 'desc': 'Black',
- 'start tag': '{b}',
- 'start html': '<span style="-webkit-text-fill-color:black">',
- 'end tag': '{/b}', 'end html': '</span>', 'protected': True,
- 'temporary': False
- },
- {
- 'desc': 'Yellow',
- 'start tag': '{y}',
- 'start html': '<span style="-webkit-text-fill-color:yellow">',
- 'end tag': '{/y}', 'end html': '</span>', 'protected': True,
- 'temporary': False
- },
- {
- 'desc': 'Green',
- 'start tag': '{g}',
- 'start html': '<span style="-webkit-text-fill-color:green">',
- 'end tag': '{/g}', 'end html': '</span>', 'protected': True,
- 'temporary': False
- }
- ]
- string_to_pass = '{b}black{/b}{y}yellow{/y}'
- wanted_string = '<span style="-webkit-text-fill-color:black">black</span>' + \
- '<span style="-webkit-text-fill-color:yellow">yellow</span>'
-
- # WHEN: Replace the tags.
- result_string = expand_tags(string_to_pass)
-
- # THEN: The strings should be identical.
- assert wanted_string == result_string, 'The strings should be identical.'
-
def test_validate_thumb_file_does_not_exist(self):
"""
Test the validate_thumb() function when the thumbnail does not exist
@@ -694,7 +632,7 @@
"""
Test the create_separated_list function with an empty list
"""
- # GIVEN: An empty list and the mocked Qt module.
+ # GIVEN: An empty list
string_list = []
# WHEN: We get a string build from the entries it the list and a separator.
@@ -742,115 +680,3 @@
# THEN: We should have "Author 1, Author 2 and Author 3"
assert string_result == 'Author 1, Author 2 and Author 3', \
'The string should be "Author 1, Author 2, and Author 3".'
-
- def test_expand_chords(self):
- """
- Test that the expanding of chords works as expected.
- """
- # GIVEN: A lyrics-line with chords
- text_with_chords = 'H[C]alleluya.[F] [G]'
-
- # WHEN: Expanding the chords
- text_with_expanded_chords = expand_chords(text_with_chords)
-
- # THEN: We should get html that looks like below
- expected_html = '<span class="chordline firstchordline">H<span class="chord"><span><strong>C</strong></span>' \
- '</span>alleluya.<span class="chord"><span><strong>F</strong></span></span><span class="ws">' \
- ' </span> <span class="chord"><span><strong>G</strong></span></span></span>'
- assert expected_html == text_with_expanded_chords, 'The expanded chords should look as expected!'
-
- def test_expand_chords2(self):
- """
- Test that the expanding of chords works as expected when special chars are involved.
- """
- # GIVEN: A lyrics-line with chords
- text_with_chords = "I[D]'M NOT MOVED BY WHAT I SEE HALLE[F]LUJA[C]H"
-
- # WHEN: Expanding the chords
- text_with_expanded_chords = expand_tags(text_with_chords, True)
-
- # THEN: We should get html that looks like below
- expected_html = '<span class="chordline firstchordline">I<span class="chord"><span><strong>D</strong></span>' \
- '</span>'M NOT MOVED BY WHAT I SEE HALLE<span class="chord"><span><strong>F</strong>' \
- '</span></span>LUJA<span class="chord"><span><strong>C</strong></span></span>H</span>'
- assert expected_html == text_with_expanded_chords, 'The expanded chords should look as expected!'
-
- def test_compare_chord_lyric_short_chord(self):
- """
- Test that the chord/lyric comparing works.
- """
- # GIVEN: A chord and some lyric
- chord = 'C'
- lyrics = 'alleluya'
-
- # WHEN: Comparing the chord and lyrics
- ret = compare_chord_lyric(chord, lyrics)
-
- # THEN: The returned value should 0 because the lyric is longer than the chord
- assert 0 == ret, 'The returned value should 0 because the lyric is longer than the chord'
-
- def test_compare_chord_lyric_long_chord(self):
- """
- Test that the chord/lyric comparing works.
- """
- # GIVEN: A chord and some lyric
- chord = 'Gsus'
- lyrics = 'me'
-
- # WHEN: Comparing the chord and lyrics
- ret = compare_chord_lyric(chord, lyrics)
-
- # THEN: The returned value should 4 because the chord is longer than the lyric
- assert 4 == ret, 'The returned value should 4 because the chord is longer than the lyric'
-
- def test_find_formatting_tags(self):
- """
- Test that find_formatting_tags works as expected
- """
- # GIVEN: Lyrics with formatting tags and a empty list of formatting tags
- lyrics = '{st}Amazing {r}grace{/r} how sweet the sound'
- tags = []
- FormattingTags.load_tags()
-
- # WHEN: Detecting active formatting tags
- active_tags = find_formatting_tags(lyrics, tags)
-
- # THEN: The list of active tags should contain only 'st'
- assert ['st'] == active_tags, 'The list of active tags should contain only "st"'
-
- def test_expand_chords_for_printing(self):
- """
- Test that the expanding of chords for printing works as expected.
- """
- # GIVEN: A lyrics-line with chords
- text_with_chords = '{st}[D]Amazing {r}gr[D7]ace{/r} how [G]sweet the [D]sound [F]{/st}'
- FormattingTags.load_tags()
-
- # WHEN: Expanding the chords
- text_with_expanded_chords = expand_chords_for_printing(text_with_chords, '{br}')
-
- # THEN: We should get html that looks like below
- expected_html = '<table class="line" width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td><table ' \
- 'class="segment" cellpadding="0" cellspacing="0" border="0" align="left"><tr class="chordrow">'\
- '<td class="chord"> </td><td class="chord">D</td></tr><tr><td class="lyrics">{st}{/st}' \
- '</td><td class="lyrics">{st}Amazing {/st}</td></tr></table><table class="segment" ' \
- 'cellpadding="0" cellspacing="0" border="0" align="left"><tr class="chordrow">' \
- '<td class="chord"> </td><td class="chord">D7</td></tr><tr><td class="lyrics">{st}{r}gr' \
- '{/r}{/st}</td><td class="lyrics">{r}{st}ace{/r} {/st}</td></tr></table><table ' \
- 'class="segment" cellpadding="0" cellspacing="0" border="0" align="left"><tr class="chordrow">'\
- '<td class="chord"> </td></tr><tr><td class="lyrics">{st} {/st}</td></tr></table>' \
- '<table class="segment" cellpadding="0" cellspacing="0" border="0" align="left"><tr ' \
- 'class="chordrow"><td class="chord"> </td></tr><tr><td class="lyrics">{st}how {/st}' \
- '</td></tr></table><table class="segment" cellpadding="0" cellspacing="0" border="0" ' \
- 'align="left"><tr class="chordrow"><td class="chord">G</td></tr><tr><td class="lyrics">{st}' \
- 'sweet {/st}</td></tr></table><table class="segment" cellpadding="0" cellspacing="0" ' \
- 'border="0" align="left"><tr class="chordrow"><td class="chord"> </td></tr><tr><td ' \
- 'class="lyrics">{st}the {/st}</td></tr></table><table class="segment" cellpadding="0" ' \
- 'cellspacing="0" border="0" align="left"><tr class="chordrow"><td class="chord">D</td></tr>' \
- '<tr><td class="lyrics">{st}sound {/st}</td></tr></table><table class="segment" ' \
- 'cellpadding="0" cellspacing="0" border="0" align="left"><tr class="chordrow"><td ' \
- 'class="chord"> </td></tr><tr><td class="lyrics">{st} {/st}</td></tr></table>' \
- '<table class="segment" cellpadding="0" cellspacing="0" border="0" align="left"><tr ' \
- 'class="chordrow"><td class="chord">F</td></tr><tr><td class="lyrics">{st}{/st} </td>' \
- '</tr></table></td></tr></table>'
- assert expected_html == text_with_expanded_chords, 'The expanded chords should look as expected!'
=== removed file 'tests/functional/openlp_core/ui/media/test_webkitplayer.py'
--- tests/functional/openlp_core/ui/media/test_webkitplayer.py 2017-12-29 09:15:48 +0000
+++ tests/functional/openlp_core/ui/media/test_webkitplayer.py 1970-01-01 00:00:00 +0000
@@ -1,66 +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 #
-###############################################################################
-"""
-Package to test the openlp.core.ui.media.webkitplayer package.
-"""
-from unittest import TestCase
-from unittest.mock import MagicMock, patch
-
-from openlp.core.ui.media.webkitplayer import WebkitPlayer
-
-
-class TestWebkitPlayer(TestCase):
- """
- Test the functions in the :mod:`webkitplayer` module.
- """
-
- def test_check_available_video_disabled(self):
- """
- Test of webkit video unavailability
- """
- # GIVEN: A WebkitPlayer instance and a mocked QWebPage
- mocked_qwebpage = MagicMock()
- mocked_qwebpage.mainFrame().evaluateJavaScript.return_value = '[object HTMLUnknownElement]'
- with patch('openlp.core.ui.media.webkitplayer.QtWebKitWidgets.QWebPage', **{'return_value': mocked_qwebpage}):
- webkit_player = WebkitPlayer(None)
-
- # WHEN: An checking if the player is available
- available = webkit_player.check_available()
-
- # THEN: The player should not be available when '[object HTMLUnknownElement]' is returned
- assert available is False, 'The WebkitPlayer should not be available when video feature detection fails'
-
- def test_check_available_video_enabled(self):
- """
- Test of webkit video availability
- """
- # GIVEN: A WebkitPlayer instance and a mocked QWebPage
- mocked_qwebpage = MagicMock()
- mocked_qwebpage.mainFrame().evaluateJavaScript.return_value = '[object HTMLVideoElement]'
- with patch('openlp.core.ui.media.webkitplayer.QtWebKitWidgets.QWebPage', **{'return_value': mocked_qwebpage}):
- webkit_player = WebkitPlayer(None)
-
- # WHEN: An checking if the player is available
- available = webkit_player.check_available()
-
- # THEN: The player should be available when '[object HTMLVideoElement]' is returned
- assert available is True, 'The WebkitPlayer should be available when video feature detection passes'
=== modified file 'tests/functional/openlp_core/ui/test_exceptionform.py'
--- tests/functional/openlp_core/ui/test_exceptionform.py 2017-12-28 08:22:55 +0000
+++ tests/functional/openlp_core/ui/test_exceptionform.py 2018-08-21 21:15:00 +0000
@@ -24,6 +24,7 @@
"""
import os
import tempfile
+from collections import OrderedDict
from unittest import TestCase
from unittest.mock import call, patch
@@ -43,40 +44,44 @@
MAIL_ITEM_TEXT = ('**OpenLP Bug Report**\nVersion: Trunk Test\n\n--- Details of the Exception. ---\n\n'
'Description Test\n\n --- Exception Traceback ---\nopenlp: Traceback Test\n'
'--- System information ---\nPlatform: Nose Test\n\n--- Library Versions ---\n'
- 'Python: Python Test\nQt5: Qt5 test\nPyQt5: PyQt5 Test\nQtWebkit: Webkit Test\n'
- 'SQLAlchemy: SqlAlchemy Test\nSQLAlchemy Migrate: Migrate Test\nBeautifulSoup: BeautifulSoup Test\n'
- 'lxml: ETree Test\nChardet: CHARDET Test\nPyEnchant: Enchant Test\nMako: Mako Test\n'
- 'pyICU: ICU Test\npyUNO bridge: UNO Bridge Test\nVLC: VLC Test\n\n')
-
-
-@patch("openlp.core.ui.exceptionform.Qt.qVersion")
-@patch("openlp.core.ui.exceptionform.QtGui.QDesktopServices.openUrl")
-@patch("openlp.core.ui.exceptionform.get_version")
-@patch("openlp.core.ui.exceptionform.sqlalchemy")
-@patch("openlp.core.ui.exceptionform.bs4")
-@patch("openlp.core.ui.exceptionform.etree")
-@patch("openlp.core.ui.exceptionform.is_linux")
-@patch("openlp.core.ui.exceptionform.platform.platform")
-@patch("openlp.core.ui.exceptionform.platform.python_version")
+ 'Python: Python Test\nQt5: Qt5 Test\nPyQt5: PyQt5 Test\n'
+ 'SQLAlchemy: SQLAlchemy Test\nAlembic: Alembic Test\nBeautifulSoup: BeautifulSoup Test\n'
+ 'lxml: ETree Test\nChardet: Chardet Test\nPyEnchant: PyEnchant Test\nMako: Mako Test\n'
+ 'pyICU: pyICU Test\nVLC: VLC Test\nPyUNO: UNO Bridge Test\n')
+LIBRARY_VERSIONS = OrderedDict([
+ ('Python', 'Python Test'),
+ ('Qt5', 'Qt5 Test'),
+ ('PyQt5', 'PyQt5 Test'),
+ ('SQLAlchemy', 'SQLAlchemy Test'),
+ ('Alembic', 'Alembic Test'),
+ ('BeautifulSoup', 'BeautifulSoup Test'),
+ ('lxml', 'ETree Test'),
+ ('Chardet', 'Chardet Test'),
+ ('PyEnchant', 'PyEnchant Test'),
+ ('Mako', 'Mako Test'),
+ ('pyICU', 'pyICU Test'),
+ ('VLC', 'VLC Test')
+])
+
+
+@patch('openlp.core.ui.exceptionform.QtGui.QDesktopServices.openUrl')
+@patch('openlp.core.ui.exceptionform.get_version')
+@patch('openlp.core.ui.exceptionform.get_library_versions')
+@patch('openlp.core.ui.exceptionform.is_linux')
+@patch('openlp.core.ui.exceptionform.platform.platform')
class TestExceptionForm(TestMixin, TestCase):
"""
Test functionality of exception form functions
"""
def __method_template_for_class_patches(self, __PLACEHOLDER_FOR_LOCAL_METHOD_PATCH_DECORATORS_GO_HERE__,
- mocked_python_version, mocked_platform, mocked_is_linux,
- mocked_etree, mocked_bs4, mocked_sqlalchemy, mocked_get_version,
- mocked_openlurl, mocked_qversion):
+ mocked_platform, mocked_is_linux, mocked_get_library_versions,
+ mocked_get_version, mocked_openlurl):
"""
Template so you don't have to remember the layout of class mock options for methods
"""
- mocked_etree.__version__ = 'ETree Test'
- mocked_bs4.__version__ = 'BeautifulSoup Test'
- mocked_sqlalchemy.__version__ = 'SqlAlchemy Test'
- mocked_python_version.return_value = 'Python Test'
- mocked_platform.return_value = 'Nose Test'
- mocked_qversion.return_value = 'Qt5 test'
mocked_is_linux.return_value = False
mocked_get_version.return_value = 'Trunk Test'
+ mocked_get_library_versions.return_value = LIBRARY_VERSIONS
def setUp(self):
self.setup_application()
@@ -96,31 +101,21 @@
@patch("openlp.core.ui.exceptionform.FileDialog")
@patch("openlp.core.ui.exceptionform.QtCore.QUrl")
@patch("openlp.core.ui.exceptionform.QtCore.QUrlQuery.addQueryItem")
- @patch("openlp.core.ui.exceptionform.Qt")
- def test_on_send_report_button_clicked(self, mocked_qt, mocked_add_query_item, mocked_qurl, mocked_file_dialog,
- mocked_ui_exception_dialog, mocked_python_version, mocked_platform,
- mocked_is_linux, mocked_etree, mocked_bs4, mocked_sqlalchemy,
- mocked_get_version, mocked_openlurl, mocked_qversion):
+ def test_on_send_report_button_clicked(self, mocked_add_query_item, mocked_qurl, mocked_file_dialog,
+ mocked_ui_exception_dialog, mocked_platform, mocked_is_linux,
+ mocked_get_library_versions, mocked_get_version, mocked_openlurl):
"""
Test send report creates the proper system information text
"""
# GIVEN: Test environment
- mocked_etree.__version__ = 'ETree Test'
- mocked_bs4.__version__ = 'BeautifulSoup Test'
- mocked_sqlalchemy.__version__ = 'SqlAlchemy Test'
- mocked_python_version.return_value = 'Python Test'
mocked_platform.return_value = 'Nose Test'
- mocked_qversion.return_value = 'Qt5 test'
- mocked_is_linux.return_value = False
- mocked_get_version.return_value = 'Trunk Test'
- mocked_qt.PYQT_VERSION_STR = 'PyQt5 Test'
- mocked_is_linux.return_value = False
- mocked_get_version.return_value = 'Trunk Test'
-
+ mocked_is_linux.return_value = False
+ mocked_get_version.return_value = 'Trunk Test'
+ mocked_get_library_versions.return_value = LIBRARY_VERSIONS
test_form = exceptionform.ExceptionForm()
test_form.file_attachment = None
- with patch.object(test_form, '_pyuno_import') as mock_pyuno, \
+ with patch.object(test_form, '_get_pyuno_version') as mock_pyuno, \
patch.object(test_form.exception_text_edit, 'toPlainText') as mock_traceback, \
patch.object(test_form.description_text_edit, 'toPlainText') as mock_description:
mock_pyuno.return_value = 'UNO Bridge Test'
@@ -134,24 +129,15 @@
mocked_add_query_item.assert_called_with('body', MAIL_ITEM_TEXT)
@patch("openlp.core.ui.exceptionform.FileDialog.getSaveFileName")
- @patch("openlp.core.ui.exceptionform.Qt")
- def test_on_save_report_button_clicked(self, mocked_qt, mocked_save_filename, mocked_python_version,
- mocked_platform, mocked_is_linux, mocked_etree, mocked_bs4,
- mocked_sqlalchemy, mocked_get_version, mocked_openlurl,
- mocked_qversion):
+ def test_on_save_report_button_clicked(self, mocked_save_filename, mocked_platform, mocked_is_linux,
+ mocked_get_library_versions, mocked_get_version, mocked_openlurl):
"""
Test save report saves the correct information to a file
"""
- mocked_etree.__version__ = 'ETree Test'
- mocked_bs4.__version__ = 'BeautifulSoup Test'
- mocked_sqlalchemy.__version__ = 'SqlAlchemy Test'
- mocked_python_version.return_value = 'Python Test'
mocked_platform.return_value = 'Nose Test'
- mocked_qversion.return_value = 'Qt5 test'
- mocked_qt.PYQT_VERSION_STR = 'PyQt5 Test'
mocked_is_linux.return_value = False
mocked_get_version.return_value = 'Trunk Test'
-
+ mocked_get_library_versions.return_value = LIBRARY_VERSIONS
with patch.object(Path, 'open') as mocked_path_open:
test_path = Path('testfile.txt')
mocked_save_filename.return_value = test_path, 'ext'
@@ -159,7 +145,7 @@
test_form = exceptionform.ExceptionForm()
test_form.file_attachment = None
- with patch.object(test_form, '_pyuno_import') as mock_pyuno, \
+ with patch.object(test_form, '_get_pyuno_version') as mock_pyuno, \
patch.object(test_form.exception_text_edit, 'toPlainText') as mock_traceback, \
patch.object(test_form.description_text_edit, 'toPlainText') as mock_description:
mock_pyuno.return_value = 'UNO Bridge Test'
=== removed file 'tests/functional/openlp_core/ui/test_maindisplay.py'
--- tests/functional/openlp_core/ui/test_maindisplay.py 2017-12-28 08:22:55 +0000
+++ tests/functional/openlp_core/ui/test_maindisplay.py 1970-01-01 00:00:00 +0000
@@ -1,301 +0,0 @@
-# -*- coding: utf-8 -*-
-# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
-
-###############################################################################
-# OpenLP - Open Source Lyrics Projection #
-# --------------------------------------------------------------------------- #
-# Copyright (c) 2008-2017 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 #
-###############################################################################
-"""
-Package to test the openlp.core.ui.slidecontroller package.
-"""
-from unittest import TestCase, skipUnless
-from unittest.mock import MagicMock, patch
-
-from PyQt5 import QtCore
-
-from openlp.core.common import is_macosx
-from openlp.core.common.path import Path
-from openlp.core.common.registry import Registry
-from openlp.core.display.screens import ScreenList
-from openlp.core.lib import PluginManager
-from openlp.core.ui import MainDisplay, AudioPlayer
-from openlp.core.ui.maindisplay import TRANSPARENT_STYLESHEET, OPAQUE_STYLESHEET
-from tests.helpers.testmixin import TestMixin
-
-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
-
-
-class TestMainDisplay(TestCase, TestMixin):
-
- def setUp(self):
- """
- Set up the components need for all tests.
- """
- # Mocked out desktop object
- self.desktop = MagicMock()
- self.desktop.primaryScreen.return_value = 0
- self.desktop.screenCount.return_value = 2
- self.desktop.screenGeometry.side_effect = lambda x: {0: QtCore.QRect(0, 0, 1024, 768),
- 1: QtCore.QRect(0, 0, 1024, 768)}[x]
- self.screens = ScreenList.create(self.desktop)
- Registry.create()
- self.registry = Registry()
- self.setup_application()
- Registry().register('application', self.app)
- self.mocked_audio_player = patch('openlp.core.ui.maindisplay.AudioPlayer')
- self.mocked_audio_player.start()
-
- def tearDown(self):
- """
- Delete QApplication.
- """
- self.mocked_audio_player.stop()
- del self.screens
-
- def test_initial_main_display(self):
- """
- Test the initial Main Display state
- """
- # GIVEN: A new SlideController instance.
- display = MagicMock()
- display.is_live = True
-
- # WHEN: The default controller is built.
- main_display = MainDisplay(display)
-
- # THEN: The controller should be a live controller.
- assert main_display.is_live is True, 'The main display should be a live controller'
-
- def test_set_transparency_enabled(self):
- """
- Test setting the display to be transparent
- """
- # GIVEN: An instance of MainDisplay
- display = MagicMock()
- main_display = MainDisplay(display)
-
- # WHEN: Transparency is enabled
- main_display.set_transparency(True)
-
- # THEN: The transparent stylesheet should be used
- assert TRANSPARENT_STYLESHEET == main_display.styleSheet(), \
- 'The MainDisplay should use the transparent stylesheet'
- assert main_display.autoFillBackground() is False, \
- 'The MainDisplay should not have autoFillBackground set'
- assert main_display.testAttribute(QtCore.Qt.WA_TranslucentBackground) is True, \
- 'The MainDisplay should have a translucent background'
-
- def test_set_transparency_disabled(self):
- """
- Test setting the display to be opaque
- """
- # GIVEN: An instance of MainDisplay
- display = MagicMock()
- main_display = MainDisplay(display)
-
- # WHEN: Transparency is disabled
- main_display.set_transparency(False)
-
- # THEN: The opaque stylesheet should be used
- assert OPAQUE_STYLESHEET == main_display.styleSheet(), \
- 'The MainDisplay should use the opaque stylesheet'
- assert main_display.testAttribute(QtCore.Qt.WA_TranslucentBackground) is False, \
- 'The MainDisplay should not have a translucent background'
-
- def test_css_changed(self):
- """
- Test that when the CSS changes, the plugins are looped over and given an opportunity to update the CSS
- """
- # GIVEN: A mocked list of plugins, a mocked display and a MainDisplay
- mocked_songs_plugin = MagicMock()
- mocked_bibles_plugin = MagicMock()
- mocked_plugin_manager = MagicMock()
- mocked_plugin_manager.plugins = [mocked_songs_plugin, mocked_bibles_plugin]
- Registry().register('plugin_manager', mocked_plugin_manager)
- display = MagicMock()
- main_display = MainDisplay(display)
- # This is set up dynamically, so we need to mock it out for now
- main_display.frame = MagicMock()
-
- # WHEN: The css_changed() method is triggered
- main_display.css_changed()
-
- # THEN: The plugins should have each been given an opportunity to add their bit to the CSS
- mocked_songs_plugin.refresh_css.assert_called_with(main_display.frame)
- mocked_bibles_plugin.refresh_css.assert_called_with(main_display.frame)
-
- @skipUnless(is_macosx(), 'Can only run test on Mac OS X due to pyobjc dependency.')
- def test_macosx_display_window_flags_state(self):
- """
- Test that on Mac OS X we set the proper window flags
- """
- # GIVEN: A new SlideController instance on Mac OS X.
- self.screens.set_current_display(0)
- display = MagicMock()
-
- # WHEN: The default controller is built.
- main_display = MainDisplay(display)
-
- # THEN: The window flags should be the same as those needed on Mac OS X.
- assert QtCore.Qt.Window | QtCore.Qt.FramelessWindowHint | QtCore.Qt.NoDropShadowWindowHint == \
- main_display.windowFlags(), \
- 'The window flags should be Qt.Window, Qt.FramelessWindowHint, and Qt.NoDropShadowWindowHint.'
-
- @skipUnless(is_macosx(), 'Can only run test on Mac OS X due to pyobjc dependency.')
- def test_macosx_display(self):
- """
- Test display on Mac OS X
- """
- # GIVEN: A new SlideController instance on Mac OS X.
- self.screens.set_current_display(0)
- display = MagicMock()
-
- # WHEN: The default contro