Apparently some files were missing after the merge

This commit is contained in:
Raoul Snyman 2017-11-06 20:32:40 -07:00
parent 4dba3b178d
commit 5e1b591f62
11 changed files with 9374 additions and 0 deletions

View 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()

View File

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

View 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>

View 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;
});

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -0,0 +1,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('&nbsp;', ' ')
for tag in FormattingTags.get_html_tags():
text = text.replace(tag['start tag'], '')
text = text.replace(tag['end tag'], '')
# Remove ChordPro tags
if can_remove_chords:
text = re.sub(r'\[.+?\]', r'', text)
return text
def has_valid_tags(text):
"""
The :func:`~openlp.core.display.render.has_valid_tags` function validates the tags within ``text``.
:param str text: The string with formatting tags in it.
:returns bool: Returns True if tags are valid, False if there are parsing problems.
"""
return True
def render_chords_in_line(match):
"""
Render the chords in the line and align them using whitespaces.
NOTE: There is equivalent javascript code in chords.js, in the updateSlide function. Make sure to update both!
:param str match: The line which contains chords
:returns str: The line with rendered html-chords
"""
whitespaces = ''
chord_length = 0
tail_length = 0
# The match could be "[G]sweet the " from a line like "A[D]mazing [D7]grace! How [G]sweet the [D]sound!"
# The actual chord, would be "G" in match "[G]sweet the "
chord = match.group(1)
# The tailing word of the chord, would be "sweet" in match "[G]sweet the "
tail = match.group(2)
# The remainder of the line, until line end or next chord. Would be " the " in match "[G]sweet the "
remainder = match.group(3)
# Line end if found, else None
end = match.group(4)
# Based on char width calculate width of chord
for chord_char in chord:
if chord_char not in SLIM_CHARS:
chord_length += 2
else:
chord_length += 1
# Based on char width calculate width of tail
for tail_char in tail:
if tail_char not in SLIM_CHARS:
tail_length += 2
else:
tail_length += 1
# Based on char width calculate width of remainder
for remainder_char in remainder:
if remainder_char not in SLIM_CHARS:
tail_length += 2
else:
tail_length += 1
# If the chord is wider than the tail+remainder and the line goes on, some padding is needed
if chord_length >= tail_length and end is None:
# Decide if the padding should be "_" for drawing out words or spaces
if tail:
if not remainder:
for c in range(math.ceil((chord_length - tail_length) / 2) + 2):
whitespaces += '_'
else:
for c in range(chord_length - tail_length + 1):
whitespaces += '&nbsp;'
else:
if not remainder:
for c in range(math.floor((chord_length - tail_length) / 2)):
whitespaces += '_'
else:
for c in range(chord_length - tail_length + 1):
whitespaces += '&nbsp;'
else:
if not tail and remainder and remainder[0] == ' ':
for c in range(chord_length):
whitespaces += '&nbsp;'
if whitespaces:
if '_' in whitespaces:
ws_length = len(whitespaces)
if ws_length == 1:
whitespaces = '&ndash;'
else:
wsl_mod = ws_length // 2
ws_right = ws_left = ' ' * wsl_mod
whitespaces = ws_left + '&ndash;' + ws_right
whitespaces = WHITESPACE_TEMPLATE.format(whitespaces=whitespaces)
return CHORD_LINE_TEMPLATE.format(chord=html.escape(chord), tail=html.escape(tail), whitespace=whitespaces,
remainder=html.escape(remainder))
def render_chords(text):
"""
Render ChordPro tags
:param str text: The text containing the chords
:returns str: The text containing the rendered chords
"""
text_lines = text.split('{br}')
rendered_lines = []
chords_on_prev_line = False
for line in text_lines:
# If a ChordPro is detected in the line, replace it with a html-span tag and wrap the line in a span tag.
if '[' in line and ']' in line:
if chords_on_prev_line:
chord_template = CHORD_TEMPLATE
else:
chord_template = FIRST_CHORD_TEMPLATE
chords_on_prev_line = True
# Matches a chord, a tail, a remainder and a line end. See expand_and_align_chords_in_line() for more info.
new_line = chord_template.format(chord=CHORD_LINE_MATCH.sub(render_chords_in_line, line))
rendered_lines.append(new_line)
else:
chords_on_prev_line = False
rendered_lines.append(html.escape(line))
return '{br}'.join(rendered_lines)
def compare_chord_lyric_width(chord, lyric):
"""
Compare the width of chord and lyrics. If chord width is greater than the lyric width the diff is returned.
:param chord:
:param lyric:
:return:
"""
chord_length = 0
if chord == '&nbsp;':
return 0
chord = re.sub(r'\{.*?\}', r'', chord)
lyric = re.sub(r'\{.*?\}', r'', lyric)
for chord_char in chord:
if chord_char not in SLIM_CHARS:
chord_length += 2
else:
chord_length += 1
lyriclen = 0
for lyric_char in lyric:
if lyric_char not in SLIM_CHARS:
lyriclen += 2
else:
lyriclen += 1
if chord_length > lyriclen:
return chord_length - lyriclen
else:
return 0
def find_formatting_tags(text, active_formatting_tags):
"""
Look for formatting tags in lyrics and adds/removes them to/from the given list. Returns the update list.
:param text:
:param active_formatting_tags:
:return:
"""
if not re.search(r'\{.*?\}', text):
return active_formatting_tags
word_iterator = iter(text)
# Loop through lyrics to find any formatting tags
for char in word_iterator:
if char == '{':
tag = ''
char = next(word_iterator)
start_tag = True
if char == '/':
start_tag = False
char = next(word_iterator)
while char != '}':
tag += char
char = next(word_iterator)
# See if the found tag has an end tag
for formatting_tag in FormattingTags.get_html_tags():
if formatting_tag['start tag'] == '{' + tag + '}':
if formatting_tag['end tag']:
if start_tag:
# prepend the new tag to the list of active formatting tags
active_formatting_tags[:0] = [tag]
else:
# remove the tag from the list
active_formatting_tags.remove(tag)
# Break out of the loop matching the found tag against the tag list.
break
return active_formatting_tags
def render_chords_for_printing(text, line_split):
"""
Render ChordPro tags for printing
:param str text: The text containing the chords to be rendered.
:param str line_split: The character(s) used to split lines
:returns str: The rendered chords
"""
if not re.search(r'\[.*?\]', text):
return text
text_lines = text.split(line_split)
rendered_text_lines = []
for line in text_lines:
# If a ChordPro is detected in the line, build html tables.
new_line = '<table class="line" width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td>'
active_formatting_tags = []
if re.search(r'\[.*?\]', line):
words = line.split(' ')
in_chord = False
for word in words:
chords = []
lyrics = []
new_line += '<table class="segment" cellpadding="0" cellspacing="0" border="0" align="left">'
# If the word contains a chord, we need to handle it.
if re.search(r'\[.*?\]', word):
chord = ''
lyric = ''
# Loop over each character of the word
for char in word:
if char == '[':
in_chord = True
if lyric != '':
if chord == '':
chord = '&nbsp;'
chords.append(chord)
lyrics.append(lyric)
chord = ''
lyric = ''
elif char == ']' and in_chord:
in_chord = False
elif in_chord:
chord += char
else:
lyric += char
if lyric != '' or chord != '':
if chord == '':
chord = '&nbsp;'
if lyric == '':
lyric = '&nbsp;'
chords.append(chord)
lyrics.append(lyric)
new_chord_line = '<tr class="chordrow">'
new_lyric_line = '</tr><tr>'
for i in range(len(lyrics)):
spacer = compare_chord_lyric_width(chords[i], lyrics[i])
# Handle formatting tags
start_formatting_tags = ''
if active_formatting_tags:
start_formatting_tags = '{' + '}{'.join(active_formatting_tags) + '}'
# Update list of active formatting tags
active_formatting_tags = find_formatting_tags(lyrics[i], active_formatting_tags)
end_formatting_tags = ''
if active_formatting_tags:
end_formatting_tags = '{/' + '}{/'.join(active_formatting_tags) + '}'
new_chord_line += '<td class="chord">%s</td>' % chords[i]
# Check if this is the last column, if so skip spacing calc and instead insert a single space
if i + 1 == len(lyrics):
new_lyric_line += '<td class="lyrics">{starttags}{lyrics}&nbsp;{endtags}</td>'.format(
starttags=start_formatting_tags, lyrics=lyrics[i], endtags=end_formatting_tags)
else:
spacing = ''
if spacer > 0:
space = '&nbsp;' * int(math.ceil(spacer / 2))
spacing = '<span class="chordspacing">%s-%s</span>' % (space, space)
new_lyric_line += '<td class="lyrics">{starttags}{lyrics}{spacing}{endtags}</td>'.format(
starttags=start_formatting_tags, lyrics=lyrics[i], spacing=spacing,
endtags=end_formatting_tags)
new_line += new_chord_line + new_lyric_line + '</tr>'
else:
start_formatting_tags = ''
if active_formatting_tags:
start_formatting_tags = '{' + '}{'.join(active_formatting_tags) + '}'
active_formatting_tags = find_formatting_tags(word, active_formatting_tags)
end_formatting_tags = ''
if active_formatting_tags:
end_formatting_tags = '{/' + '}{/'.join(active_formatting_tags) + '}'
new_line += '<tr class="chordrow"><td class="chord">&nbsp;</td></tr><tr><td class="lyrics">' \
'{starttags}{lyrics}&nbsp;{endtags}</td></tr>'.format(
starttags=start_formatting_tags, lyrics=word, endtags=end_formatting_tags)
new_line += '</table>'
else:
new_line += line
new_line += '</td></tr></table>'
rendered_text_lines.append(new_line)
# the {br} tag used to split lines is not inserted again since the style of the line-tables makes them redundant.
return ''.join(rendered_text_lines)
def render_tags(text, can_render_chords=False, is_printing=False):
"""
The :func:`~openlp.core.display.render.render_tags` function takes a stirng with OpenLP-style tags in it
and replaces them with the HTML version.
:param str text: The string with OpenLP-style tags to be rendered.
:param bool can_render_chords: Should the chords be rendererd?
:param bool is_printing: Are we going to print this?
:returns str: The HTML version of the tags is returned as a string.
"""
if can_render_chords:
if is_printing:
text = render_chords_for_printing(text, '{br}')
else:
text = render_chords(text)
for tag in FormattingTags.get_html_tags():
text = text.replace(tag['start tag'], tag['start html'])
text = text.replace(tag['end tag'], tag['end html'])
return text

