Display API abstraction

This commit is contained in:
Mateus Meyer Jiacomelli 2023-08-12 15:59:37 +00:00 committed by Raoul Snyman
parent 49190fe6b3
commit a04ed99dfc
18 changed files with 575 additions and 370 deletions

View File

@ -12,6 +12,7 @@ module.exports = function(config) {
"tests/js/polyfill.js",
"tests/js/fake_webchannel.js",
"openlp/core/display/html/reveal.js",
"openlp/core/display/html/display-init.js",
"openlp/core/display/html/display.js",
"tests/js/test_*.js"
],

View File

@ -46,8 +46,8 @@ from openlp.core.common.platform import is_macosx, is_win
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 init_webview_custom_schemes, set_webview_display_path
from openlp.core.lib.filelock import FileLock
from openlp.core.display.webengine import init_webview_custom_schemes
from openlp.core.loader import loader
from openlp.core.resources import qInitResources
from openlp.core.server import Server
@ -295,6 +295,8 @@ def parse_options():
dir_name=os.path.join('<AppDir>', '..', '..')))
parser.add_argument('-w', '--no-web-server', dest='no_web_server', action='store_true',
help='Turn off the Web and Socket Server ')
parser.add_argument('--display-custom-path', dest='display_custom_path', default=None,
help='Specify the custom path for display renderer (HTML). Useful for development.')
parser.add_argument('rargs', nargs='*', default=[])
# Parse command line options and deal with them.
return parser.parse_args()
@ -485,6 +487,11 @@ def main():
Registry().register('settings_thread', settings_thread)
Registry().register('application-qt', application)
Registry().register('application', app)
if args.display_custom_path:
if (args.display_custom_path.startswith('http:') or args.display_custom_path.startswith('https:')):
Registry().register('display_custom_url', args.display_custom_path)
else:
set_webview_display_path(args.display_custom_path)
Registry().set_flag('no_web_server', args.no_web_server)
# Upgrade settings.
app.settings = settings

View File

