forked from openlp/openlp
Make tests runnable and clean up a bit
This commit is contained in:
parent
e7526f1e59
commit
38c9514b80
@ -1,769 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||
|
||||
###############################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2017 OpenLP Developers #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# This program is free software; you can redistribute it and/or modify it #
|
||||
# under the terms of the GNU General Public License as published by the Free #
|
||||
# Software Foundation; version 2 of the License. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT #
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
|
||||
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
|
||||
# more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License along #
|
||||
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
"""
|
||||
The :mod:`canvas` module provides the functionality to display screens and play multimedia within OpenLP.
|
||||
|
||||
Some of the code for this form is based on the examples at:
|
||||
|
||||
* `http://www.steveheffernan.com/html5-video-player/demo-video-player.html`_
|
||||
* `http://html5demos.com/two-videos`_
|
||||
"""
|
||||
import html
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtMultimedia, QtWebChannel, QtWebEngineWidgets, QtWidgets
|
||||
|
||||
from openlp.core.common import is_macosx, is_win
|
||||
from openlp.core.common.applocation import AppLocation
|
||||
from openlp.core.common.i18n import translate
|
||||
from openlp.core.common.mixins import LogMixin, RegistryProperties
|
||||
from openlp.core.common.path import path_to_str
|
||||
from openlp.core.common.registry import Registry
|
||||
from openlp.core.common.settings import Settings
|
||||
from openlp.core.display.screens import ScreenList
|
||||
from openlp.core.display.webengine import WebEngineView
|
||||
from openlp.core.display.window import MediaWatcher
|
||||
from openlp.core.lib import ImageSource, ServiceItem, build_html, expand_tags, image_to_byte
|
||||
from openlp.core.lib.theme import BackgroundType
|
||||
from openlp.core.ui import AlertLocation, DisplayControllerType, HideMode
|
||||
|
||||
|
||||
if is_macosx():
|
||||
from ctypes import pythonapi, c_void_p, c_char_p, py_object
|
||||
|
||||
from sip import voidptr
|
||||
from objc import objc_object
|
||||
from AppKit import NSMainMenuWindowLevel, NSWindowCollectionBehaviorManaged
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
OPAQUE_STYLESHEET = """
|
||||
QWidget {
|
||||
border: 0px;
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
}
|
||||
QGraphicsView {}
|
||||
"""
|
||||
TRANSPARENT_STYLESHEET = """
|
||||
QWidget {
|
||||
border: 0px;
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
}
|
||||
QGraphicsView {
|
||||
background: transparent;
|
||||
border: 0px;
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
class Canvas(QtWidgets.QGraphicsView):
|
||||
"""
|
||||
This is a general display screen class. Here the general display settings will done. It will be used as
|
||||
specialized classes by Main Display and Preview display.
|
||||
"""
|
||||
def __init__(self, parent):
|
||||
"""
|
||||
Constructor
|
||||
"""
|
||||
self.is_live = False
|
||||
if hasattr(parent, 'is_live') and parent.is_live:
|
||||
self.is_live = True
|
||||
if self.is_live:
|
||||
self.parent = lambda: parent
|
||||
super(Canvas, self).__init__()
|
||||
self.controller = parent
|
||||
self.screen = {}
|
||||
|
||||
def setup(self):
|
||||
"""
|
||||
Set up and build the screen base
|
||||
"""
|
||||
self.setGeometry(self.screen['size'])
|
||||
#self.web_view = QtWebKitWidgets.QWebView(self)
|
||||
#self.web_view.setGeometry(0, 0, self.screen['size'].width(), self.screen['size'].height())
|
||||
#self.web_view.settings().setAttribute(QtWebKit.QWebSettings.PluginsEnabled, True)
|
||||
#palette = self.web_view.palette()
|
||||
#palette.setBrush(QtGui.QPalette.Base, QtCore.Qt.transparent)
|
||||
#self.web_view.page().setPalette(palette)
|
||||
#self.web_view.setAttribute(QtCore.Qt.WA_OpaquePaintEvent, False)
|
||||
#self.page = self.web_view.page()
|
||||
#self.frame = self.page.mainFrame()
|
||||
#if self.is_live and log.getEffectiveLevel() == logging.DEBUG:
|
||||
# self.web_view.settings().setAttribute(QtWebKit.QWebSettings.DeveloperExtrasEnabled, True)
|
||||
|
||||
|
||||
self.webview = WebEngineView(self)
|
||||
self.webview.setGeometry(0, 0, self.screen['size'].width(), self.screen['size'].height())
|
||||
self.webview.settings().setAttribute(QtWebEngineWidgets.QWebEngineSettings.PluginsEnabled, True)
|
||||
self.layout = QtWidgets.QVBoxLayout(self)
|
||||
self.layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.layout.addWidget(self.webview)
|
||||
self.webview.loadFinished.connect(self.after_loaded)
|
||||
self.set_url(QtCore.QUrl('file://' + os.getcwd() + '/display.html'))
|
||||
self.media_watcher = MediaWatcher(self)
|
||||
self.channel = QtWebChannel.QWebChannel(self)
|
||||
self.channel.registerObject('mediaWatcher', self.media_watcher)
|
||||
self.webview.page().setWebChannel(self.channel)
|
||||
self.webview.loadFinished.connect(self.is_web_loaded)
|
||||
|
||||
self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
|
||||
self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
|
||||
|
||||
def resizeEvent(self, event):
|
||||
"""
|
||||
React to resizing of this display
|
||||
|
||||
:param event: The event to be handled
|
||||
"""
|
||||
if hasattr(self, 'web_view'):
|
||||
self.web_view.setGeometry(0, 0, self.width(), self.height())
|
||||
|
||||
def is_web_loaded(self, field=None):
|
||||
"""
|
||||
Called by webView event to show display is fully loaded
|
||||
"""
|
||||
self.web_loaded = True
|
||||
|
||||
def set_url(self, url):
|
||||
"""
|
||||
Set the URL of the webview
|
||||
"""
|
||||
if not isinstance(url, QtCore.QUrl):
|
||||
url = QtCore.QUrl(url)
|
||||
self.webview.setUrl(url)
|
||||
|
||||
def set_html(self, html):
|
||||
"""
|
||||
Set the html
|
||||
"""
|
||||
self.webview.setHtml(html)
|
||||
|
||||
def after_loaded(self, state):
|
||||
"""
|
||||
Add stuff after page initialisation
|
||||
"""
|
||||
self.run_javascript('Display.init();')
|
||||
|
||||
def add_script_source(self, fname, source):
|
||||
"""
|
||||
Add a script of source code
|
||||
"""
|
||||
js = QtWebEngineWidgets.QWebEngineScript()
|
||||
js.setSourceCode(source)
|
||||
js.setName(fname)
|
||||
js.setWorldId(QtWebEngineWidgets.QWebEngineScript.MainWorld)
|
||||
self.webview.page().scripts().insert(js)
|
||||
|
||||
def add_script(self, fname):
|
||||
"""
|
||||
Add a script to the page
|
||||
"""
|
||||
js_file = QtCore.QFile(fname)
|
||||
if not js_file.open(QtCore.QIODevice.ReadOnly):
|
||||
log.warning('Could not open %s: %s', fname, js_file.errorString())
|
||||
return
|
||||
self.add_script_source(os.path.basename(fname), str(bytes(js_file.readAll()), 'utf-8'))
|
||||
|
||||
def run_javascript(self, script, is_sync=False):
|
||||
"""
|
||||
Run some Javascript in the WebView
|
||||
|
||||
:param script: The script to run, a string
|
||||
:param is_sync: Run the script synchronously. Defaults to False
|
||||
"""
|
||||
if not is_sync:
|
||||
self.webview.page().runJavaScript(script)
|
||||
else:
|
||||
self.__script_done = False
|
||||
self.__script_result = None
|
||||
|
||||
def handle_result(result):
|
||||
"""
|
||||
Handle the result from the asynchronous call
|
||||
"""
|
||||
self.__script_done = True
|
||||
self.__script_result = result
|
||||
|
||||
self.webview.page().runJavaScript(script, handle_result)
|
||||
while not self.__script_done:
|
||||
# TODO: Figure out how to break out of a potentially infinite loop
|
||||
QtWidgets.QApplication.instance().processEvents()
|
||||
return self.__script_result
|
||||
|
||||
|
||||
class MainCanvas(OpenLPMixin, Canvas, RegistryProperties):
|
||||
"""
|
||||
This is the display screen as a specialized class from the Display class
|
||||
"""
|
||||
def __init__(self, parent):
|
||||
"""
|
||||
Constructor
|
||||
"""
|
||||
super(MainCanvas, self).__init__(parent)
|
||||
self.screens = ScreenList()
|
||||
self.rebuild_css = False
|
||||
self.hide_mode = None
|
||||
self.override = {}
|
||||
self.retranslate_ui()
|
||||
self.media_object = None
|
||||
if self.is_live:
|
||||
self.audio_player = AudioPlayer(self)
|
||||
else:
|
||||
self.audio_player = None
|
||||
self.first_time = True
|
||||
self.web_loaded = True
|
||||
self.setStyleSheet(OPAQUE_STYLESHEET)
|
||||
window_flags = QtCore.Qt.FramelessWindowHint | QtCore.Qt.Tool | QtCore.Qt.WindowStaysOnTopHint
|
||||
if Settings().value('advanced/x11 bypass wm'):
|
||||
window_flags |= QtCore.Qt.X11BypassWindowManagerHint
|
||||
# TODO: The following combination of window_flags works correctly
|
||||
# on Mac OS X. For next OpenLP version we should test it on other
|
||||
# platforms. For OpenLP 2.0 keep it only for OS X to not cause any
|
||||
# regressions on other platforms.
|
||||
if is_macosx():
|
||||
window_flags = QtCore.Qt.FramelessWindowHint | QtCore.Qt.Window | QtCore.Qt.NoDropShadowWindowHint
|
||||
self.setWindowFlags(window_flags)
|
||||
self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
|
||||
self.set_transparency(False)
|
||||
if is_macosx():
|
||||
if self.is_live:
|
||||
# Get a pointer to the underlying NSView
|
||||
try:
|
||||
nsview_pointer = self.winId().ascapsule()
|
||||
except:
|
||||
nsview_pointer = voidptr(self.winId()).ascapsule()
|
||||
# Set PyCapsule name so pyobjc will accept it
|
||||
pythonapi.PyCapsule_SetName.restype = c_void_p
|
||||
pythonapi.PyCapsule_SetName.argtypes = [py_object, c_char_p]
|
||||
pythonapi.PyCapsule_SetName(nsview_pointer, c_char_p(b"objc.__object__"))
|
||||
# Covert the NSView pointer into a pyobjc NSView object
|
||||
self.pyobjc_nsview = objc_object(cobject=nsview_pointer)
|
||||
# Set the window level so that the MainCanvas is above the menu bar and dock
|
||||
self.pyobjc_nsview.window().setLevel_(NSMainMenuWindowLevel + 2)
|
||||
# Set the collection behavior so the window is visible when Mission Control is activated
|
||||
self.pyobjc_nsview.window().setCollectionBehavior_(NSWindowCollectionBehaviorManaged)
|
||||
if self.screens.current['primary']:
|
||||
# Connect focusWindowChanged signal so we can change the window level when the display is not in
|
||||
# focus on the primary screen
|
||||
self.application.focusWindowChanged.connect(self.change_window_level)
|
||||
if self.is_live:
|
||||
Registry().register_function('live_display_hide', self.hide_display)
|
||||
Registry().register_function('live_display_show', self.show_display)
|
||||
Registry().register_function('update_display_css', self.css_changed)
|
||||
self.close_display = False
|
||||
|
||||
def closeEvent(self, event):
|
||||
"""
|
||||
Catch the close event, and check that the close event is triggered by OpenLP closing the display.
|
||||
On Windows this event can be triggered by pressing ALT+F4, which we want to ignore.
|
||||
|
||||
:param event: The triggered event
|
||||
"""
|
||||
if self.close_display:
|
||||
super().closeEvent(event)
|
||||
else:
|
||||
event.ignore()
|
||||
|
||||
def close(self):
|
||||
"""
|
||||
Remove registered function on close.
|
||||
"""
|
||||
if self.is_live:
|
||||
if is_macosx():
|
||||
# Block signals so signal we are disconnecting can't get called while we disconnect it
|
||||
self.blockSignals(True)
|
||||
if self.screens.current['primary']:
|
||||
self.application.focusWindowChanged.disconnect()
|
||||
self.blockSignals(False)
|
||||
Registry().remove_function('live_display_hide', self.hide_display)
|
||||
Registry().remove_function('live_display_show', self.show_display)
|
||||
Registry().remove_function('update_display_css', self.css_changed)
|
||||
self.close_display = True
|
||||
super().close()
|
||||
|
||||
def set_transparency(self, enabled):
|
||||
"""
|
||||
Set the transparency of the window
|
||||
|
||||
:param enabled: Is transparency enabled
|
||||
"""
|
||||
if enabled:
|
||||
self.setAutoFillBackground(False)
|
||||
self.setStyleSheet(TRANSPARENT_STYLESHEET)
|
||||
else:
|
||||
self.setAttribute(QtCore.Qt.WA_NoSystemBackground, False)
|
||||
self.setStyleSheet(OPAQUE_STYLESHEET)
|
||||
self.setAttribute(QtCore.Qt.WA_TranslucentBackground, enabled)
|
||||
self.repaint()
|
||||
|
||||
def css_changed(self):
|
||||
"""
|
||||
We need to rebuild the CSS on the live display.
|
||||
"""
|
||||
for plugin in self.plugin_manager.plugins:
|
||||
plugin.refresh_css(self.frame)
|
||||
|
||||
def retranslate_ui(self):
|
||||
"""
|
||||
Setup the interface translation strings.
|
||||
"""
|
||||
self.setWindowTitle(translate('OpenLP.MainCanvas', 'OpenLP Display'))
|
||||
|
||||
def setup(self):
|
||||
"""
|
||||
Set up and build the output screen
|
||||
"""
|
||||
self.log_debug('Start MainCanvas setup (live = {islive})'.format(islive=self.is_live))
|
||||
self.screen = self.screens.current
|
||||
self.setVisible(False)
|
||||
Canvas.setup(self)
|
||||
if self.is_live:
|
||||
# Build the initial frame.
|
||||
background_color = QtGui.QColor()
|
||||
background_color.setNamedColor(Settings().value('core/logo background color'))
|
||||
if not background_color.isValid():
|
||||
background_color = QtCore.Qt.white
|
||||
image_file = path_to_str(Settings().value('core/logo file'))
|
||||
splash_image = QtGui.QImage(image_file)
|
||||
self.initial_fame = QtGui.QImage(
|
||||
self.screen['size'].width(),
|
||||
self.screen['size'].height(),
|
||||
QtGui.QImage.Format_ARGB32_Premultiplied)
|
||||
painter_image = QtGui.QPainter()
|
||||
painter_image.begin(self.initial_fame)
|
||||
painter_image.fillRect(self.initial_fame.rect(), background_color)
|
||||
painter_image.drawImage(
|
||||
(self.screen['size'].width() - splash_image.width()) // 2,
|
||||
(self.screen['size'].height() - splash_image.height()) // 2,
|
||||
splash_image)
|
||||
service_item = ServiceItem()
|
||||
service_item.bg_image_bytes = image_to_byte(self.initial_fame)
|
||||
self.webview.setHtml(build_html(service_item, self.screen, self.is_live, None,
|
||||
plugins=self.plugin_manager.plugins))
|
||||
self._hide_mouse()
|
||||
|
||||
def text(self, slide, animate=True):
|
||||
"""
|
||||
Add the slide text from slideController
|
||||
|
||||
:param slide: The slide text to be displayed
|
||||
:param animate: Perform transitions if applicable when setting the text
|
||||
"""
|
||||
# Wait for the webview to update before displaying text.
|
||||
while not self.web_loaded:
|
||||
self.application.process_events()
|
||||
self.setGeometry(self.screen['size'])
|
||||
json_verses = json.dumps(slide)
|
||||
self.run_javascript('Display.setTextSlides({verses});'.format(verses=json_verses))
|
||||
#if animate:
|
||||
# # NOTE: Verify this works with ''.format()
|
||||
# _text = slide.replace('\\', '\\\\').replace('\"', '\\\"')
|
||||
# self.frame.runJavaScript('show_text("{text}")'.format(text=_text))
|
||||
#else:
|
||||
# # This exists for https://bugs.launchpad.net/openlp/+bug/1016843
|
||||
# # For unknown reasons if evaluateJavaScript is called
|
||||
# # from the themewizard, then it causes a crash on
|
||||
# # Windows if there are many items in the service to re-render.
|
||||
# # Setting the div elements direct seems to solve the issue
|
||||
# self.frame.findFirstElement("#lyricsmain").setInnerXml(slide)
|
||||
|
||||
def alert(self, text, location):
|
||||
"""
|
||||
Display an alert.
|
||||
|
||||
:param text: The text to be displayed.
|
||||
:param location: Where on the screen is the text to be displayed
|
||||
"""
|
||||
# First we convert <>& marks to html variants, then apply
|
||||
# formattingtags, finally we double all backslashes for JavaScript.
|
||||
text_prepared = expand_tags(html.escape(text)).replace('\\', '\\\\').replace('\"', '\\\"')
|
||||
if self.height() != self.screen['size'].height() or not self.isVisible():
|
||||
shrink = True
|
||||
js = 'show_alert("{text}", "{top}")'.format(text=text_prepared, top='top')
|
||||
else:
|
||||
shrink = False
|
||||
js = 'show_alert("{text}", "")'.format(text=text_prepared)
|
||||
height = self.run_javascript(js)
|
||||
if shrink:
|
||||
if text:
|
||||
alert_height = int(height)
|
||||
self.resize(self.width(), alert_height)
|
||||
self.setVisible(True)
|
||||
if location == AlertLocation.Middle:
|
||||
self.move(self.screen['size'].left(), (self.screen['size'].height() - alert_height) // 2)
|
||||
elif location == AlertLocation.Bottom:
|
||||
self.move(self.screen['size'].left(), self.screen['size'].height() - alert_height)
|
||||
else:
|
||||
self.setVisible(False)
|
||||
self.setGeometry(self.screen['size'])
|
||||
|
||||
def direct_image(self, path, background):
|
||||
"""
|
||||
API for replacement backgrounds so Images are added directly to cache.
|
||||
|
||||
:param path: Path to Image
|
||||
:param background: The background color
|
||||
"""
|
||||
self.image_manager.add_image(path, ImageSource.ImagePlugin, background)
|
||||
if not hasattr(self, 'service_item'):
|
||||
return False
|
||||
self.override['image'] = path
|
||||
self.override['theme'] = path_to_str(self.service_item.theme_data.background_filename)
|
||||
self.image(path)
|
||||
# Update the preview frame.
|
||||
if self.is_live:
|
||||
self.live_controller.update_preview()
|
||||
return True
|
||||
|
||||
def image(self, path):
|
||||
"""
|
||||
Add an image as the background. The image has already been added to the
|
||||
cache.
|
||||
|
||||
:param path: The path to the image to be displayed. **Note**, the path is only passed to identify the image.
|
||||
If the image has changed it has to be re-added to the image manager.
|
||||
"""
|
||||
image = self.image_manager.get_image_bytes(path, ImageSource.ImagePlugin)
|
||||
self.controller.media_controller.media_reset(self.controller)
|
||||
self.display_image(image)
|
||||
|
||||
def display_image(self, image):
|
||||
"""
|
||||
Display an image, as is.
|
||||
|
||||
:param image: The image to be displayed
|
||||
"""
|
||||
self.setGeometry(self.screen['size'])
|
||||
#if image:
|
||||
# self.set_im
|
||||
# js = 'show_image("data:image/png;base64,{image}");'.format(image=image)
|
||||
#else:
|
||||
# js = 'show_image("");'
|
||||
#self.frame.evaluateJavaScript(js)
|
||||
if not image['file'].startswith('file://'):
|
||||
image['file'] = 'file://' + image['file']
|
||||
json_images = json.dumps(images)
|
||||
self.run_javascript('Display.setImageSlides({images});'.format(images=json_images))
|
||||
|
||||
|
||||
def reset_image(self):
|
||||
"""
|
||||
Reset the background image to the service item image. Used after the image plugin has changed the background.
|
||||
"""
|
||||
if hasattr(self, 'service_item'):
|
||||
self.display_image(self.service_item.bg_image_bytes)
|
||||
else:
|
||||
self.display_image(None)
|
||||
# Update the preview frame.
|
||||
if self.is_live:
|
||||
self.live_controller.update_preview()
|
||||
# clear the cache
|
||||
self.override = {}
|
||||
|
||||
def preview(self):
|
||||
"""
|
||||
Generates a preview of the image displayed.
|
||||
"""
|
||||
was_visible = self.isVisible()
|
||||
self.application.process_events()
|
||||
# We must have a service item to preview.
|
||||
if self.is_live and hasattr(self, 'service_item'):
|
||||
# Wait for the fade to finish before geting the preview.
|
||||
# Important otherwise preview will have incorrect text if at all!
|
||||
if self.service_item.theme_data and self.service_item.theme_data.display_slide_transition:
|
||||
while not self.run_javascript('show_text_completed()'):
|
||||
self.application.process_events()
|
||||
# Wait for the webview to update before getting the preview.
|
||||
# Important otherwise first preview will miss the background !
|
||||
while not self.web_loaded:
|
||||
self.application.process_events()
|
||||
# if was hidden keep it hidden
|
||||
if self.is_live:
|
||||
if self.hide_mode:
|
||||
self.hide_display(self.hide_mode)
|
||||
# Only continue if the visibility wasn't changed during method call.
|
||||
elif was_visible == self.isVisible():
|
||||
# Single screen active
|
||||
if self.screens.display_count == 1:
|
||||
# Only make visible if setting enabled.
|
||||
if Settings().value('core/display on monitor'):
|
||||
self.setVisible(True)
|
||||
else:
|
||||
self.setVisible(True)
|
||||
return self.grab()
|
||||
|
||||
def build_html(self, service_item, image_path=''):
|
||||
"""
|
||||
Store the service_item and build the new HTML from it. Add the HTML to the display
|
||||
|
||||
:param service_item: The Service item to be used
|
||||
:param image_path: Where the image resides.
|
||||
"""
|
||||
self.web_loaded = False
|
||||
self.initial_fame = None
|
||||
self.service_item = service_item
|
||||
background = None
|
||||
# We have an image override so keep the image till the theme changes.
|
||||
if self.override:
|
||||
# We have an video override so allow it to be stopped.
|
||||
if 'video' in self.override:
|
||||
Registry().execute('video_background_replaced')
|
||||
self.override = {}
|
||||
# We have a different theme.
|
||||
elif self.override['theme'] != path_to_str(service_item.theme_data.background_filename):
|
||||
Registry().execute('live_theme_changed')
|
||||
self.override = {}
|
||||
else:
|
||||
# replace the background
|
||||
background = self.image_manager.get_image_bytes(self.override['image'], ImageSource.ImagePlugin)
|
||||
self.set_transparency(self.service_item.theme_data.background_type ==
|
||||
BackgroundType.to_string(BackgroundType.Transparent))
|
||||
image_bytes = None
|
||||
if self.service_item.theme_data.background_type == 'image':
|
||||
if self.service_item.theme_data.background_filename:
|
||||
self.service_item.bg_image_bytes = self.image_manager.get_image_bytes(
|
||||
path_to_str(self.service_item.theme_data.background_filename), ImageSource.Theme)
|
||||
if image_path:
|
||||
image_bytes = self.image_manager.get_image_bytes(image_path, ImageSource.ImagePlugin)
|
||||
created_html = build_html(self.service_item, self.screen, self.is_live, background, image_bytes,
|
||||
plugins=self.plugin_manager.plugins)
|
||||
self.webview.setHtml(created_html)
|
||||
if service_item.foot_text:
|
||||
self.footer(service_item.foot_text)
|
||||
# if was hidden keep it hidden
|
||||
if self.hide_mode and self.is_live and not service_item.is_media():
|
||||
if Settings().value('core/auto unblank'):
|
||||
Registry().execute('slidecontroller_live_unblank')
|
||||
else:
|
||||
self.hide_display(self.hide_mode)
|
||||
if self.service_item.theme_data.background_type == 'video' and self.is_live:
|
||||
if self.service_item.theme_data.background_filename:
|
||||
service_item = ServiceItem()
|
||||
service_item.title = 'webkit'
|
||||
service_item.processor = 'webkit'
|
||||
path = os.path.join(str(AppLocation.get_section_data_path('themes')),
|
||||
self.service_item.theme_data.theme_name)
|
||||
service_item.add_from_command(path,
|
||||
path_to_str(self.service_item.theme_data.background_filename),
|
||||
':/media/slidecontroller_multimedia.png')
|
||||
self.media_controller.video(DisplayControllerType.Live, service_item, video_behind_text=True)
|
||||
self._hide_mouse()
|
||||
|
||||
def footer(self, text):
|
||||
"""
|
||||
Display the Footer
|
||||
|
||||
:param text: footer text to be displayed
|
||||
"""
|
||||
js = 'show_footer(\'' + text.replace('\\', '\\\\').replace('\'', '\\\'') + '\')'
|
||||
self.run_javascript(js)
|
||||
|
||||
def hide_display(self, mode=HideMode.Screen):
|
||||
"""
|
||||
Hide the display by making all layers transparent Store the images so they can be replaced when required
|
||||
|
||||
:param mode: How the screen is to be hidden
|
||||
"""
|
||||
self.log_debug('hide_display mode = {mode:d}'.format(mode=mode))
|
||||
if self.screens.display_count == 1:
|
||||
# Only make visible if setting enabled.
|
||||
if not Settings().value('core/display on monitor'):
|
||||
return
|
||||
if mode == HideMode.Screen:
|
||||
self.run_javascript('show_blank("desktop");')
|
||||
self.setVisible(False)
|
||||
elif mode == HideMode.Blank or self.initial_fame:
|
||||
self.run_javascript('show_blank("black");')
|
||||
else:
|
||||
self.run_javascript('show_blank("theme");')
|
||||
if mode != HideMode.Screen:
|
||||
if self.isHidden():
|
||||
self.setVisible(True)
|
||||
self.webview.setVisible(True)
|
||||
self.hide_mode = mode
|
||||
|
||||
def show_display(self):
|
||||
"""
|
||||
Show the stored layers so the screen reappears as it was originally.
|
||||
Make the stored images None to release memory.
|
||||
"""
|
||||
if self.screens.display_count == 1:
|
||||
# Only make visible if setting enabled.
|
||||
if not Settings().value('core/display on monitor'):
|
||||
return
|
||||
self.run_javascript('show_blank("show");')
|
||||
# Check if setting for hiding logo on startup is enabled.
|
||||
# If it is, display should remain hidden, otherwise logo is shown. (from def setup)
|
||||
if self.isHidden() and not Settings().value('core/logo hide on startup'):
|
||||
self.setVisible(True)
|
||||
self.hide_mode = None
|
||||
# Trigger actions when display is active again.
|
||||
if self.is_live:
|
||||
Registry().execute('live_display_active')
|
||||
|
||||
def _hide_mouse(self):
|
||||
"""
|
||||
Hide mouse cursor when moved over display.
|
||||
"""
|
||||
if Settings().value('advanced/hide mouse'):
|
||||
self.setCursor(QtCore.Qt.BlankCursor)
|
||||
self.run_javascript('document.body.style.cursor = "none"')
|
||||
else:
|
||||
self.setCursor(QtCore.Qt.ArrowCursor)
|
||||
self.run_javascript('document.body.style.cursor = "auto"')
|
||||
|
||||
def change_window_level(self, window):
|
||||
"""
|
||||
Changes the display window level on Mac OS X so that the main window can be brought into focus but still allow
|
||||
the main display to be above the menu bar and dock when it in focus.
|
||||
|
||||
:param window: Window from our application that focus changed to or None if outside our application
|
||||
"""
|
||||
if is_macosx():
|
||||
if window:
|
||||
# Get different window ids' as int's
|
||||
try:
|
||||
window_id = window.winId().__int__()
|
||||
main_window_id = self.main_window.winId().__int__()
|
||||
self_id = self.winId().__int__()
|
||||
except:
|
||||
return
|
||||
# If the passed window has the same id as our window make sure the display has the proper level and
|
||||
# collection behavior.
|
||||
if window_id == self_id:
|
||||
self.pyobjc_nsview.window().setLevel_(NSMainMenuWindowLevel + 2)
|
||||
self.pyobjc_nsview.window().setCollectionBehavior_(NSWindowCollectionBehaviorManaged)
|
||||
# Else set the displays window level back to normal since we are trying to focus a window other than
|
||||
# the display.
|
||||
else:
|
||||
self.pyobjc_nsview.window().setLevel_(0)
|
||||
self.pyobjc_nsview.window().setCollectionBehavior_(NSWindowCollectionBehaviorManaged)
|
||||
# If we are trying to focus the main window raise it now to complete the focus change.
|
||||
if window_id == main_window_id:
|
||||
self.main_window.raise_()
|
||||
|
||||
|
||||
class AudioPlayer(LogMixin, QtCore.QObject):
|
||||
"""
|
||||
This Class will play audio only allowing components to work with a soundtrack independent of the user interface.
|
||||
"""
|
||||
position_changed = QtCore.pyqtSignal(int)
|
||||
|
||||
def __init__(self, parent):
|
||||
"""
|
||||
The constructor for the display form.
|
||||
|
||||
:param parent: The parent widget.
|
||||
"""
|
||||
super(AudioPlayer, self).__init__(parent)
|
||||
self.player = QtMultimedia.QMediaPlayer()
|
||||
self.playlist = QtMultimedia.QMediaPlaylist(self.player)
|
||||
self.volume_slider = None
|
||||
self.player.setPlaylist(self.playlist)
|
||||
self.player.positionChanged.connect(self._on_position_changed)
|
||||
|
||||
def __del__(self):
|
||||
"""
|
||||
Shutting down so clean up connections
|
||||
"""
|
||||
self.stop()
|
||||
|
||||
def _on_position_changed(self, position):
|
||||
"""
|
||||
Emit a signal when the position of the media player updates
|
||||
"""
|
||||
self.position_changed.emit(position)
|
||||
|
||||
def set_volume_slider(self, slider):
|
||||
"""
|
||||
Connect the volume slider to the media player
|
||||
:param slider:
|
||||
"""
|
||||
self.volume_slider = slider
|
||||
self.volume_slider.setMinimum(0)
|
||||
self.volume_slider.setMaximum(100)
|
||||
self.volume_slider.setValue(self.player.volume())
|
||||
self.volume_slider.valueChanged.connect(self.set_volume)
|
||||
|
||||
def set_volume(self, volume):
|
||||
"""
|
||||
Set the volume of the media player
|
||||
|
||||
:param volume:
|
||||
"""
|
||||
self.player.setVolume(volume)
|
||||
|
||||
def reset(self):
|
||||
"""
|
||||
Reset the audio player, clearing the playlist and the queue.
|
||||
"""
|
||||
self.stop()
|
||||
self.playlist.clear()
|
||||
|
||||
def play(self):
|
||||
"""
|
||||
We want to play the file so start it
|
||||
"""
|
||||
self.player.play()
|
||||
|
||||
def pause(self):
|
||||
"""
|
||||
Pause the Audio
|
||||
"""
|
||||
self.player.pause()
|
||||
|
||||
def stop(self):
|
||||
"""
|
||||
Stop the Audio and clean up
|
||||
"""
|
||||
self.player.stop()
|
||||
|
||||
def add_to_playlist(self, file_names):
|
||||
"""
|
||||
Add another file to the playlist.
|
||||
|
||||
:param file_names: A list with files to be added to the playlist.
|
||||
"""
|
||||
if not isinstance(file_names, list):
|
||||
file_names = [file_names]
|
||||
for file_name in file_names:
|
||||
self.playlist.addMedia(QtMultimedia.QMediaContent(QtCore.QUrl.fromLocalFile(str(file_name))))
|
||||
|
||||
def next(self):
|
||||
"""
|
||||
Skip forward to the next track in the list
|
||||
"""
|
||||
self.playlist.next()
|
||||
|
||||
def go_to(self, index):
|
||||
"""
|
||||
Go to a particular track in the list
|
||||
|
||||
:param index: The track to go to
|
||||
"""
|
||||
self.playlist.setCurrentIndex(index)
|
||||
if self.player.state() == QtMultimedia.QMediaPlayer.PlayingState:
|
||||
self.player.play()
|
@ -202,60 +202,6 @@ class ServiceItem(RegistryProperties):
|
||||
self._create_slides()
|
||||
return self._display_slides
|
||||
|
||||
# def render(self, provides_own_theme_data=False):
|
||||
# """
|
||||
# The render method is what generates the frames for the screen and obtains the display information from the
|
||||
# renderer. At this point all slides are built for the given display size.
|
||||
#
|
||||
# :param provides_own_theme_data: This switch disables the usage of the item's theme. However, this is
|
||||
# disabled by default. If this is used, it has to be taken care, that
|
||||
# the renderer knows the correct theme data. However, this is needed
|
||||
# for the theme manager.
|
||||
# """
|
||||
# log.debug('Render called')
|
||||
# self._display_frames = []
|
||||
# self.bg_image_bytes = None
|
||||
# # if not provides_own_theme_data:
|
||||
# # self.renderer.set_item_theme(self.theme)
|
||||
# # self.theme_data, self.main, self.footer = self.renderer.pre_render()
|
||||
# if self.service_item_type == ServiceItemType.Text:
|
||||
# can_render_chords = hasattr(self, 'name') and self.name == 'songs' and Settings().value(
|
||||
# 'songs/enable chords')
|
||||
# log.debug('Formatting slides: {title}'.format(title=self.title))
|
||||
# # Save rendered pages to this dict. In the case that a slide is used twice we can use the pages saved to
|
||||
# # the dict instead of rendering them again.
|
||||
# previous_pages = {}
|
||||
# for slide in self._raw_frames:
|
||||
# verse_tag = slide['verseTag']
|
||||
# if verse_tag in previous_pages and previous_pages[verse_tag][0] == slide['raw_slide']:
|
||||
# pages = previous_pages[verse_tag][1]
|
||||
# else:
|
||||
# # pages = self.renderer.format_slide(slide['raw_slide'], self)
|
||||
# previous_pages[verse_tag] = (slide['raw_slide'], pages)
|
||||
# for page in pages:
|
||||
# page = page.replace('<br>', '{br}')
|
||||
# html_data = render_tags(page.rstrip(), can_render_chords)
|
||||
# new_frame = {
|
||||
# 'title': remove_tags(page),
|
||||
# 'text': remove_tags(page.rstrip(), can_render_chords),
|
||||
# 'chords_text': render_chords(remove_tags(page.rstrip(), False)),
|
||||
# 'html': html_data.replace('&nbsp;', ' '),
|
||||
# 'printing_html': render_tags(html.escape(page.rstrip()), can_render_chords, is_printing=True),
|
||||
# 'verseTag': verse_tag,
|
||||
# }
|
||||
# self._display_frames.append(new_frame)
|
||||
# elif self.service_item_type == ServiceItemType.Image or self.service_item_type == ServiceItemType.Command:
|
||||
# pass
|
||||
# else:
|
||||
# log.error('Invalid value renderer: {item}'.format(item=self.service_item_type))
|
||||
# self.title = remove_tags(self.title)
|
||||
# # The footer should never be None, but to be compatible with a few
|
||||
# # nightly builds between 1.9.4 and 1.9.5, we have to correct this to
|
||||
# # avoid tracebacks.
|
||||
# if self.raw_footer is None:
|
||||
# self.raw_footer = []
|
||||
# self.foot_text = '<br>'.join([_f for _f in self.raw_footer if _f])
|
||||
|
||||
def add_from_image(self, filename, title, background=None, thumbnail=None):
|
||||
"""
|
||||
Add an image slide to the service item.
|
||||
@ -313,8 +259,6 @@ class ServiceItem(RegistryProperties):
|
||||
file_location_hash = md5_hash(file_location.encode('utf-8'))
|
||||
image = os.path.join(str(AppLocation.get_section_data_path(self.name)), 'thumbnails',
|
||||
file_location_hash, ntpath.basename(image))
|
||||
#self.slides.append({'title': file_name, 'image': image, 'path': path,
|
||||
# 'display_title': display_title, 'notes': notes})
|
||||
self.slides.append({'title': file_name, 'image': image, 'path': path,
|
||||
'display_title': display_title, 'notes': notes,
|
||||
'thumbnail' : image})
|
||||
|
@ -52,7 +52,6 @@ LIBRARIES = OrderedDict([
|
||||
('Chardet', ('chardet',)),
|
||||
('PyEnchant', ('enchant',)),
|
||||
('Mako', ('mako',)),
|
||||
('pyICU', ('icu', 'VERSION')),
|
||||
('VLC', ('openlp.core.ui.media.vlcplayer', 'VERSION')),
|
||||
])
|
||||
|
||||
|
@ -15,10 +15,11 @@
|
||||
# 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 #
|
||||
###############################################################################
|
||||
import sys
|
||||
from unittest import TestCase
|
||||
# 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 #
|
||||
@ -26,9 +27,12 @@ from unittest.mock import MagicMock
|
||||
|
||||
from PyQt5 import QtCore
|
||||
|
||||
# Mock QtWebEngineWidgets
|
||||
sys.modules['PyQt5.QtWebEngineWidgets'] = MagicMock()
|
||||
|
||||
from openlp.core.api.endpoint.controller import controller_direction, controller_text
|
||||
from openlp.core.common.registry import Registry
|
||||
from openlp.core.display.renderer import Renderer
|
||||
from openlp.core.display.render import Renderer
|
||||
from openlp.core.display.screens import ScreenList
|
||||
from openlp.core.lib.serviceitem import ServiceItem
|
||||
from tests.utils import convert_file_service_item
|
||||
|
@ -25,6 +25,9 @@ from unittest.mock import MagicMock, patch
|
||||
|
||||
from PyQt5 import QtCore, QtWidgets
|
||||
|
||||
# Mock QtWebEngineWidgets
|
||||
sys.modules['PyQt5.QtWebEngineWidgets'] = MagicMock()
|
||||
|
||||
from openlp.core.app import OpenLP, parse_options
|
||||
from openlp.core.common.settings import Settings
|
||||
from tests.utils.constants import RESOURCE_PATH
|
||||
|
@ -26,6 +26,7 @@ from unittest import TestCase
|
||||
|
||||
from openlp.core.common import is_not_image_file
|
||||
from tests.utils.constants import RESOURCE_PATH
|
||||
from tests.helpers.testmixin import TestMixin
|
||||
|
||||
|
||||
class TestUtils(TestCase, TestMixin):
|
||||
|
Loading…
Reference in New Issue
Block a user