View 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)

View 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)

View 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&nbsp;{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">' \
'&nbsp;&nbsp;</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>&#x27;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">&nbsp;</td><td class="chord">D</td></tr><tr><td class="lyrics">{st}{/st}' \
'</td><td class="lyrics">{st}Amazing&nbsp;{/st}</td></tr></table><table class="segment" ' \
'cellpadding="0" cellspacing="0" border="0" align="left"><tr class="chordrow">' \
'<td class="chord">&nbsp;</td><td class="chord">D7</td></tr><tr><td class="lyrics">{st}{r}gr' \
'{/r}{/st}</td><td class="lyrics">{r}{st}ace{/r}&nbsp;{/st}</td></tr></table><table ' \
'class="segment" cellpadding="0" cellspacing="0" border="0" align="left"><tr class="chordrow">'\
'<td class="chord">&nbsp;</td></tr><tr><td class="lyrics">{st}&nbsp;{/st}</td></tr></table>' \
'<table class="segment" cellpadding="0" cellspacing="0" border="0" align="left"><tr ' \
'class="chordrow"><td class="chord">&nbsp;</td></tr><tr><td class="lyrics">{st}how&nbsp;{/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&nbsp;{/st}</td></tr></table><table class="segment" cellpadding="0" cellspacing="0" ' \
'border="0" align="left"><tr class="chordrow"><td class="chord">&nbsp;</td></tr><tr><td ' \
'class="lyrics">{st}the&nbsp;{/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&nbsp;{/st}</td></tr></table><table class="segment" ' \
'cellpadding="0" cellspacing="0" border="0" align="left"><tr class="chordrow"><td ' \
'class="chord">&nbsp;</td></tr><tr><td class="lyrics">{st}&nbsp;{/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}&nbsp;</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"'