2017-11-07 03:32:40 +00:00
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
|
|
|
|
2019-04-13 13:00:22 +00:00
|
|
|
##########################################################################
|
|
|
|
# OpenLP - Open Source Lyrics Projection #
|
|
|
|
# ---------------------------------------------------------------------- #
|
|
|
|
# Copyright (c) 2008-2019 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, either version 3 of the License, or #
|
|
|
|
# (at your option) any later version. #
|
|
|
|
# #
|
|
|
|
# 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, see <https://www.gnu.org/licenses/>. #
|
|
|
|
##########################################################################
|
2017-11-07 03:32:40 +00:00
|
|
|
"""
|
|
|
|
The :mod:`~openlp.core.display.window` module contains the display window
|
|
|
|
"""
|
2018-10-02 04:39:42 +00:00
|
|
|
import json
|
2017-11-07 03:32:40 +00:00
|
|
|
import logging
|
|
|
|
import os
|
2018-10-12 19:51:51 +00:00
|
|
|
import copy
|
2017-11-07 03:32:40 +00:00
|
|
|
|
2018-10-02 04:39:42 +00:00
|
|
|
from PyQt5 import QtCore, QtWebChannel, QtWidgets
|
2017-11-07 03:32:40 +00:00
|
|
|
|
2019-03-07 19:23:04 +00:00
|
|
|
from openlp.core.common.path import path_to_str
|
2018-10-24 20:10:32 +00:00
|
|
|
from openlp.core.common.settings import Settings
|
2018-11-02 19:01:38 +00:00
|
|
|
from openlp.core.common.registry import Registry
|
2019-02-27 20:17:00 +00:00
|
|
|
from openlp.core.common.applocation import AppLocation
|
2018-11-02 19:01:38 +00:00
|
|
|
from openlp.core.ui import HideMode
|
2018-11-09 15:31:33 +00:00
|
|
|
from openlp.core.display.screens import ScreenList
|
2018-10-02 04:39:42 +00:00
|
|
|
|
2017-11-07 03:32:40 +00:00
|
|
|
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
|
|
|
|
"""
|
2017-12-01 21:53:32 +00:00
|
|
|
def __init__(self, parent=None, screen=None):
|
2017-11-07 03:32:40 +00:00
|
|
|
"""
|
|
|
|
Create the display window
|
|
|
|
"""
|
|
|
|
super(DisplayWindow, self).__init__(parent)
|
|
|
|
# Need to import this inline to get around a QtWebEngine issue
|
|
|
|
from openlp.core.display.webengine import WebEngineView
|
|
|
|
self._is_initialised = False
|
|
|
|
self._fbo = None
|
2018-11-06 20:39:09 +00:00
|
|
|
self.setWindowFlags(QtCore.Qt.FramelessWindowHint | QtCore.Qt.Tool | QtCore.Qt.WindowStaysOnTopHint)
|
2018-11-03 05:48:43 +00:00
|
|
|
self.setAttribute(QtCore.Qt.WA_TranslucentBackground)
|
|
|
|
self.setAutoFillBackground(True)
|
2017-11-07 03:32:40 +00:00
|
|
|
self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
|
|
|
|
self.layout = QtWidgets.QVBoxLayout(self)
|
|
|
|
self.layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
self.webview = WebEngineView(self)
|
2018-11-03 05:48:43 +00:00
|
|
|
self.webview.setAttribute(QtCore.Qt.WA_TranslucentBackground)
|
|
|
|
self.webview.page().setBackgroundColor(QtCore.Qt.transparent)
|
2017-11-07 03:32:40 +00:00
|
|
|
self.layout.addWidget(self.webview)
|
|
|
|
self.webview.loadFinished.connect(self.after_loaded)
|
2019-02-27 20:17:00 +00:00
|
|
|
display_base_path = AppLocation.get_directory(AppLocation.AppDir) / 'core' / 'display' / 'html'
|
|
|
|
self.display_path = display_base_path / 'display.html'
|
|
|
|
self.checkerboard_path = display_base_path / 'checkerboard.png'
|
|
|
|
self.openlp_splash_screen_path = display_base_path / 'openlp-splash-screen.png'
|
|
|
|
self.set_url(QtCore.QUrl.fromLocalFile(path_to_str(self.display_path)))
|
2017-11-07 03:32:40 +00:00
|
|
|
self.media_watcher = MediaWatcher(self)
|
|
|
|
self.channel = QtWebChannel.QWebChannel(self)
|
|
|
|
self.channel.registerObject('mediaWatcher', self.media_watcher)
|
|
|
|
self.webview.page().setWebChannel(self.channel)
|
2018-10-12 19:51:51 +00:00
|
|
|
self.is_display = False
|
2018-10-24 20:10:32 +00:00
|
|
|
self.scale = 1
|
2018-11-02 19:01:38 +00:00
|
|
|
self.hide_mode = None
|
2017-12-01 21:53:32 +00:00
|
|
|
if screen and screen.is_display:
|
2018-11-02 19:01:38 +00:00
|
|
|
Registry().register_function('live_display_hide', self.hide_display)
|
|
|
|
Registry().register_function('live_display_show', self.show_display)
|
2017-12-01 21:53:32 +00:00
|
|
|
self.update_from_screen(screen)
|
2018-10-12 19:51:51 +00:00
|
|
|
self.is_display = True
|
2018-11-09 15:31:33 +00:00
|
|
|
# Only make visible on single monitor setup if setting enabled.
|
|
|
|
if len(ScreenList()) > 1 or Settings().value('core/display on monitor'):
|
2018-11-06 20:39:09 +00:00
|
|
|
self.show()
|
2017-12-01 21:53:32 +00:00
|
|
|
|
|
|
|
def update_from_screen(self, screen):
|
|
|
|
"""
|
|
|
|
Update the number and the geometry from the screen.
|
|
|
|
|
|
|
|
:param Screen screen: A `~openlp.core.display.screens.Screen` instance
|
|
|
|
"""
|
|
|
|
self.setGeometry(screen.display_geometry)
|
|
|
|
self.screen_number = screen.number
|
2017-11-07 03:32:40 +00:00
|
|
|
|
2018-11-13 21:00:14 +00:00
|
|
|
def set_single_image(self, bg_color, image_path):
|
|
|
|
"""
|
2019-02-13 20:28:10 +00:00
|
|
|
:param str bg_color: Background color
|
|
|
|
:param Path image_path: Path to the image
|
2018-11-13 21:00:14 +00:00
|
|
|
"""
|
|
|
|
image_uri = image_path.as_uri()
|
2018-11-03 05:48:43 +00:00
|
|
|
self.run_javascript('Display.setFullscreenImage("{bg_color}", "{image}");'.format(bg_color=bg_color,
|
2018-11-09 15:31:33 +00:00
|
|
|
image=image_uri))
|
2018-10-28 06:28:33 +00:00
|
|
|
|
|
|
|
def set_single_image_data(self, bg_color, image_data):
|
2018-11-03 05:48:43 +00:00
|
|
|
self.run_javascript('Display.setFullscreenImageFromData("{bg_color}", '
|
|
|
|
'"{image_data}");'.format(bg_color=bg_color, image_data=image_data))
|
2018-10-28 06:28:33 +00:00
|
|
|
|
2018-10-24 20:10:32 +00:00
|
|
|
def set_startup_screen(self):
|
|
|
|
bg_color = Settings().value('core/logo background color')
|
|
|
|
image = Settings().value('core/logo file')
|
|
|
|
if path_to_str(image).startswith(':'):
|
2019-02-27 20:17:00 +00:00
|
|
|
image = self.openlp_splash_screen_path
|
2018-11-09 15:31:33 +00:00
|
|
|
image_uri = image.as_uri()
|
2018-11-03 05:48:43 +00:00
|
|
|
self.run_javascript('Display.setStartupSplashScreen("{bg_color}", "{image}");'.format(bg_color=bg_color,
|
2018-11-09 15:31:33 +00:00
|
|
|
image=image_uri))
|
2018-10-24 20:10:32 +00:00
|
|
|
|
2017-11-07 03:32:40 +00:00
|
|
|
def set_url(self, url):
|
|
|
|
"""
|
|
|
|
Set the URL of the webview
|
2017-12-01 21:53:32 +00:00
|
|
|
|
2019-05-04 09:13:29 +00:00
|
|
|
:param QtCore.QUrl | str url: The URL to set
|
2017-11-07 03:32:40 +00:00
|
|
|
"""
|
|
|
|
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();')
|
2018-09-28 19:33:40 +00:00
|
|
|
self._is_initialised = True
|
2018-10-24 20:10:32 +00:00
|
|
|
self.set_startup_screen()
|
|
|
|
# Make sure the scale is set if it was attempted set before init
|
|
|
|
if self.scale != 1:
|
|
|
|
self.set_scale(self.scale)
|
2017-11-07 03:32:40 +00:00
|
|
|
|
|
|
|
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
|
|
|
|
"""
|
2018-10-12 19:51:51 +00:00
|
|
|
log.debug(script)
|
2017-11-07 03:32:40 +00:00
|
|
|
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
|
|
|
|
|
2018-03-28 05:39:47 +00:00
|
|
|
def go_to_slide(self, verse):
|
|
|
|
"""
|
|
|
|
Go to a particular slide.
|
|
|
|
|
|
|
|
:param str verse: The verse to go to, e.g. "V1" for songs, or just "0" for other types
|
|
|
|
"""
|
|
|
|
self.run_javascript('Display.goToSlide("{verse}");'.format(verse=verse))
|
|
|
|
|
2017-11-07 03:32:40 +00:00
|
|
|
def load_verses(self, verses):
|
|
|
|
"""
|
|
|
|
Set verses in the display
|
|
|
|
"""
|
|
|
|
json_verses = json.dumps(verses)
|
|
|
|
self.run_javascript('Display.setTextSlides({verses});'.format(verses=json_verses))
|
|
|
|
|
|
|
|
def load_images(self, images):
|
|
|
|
"""
|
|
|
|
Set images in the display
|
|
|
|
"""
|
2019-05-23 19:33:46 +00:00
|
|
|
imagesr = copy.deepcopy(images)
|
|
|
|
for image in imagesr:
|
|
|
|
image['path'] = image['path'].as_uri()
|
|
|
|
json_images = json.dumps(imagesr)
|
2017-11-07 03:32:40 +00:00
|
|
|
self.run_javascript('Display.setImageSlides({images});'.format(images=json_images))
|
|
|
|
|
|
|
|
def load_video(self, video):
|
|
|
|
"""
|
|
|
|
Load video in the display
|
|
|
|
"""
|
2019-05-23 19:33:46 +00:00
|
|
|
video = copy.deepcopy(video)
|
|
|
|
video['path'] = video['path'].as_uri()
|
2017-11-07 03:32:40 +00:00
|
|
|
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.
|
|
|
|
"""
|
2019-02-13 20:28:10 +00:00
|
|
|
self.run_javascript('Display.setPlaybackRate({rate});'.format(rate=rate))
|
2017-11-07 03:32:40 +00:00
|
|
|
|
|
|
|
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))
|
2019-02-13 20:28:10 +00:00
|
|
|
self.run_javascript('Display.setVideoVolume({level});'.format(level=level))
|
2017-11-07 03:32:40 +00:00
|
|
|
|
|
|
|
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
|
|
|
|
"""
|
2018-10-12 19:51:51 +00:00
|
|
|
# If background is transparent and this is not a display, inject checkerboard background image instead
|
|
|
|
if theme.background_type == 'transparent' and not self.is_display:
|
|
|
|
theme_copy = copy.deepcopy(theme)
|
|
|
|
theme_copy.background_type = 'image'
|
2019-02-27 20:17:00 +00:00
|
|
|
theme_copy.background_filename = self.checkerboard_path
|
2019-06-21 22:09:36 +00:00
|
|
|
exported_theme = theme_copy.export_theme(is_js=True)
|
2018-10-12 19:51:51 +00:00
|
|
|
else:
|
2019-06-21 22:09:36 +00:00
|
|
|
exported_theme = theme.export_theme(is_js=True)
|
2018-10-12 19:51:51 +00:00
|
|
|
self.run_javascript('Display.setTheme({theme});'.format(theme=exported_theme))
|
2017-11-07 03:32:40 +00:00
|
|
|
|
|
|
|
def get_video_types(self):
|
|
|
|
"""
|
|
|
|
Get the types of videos playable by the embedded media player
|
|
|
|
"""
|
|
|
|
return self.run_javascript('Display.getVideoTypes();', is_sync=True)
|
2018-03-25 00:31:12 +00:00
|
|
|
|
|
|
|
def show_display(self):
|
|
|
|
"""
|
|
|
|
Show the display
|
|
|
|
"""
|
2018-11-06 20:39:09 +00:00
|
|
|
if self.is_display:
|
2018-11-09 15:31:33 +00:00
|
|
|
# Only make visible on single monitor setup if setting enabled.
|
|
|
|
if len(ScreenList()) == 1 and not Settings().value('core/display on monitor'):
|
2018-11-06 20:39:09 +00:00
|
|
|
return
|
2018-03-25 00:31:12 +00:00
|
|
|
self.run_javascript('Display.show();')
|
2018-11-02 19:01:38 +00:00
|
|
|
# Check if setting for hiding logo on startup is enabled.
|
|
|
|
# If it is, display should remain hidden, otherwise logo is shown. (from def setup)
|
|
|
|
if self.isHidden() and not Settings().value('core/logo hide on startup'):
|
|
|
|
self.setVisible(True)
|
|
|
|
self.hide_mode = None
|
|
|
|
# Trigger actions when display is active again.
|
|
|
|
if self.is_display:
|
|
|
|
Registry().execute('live_display_active')
|
|
|
|
|
|
|
|
def blank_to_theme(self):
|
|
|
|
"""
|
|
|
|
Blank to theme
|
|
|
|
"""
|
|
|
|
self.run_javascript('Display.blankToTheme();')
|
|
|
|
|
|
|
|
def hide_display(self, mode=HideMode.Screen):
|
|
|
|
"""
|
|
|
|
Hide the display by making all layers transparent Store the images so they can be replaced when required
|
|
|
|
|
|
|
|
:param mode: How the screen is to be hidden
|
|
|
|
"""
|
|
|
|
log.debug('hide_display mode = {mode:d}'.format(mode=mode))
|
2018-11-06 20:39:09 +00:00
|
|
|
if self.is_display:
|
2018-11-09 15:31:33 +00:00
|
|
|
# Only make visible on single monitor setup if setting enabled.
|
|
|
|
if len(ScreenList()) == 1 and not Settings().value('core/display on monitor'):
|
2018-11-06 20:39:09 +00:00
|
|
|
return
|
2018-11-02 19:01:38 +00:00
|
|
|
if mode == HideMode.Screen:
|
|
|
|
self.setVisible(False)
|
|
|
|
elif mode == HideMode.Blank:
|
|
|
|
self.run_javascript('Display.blankToBlack();')
|
|
|
|
else:
|
|
|
|
self.run_javascript('Display.blankToTheme();')
|
|
|
|
if mode != HideMode.Screen:
|
|
|
|
if self.isHidden():
|
|
|
|
self.setVisible(True)
|
|
|
|
self.webview.setVisible(True)
|
|
|
|
self.hide_mode = mode
|
2018-10-13 20:55:36 +00:00
|
|
|
|
|
|
|
def set_scale(self, scale):
|
|
|
|
"""
|
|
|
|
Set the HTML scale
|
|
|
|
"""
|
2018-10-24 20:10:32 +00:00
|
|
|
self.scale = scale
|
2018-11-03 05:48:43 +00:00
|
|
|
self.run_javascript('Display.setScale({scale});'.format(scale=scale * 100))
|
2018-11-08 21:06:35 +00:00
|
|
|
|
2019-02-22 11:31:38 +00:00
|
|
|
def alert(self, text, settings):
|
2018-11-08 21:06:35 +00:00
|
|
|
"""
|
|
|
|
Set an alert
|
|
|
|
"""
|
2019-02-22 11:31:38 +00:00
|
|
|
self.run_javascript('Display.alert("{text}", \'{settings}\');'.format(text=text, settings=settings))
|
2019-03-04 10:29:08 +00:00
|
|
|
# TODO: Add option to prevent scrolling
|