@ -114,12 +114,20 @@ class Registry(metaclass=Singleton):
def has_function(self, event):
"""
Returns whether there's any hander associated with the event.
Returns whether there's any handler associated with the event.
:param event: The function to be checked
"""
return event in self.functions_list
def has(self, service_name: str) -> bool:
"""
Returns whether there's any service registered with provided name
:param service_name: The service name to be checked
"""
return service_name in self.service_list
def execute(self, event, *args, **kwargs):
"""
Execute all the handlers associated with the event and return an array of results.

View File

@ -0,0 +1,101 @@
/*jshint esversion: 9 */
class CommunicationBridge {
constructor() {
this.target = null;
this.initOptions = null;
}
requestAction(action, ...values) {
if (action == 'init') {
this.initOptions = (values && values[0]) || null;
}
let returnValue;
if (this.target) {
returnValue = this.target._handleNativeCall(action, ...values);
}
if (action == 'init') {
this._onInitialized();
}
return returnValue;
}
requestActionAsync(action, returnEvent, ...values) {
let returnValue;
if (this.target) {
returnValue = this.target._handleNativeCall(action, ...values);
}
if (returnValue && ('then' in returnValue)) {
returnValue.then((value) => {
this._dispatchEvent(returnEvent, value || {});
});
} else {
this._dispatchEvent(returnEvent, returnValue || {});
}
return returnValue;
}
setDisplayTarget(newTarget) {
this.target = newTarget;
if (this.initOptions) {
this.target._handleNativeCall('init', ...[this.initOptions]);
this._onInitialized();
}
}
isReady() {
return !!this.target;
}
pleaseRepaint() {
if (window.displayWatcher) {
return window.displayWatcher.pleaseRepaint();
}
}
_onInitialized() {
if (window.displayWatcher) {
window.displayWatcher.setInitialised(true);
}
}
_dispatchEvent(eventName, eventParameter) {
if (window.displayWatcher) {
window.displayWatcher.dispatchEvent(eventName, eventParameter || {});
}
}
}
function initNativeHandlerIfAvailable() {
if (window.QWebChannel) {
// Means we're running inside OpenLP
new window.QWebChannel(window.qt.webChannelTransport, (channel) => {
window.displayWatcher = channel.objects.displayWatcher;
// Defining window title as exposed by OpenLP
(window.displayWatcher.getWindowTitle || (() => Promise.resolve('')))()
.then((windowTitle) => {
if (windowTitle) {
const titleTag = document.head.getElementsByTagName('title')[0];
if (titleTag) {
titleTag.innerText = `${titleTag.innerText} (${windowTitle})`;
}
}
});
});
}
}
window.initCommunicationBridge = () => {
initNativeHandlerIfAvailable();
var communicationBridge = new CommunicationBridge();
window.communicationBridge = communicationBridge;
window.requestAction = communicationBridge.requestAction.bind(communicationBridge);
window.requestActionAsync = communicationBridge.requestActionAsync.bind(communicationBridge);
window.isReady = communicationBridge.isReady.bind(communicationBridge);
};

View File

@ -6,6 +6,7 @@
<link type="text/css" rel="stylesheet" href="display.css">
<script type="text/javascript" src="qrc:///qtwebchannel/qwebchannel.js"></script>
<script type="text/javascript" src="reveal.js"></script>
<script type="text/javascript" src="display-init.js"></script>
<script type="text/javascript" src="display.js"></script>
</head>
<body>

View File

@ -2,6 +2,7 @@
* display.js is the main Javascript file that is used to drive the display.
*/
/**
* Background type enumeration
*/
@ -892,122 +893,122 @@ var Display = {
/**
* Blank the screen
*/
toBlack: function (onFinishedEventName) {
/* Avoid race conditions where display goes to transparent and quickly goes to black */
Display._abortLastTransitionOperation();
/*
Reveal's black overlay should be shown before the transitions are
restored, to avoid screen flashes
*/
Display._restorePauseBehavior();
Display._requestAnimationFrameExclusive(function() {
if (!Reveal.isPaused()) {
Reveal.togglePause();
}
Display._reenableGlobalTransitions(function() {
var documentBody = $("body")[0];
documentBody.style.opacity = 1;
if (onFinishedEventName) {
displayWatcher.dispatchEvent(onFinishedEventName, {});
toBlack: function () {
return new Promise((resolve, reject) => {
/* Avoid race conditions where display goes to transparent and quickly goes to black */
Display._abortLastTransitionOperation();
/*
Reveal's black overlay should be shown before the transitions are
restored, to avoid screen flashes
*/
Display._restorePauseBehavior();
Display._requestAnimationFrameExclusive(function() {
if (!Reveal.isPaused()) {
Reveal.togglePause();
}
Display._reenableGlobalTransitions(function() {
var documentBody = $("body")[0];
documentBody.style.opacity = 1;
resolve();
});
});
});
},
/**
* Hide all but theme background
*/
toTheme: function (onFinishedEventName) {
Display._abortLastTransitionOperation();
/*
Reveal's black overlay should be shown before the transitions are
restored, to avoid screen flashes
*/
Display._restorePauseBehavior();
var documentBody = $("body")[0];
documentBody.style.opacity = 1;
Display._slidesContainer.style.opacity = 0;
Display._footerContainer.style.opacity = 0;
if (Reveal.isPaused()) {
Reveal.togglePause();
}
Display._reenableGlobalTransitions(function() {
if (onFinishedEventName) {
displayWatcher.dispatchEvent(onFinishedEventName, {});
toTheme: function () {
return new Promise((resolve, reject) => {
Display._abortLastTransitionOperation();
/*
Reveal's black overlay should be shown before the transitions are
restored, to avoid screen flashes
*/
Display._restorePauseBehavior();
var documentBody = $("body")[0];
documentBody.style.opacity = 1;
Display._slidesContainer.style.opacity = 0;
Display._footerContainer.style.opacity = 0;
if (Reveal.isPaused()) {
Reveal.togglePause();
}
Display._reenableGlobalTransitions(function() {
resolve();
});
});
},
/**
* Hide everything (CAUTION: Causes a invisible mouse barrier)
*/
toTransparent: function (onFinishedEventName) {
Display._abortLastTransitionOperation();
var documentBody = $("body")[0];
documentBody.style.opacity = 0;
if (!Reveal.isPaused()) {
/*
Removing previously the overlay if it's not paused, to avoid a
content flash while going from black screen to transparent
*/
document.body.classList.add('is-desktop');
Reveal.togglePause();
}
/*
Waiting for body transition to happen, now it would be safe to
hide the Webview (as other transitions were suppressed)
*/
Display._abortLastTransitionOperation();
Display._addTransitionEndEventToBody(transitionEndEvent);
function transitionEndEvent(e) {
// Targeting only body
if (e.target != documentBody) {
return;
toTransparent: function () {
return new Promise((resolve, reject) => {
Display._abortLastTransitionOperation();
var documentBody = $("body")[0];
documentBody.style.opacity = 0;
if (!Reveal.isPaused()) {
/*
Removing previously the overlay if it's not paused, to avoid a
content flash while going from black screen to transparent
*/
document.body.classList.add('is-desktop');
Reveal.togglePause();
}
/*
Disabling all transitions (except body) to allow the Webview to attain the
transparent state before it gets hidden by Qt.
Waiting for body transition to happen, now it would be safe to
hide the Webview (as other transitions were suppressed)
*/
document.body.classList.add('disable-transitions');
document.body.classList.add('is-desktop');
Display._slidesContainer.style.opacity = 0;
Display._footerContainer.style.opacity = 0;
/*
Repainting before hiding the Webview to avoid flashes when
showing it again.
*/
displayWatcher.pleaseRepaint();
/* Waiting for repaint to happen before saying that it's done. */
Display._requestAnimationFrameExclusive(function() {
/* We're transparent now, aborting any transition event between */
Display._abortLastTransitionOperation();
if (onFinishedEventName) {
displayWatcher.dispatchEvent(onFinishedEventName, {});
Display._abortLastTransitionOperation();
Display._addTransitionEndEventToBody(transitionEndEvent);
function transitionEndEvent(e) {
// Targeting only body
if (e.target != documentBody) {
return;
}
});
}
/*
Disabling all transitions (except body) to allow the Webview to attain the
transparent state before it gets hidden by Qt.
*/
document.body.classList.add('disable-transitions');
document.body.classList.add('is-desktop');
Display._slidesContainer.style.opacity = 0;
Display._footerContainer.style.opacity = 0;
/*
Repainting before hiding the Webview to avoid flashes when
showing it again.
*/
displayWatcher.pleaseRepaint();
/* Waiting for repaint to happen before saying that it's done. */
Display._requestAnimationFrameExclusive(function() {
/* We're transparent now, aborting any transition event between */
Display._abortLastTransitionOperation();
resolve();
});
}
});
},
/**
* Show the screen
*/
show: function (onFinishedEventName) {
var documentBody = $("body")[0];
/*
Removing transitionend event, avoids the content being hidden if the user
tries to show content again before toTransparent() transitionend event
happens
*/
Display._abortLastTransitionOperation();
show: function () {
return new Promise((resolve, reject) => {
var documentBody = $("body")[0];
/*
Removing transitionend event, avoids the content being hidden if the user
tries to show content again before toTransparent() transitionend event
happens
*/
Display._abortLastTransitionOperation();
Display._slidesContainer.style.opacity = 1;
Display._footerContainer.style.opacity = 1;
if (Reveal.isPaused()) {
Reveal.togglePause();
}
Display._restorePauseBehavior();
Display._reenableGlobalTransitions(function() {
documentBody.style.opacity = 1;
if (onFinishedEventName) {
displayWatcher.dispatchEvent(onFinishedEventName, {});
Display._slidesContainer.style.opacity = 1;
Display._footerContainer.style.opacity = 1;
if (Reveal.isPaused()) {
Reveal.togglePause();
}
Display._restorePauseBehavior();
Display._reenableGlobalTransitions(function() {
documentBody.style.opacity = 1;
resolve();
});
});
},
@ -1413,6 +1414,12 @@ var Display = {
return url;
},
};
new QWebChannel(qt.webChannelTransport, function (channel) {
window.displayWatcher = channel.objects.displayWatcher;
});
Display._handleNativeCall = (action, ...values) => {
if (Display[action]) {
return Display[action](...values);
}
};
initCommunicationBridge();
communicationBridge.setDisplayTarget(Display);

View File

@ -557,13 +557,13 @@ class ThemePreviewRenderer(DisplayWindow, LogMixin):
"""
Calculate the number of lines that fits on one slide
"""
return self.run_javascript('Display.calculateLineCount();', is_sync=True)
return self.run_in_display('calculateLineCount', is_sync=True)
def clear_slides(self):
"""
Clear slides
"""
return self.run_javascript('Display.clearSlides();')
return self.run_in_display('clearSlides')
def generate_footer(self):
"""
@ -859,10 +859,9 @@ class ThemePreviewRenderer(DisplayWindow, LogMixin):
return True
self.clear_slides()
self.log_debug('_text_fits_on_slide: 1\n{text}'.format(text=text))
self.run_javascript('Display.setTextSlide("{text}");'
.format(text=text.replace('"', '\\"')), is_sync=True)
self.run_in_display('setTextSlide', text.replace('"', '\\"'), is_sync=True)
self.log_debug('_text_fits_on_slide: 2')
does_text_fit = self.run_javascript('Display.doesContentFit();', is_sync=True)
does_text_fit = self.run_in_display('doesContentFit', is_sync=True)
return does_text_fit
def save_screenshot(self, fname=None):

View File

@ -212,7 +212,7 @@ class WebViewCustomScheme(QtCore.QObject):
| QtWebEngineCore.QWebEngineUrlScheme.Flag.LocalScheme
| QtWebEngineCore.QWebEngineUrlScheme.Flag.LocalAccessAllowed)
def create_scheme_handler(self):
def create_scheme_handler(self) -> QtWebEngineCore.QWebEngineUrlSchemeHandler:
raise Exception('Needs to be implemented.')
def init_handler(self, profile=None):

View File

