Initial copy of new renderer into the source tree plus some rearranged files. Nothing working or running yet.

This commit is contained in:
Tomas Groth 2017-09-19 21:52:24 +02:00
parent 2cfb03a606
commit 3923601202
12 changed files with 8073 additions and 20 deletions

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

View File

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

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

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

View File

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

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