forked from openlp/openlp
Initial copy of new renderer into the source tree plus some rearranged files. Nothing working or running yet.
This commit is contained in:
parent
2cfb03a606
commit
3923601202
24
openlp/core/display/__init__.py
Normal file
24
openlp/core/display/__init__.py
Normal file
@ -0,0 +1,24 @@
|
||||
# -*- 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 Display module.
|
||||
"""
|
@ -20,7 +20,7 @@
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
"""
|
||||
The :mod:`maindisplay` module provides the functionality to display screens and play multimedia within OpenLP.
|
||||
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:
|
||||
|
||||
@ -72,7 +72,7 @@ QGraphicsView {
|
||||
"""
|
||||
|
||||
|
||||
class Display(QtWidgets.QGraphicsView):
|
||||
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.
|
||||
@ -86,7 +86,7 @@ class Display(QtWidgets.QGraphicsView):
|
||||
self.is_live = True
|
||||
if self.is_live:
|
||||
self.parent = lambda: parent
|
||||
super(Display, self).__init__()
|
||||
super(Canvas, self).__init__()
|
||||
self.controller = parent
|
||||
self.screen = {}
|
||||
|
||||
@ -128,7 +128,7 @@ class Display(QtWidgets.QGraphicsView):
|
||||
self.web_loaded = True
|
||||
|
||||
|
||||
class MainDisplay(OpenLPMixin, Display, RegistryProperties):
|
||||
class MainCanvas(OpenLPMixin, Display, RegistryProperties):
|
||||
"""
|
||||
This is the display screen as a specialized class from the Display class
|
||||
"""
|
||||
@ -136,7 +136,7 @@ class MainDisplay(OpenLPMixin, Display, RegistryProperties):
|
||||
"""
|
||||
Constructor
|
||||
"""
|
||||
super(MainDisplay, self).__init__(parent)
|
||||
super(MainCanvas, self).__init__(parent)
|
||||
self.screens = ScreenList()
|
||||
self.rebuild_css = False
|
||||
self.hide_mode = None
|
||||
@ -175,7 +175,7 @@ class MainDisplay(OpenLPMixin, Display, RegistryProperties):
|
||||
pythonapi.PyCapsule_SetName(nsview_pointer, c_char_p(b"objc.__object__"))
|
||||
# Covert the NSView pointer into a pyobjc NSView object
|
||||
self.pyobjc_nsview = objc_object(cobject=nsview_pointer)
|
||||
# Set the window level so that the MainDisplay is above the menu bar and dock
|
||||
# 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)
|
||||
@ -244,16 +244,16 @@ class MainDisplay(OpenLPMixin, Display, RegistryProperties):
|
||||
"""
|
||||
Setup the interface translation strings.
|
||||
"""
|
||||
self.setWindowTitle(translate('OpenLP.MainDisplay', 'OpenLP Display'))
|
||||
self.setWindowTitle(translate('OpenLP.MainCanvas', 'OpenLP Display'))
|
||||
|
||||
def setup(self):
|
||||
"""
|
||||
Set up and build the output screen
|
||||
"""
|
||||
self.log_debug('Start MainDisplay setup (live = {islive})'.format(islive=self.is_live))
|
||||
self.log_debug('Start MainCanvas setup (live = {islive})'.format(islive=self.is_live))
|
||||
self.screen = self.screens.current
|
||||
self.setVisible(False)
|
||||
Display.setup(self)
|
||||
Canvas.setup(self)
|
||||
if self.is_live:
|
||||
# Build the initial frame.
|
||||
background_color = QtGui.QColor()
|
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>
|
606
openlp/core/display/html/display.js
Normal file
606
openlp/core/display/html/display.js
Normal file
@ -0,0 +1,606 @@
|
||||
/**
|
||||
* 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);
|
||||
};
|
||||
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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
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);
|
||||
}
|
||||
}));
|
25
openlp/core/display/render.py
Normal file
25
openlp/core/display/render.py
Normal file
@ -0,0 +1,25 @@
|
||||
class Renderer(object):
|
||||
"""
|
||||
The renderer builds up a web page and then passes it on to the Display to show
|
||||
"""
|
||||
def __init__(self, displays=None):
|
||||
"""
|
||||
Set up the renderer
|
||||
"""
|
||||
self.displays = displays if displays else []
|
||||
self.scripts = []
|
||||
self.styles = []
|
||||
|
||||
def add_display(self, display):
|
||||
"""
|
||||
Add a display to the renderer.
|
||||
|
||||
The renderer will render the HTML, Javascript and CSS and send it to all of its displays
|
||||
"""
|
||||
self.displays.append(display)
|
||||
|
||||
def add_javascript(self, script):
|
||||
"""
|
||||
Add Javascript to the slide
|
||||
"""
|
||||
pass
|
@ -29,7 +29,7 @@ from openlp.core.common import Registry, RegistryProperties, OpenLPMixin, Regist
|
||||
from openlp.core.lib import FormattingTags, ImageSource, ItemCapabilities, ScreenList, ServiceItem, expand_tags, \
|
||||
build_lyrics_format_css, build_lyrics_outline_css, build_chords_css
|
||||
from openlp.core.common import ThemeLevel
|
||||
from openlp.core.ui import MainDisplay
|
||||
from openlp.core.display.canvas import MainCanvas
|
||||
|
||||
VERSE = 'The Lord said to {r}Noah{/r}: \n' \
|
||||
'There\'s gonna be a {su}floody{/su}, {sb}floody{/sb}\n' \
|
||||
@ -53,7 +53,7 @@ class Renderer(OpenLPMixin, RegistryMixin, RegistryProperties):
|
||||
Initialise the renderer.
|
||||
"""
|
||||
super(Renderer, self).__init__(None)
|
||||
# Need live behaviour if this is also working as a pseudo MainDisplay.
|
||||
# Need live behaviour if this is also working as a pseudo MainCanvas.
|
||||
self.screens = ScreenList()
|
||||
self.theme_level = ThemeLevel.Global
|
||||
self.global_theme_name = ''
|
||||
@ -71,18 +71,18 @@ class Renderer(OpenLPMixin, RegistryMixin, RegistryProperties):
|
||||
"""
|
||||
Initialise functions
|
||||
"""
|
||||
self.display = MainDisplay(self)
|
||||
self.display.setup()
|
||||
self.canvas = MainCanvas(self)
|
||||
self.canvas.setup()
|
||||
|
||||
def update_display(self):
|
||||
"""
|
||||
Updates the renderer's information about the current screen.
|
||||
"""
|
||||
self._calculate_default()
|
||||
if self.display:
|
||||
self.display.close()
|
||||
self.display = MainDisplay(self)
|
||||
self.display.setup()
|
||||
if self.canvas:
|
||||
self.canvas.close()
|
||||
self.canvas = MainCanvas(self)
|
||||
self.canvas.setup()
|
||||
self._theme_dimensions = {}
|
||||
|
||||
def update_theme(self, theme_name, old_theme_name=None, only_delete=False):
|
||||
@ -215,10 +215,10 @@ class Renderer(OpenLPMixin, RegistryMixin, RegistryProperties):
|
||||
service_item.footer = footer
|
||||
service_item.render(True)
|
||||
if not self.force_page:
|
||||
self.display.build_html(service_item)
|
||||
self.canvas.build_html(service_item)
|
||||
raw_html = service_item.get_rendered_frame(0)
|
||||
self.display.text(raw_html, False)
|
||||
preview = self.display.preview()
|
||||
self.canvas.text(raw_html, False)
|
||||
preview = self.canvas.preview()
|
||||
return preview
|
||||
self.force_page = False
|
||||
|
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)
|
253
openlp/core/display/window.py
Normal file
253
openlp/core/display/window.py
Normal file
@ -0,0 +1,253 @@
|
||||
import logging
|
||||
import os
|
||||
import json
|
||||
|
||||
from PyQt5 import QtCore, QtWidgets, QtWebEngineWidgets, QtWebChannel
|
||||
|
||||
from openlp.core.display.webengine import WebEngineView
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
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)
|
||||
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 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
|
||||
|
||||
def set_verses(self, verses):
|
||||
"""
|
||||
Set verses in the display
|
||||
"""
|
||||
json_verses = json.dumps(verses)
|
||||
self.run_javascript('Display.setTextSlides({verses});'.format(verses=json_verses))
|
||||
|
||||
def set_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 set_video(self, video):
|
||||
"""
|
||||
Set 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()))
|
Loading…
Reference in New Issue
Block a user