@ -51,12 +51,17 @@ class DisplayWatcher(QtCore.QObject):
"""
initialised = QtCore.pyqtSignal(bool)
def __init__(self, parent):
def __init__(self, parent, window_title=None):
super().__init__()
self._display_window = parent
self._transient_dispatch_events = {}
self._permanent_dispatch_events = {}
self._event_counter = 0
self._window_title = window_title
@QtCore.pyqtSlot(result=str)
def getWindowTitle(self):
return self._window_title
@QtCore.pyqtSlot(bool)
def setInitialised(self, is_initialised):
@ -130,7 +135,7 @@ class DisplayWindow(QtWidgets.QWidget, RegistryProperties, LogMixin):
This is a window to show the output
"""
def __init__(self, parent=None, screen=None, can_show_startup_screen=True, start_hidden=False,
after_loaded_callback=None):
after_loaded_callback=None, window_title=None):
"""
Create the display window
"""
@ -149,6 +154,7 @@ class DisplayWindow(QtWidgets.QWidget, RegistryProperties, LogMixin):
self._is_manual_close = False
self._can_show_startup_screen = can_show_startup_screen
self._fbo = None
self.window_title = window_title
self.setWindowTitle(translate('OpenLP.DisplayWindow', 'Display Window'))
self.setWindowFlags(flags)
self.setAttribute(QtCore.Qt.WA_TranslucentBackground)
@ -162,15 +168,24 @@ class DisplayWindow(QtWidgets.QWidget, RegistryProperties, LogMixin):
self.webview.display_clicked = self.disable_display
self.layout.addWidget(self.webview)
self.webview.loadFinished.connect(self.after_loaded)
self.display_path = 'openlp://display/display.html'
self.checkerboard_path = 'openlp://display/checkerboard.png'
self.openlp_splash_screen_path = 'openlp://display/openlp-splash-screen.png'
self.channel = QtWebChannel.QWebChannel(self)
self.display_watcher = DisplayWatcher(self)
if not window_title and screen and screen.is_display:
window_title = 'Display Window'
self.display_watcher = DisplayWatcher(self, window_title)
self.channel.registerObject('displayWatcher', self.display_watcher)
self.webview.page().setWebChannel(self.channel)
self.display_watcher.initialised.connect(self.on_initialised)
qUrl = QtCore.QUrl(self.display_path)
self.openlp_splash_screen_path = 'openlp://display/openlp-splash-screen.png'
# Using custom display if provided
if Registry().has('display_custom_url') and Registry().get('display_custom_url') is not None:
display_custom_url = Registry().get('display_custom_url')
self.display_path = display_custom_url + '/display.html'
self.checkerboard_path = display_custom_url + '/checkerboard.png'
qUrl = QtCore.QUrl(display_custom_url)
else:
self.display_path = 'openlp://display/display.html'
self.checkerboard_path = 'openlp://display/checkerboard.png'
qUrl = QtCore.QUrl(self.display_path)
self.set_url(qUrl)
self.is_display = False
self.scale = 1
@ -248,7 +263,7 @@ class DisplayWindow(QtWidgets.QWidget, RegistryProperties, LogMixin):
def set_background_image(self, image_path):
image_uri = image_path.as_uri()
self.run_javascript('Display.setBackgroundImage("{image}");'.format(image=image_uri))
self.run_in_display('setBackgroundImage', image_uri)
def set_single_image(self, bg_color, image_path):
"""
@ -256,12 +271,10 @@ class DisplayWindow(QtWidgets.QWidget, RegistryProperties, LogMixin):
:param Path image_path: Path to the image
"""
image_uri = image_path.as_uri()
self.run_javascript('Display.setFullscreenImage("{bg_color}", "{image}");'.format(bg_color=bg_color,
image=image_uri))
self.run_in_display('setFullscreenImage', bg_color, image_uri)
def set_single_image_data(self, bg_color, image_data):
self.run_javascript('Display.setFullscreenImageFromData("{bg_color}", '
'"{image_data}");'.format(bg_color=bg_color, image_data=image_data))
self.run_in_display('setFullscreenImageFromData', bg_color, image_data)
def set_startup_screen(self):
bg_color = self.settings.value('core/logo background color')
@ -276,8 +289,7 @@ class DisplayWindow(QtWidgets.QWidget, RegistryProperties, LogMixin):
# if set to hide logo on startup, do not send the logo
if self.settings.value('core/logo hide on startup'):
image_uri = ''
self.run_javascript('Display.setStartupSplashScreen("{bg_color}", "{image}");'.format(bg_color=bg_color,
image=image_uri))
self.run_in_display('setStartupSplashScreen', bg_color, image_uri)
def set_url(self, url):
"""
@ -299,18 +311,16 @@ class DisplayWindow(QtWidgets.QWidget, RegistryProperties, LogMixin):
"""
Add stuff after page initialisation
"""
js_is_display = str(self.is_display).lower()
item_transitions = str(self.settings.value('themes/item transitions')).lower()
hide_mouse = str(self.settings.value('advanced/hide mouse') and self.is_display).lower()
slide_numbers_in_footer = str(self.settings.value('advanced/slide numbers in footer')).lower()
self.run_javascript('Display.init({{'
'isDisplay: {is_display},'
'doItemTransitions: {do_item_transitions},'
'slideNumbersInFooter: {slide_numbers_in_footer},'
'hideMouse: {hide_mouse}'
'}});'
.format(is_display=js_is_display, do_item_transitions=item_transitions,
slide_numbers_in_footer=slide_numbers_in_footer, hide_mouse=hide_mouse))
item_transitions = self.settings.value('themes/item transitions')
hide_mouse = (self.settings.value('advanced/hide mouse') and self.is_display)
slide_numbers_in_footer = self.settings.value('advanced/slide numbers in footer')
self.run_in_display('init', {
'isDisplay': self.is_display,
'doItemTransitions': item_transitions,
'slideNumbersInFooter': slide_numbers_in_footer,
'hideMouse': hide_mouse,
'displayTitle': self.window_title
})
wait_for(lambda: self._is_initialised)
if self.scale != 1:
self.set_scale(self.scale)
@ -319,7 +329,33 @@ class DisplayWindow(QtWidgets.QWidget, RegistryProperties, LogMixin):
if self.after_loaded_callback:
self.after_loaded_callback()
def run_javascript(self, script, is_sync=False):
def run_in_display(self, action, *parameters, raw_parameters=None, is_sync=False, return_event_name=None):
if len(parameters):
raw_parameters = ''
first = True
for parameter in parameters:
if not first:
raw_parameters += ', '
else:
first = False
raw_parameters += json.dumps(parameter)
action_name = 'requestAction'
action_async = ''
if return_event_name:
action_name = 'requestActionAsync'
action_async = ', \'{event_name}\''.format(event_name=return_event_name)
if not raw_parameters:
return self._run_javascript('{action_name}(\'{action}\'{action_async})'.format(action_name=action_name,
action=action,
action_async=action_async),
is_sync)
else:
return self._run_javascript('{action_name}(\'{action}\'{action_async}, {raw_parameters})'
.format(action_name=action_name, action=action, action_async=action_async,
raw_parameters=raw_parameters),
is_sync)
def _run_javascript(self, script, is_sync=False):
"""
Run some Javascript in the WebView
@ -355,14 +391,13 @@ class DisplayWindow(QtWidgets.QWidget, RegistryProperties, LogMixin):
: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))
self.run_in_display('goToSlide', verse)
def load_verses(self, verses, is_sync=False):
"""
Set verses in the display
"""
json_verses = json.dumps(verses)
self.run_javascript('Display.setTextSlides({verses});'.format(verses=json_verses), is_sync=is_sync)
self.run_in_display('setTextSlides', verses, is_sync=is_sync)
def load_images(self, images):
"""
@ -377,65 +412,7 @@ class DisplayWindow(QtWidgets.QWidget, RegistryProperties, LogMixin):
image['thumbnail'] = image['thumbnail'].as_uri()
else:
image['thumbnail'] = image['path']
json_images = json.dumps(imagesr)
self.run_javascript('Display.setImageSlides({images});'.format(images=json_images))
def load_video(self, video):
"""
Load video in the display
"""
video = copy.deepcopy(video)
video['path'] = video['path'].as_uri()
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=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=level))
def toggle_video_mute(self):
"""
Toggle the mute of the current video
"""
self.run_javascript('Display.toggleVideoMute();')
self.run_in_display('setImageSlides', imagesr)
def save_screenshot(self, fname=None):
"""
@ -477,7 +454,7 @@ class DisplayWindow(QtWidgets.QWidget, RegistryProperties, LogMixin):
theme_copy.font_main_name = self._fix_font_name(theme.font_main_name)
theme_copy.font_footer_name = self._fix_font_name(theme.font_footer_name)
exported_theme = theme_copy.export_theme(is_js=True)
self.run_javascript('Display.setTheme({theme});'.format(theme=exported_theme), is_sync=is_sync)
self.run_in_display('setTheme', raw_parameters=exported_theme, is_sync=is_sync)
def reload_theme(self):
"""
@ -485,13 +462,13 @@ class DisplayWindow(QtWidgets.QWidget, RegistryProperties, LogMixin):
DO NOT use this when changing slides. Only use this if you need to force an update
to the current visible slides.
"""
self.run_javascript('Display.resetTheme();')
self.run_in_display('resetTheme')
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)
return self.run_in_display('getVideoTypes', is_sync=True)
def show_display(self):
"""
@ -505,7 +482,7 @@ class DisplayWindow(QtWidgets.QWidget, RegistryProperties, LogMixin):
self.display_watcher.unregister_event_listener(TRANSITION_END_EVENT_NAME)
if self.isHidden():
self.setVisible(True)
self.run_javascript('Display.show();')
self.run_in_display('show')
self.hide_mode = None
def hide_display(self, mode=HideMode.Screen):
@ -530,13 +507,13 @@ class DisplayWindow(QtWidgets.QWidget, RegistryProperties, LogMixin):
# Hide window only after all webview CSS ransitions are done
self.display_watcher.register_event_listener(TRANSITION_END_EVENT_NAME,
lambda _: self.setVisible(False))
self.run_javascript("Display.toTransparent('{}');".format(TRANSITION_END_EVENT_NAME))
self.run_in_display('toTransparent', return_event_name=TRANSITION_END_EVENT_NAME)
else:
self.run_javascript('Display.toTransparent();')
self.run_in_display('toTransparent')
elif mode == HideMode.Blank:
self.run_javascript('Display.toBlack();')
self.run_in_display('toBlack')
elif mode == HideMode.Theme:
self.run_javascript('Display.toTheme();')
self.run_in_display('toTheme')
self.hide_mode = mode
def disable_display(self):
@ -554,7 +531,7 @@ class DisplayWindow(QtWidgets.QWidget, RegistryProperties, LogMixin):
This function ensures that the current item won't flash momentarily when the webengineview
is displayed for a subsequent song or image.
"""
self.run_javascript('Display.finishWithCurrentItem();', True)
self.run_in_display('finishWithCurrentItem', is_sync=True)
self.webview.update()
def set_scale(self, scale):
@ -564,7 +541,7 @@ class DisplayWindow(QtWidgets.QWidget, RegistryProperties, LogMixin):
self.scale = scale
# Only scale if initialised (scale run again once initialised)
if self._is_initialised:
self.run_javascript('Display.setScale({scale});'.format(scale=scale * 100))
self.run_in_display('setScale', scale * 100)
def alert(self, text, settings):
"""

View File

@ -39,7 +39,7 @@ def loader():
MediaController()
PluginManager()
# Set up the path with plugins
Renderer()
Renderer(window_title='Renderer')
# Create slide controllers
PreviewController()
LiveController()

View File

@ -147,7 +147,8 @@ class SlideController(QtWidgets.QWidget, LogMixin, RegistryProperties):
if screen.is_display:
will_start_hidden = self._current_hide_mode == HideMode.Screen
display = DisplayWindow(self, screen, start_hidden=will_start_hidden,
after_loaded_callback=self._display_after_loaded_callback)
after_loaded_callback=self._display_after_loaded_callback,
window_title='Live Screen' if self.is_live else 'Preview Screen')
self.displays.append(display)
self._reset_blank(False)
if self.display:
@ -398,7 +399,7 @@ class SlideController(QtWidgets.QWidget, LogMixin, RegistryProperties):
self.slide_layout.setSpacing(0)
self.slide_layout.setObjectName('SlideLayout')
# Set up the preview display
self.preview_display = DisplayWindow(self)
self.preview_display = DisplayWindow(self, window_title='Live' if self.is_live else 'Preview')
self.slide_layout.addWidget(self.preview_display)
self.slide_layout.resize.connect(self.on_preview_resize)
# Actual preview screen

View File

@ -48,7 +48,8 @@ class UiThemeProgressDialog(object):
self.theme_preview_layout.margin = 8
self.theme_preview_layout.setSpacing(0)
self.theme_preview_layout.setObjectName('preview_web_layout')
self.theme_display = ThemePreviewRenderer(theme_progress_dialog, can_show_startup_screen=False)
self.theme_display = ThemePreviewRenderer(theme_progress_dialog, can_show_startup_screen=False,
window_title='Theme Progress Dialog')
self.theme_display.setObjectName('theme_display')
self.theme_preview_layout.addWidget(self.theme_display)
self.theme_progress_layout.addWidget(self.preview_area)

View File

@ -100,7 +100,7 @@ class Ui_ThemeWizard(object):
self.preview_area_layout.margin = 8
self.preview_area_layout.setSpacing(0)
self.preview_area_layout.setObjectName('preview_web_layout')
self.preview_box = ThemePreviewRenderer(self)
self.preview_box = ThemePreviewRenderer(self, window_title="Theme Editor Preview")
self.preview_box.setObjectName('preview_box')
self.preview_area_layout.addWidget(self.preview_box)
self.preview_layout.addWidget(self.preview_area)

View File

@ -22,7 +22,6 @@
The :mod:`~openlp.plugins.alerts.lib.alertsmanager` module contains the part of the plugin which manages storing and
displaying of alerts.
"""
import json
from PyQt5 import QtCore, QtGui
@ -77,7 +76,7 @@ class AlertsManager(QtCore.QObject, RegistryBase, LogMixin, RegistryProperties):
'repeat': self.settings.value('alerts/repeat'),
'scroll': self.settings.value('alerts/scroll')
}
self.live_controller.display.alert(text, json.dumps(alert_settings))
self.live_controller.display.alert(text, alert_settings)
def _hex_to_rgb(self, rgb_values):
"""

View File

@ -19,7 +19,7 @@
},
"scripts": {
"test": "karma start --single-run",
"lint": "jshint openlp/core/display/html/display.js"
"lint": "jshint openlp/core/display/html/display*.js"
},
"jshintConfig": {
"esversion": 6

View File

@ -1,9 +1,13 @@
function _createDiv(attrs) {
function _createDiv(attrs, inElement) {
var div = document.createElement("div");
for (key in attrs) {
div.setAttribute(key, attrs[key]);
}
document.body.appendChild(div);
if (inElement) {
inElement.appendChild(div);
} else {
document.body.appendChild(div);
}
return div;
}
@ -321,92 +325,6 @@ describe("Transitions", function () {
});
});
describe("Screen Visibility", function () {
var TRANSITION_TIMEOUT = 2000;
beforeEach(function() {
window.displayWatcher = jasmine.createSpyObj('DisplayWatcher', ['dispatchEvent', 'setInitialised', 'pleaseRepaint']);
document.body.innerHTML = "";
var revealDiv = _createDiv({"class": "reveal"});
var slidesDiv = _createDiv({"class": "slides"});
var footerDiv = _createDiv({"class": "footer"});
slidesDiv.innerHTML = "<section><section><p></p></section></section>";
revealDiv.append(slidesDiv);
revealDiv.append(footerDiv);
document.body.style.transition = "opacity 75ms ease-in-out";
Display.init({isDisplay: true, doItemTransition: false});
});
afterEach(function() {
// Reset theme
Display._theme = null;
});
it("should trigger dispatchEvent when toTransparent(eventName) is called with an event parameter", function (done) {
var testEventName = 'event_32';
displayWatcher.dispatchEvent = function(eventName) {
if (eventName == testEventName) {
done();
}
};
Display.toTransparent(testEventName);
setTimeout(function() {
fail('dispatchEvent not called');
done();
}, TRANSITION_TIMEOUT);
});
it("should trigger dispatchEvent when toBlack(eventName) is called with an event parameter", function (done) {
var testEventName = 'event_33';
displayWatcher.dispatchEvent = function(eventName) {
if (eventName == testEventName) {
done();
}
};
Display.toBlack(testEventName);
setTimeout(function() {
fail('dispatchEvent not called');
done();
}, TRANSITION_TIMEOUT);
});
it("should trigger dispatchEvent when toTheme(eventName) is called with an event parameter", function (done) {
var testEventName = 'event_34';
displayWatcher.dispatchEvent = function(eventName) {
if (eventName == testEventName) {
done();
}
};
Display.toTheme(testEventName);
setTimeout(function() {
fail('dispatchEvent not called');
done();
}, TRANSITION_TIMEOUT);
});
it("should trigger dispatchEvent when show(eventName) is called with an event parameter", function (done) {
var testEventName = 'event_35';
displayWatcher.dispatchEvent = function(eventName) {
if (eventName == testEventName) {
done();
}
};
Display.show(testEventName);
setTimeout(function() {
fail('dispatchEvent not called');
done();
}, TRANSITION_TIMEOUT);
});
});
describe("Display.alert", function () {
var alertContainer, alertBackground, alertText, settings, text;
@ -785,8 +703,9 @@ describe("Display.setTextSlides", function () {
beforeEach(function() {
document.body.innerHTML = "";
var slides_container = _createDiv({"class": "slides"});
var footer_container = _createDiv({"class": "footer"});
var revealContainer = Display._revealContainer = _createDiv({"class": "reveal"})
var slides_container = _createDiv({"class": "slides"}, revealContainer);
var footer_container = _createDiv({"class": "footer"}, revealContainer);
Display._slidesContainer = slides_container;
Display._footerContainer = footer_container;
Display._slides = {};
@ -1203,15 +1122,14 @@ describe("Reveal slidechanged event", function () {
}
];
var slidesDiv = _createDiv({"class": "slides"});
document.body.innerHTML = '';
var revealContainer = Display._revealContainer = _createDiv({"class": "reveal"});
var slidesDiv = _createDiv({"class": "slides"}, revealContainer);
slidesDiv.innerHTML = "<section><p></p></section>";
Display._slidesContainer = slidesDiv;
var footerDiv = _createDiv({"class": "footer"});
var footerDiv = _createDiv({"class": "footer"}, revealContainer);
Display._footerContainer = footerDiv;
var revealDiv = _createDiv({"class": "reveal"});
revealDiv.append(slidesDiv);
revealDiv.append(footerDiv);
document.body.appendChild(revealDiv);
Display.init({isDisplay: false, doItemTransitions: false});
var oldDisplaySlideChanged = Display._onSlideChanged;

View File

@ -0,0 +1,82 @@
"use strict";
describe("The CommunicationBridge object", () => {
/** @type {CommunicationBridge} */
let testCommunicationBridge;
let SimpleTarget;
beforeEach(() => {
testCommunicationBridge = new CommunicationBridge();
SimpleTarget = class {
_handleNativeCall(action, ...values) {}
}
});
it("should exist", () => {
expect(CommunicationBridge).toBeDefined()
});
it("should not be ready after instantiated", () => {
expect(testCommunicationBridge.isReady()).toBe(false);
});
it("should register target", () => {
testCommunicationBridge.setDisplayTarget(new SimpleTarget());
expect(testCommunicationBridge.isReady()).toBe(true);
});
it("should be able to requestAction", (done) => {
const target = new SimpleTarget()
target._handleNativeCall = (action, ...values) => done();
testCommunicationBridge.setDisplayTarget(target);
testCommunicationBridge.requestAction('dummy');
});
it("should be able to replay init action before setDisplayTarget", (done) => {
const target = new SimpleTarget()
target._handleNativeCall = (action, ...values) => {
if (action == 'init') {
expect(values[0]).toBe('replayed init');
done();
}
}
testCommunicationBridge.requestAction('init', 'replayed init');
testCommunicationBridge.setDisplayTarget(target);
});
it("should dispatch initialized event", (done) => {
displayWatcher.setInitialised = () => done();
testCommunicationBridge.setDisplayTarget(new SimpleTarget());
testCommunicationBridge.requestAction('init', {do_init: true});
});
it("should dispatch event on async call and promise-based handlers", (done) => {
const target = new SimpleTarget()
target._handleNativeCall = (action, ...values) => {
if (action == 'async') {
return new Promise((resolve) => resolve({value: 'async value'}));
}
}
testCommunicationBridge.setDisplayTarget(target);
displayWatcher.dispatchEvent = (event, parameter) => {
expect(event).toBe('async_callback');
expect(parameter).toEqual({value: 'async value'});
done();
}
// THEN: async callback should return value over displayWatch.dispatchEvent()
testCommunicationBridge.requestActionAsync('async', 'async_callback');
});
it("should dispatch event on async call and normal handlers", (done) => {
const target = new SimpleTarget()
target._handleNativeCall = (action, ...values) => {
if (action == 'sync') {
return {value: 'sync value'};
}
}
testCommunicationBridge.setDisplayTarget(target);
displayWatcher.dispatchEvent = (event, parameter) => {
expect(event).toBe('sync_callback');
expect(parameter).toEqual({value: 'sync value'});
done();
}
// THEN: async callback should return value over displayWatch.dispatchEvent()
testCommunicationBridge.requestActionAsync('sync', 'sync_callback');
});
});

View File

@ -173,13 +173,13 @@ def test_set_scale_not_initialised(display_window_env, mock_settings):
# GIVEN: A display window not yet initialised
display_window = DisplayWindow()
display_window._is_initialised = False
display_window.run_javascript = MagicMock()
display_window.run_in_display = MagicMock()
# WHEN: set scale is run
display_window.set_scale(0.5)
# THEN: javascript should not be run
display_window.run_javascript.assert_not_called()
display_window.run_in_display.assert_not_called()
def test_set_scale_initialised(display_window_env, mock_settings):
@ -189,13 +189,31 @@ def test_set_scale_initialised(display_window_env, mock_settings):
# GIVEN: A initialised display window
display_window = DisplayWindow()
display_window._is_initialised = True
display_window.run_javascript = MagicMock()
display_window.run_in_display = MagicMock()
# WHEN: set scale is run
display_window.set_scale(0.5)
# THEN: javascript should not be run
display_window.run_javascript.assert_called_once_with('Display.setScale(50.0);')
display_window.run_in_display.assert_called_once_with('setScale', 50.0)
def test_set_display_custom_url_works_http(registry, display_window_env, mock_settings):
"""
Test that setting a display custom url works with HTTP path
"""
# GIVEN: A mocked set_url and a custom display path
test_path = 'http://localhost:4200?testing=true'
registry.register('display_custom_url', test_path)
with patch('openlp.core.display.window.DisplayWindow.set_url') as mocked_set_url, \
patch('openlp.core.display.window.QtCore.QUrl') as mocked_qurl:
mocked_qurl.side_effect = lambda input: input
# WHEN: creating a DisplayWindow
DisplayWindow()
# THEN: URL should be set with the custom path
mocked_set_url.assert_called_once_with(test_path)
def test_set_startup_screen(display_window_env, mock_settings):
@ -205,7 +223,7 @@ def test_set_startup_screen(display_window_env, mock_settings):
# GIVEN: A display window and mocked settings with logo path
display_window = DisplayWindow()
display_window._is_initialised = True
display_window.run_javascript = MagicMock()
display_window.run_in_display = MagicMock()
if is_win():
image_path = 'c:/my/image.png'
@ -225,8 +243,9 @@ def test_set_startup_screen(display_window_env, mock_settings):
display_window.set_startup_screen()
# THEN: javascript should be run
display_window.run_javascript.assert_called_once_with(
'Display.setStartupSplashScreen("red", "openlp-library://local-file/{path}");'.format(path=expect_image_path))
display_window.run_in_display.assert_called_once_with('setStartupSplashScreen', "red",
"openlp-library://local-file/{path}"
.format(path=expect_image_path))
def test_set_startup_screen_default_image(display_window_env, mock_settings):
@ -236,7 +255,7 @@ def test_set_startup_screen_default_image(display_window_env, mock_settings):
# GIVEN: A display window and mocked settings with logo path
display_window = DisplayWindow()
display_window._is_initialised = True
display_window.run_javascript = MagicMock()
display_window.run_in_display = MagicMock()
splash_screen_path = 'openlp://display/openlp-splash-screen.png'
expect_splash_screen_path = splash_screen_path
display_window.openlp_splash_screen_path = splash_screen_path
@ -251,8 +270,8 @@ def test_set_startup_screen_default_image(display_window_env, mock_settings):
display_window.set_startup_screen()
# THEN: javascript should be run
display_window.run_javascript.assert_called_with(
'Display.setStartupSplashScreen("blue", "{path}");'.format(path=expect_splash_screen_path))
display_window.run_in_display.assert_called_with('setStartupSplashScreen', 'blue',
"{path}".format(path=expect_splash_screen_path))
def test_set_startup_screen_missing(display_window_env, mock_settings):
@ -262,7 +281,7 @@ def test_set_startup_screen_missing(display_window_env, mock_settings):
# GIVEN: A display window and mocked settings with logo path missing
display_window = DisplayWindow()
display_window._is_initialised = True
display_window.run_javascript = MagicMock()
display_window.run_in_display = MagicMock()
display_window.openlp_splash_screen_path = Path('/default/splash_screen.png')
settings = {
'core/logo background color': 'green',
@ -275,8 +294,7 @@ def test_set_startup_screen_missing(display_window_env, mock_settings):
display_window.set_startup_screen()
# THEN: javascript should be run
display_window.run_javascript.assert_called_with(
'Display.setStartupSplashScreen("green", "");')
display_window.run_in_display.assert_called_with('setStartupSplashScreen', 'green', '')
def test_set_startup_screen_hide(display_window_env, mock_settings):
@ -286,7 +304,7 @@ def test_set_startup_screen_hide(display_window_env, mock_settings):
# GIVEN: A display window and mocked settings with hide logo true
display_window = DisplayWindow()
display_window._is_initialised = True
display_window.run_javascript = MagicMock()
display_window.run_in_display = MagicMock()
display_window.openlp_splash_screen_path = Path('/default/splash_screen.png')
settings = {
'core/logo background color': 'orange',
@ -299,8 +317,7 @@ def test_set_startup_screen_hide(display_window_env, mock_settings):
display_window.set_startup_screen()
# THEN: javascript should be run
display_window.run_javascript.assert_called_once_with(
'Display.setStartupSplashScreen("orange", "");')
display_window.run_in_display.assert_called_once_with('setStartupSplashScreen', 'orange', '')
def test_after_loaded(display_window_env, mock_settings, registry):
@ -313,7 +330,7 @@ def test_after_loaded(display_window_env, mock_settings, registry):
mock_settings.value.return_value = True
display_window.scale = 2
display_window._is_initialised = True
display_window.run_javascript = MagicMock()
display_window.run_in_display = MagicMock()
display_window.set_scale = MagicMock()
display_window.set_startup_screen = MagicMock()
@ -321,17 +338,16 @@ def test_after_loaded(display_window_env, mock_settings, registry):
display_window.after_loaded()
# THEN: The following functions should have been called
display_window.run_javascript.assert_called_once_with('Display.init({'
'isDisplay: true,'
'doItemTransitions: true,'
'slideNumbersInFooter: true,'
'hideMouse: true'
'});')
display_window.run_in_display.assert_called_once_with('init', {'isDisplay': True,
'doItemTransitions': True,
'slideNumbersInFooter': True,
'hideMouse': True,
'displayTitle': None})
display_window.set_scale.assert_called_once_with(2)
display_window.set_startup_screen.assert_called_once()
def test_after_loaded_hide_mouse_not_display(display_window_env, mock_settings, registry):
def test_after_loaded_hide_mouse_not_display(display_window_env, mock_settings):
"""
Test the mouse is showing even if the `hide mouse` setting is set while is_display=false
"""
@ -341,7 +357,7 @@ def test_after_loaded_hide_mouse_not_display(display_window_env, mock_settings,
mock_settings.value.return_value = True
display_window.scale = 2
display_window._is_initialised = True
display_window.run_javascript = MagicMock()
display_window.run_in_display = MagicMock()
display_window.set_scale = MagicMock()
display_window.set_startup_screen = MagicMock()
@ -349,12 +365,11 @@ def test_after_loaded_hide_mouse_not_display(display_window_env, mock_settings,
display_window.after_loaded()
# THEN: Display.init should be called where is_display=false, do_item_transitions=true, show_mouse=false
display_window.run_javascript.assert_called_once_with('Display.init({'
'isDisplay: false,'
'doItemTransitions: true,'
'slideNumbersInFooter: true,'
'hideMouse: false'
'});')
display_window.run_in_display.assert_called_once_with('init', {'isDisplay': False,
'doItemTransitions': True,
'slideNumbersInFooter': True,
'hideMouse': False,
'displayTitle': None})
def test_after_loaded_callback(display_window_env, mock_settings, registry):
@ -367,7 +382,7 @@ def test_after_loaded_callback(display_window_env, mock_settings, registry):
display_window.is_display = True
mock_settings.value.return_value = True
display_window._is_initialised = True
display_window.run_javascript = MagicMock()
display_window.run_in_display = MagicMock()
display_window.set_scale = MagicMock()
display_window.set_startup_screen = MagicMock()
@ -379,7 +394,7 @@ def test_after_loaded_callback(display_window_env, mock_settings, registry):
@patch.object(time, 'time')
def test_run_javascript_no_sync_no_wait(mock_time, display_window_env, mock_settings):
def test_run_in_display_no_sync_no_wait(mock_time, display_window_env, mock_settings):
"""
test a script is run on the webview
"""
@ -389,7 +404,7 @@ def test_run_javascript_no_sync_no_wait(mock_time, display_window_env, mock_sett
display_window.webview.page = MagicMock(return_value=webengine_page)
# WHEN: javascript is requested to run
display_window.run_javascript('javascript to execute')
display_window._run_javascript('javascript to execute')
# THEN: javascript should be run with no delay
webengine_page.runJavaScript.assert_called_once_with('javascript to execute')
@ -397,7 +412,7 @@ def test_run_javascript_no_sync_no_wait(mock_time, display_window_env, mock_sett
@patch.object(time, 'time')
def test_run_javascript_sync_no_wait(mock_time, display_window_env, mock_settings):
def test_run_in_display_sync_no_wait(mock_time, display_window_env, mock_settings):
"""
test a synced script is run on the webview and immediately returns a result
"""
@ -411,7 +426,7 @@ def test_run_javascript_sync_no_wait(mock_time, display_window_env, mock_setting
display_window.webview.page.return_value = webengine_page
# WHEN: javascript is requested to run
result = display_window.run_javascript('javascript to execute', True)
result = display_window._run_javascript('javascript to execute', True)
# THEN: javascript should be run with no delay and return with the correct result
assert result == 1234
@ -428,7 +443,7 @@ def test_fix_font_bold_windows(mocked_is_win, display_window_env, mock_settings)
mocked_is_win.return_value = True
display_window = DisplayWindow()
display_window.is_display = True
display_window.run_javascript = MagicMock()
display_window.run_in_display = MagicMock()
font_name = 'Arial Rounded MT Bold'
# WHEN: The font is processed
@ -447,7 +462,7 @@ def test_fix_font_bold_not_windows(mocked_is_win, display_window_env, mock_setti
mocked_is_win.return_value = False
display_window = DisplayWindow()
display_window.is_display = True
display_window.run_javascript = MagicMock()
display_window.run_in_display = MagicMock()
font_name = 'Arial Rounded MT Bold'
# WHEN: The font is processed
@ -466,7 +481,7 @@ def test_fix_font_foundry(mocked_is_win, display_window_env, mock_settings):
mocked_is_win.return_value = False
display_window = DisplayWindow()
display_window.is_display = True
display_window.run_javascript = MagicMock()
display_window.run_in_display = MagicMock()
font_name = 'CMG Sans [Foundry]'
# WHEN: The font is processed
@ -483,7 +498,7 @@ def test_set_theme_is_display_video(display_window_env, mock_settings, mock_geom
# GIVEN: A display window and a video theme
display_window = DisplayWindow()
display_window.is_display = True
display_window.run_javascript = MagicMock()
display_window.run_in_display = MagicMock()
theme = Theme()
theme.background_type = 'video'
result_theme = Theme()
@ -494,8 +509,7 @@ def test_set_theme_is_display_video(display_window_env, mock_settings, mock_geom
display_window.set_theme(theme, is_sync=False, service_item_type=ServiceItemType.Text)
# THEN: The final theme should be transparent
display_window.run_javascript.assert_called_once_with('Display.setTheme({theme});'.format(theme=result_theme),
is_sync=False)
display_window.run_in_display.assert_called_once_with('setTheme', raw_parameters=result_theme, is_sync=False)
def test_set_theme_not_display_video(display_window_env, mock_settings, mock_geometry):
@ -505,7 +519,7 @@ def test_set_theme_not_display_video(display_window_env, mock_settings, mock_geo
# GIVEN: A display window and a video theme
display_window = DisplayWindow()
display_window.is_display = False
display_window.run_javascript = MagicMock()
display_window.run_in_display = MagicMock()
theme = Theme()
theme.background_type = 'video'
theme.background_border_color = 'border_colour'
@ -523,8 +537,7 @@ def test_set_theme_not_display_video(display_window_env, mock_settings, mock_geo
display_window.set_theme(theme, is_sync=False, service_item_type=False)
# THEN: The final theme should use 'border_colour' for it's colour values
display_window.run_javascript.assert_called_once_with('Display.setTheme({theme});'.format(theme=result_theme),
is_sync=False)
display_window.run_in_display.assert_called_once_with('setTheme', raw_parameters=result_theme, is_sync=False)
def test_set_theme_not_display_live(display_window_env, mock_settings, mock_geometry):
@ -534,7 +547,7 @@ def test_set_theme_not_display_live(display_window_env, mock_settings, mock_geom
# GIVEN: A display window and a video theme
display_window = DisplayWindow()
display_window.is_display = False
display_window.run_javascript = MagicMock()
display_window.run_in_display = MagicMock()
theme = Theme()
theme.background_type = 'live'
result_theme = Theme()
@ -549,8 +562,7 @@ def test_set_theme_not_display_live(display_window_env, mock_settings, mock_geom
display_window.set_theme(theme, is_sync=False, service_item_type=False)
# THEN: The final theme should use the preset colour values
display_window.run_javascript.assert_called_once_with('Display.setTheme({theme});'.format(theme=result_theme),
is_sync=False)
display_window.run_in_display.assert_called_once_with('setTheme', raw_parameters=result_theme, is_sync=False)
@patch('openlp.core.display.window.Registry.execute')
@ -564,7 +576,7 @@ def test_show_display(mocked_screenlist, mocked_registry_execute, display_window
display_window.is_display = True
display_window.isHidden = MagicMock(return_value=True)
display_window.setVisible = MagicMock()
display_window.run_javascript = MagicMock()
display_window.run_in_display = MagicMock()
mocked_screenlist.screens = [1, 2]
# WHEN: Show display is run
@ -572,7 +584,7 @@ def test_show_display(mocked_screenlist, mocked_registry_execute, display_window
# THEN: Should show the display and set the hide mode to none
display_window.setVisible.assert_called_once_with(True)
display_window.run_javascript.assert_called_once_with('Display.show();')
display_window.run_in_display.assert_called_once_with('show')
@patch('openlp.core.display.window.ScreenList')
@ -582,7 +594,7 @@ def test_show_display_no_display(mocked_screenlist, display_window_env, mock_set
"""
# GIVEN: A Display window, one screen and core/display on monitor disabled
display_window = DisplayWindow()
display_window.run_javascript = MagicMock()
display_window.run_in_display = MagicMock()
display_window.is_display = True
mocked_screenlist.return_value = [1]
mock_settings.value.return_value = False
@ -591,7 +603,7 @@ def test_show_display_no_display(mocked_screenlist, display_window_env, mock_set
display_window.show_display()
# THEN: Shouldn't run the js show fn
assert display_window.run_javascript.call_count == 0
assert display_window.run_in_display.call_count == 0
def test_hide_display_to_screen(display_window_env, mock_settings):
@ -600,7 +612,7 @@ def test_hide_display_to_screen(display_window_env, mock_settings):
"""
# GIVEN: Display window and setting advanced/disable transparent display = False
display_window = DisplayWindow()
display_window.run_javascript = MagicMock()
display_window.run_in_display = MagicMock()
display_window.setVisible = MagicMock()
mock_settings.value.return_value = False
@ -609,7 +621,7 @@ def test_hide_display_to_screen(display_window_env, mock_settings):
# THEN: Should hide the display with the js transparency function (not setVisible)
display_window.setVisible.call_count == 0
display_window.run_javascript.assert_called_once_with('Display.toTransparent();')
display_window.run_in_display.assert_called_once_with('toTransparent')
def test_hide_display_to_blank(display_window_env, mock_settings):
@ -618,14 +630,14 @@ def test_hide_display_to_blank(display_window_env, mock_settings):
"""
# GIVEN: Display window and setting advanced/disable transparent display = False
display_window = DisplayWindow()
display_window.run_javascript = MagicMock()
display_window.run_in_display = MagicMock()
mock_settings.value.return_value = False
# WHEN: Hide display is run with HideMode.Blank
display_window.hide_display(HideMode.Blank)
# THEN: Should run the correct javascript on the display and set the hide mode
display_window.run_javascript.assert_called_once_with('Display.toBlack();')
display_window.run_in_display.assert_called_once_with('toBlack')
def test_hide_display_to_theme(display_window_env, mock_settings):
@ -634,14 +646,14 @@ def test_hide_display_to_theme(display_window_env, mock_settings):
"""
# GIVEN: Display window and setting advanced/disable transparent display = False
display_window = DisplayWindow()
display_window.run_javascript = MagicMock()
display_window.run_in_display = MagicMock()
mock_settings.value.return_value = False
# WHEN: Hide display is run with HideMode.Theme
display_window.hide_display(HideMode.Theme)
# THEN: Should run the correct javascript on the display and set the hide mode
display_window.run_javascript.assert_called_once_with('Display.toTheme();')
display_window.run_in_display.assert_called_once_with('toTheme')
def test_hide_display_to_transparent(display_window_env, mock_settings):
@ -650,7 +662,7 @@ def test_hide_display_to_transparent(display_window_env, mock_settings):
"""
# GIVEN: Display window and setting advanced/disable transparent display = False
display_window = DisplayWindow()
display_window.run_javascript = MagicMock()
display_window.run_in_display = MagicMock()
display_window.setVisible = MagicMock()
mock_settings.value.return_value = False
@ -658,7 +670,7 @@ def test_hide_display_to_transparent(display_window_env, mock_settings):
display_window.hide_display(HideMode.Screen)
# THEN: Should run the correct javascript on the display and not set the visiblity
display_window.run_javascript.assert_called_once_with('Display.toTransparent();')
display_window.run_in_display.assert_called_once_with('toTransparent')
assert display_window.setVisible.call_count == 0
@ -774,12 +786,27 @@ def test_display_watcher_unregisters_registered_permanent_and_transient_event(di
event_listener_permanent.assert_not_called()
def test_display_watcher_generates_event_names(display_window_env, mock_settings):
"""
Test that the display watcher generate unique event names
"""
# GIVEN: Display window
display_window = DisplayWindow()
# WHEN: Getting unique event names
first_event_name = display_window.display_watcher.get_unique_event_name()
second_event_name = display_window.display_watcher.get_unique_event_name()
# THEN: Event names should be different
assert first_event_name != second_event_name
def test_hide_transparent_to_screen(display_window_env, mock_settings):
"""
Test that when going transparent, and the disable transparent setting is enabled,
the screen mode should be used.
"""
# GIVEN: Display window, setting advanced/disable transparent display = True and mocked run_javascript
# GIVEN: Display window, setting advanced/disable transparent display = True and mocked run_in_display
display_window = DisplayWindow()
display_window.setVisible = MagicMock()
has_ran_event = False
@ -788,11 +815,11 @@ def test_hide_transparent_to_screen(display_window_env, mock_settings):
nonlocal has_ran_event
has_ran_event = True
def on_dispatch_event(_):
def on_dispatch_event(*args, **kwargs):
display_window.display_watcher.register_event_listener(TRANSITION_END_EVENT_NAME, set_has_ran_event, False)
display_window.display_watcher.dispatchEvent(TRANSITION_END_EVENT_NAME, {})
display_window.run_javascript = MagicMock(side_effect=on_dispatch_event)
display_window.run_in_display = MagicMock(side_effect=on_dispatch_event)
mock_settings.value.return_value = True
# WHEN: Hide display is run with HideMode.Screen
@ -887,3 +914,79 @@ def test_close_event_accepts_event_manual_close(display_window_env, mock_setting
# THEN: The event should have been ignored
assert mocked_event.ignore.called is False
def test_run_in_display_run(display_window_env, mock_settings):
"""
Test that when run_in_display is called
"""
# GIVEN: A DisplayWindow instance and a mocked _run_javascript
display_window = DisplayWindow()
display_window._run_javascript = MagicMock()
# WHEN: The run_is_display is called
display_window.run_in_display('test_event')
# THEN: The event should be called
display_window._run_javascript.assert_called_once_with('requestAction(\'test_event\')', False)
def test_run_in_display_honors_is_sync(display_window_env, mock_settings):
"""
Test that when run_in_display honors is_sync flag
"""
# GIVEN: A DisplayWindow instance and a mocked _run_javascript
display_window = DisplayWindow()
display_window._run_javascript = MagicMock()
# WHEN: The run_is_display is called
display_window.run_in_display('test_event', is_sync=True)
# THEN: The event should be called
display_window._run_javascript.assert_called_once_with('requestAction(\'test_event\')', True)
def test_run_in_display_honors_raw_parameters(display_window_env, mock_settings):
"""
Test that when run_in_display honors raw_parameters parameters
"""
# GIVEN: A DisplayWindow instance and a mocked _run_javascript
display_window = DisplayWindow()
display_window._run_javascript = MagicMock()
# WHEN: The run_is_display is called
display_window.run_in_display('test_event', raw_parameters='a test: testing')
# THEN: The event should be called
display_window._run_javascript.assert_called_once_with('requestAction(\'test_event\', a test: testing)', False)
def test_run_in_display_honors_return_event_name(display_window_env, mock_settings):
"""
Test that when run_in_display honors return_event_name parameter
"""
# GIVEN: A DisplayWindow instance and a mocked _run_javascript
display_window = DisplayWindow()
display_window._run_javascript = MagicMock()
# WHEN: The run_is_display is called
display_window.run_in_display('test_event', return_event_name='test_event')
# THEN: The event should be called
display_window._run_javascript.assert_called_once_with('requestActionAsync(\'test_event\', \'test_event\')', False)
def test_run_in_display_dumps_json(display_window_env, mock_settings):
"""
Test that when run_in_display is called with parameters, each of it will be dumped as a JSON string
"""
# GIVEN: A DisplayWindow instance and a mocked _run_javascript
display_window = DisplayWindow()
display_window._run_javascript = MagicMock()
# WHEN: The run_is_display is called
display_window.run_in_display('test_event', 1.23, 'a "string', [1, 2, 'test'], {"test1": "test2"})
# THEN: The parameters should be correctly converted to JSON
display_window._run_javascript.assert_called_once_with('requestAction(\'test_event\', 1.23, "a \\"string\", '
'[1, 2, "test"], {"test1": "test2"})', False)