forked from openlp/openlp
Apparently some files were missing after the merge
This commit is contained in:
parent
4dba3b178d
commit
5e1b591f62
769
openlp/core/display/canvas.py
Normal file
769
openlp/core/display/canvas.py
Normal file
@ -0,0 +1,769 @@
|
|||||||
|
# -*- 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)
|
||||||
|
print(json_verses)
|
||||||
|
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()
|
292
openlp/core/display/html/black.css
Normal file
292
openlp/core/display/html/black.css
Normal file
@ -0,0 +1,292 @@
|
|||||||
|
/**
|
||||||
|
* Black theme for reveal.js. This is the opposite of the 'white' theme.
|
||||||
|
*
|
||||||
|
* By Hakim El Hattab, http://hakim.se
|
||||||
|
*/
|
||||||
|
@import url(../../lib/font/source-sans-pro/source-sans-pro.css);
|
||||||
|
section.has-light-background, section.has-light-background h1, section.has-light-background h2, section.has-light-background h3, section.has-light-background h4, section.has-light-background h5, section.has-light-background h6 {
|
||||||
|
color: #222; }
|
||||||
|
|
||||||
|
/*********************************************
|
||||||
|
* GLOBAL STYLES
|
||||||
|
*********************************************/
|
||||||
|
body {
|
||||||
|
background: #222;
|
||||||
|
background-color: #222; }
|
||||||
|
|
||||||
|
.reveal {
|
||||||
|
font-family: "Source Sans Pro", Helvetica, sans-serif;
|
||||||
|
font-size: 42px;
|
||||||
|
font-weight: normal;
|
||||||
|
color: #fff; }
|
||||||
|
|
||||||
|
::selection {
|
||||||
|
color: #fff;
|
||||||
|
background: #bee4fd;
|
||||||
|
text-shadow: none; }
|
||||||
|
|
||||||
|
::-moz-selection {
|
||||||
|
color: #fff;
|
||||||
|
background: #bee4fd;
|
||||||
|
text-shadow: none; }
|
||||||
|
|
||||||
|
.reveal .slides > section,
|
||||||
|
.reveal .slides > section > section {
|
||||||
|
line-height: 1.3;
|
||||||
|
font-weight: inherit; }
|
||||||
|
|
||||||
|
/*********************************************
|
||||||
|
* HEADERS
|
||||||
|
*********************************************/
|
||||||
|
.reveal h1,
|
||||||
|
.reveal h2,
|
||||||
|
.reveal h3,
|
||||||
|
.reveal h4,
|
||||||
|
.reveal h5,
|
||||||
|
.reveal h6 {
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
color: #fff;
|
||||||
|
font-family: "Source Sans Pro", Helvetica, sans-serif;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.2;
|
||||||
|
letter-spacing: normal;
|
||||||
|
text-transform: uppercase;
|
||||||
|
text-shadow: none;
|
||||||
|
word-wrap: break-word; }
|
||||||
|
|
||||||
|
.reveal h1 {
|
||||||
|
font-size: 2.5em; }
|
||||||
|
|
||||||
|
.reveal h2 {
|
||||||
|
font-size: 1.6em; }
|
||||||
|
|
||||||
|
.reveal h3 {
|
||||||
|
font-size: 1.3em; }
|
||||||
|
|
||||||
|
.reveal h4 {
|
||||||
|
font-size: 1em; }
|
||||||
|
|
||||||
|
.reveal h1 {
|
||||||
|
text-shadow: none; }
|
||||||
|
|
||||||
|
/*********************************************
|
||||||
|
* OTHER
|
||||||
|
*********************************************/
|
||||||
|
.reveal p {
|
||||||
|
margin: 20px 0;
|
||||||
|
line-height: 1.3; }
|
||||||
|
|
||||||
|
/* Ensure certain elements are never larger than the slide itself */
|
||||||
|
.reveal img,
|
||||||
|
.reveal video,
|
||||||
|
.reveal iframe {
|
||||||
|
max-width: 95%;
|
||||||
|
max-height: 95%; }
|
||||||
|
|
||||||
|
.reveal strong,
|
||||||
|
.reveal b {
|
||||||
|
font-weight: bold; }
|
||||||
|
|
||||||
|
.reveal em {
|
||||||
|
font-style: italic; }
|
||||||
|
|
||||||
|
.reveal ol,
|
||||||
|
.reveal dl,
|
||||||
|
.reveal ul {
|
||||||
|
display: inline-block;
|
||||||
|
text-align: left;
|
||||||
|
margin: 0 0 0 1em; }
|
||||||
|
|
||||||
|
.reveal ol {
|
||||||
|
list-style-type: decimal; }
|
||||||
|
|
||||||
|
.reveal ul {
|
||||||
|
list-style-type: disc; }
|
||||||
|
|
||||||
|
.reveal ul ul {
|
||||||
|
list-style-type: square; }
|
||||||
|
|
||||||
|
.reveal ul ul ul {
|
||||||
|
list-style-type: circle; }
|
||||||
|
|
||||||
|
.reveal ul ul,
|
||||||
|
.reveal ul ol,
|
||||||
|
.reveal ol ol,
|
||||||
|
.reveal ol ul {
|
||||||
|
display: block;
|
||||||
|
margin-left: 40px; }
|
||||||
|
|
||||||
|
.reveal dt {
|
||||||
|
font-weight: bold; }
|
||||||
|
|
||||||
|
.reveal dd {
|
||||||
|
margin-left: 40px; }
|
||||||
|
|
||||||
|
.reveal q,
|
||||||
|
.reveal blockquote {
|
||||||
|
quotes: none; }
|
||||||
|
|
||||||
|
.reveal blockquote {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
width: 70%;
|
||||||
|
margin: 20px auto;
|
||||||
|
padding: 5px;
|
||||||
|
font-style: italic;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
box-shadow: 0px 0px 2px rgba(0, 0, 0, 0.2); }
|
||||||
|
|
||||||
|
.reveal blockquote p:first-child,
|
||||||
|
.reveal blockquote p:last-child {
|
||||||
|
display: inline-block; }
|
||||||
|
|
||||||
|
.reveal q {
|
||||||
|
font-style: italic; }
|
||||||
|
|
||||||
|
.reveal pre {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
width: 90%;
|
||||||
|
margin: 20px auto;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 0.55em;
|
||||||
|
font-family: monospace;
|
||||||
|
line-height: 1.2em;
|
||||||
|
word-wrap: break-word;
|
||||||
|
box-shadow: 0px 0px 6px rgba(0, 0, 0, 0.3); }
|
||||||
|
|
||||||
|
.reveal code {
|
||||||
|
font-family: monospace; }
|
||||||
|
|
||||||
|
.reveal pre code {
|
||||||
|
display: block;
|
||||||
|
padding: 5px;
|
||||||
|
overflow: auto;
|
||||||
|
max-height: 400px;
|
||||||
|
word-wrap: normal; }
|
||||||
|
|
||||||
|
.reveal table {
|
||||||
|
margin: auto;
|
||||||
|
border-collapse: collapse;
|
||||||
|
border-spacing: 0; }
|
||||||
|
|
||||||
|
.reveal table th {
|
||||||
|
font-weight: bold; }
|
||||||
|
|
||||||
|
.reveal table th,
|
||||||
|
.reveal table td {
|
||||||
|
text-align: left;
|
||||||
|
padding: 0.2em 0.5em 0.2em 0.5em;
|
||||||
|
border-bottom: 1px solid; }
|
||||||
|
|
||||||
|
.reveal table th[align="center"],
|
||||||
|
.reveal table td[align="center"] {
|
||||||
|
text-align: center; }
|
||||||
|
|
||||||
|
.reveal table th[align="right"],
|
||||||
|
.reveal table td[align="right"] {
|
||||||
|
text-align: right; }
|
||||||
|
|
||||||
|
.reveal table tbody tr:last-child th,
|
||||||
|
.reveal table tbody tr:last-child td {
|
||||||
|
border-bottom: none; }
|
||||||
|
|
||||||
|
.reveal sup {
|
||||||
|
vertical-align: super; }
|
||||||
|
|
||||||
|
.reveal sub {
|
||||||
|
vertical-align: sub; }
|
||||||
|
|
||||||
|
.reveal small {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 0.6em;
|
||||||
|
line-height: 1.2em;
|
||||||
|
vertical-align: top; }
|
||||||
|
|
||||||
|
.reveal small * {
|
||||||
|
vertical-align: top; }
|
||||||
|
|
||||||
|
/*********************************************
|
||||||
|
* LINKS
|
||||||
|
*********************************************/
|
||||||
|
.reveal a {
|
||||||
|
color: #42affa;
|
||||||
|
text-decoration: none;
|
||||||
|
-webkit-transition: color .15s ease;
|
||||||
|
-moz-transition: color .15s ease;
|
||||||
|
transition: color .15s ease; }
|
||||||
|
|
||||||
|
.reveal a:hover {
|
||||||
|
color: #8dcffc;
|
||||||
|
text-shadow: none;
|
||||||
|
border: none; }
|
||||||
|
|
||||||
|
.reveal .roll span:after {
|
||||||
|
color: #fff;
|
||||||
|
background: #068de9; }
|
||||||
|
|
||||||
|
/*********************************************
|
||||||
|
* IMAGES
|
||||||
|
*********************************************/
|
||||||
|
.reveal section img {
|
||||||
|
margin: 15px 0px;
|
||||||
|
background: rgba(255, 255, 255, 0.12);
|
||||||
|
border: 4px solid #fff;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.15); }
|
||||||
|
|
||||||
|
.reveal section img.plain {
|
||||||
|
border: 0;
|
||||||
|
box-shadow: none; }
|
||||||
|
|
||||||
|
.reveal a img {
|
||||||
|
-webkit-transition: all .15s linear;
|
||||||
|
-moz-transition: all .15s linear;
|
||||||
|
transition: all .15s linear; }
|
||||||
|
|
||||||
|
.reveal a:hover img {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border-color: #42affa;
|
||||||
|
box-shadow: 0 0 20px rgba(0, 0, 0, 0.55); }
|
||||||
|
|
||||||
|
/*********************************************
|
||||||
|
* NAVIGATION CONTROLS
|
||||||
|
*********************************************/
|
||||||
|
.reveal .controls .navigate-left,
|
||||||
|
.reveal .controls .navigate-left.enabled {
|
||||||
|
border-right-color: #42affa; }
|
||||||
|
|
||||||
|
.reveal .controls .navigate-right,
|
||||||
|
.reveal .controls .navigate-right.enabled {
|
||||||
|
border-left-color: #42affa; }
|
||||||
|
|
||||||
|
.reveal .controls .navigate-up,
|
||||||
|
.reveal .controls .navigate-up.enabled {
|
||||||
|
border-bottom-color: #42affa; }
|
||||||
|
|
||||||
|
.reveal .controls .navigate-down,
|
||||||
|
.reveal .controls .navigate-down.enabled {
|
||||||
|
border-top-color: #42affa; }
|
||||||
|
|
||||||
|
.reveal .controls .navigate-left.enabled:hover {
|
||||||
|
border-right-color: #8dcffc; }
|
||||||
|
|
||||||
|
.reveal .controls .navigate-right.enabled:hover {
|
||||||
|
border-left-color: #8dcffc; }
|
||||||
|
|
||||||
|
.reveal .controls .navigate-up.enabled:hover {
|
||||||
|
border-bottom-color: #8dcffc; }
|
||||||
|
|
||||||
|
.reveal .controls .navigate-down.enabled:hover {
|
||||||
|
border-top-color: #8dcffc; }
|
||||||
|
|
||||||
|
/*********************************************
|
||||||
|
* PROGRESS BAR
|
||||||
|
*********************************************/
|
||||||
|
.reveal .progress {
|
||||||
|
background: rgba(0, 0, 0, 0.2); }
|
||||||
|
|
||||||
|
.reveal .progress span {
|
||||||
|
background: #42affa;
|
||||||
|
-webkit-transition: width 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985);
|
||||||
|
-moz-transition: width 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985);
|
||||||
|
transition: width 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985); }
|
25
openlp/core/display/html/display.html
Normal file
25
openlp/core/display/html/display.html
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Display Window</title>
|
||||||
|
<link href="reveal.css" rel="stylesheet">
|
||||||
|
<style type="text/css">
|
||||||
|
body {
|
||||||
|
background: #000 !important;
|
||||||
|
}
|
||||||
|
.reveal .slides > section, .reveal .slides > section > section {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
</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>
|
629
openlp/core/display/html/display.js
Normal file
629
openlp/core/display/html/display.js
Normal file
@ -0,0 +1,629 @@
|
|||||||
|
/**
|
||||||
|
* 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>";
|
||||||
|
}
|
||||||
|
// 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();
|
||||||
|
};
|
||||||
|
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.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
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.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
AudioPlayer.prototype.pause = function () {
|
||||||
|
this._audioElement.pause();
|
||||||
|
this._state = AudioState.Paused;
|
||||||
|
};
|
||||||
|
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: "slide",
|
||||||
|
backgroundTransition: "fade",
|
||||||
|
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", "html": "line 1<br>line2"}
|
||||||
|
*/
|
||||||
|
setTextSlides: function (slides) {
|
||||||
|
Display.clearSlides();
|
||||||
|
slides.forEach(function (slide) {
|
||||||
|
Display.addTextSlide(slide.verse, slide.text, false);
|
||||||
|
});
|
||||||
|
this.reinit();
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Set image slides
|
||||||
|
* @param {Object[]} slides - A list of images to add as JS objects [{"file": "url/to/file"}]
|
||||||
|
*/
|
||||||
|
setImageSlides: function (slides) {
|
||||||
|
var $this = this;
|
||||||
|
$this.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["file"];
|
||||||
|
img.setAttribute("style", "height: 100%; width: 100%;");
|
||||||
|
section.appendChild(img);
|
||||||
|
slidesDiv.appendChild(section);
|
||||||
|
$this._slides[index.toString()] = index;
|
||||||
|
});
|
||||||
|
this.reinit();
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Set a video
|
||||||
|
* @param {Object} video - The video to show as a JS object: {"file": "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["file"];
|
||||||
|
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) {
|
||||||
|
Reveal.slide(this._slides[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:
|
||||||
|
backgroundStyle["background-color"] = theme.background_border_color;
|
||||||
|
backgroundStyle["background-image"] = "url('file://" + theme.background_filename + "')";
|
||||||
|
backgroundStyle["background-size"] = "cover";
|
||||||
|
break;
|
||||||
|
case BackgroundType.Video:
|
||||||
|
backgroundStyle["background-color"] = theme.background_border_color;
|
||||||
|
backgroundHtml = "<video loop autoplay muted><source src='" + theme.background_filename + "'></video>";
|
||||||
|
backgroundStyle["background-size"] = "cover";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
backgroundStyle["background"] = "#000";
|
||||||
|
}
|
||||||
|
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"] = "" + (parseFloat(theme.font_main_outline_size) / 16.0) + "em " +
|
||||||
|
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) + "px" : "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];
|
||||||
|
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];
|
||||||
|
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;
|
||||||
|
});
|
1387
openlp/core/display/html/reveal.css
Normal file
1387
openlp/core/display/html/reveal.css
Normal file
File diff suppressed because it is too large
Load Diff
5121
openlp/core/display/html/reveal.js
Normal file
5121
openlp/core/display/html/reveal.js
Normal file
File diff suppressed because it is too large
Load Diff
237
openlp/core/display/html/textFit.js
Normal file
237
openlp/core/display/html/textFit.js
Normal file
@ -0,0 +1,237 @@
|
|||||||
|
/**
|
||||||
|
* textFit v2.3.1
|
||||||
|
* Previously known as jQuery.textFit
|
||||||
|
* 11/2014 by STRML (strml.github.com)
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* To use: textFit(document.getElementById('target-div'), options);
|
||||||
|
*
|
||||||
|
* Will make the *text* content inside a container scale to fit the container
|
||||||
|
* The container is required to have a set width and height
|
||||||
|
* Uses binary search to fit text with minimal layout calls.
|
||||||
|
* Version 2.0 does not use jQuery.
|
||||||
|
*/
|
||||||
|
/*global define:true, document:true, window:true, HTMLElement:true*/
|
||||||
|
|
||||||
|
(function(root, factory) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
// UMD shim
|
||||||
|
if (typeof define === "function" && define.amd) {
|
||||||
|
// AMD
|
||||||
|
define([], factory);
|
||||||
|
} else if (typeof exports === "object") {
|
||||||
|
// Node/CommonJS
|
||||||
|
module.exports = factory();
|
||||||
|
} else {
|
||||||
|
// Browser
|
||||||
|
root.textFit = factory();
|
||||||
|
}
|
||||||
|
|
||||||
|
}(typeof global === "object" ? global : this, function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var defaultSettings = {
|
||||||
|
alignVert: false, // if true, textFit will align vertically using css tables
|
||||||
|
alignHoriz: false, // if true, textFit will set text-align: center
|
||||||
|
multiLine: false, // if true, textFit will not set white-space: no-wrap
|
||||||
|
detectMultiLine: true, // disable to turn off automatic multi-line sensing
|
||||||
|
minFontSize: 6,
|
||||||
|
maxFontSize: 80,
|
||||||
|
reProcess: true, // if true, textFit will re-process already-fit nodes. Set to 'false' for better performance
|
||||||
|
widthOnly: false, // if true, textFit will fit text to element width, regardless of text height
|
||||||
|
alignVertWithFlexbox: false, // if true, textFit will use flexbox for vertical alignment
|
||||||
|
};
|
||||||
|
|
||||||
|
return function textFit(els, options) {
|
||||||
|
|
||||||
|
if (!options) options = {};
|
||||||
|
|
||||||
|
// Extend options.
|
||||||
|
var settings = {};
|
||||||
|
for(var key in defaultSettings){
|
||||||
|
if(options.hasOwnProperty(key)){
|
||||||
|
settings[key] = options[key];
|
||||||
|
} else {
|
||||||
|
settings[key] = defaultSettings[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert jQuery objects into arrays
|
||||||
|
if (typeof els.toArray === "function") {
|
||||||
|
els = els.toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Support passing a single el
|
||||||
|
var elType = Object.prototype.toString.call(els);
|
||||||
|
if (elType !== '[object Array]' && elType !== '[object NodeList]' &&
|
||||||
|
elType !== '[object HTMLCollection]'){
|
||||||
|
els = [els];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process each el we've passed.
|
||||||
|
for(var i = 0; i < els.length; i++){
|
||||||
|
processItem(els[i], settings);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The meat. Given an el, make the text inside it fit its parent.
|
||||||
|
* @param {DOMElement} el Child el.
|
||||||
|
* @param {Object} settings Options for fit.
|
||||||
|
*/
|
||||||
|
function processItem(el, settings){
|
||||||
|
if (!isElement(el) || (!settings.reProcess && el.getAttribute('textFitted'))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set textFitted attribute so we know this was processed.
|
||||||
|
if(!settings.reProcess){
|
||||||
|
el.setAttribute('textFitted', 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
var innerSpan, originalHeight, originalHTML, originalWidth;
|
||||||
|
var low, mid, high;
|
||||||
|
|
||||||
|
// Get element data.
|
||||||
|
originalHTML = el.innerHTML;
|
||||||
|
originalWidth = innerWidth(el);
|
||||||
|
originalHeight = innerHeight(el);
|
||||||
|
|
||||||
|
// Don't process if we can't find box dimensions
|
||||||
|
if (!originalWidth || (!settings.widthOnly && !originalHeight)) {
|
||||||
|
if(!settings.widthOnly)
|
||||||
|
throw new Error('Set a static height and width on the target element ' + el.outerHTML +
|
||||||
|
' before using textFit!');
|
||||||
|
else
|
||||||
|
throw new Error('Set a static width on the target element ' + el.outerHTML +
|
||||||
|
' before using textFit!');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add textFitted span inside this container.
|
||||||
|
if (originalHTML.indexOf('textFitted') === -1) {
|
||||||
|
innerSpan = document.createElement('span');
|
||||||
|
innerSpan.className = 'textFitted';
|
||||||
|
// Inline block ensure it takes on the size of its contents, even if they are enclosed
|
||||||
|
// in other tags like <p>
|
||||||
|
innerSpan.style['display'] = 'inline-block';
|
||||||
|
innerSpan.innerHTML = originalHTML;
|
||||||
|
el.innerHTML = '';
|
||||||
|
el.appendChild(innerSpan);
|
||||||
|
} else {
|
||||||
|
// Reprocessing.
|
||||||
|
innerSpan = el.querySelector('span.textFitted');
|
||||||
|
// Remove vertical align if we're reprocessing.
|
||||||
|
if (hasClass(innerSpan, 'textFitAlignVert')){
|
||||||
|
innerSpan.className = innerSpan.className.replace('textFitAlignVert', '');
|
||||||
|
innerSpan.style['height'] = '';
|
||||||
|
el.className.replace('textFitAlignVertFlex', '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare & set alignment
|
||||||
|
if (settings.alignHoriz) {
|
||||||
|
el.style['text-align'] = 'center';
|
||||||
|
innerSpan.style['text-align'] = 'center';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this string is multiple lines
|
||||||
|
// Not guaranteed to always work if you use wonky line-heights
|
||||||
|
var multiLine = settings.multiLine;
|
||||||
|
if (settings.detectMultiLine && !multiLine &&
|
||||||
|
innerSpan.scrollHeight >= parseInt(window.getComputedStyle(innerSpan)['font-size'], 10) * 2){
|
||||||
|
multiLine = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're not treating this as a multiline string, don't let it wrap.
|
||||||
|
if (!multiLine) {
|
||||||
|
el.style['white-space'] = 'nowrap';
|
||||||
|
}
|
||||||
|
|
||||||
|
low = settings.minFontSize + 1;
|
||||||
|
high = settings.maxFontSize + 1;
|
||||||
|
|
||||||
|
// Binary search for best fit
|
||||||
|
while (low <= high) {
|
||||||
|
mid = parseInt((low + high) / 2, 10);
|
||||||
|
innerSpan.style.fontSize = mid + 'px';
|
||||||
|
if(innerSpan.scrollWidth <= originalWidth && (settings.widthOnly || innerSpan.scrollHeight <= originalHeight)){
|
||||||
|
low = mid + 1;
|
||||||
|
} else {
|
||||||
|
high = mid - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Sub 1 at the very end, this is closer to what we wanted.
|
||||||
|
innerSpan.style.fontSize = (mid - 1) + 'px';
|
||||||
|
|
||||||
|
// Our height is finalized. If we are aligning vertically, set that up.
|
||||||
|
if (settings.alignVert) {
|
||||||
|
addStyleSheet();
|
||||||
|
var height = innerSpan.scrollHeight;
|
||||||
|
if (window.getComputedStyle(el)['position'] === "static"){
|
||||||
|
el.style['position'] = 'relative';
|
||||||
|
}
|
||||||
|
if (!hasClass(innerSpan, "textFitAlignVert")){
|
||||||
|
innerSpan.className = innerSpan.className + " textFitAlignVert";
|
||||||
|
}
|
||||||
|
innerSpan.style['height'] = height + "px";
|
||||||
|
if (settings.alignVertWithFlexbox && !hasClass(el, "textFitAlignVertFlex")) {
|
||||||
|
el.className = el.className + " textFitAlignVertFlex";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate height without padding.
|
||||||
|
function innerHeight(el){
|
||||||
|
var style = window.getComputedStyle(el, null);
|
||||||
|
return el.clientHeight -
|
||||||
|
parseInt(style.getPropertyValue('padding-top'), 10) -
|
||||||
|
parseInt(style.getPropertyValue('padding-bottom'), 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate width without padding.
|
||||||
|
function innerWidth(el){
|
||||||
|
var style = window.getComputedStyle(el, null);
|
||||||
|
return el.clientWidth -
|
||||||
|
parseInt(style.getPropertyValue('padding-left'), 10) -
|
||||||
|
parseInt(style.getPropertyValue('padding-right'), 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Returns true if it is a DOM element
|
||||||
|
function isElement(o){
|
||||||
|
return (
|
||||||
|
typeof HTMLElement === "object" ? o instanceof HTMLElement : //DOM2
|
||||||
|
o && typeof o === "object" && o !== null && o.nodeType === 1 && typeof o.nodeName==="string"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasClass(element, cls) {
|
||||||
|
return (' ' + element.className + ' ').indexOf(' ' + cls + ' ') > -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Better than a stylesheet dependency
|
||||||
|
function addStyleSheet() {
|
||||||
|
if (document.getElementById("textFitStyleSheet")) return;
|
||||||
|
var style = [
|
||||||
|
".textFitAlignVert{",
|
||||||
|
"position: absolute;",
|
||||||
|
"top: 0; right: 0; bottom: 0; left: 0;",
|
||||||
|
"margin: auto;",
|
||||||
|
"display: flex;",
|
||||||
|
"justify-content: center;",
|
||||||
|
"flex-direction: column;",
|
||||||
|
"}",
|
||||||
|
".textFitAlignVertFlex{",
|
||||||
|
"display: flex;",
|
||||||
|
"}",
|
||||||
|
".textFitAlignVertFlex .textFitAlignVert{",
|
||||||
|
"position: static;",
|
||||||
|
"}",].join("");
|
||||||
|
|
||||||
|
var css = document.createElement("style");
|
||||||
|
css.type = "text/css";
|
||||||
|
css.id = "textFitStyleSheet";
|
||||||
|
css.innerHTML = style;
|
||||||
|
document.body.appendChild(css);
|
||||||
|
}
|
||||||
|
}));
|
356
openlp/core/display/render.py
Normal file
356
openlp/core/display/render.py
Normal file
@ -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
|
83
openlp/core/display/webengine.py
Normal file
83
openlp/core/display/webengine.py
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
# -*- 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], 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))
|
||||||
|
|
||||||
|
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)
|
265
openlp/core/display/window.py
Normal file
265
openlp/core/display/window.py
Normal file
@ -0,0 +1,265 @@
|
|||||||
|
# -*- 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 pathlib import Path
|
||||||
|
|
||||||
|
from PyQt5 import QtCore, QtWidgets, QtWebChannel
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
DISPLAY_PATH = Path(__file__) / '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):
|
||||||
|
"""
|
||||||
|
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('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)
|
||||||
|
|
||||||
|
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):
|
||||||
|
"""
|
||||||
|
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 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['file'].startswith('file://'):
|
||||||
|
image['file'] = 'file://' + image['file']
|
||||||
|
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['file'].startswith('file://'):
|
||||||
|
video['file'] = 'file://' + video['file']
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
print(theme.export_theme())
|
||||||
|
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)
|
210
tests/functional/openlp_core/display/test_render.py
Normal file
210
tests/functional/openlp_core/display/test_render.py
Normal file
@ -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"'
|
Loading…
Reference in New Issue
Block a user