From a04ed99dfc5b3fd2aa871f1244283e38c31b7a0f Mon Sep 17 00:00:00 2001 From: Mateus Meyer Jiacomelli Date: Sat, 12 Aug 2023 15:59:37 +0000 Subject: [PATCH] Display API abstraction --- karma.conf.js | 1 + openlp/core/app.py | 9 +- openlp/core/common/registry.py | 10 +- openlp/core/display/html/display-init.js | 101 +++++++++ openlp/core/display/html/display.html | 1 + openlp/core/display/html/display.js | 199 +++++++++--------- openlp/core/display/render.py | 9 +- openlp/core/display/webengine.py | 2 +- openlp/core/display/window.py | 175 +++++++--------- openlp/core/loader.py | 2 +- openlp/core/ui/slidecontroller.py | 5 +- openlp/core/ui/themeprogressdialog.py | 3 +- openlp/core/ui/themewizard.py | 2 +- openlp/plugins/alerts/lib/alertsmanager.py | 3 +- package.json | 2 +- tests/js/test_display.js | 110 ++-------- tests/js/test_display_init.js | 82 ++++++++ tests/openlp_core/display/test_window.py | 229 +++++++++++++++------ 18 files changed, 575 insertions(+), 370 deletions(-) create mode 100644 openlp/core/display/html/display-init.js create mode 100644 tests/js/test_display_init.js diff --git a/karma.conf.js b/karma.conf.js index 40d867e8f..752ff0f6b 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -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" ], diff --git a/openlp/core/app.py b/openlp/core/app.py index 7a018b92f..f39208032 100644 --- a/openlp/core/app.py +++ b/openlp/core/app.py @@ -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('', '..', '..'))) 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 diff --git a/openlp/core/common/registry.py b/openlp/core/common/registry.py index 4df348760..917643ee4 100644 --- a/openlp/core/common/registry.py +++ b/openlp/core/common/registry.py @@ -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. diff --git a/openlp/core/display/html/display-init.js b/openlp/core/display/html/display-init.js new file mode 100644 index 000000000..185fcabd1 --- /dev/null +++ b/openlp/core/display/html/display-init.js @@ -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); +}; \ No newline at end of file diff --git a/openlp/core/display/html/display.html b/openlp/core/display/html/display.html index 7cff9a910..2809389a8 100644 --- a/openlp/core/display/html/display.html +++ b/openlp/core/display/html/display.html @@ -6,6 +6,7 @@ + diff --git a/openlp/core/display/html/display.js b/openlp/core/display/html/display.js index 4dfd3a3c4..9a3f0c3b4 100644 --- a/openlp/core/display/html/display.js +++ b/openlp/core/display/html/display.js @@ -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); diff --git a/openlp/core/display/render.py b/openlp/core/display/render.py index d973f6f6d..d40fafd8f 100644 --- a/openlp/core/display/render.py +++ b/openlp/core/display/render.py @@ -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): diff --git a/openlp/core/display/webengine.py b/openlp/core/display/webengine.py index 9ff4872bf..04897f2df 100644 --- a/openlp/core/display/webengine.py +++ b/openlp/core/display/webengine.py @@ -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): diff --git a/openlp/core/display/window.py b/openlp/core/display/window.py index 5e56a323f..e352dffa4 100644 --- a/openlp/core/display/window.py +++ b/openlp/core/display/window.py @@ -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): """ diff --git a/openlp/core/loader.py b/openlp/core/loader.py index 886033793..4184138b6 100644 --- a/openlp/core/loader.py +++ b/openlp/core/loader.py @@ -39,7 +39,7 @@ def loader(): MediaController() PluginManager() # Set up the path with plugins - Renderer() + Renderer(window_title='Renderer') # Create slide controllers PreviewController() LiveController() diff --git a/openlp/core/ui/slidecontroller.py b/openlp/core/ui/slidecontroller.py index b02bffb77..e4aae743b 100644 --- a/openlp/core/ui/slidecontroller.py +++ b/openlp/core/ui/slidecontroller.py @@ -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 diff --git a/openlp/core/ui/themeprogressdialog.py b/openlp/core/ui/themeprogressdialog.py index 75f147ed8..88e88f95d 100644 --- a/openlp/core/ui/themeprogressdialog.py +++ b/openlp/core/ui/themeprogressdialog.py @@ -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) diff --git a/openlp/core/ui/themewizard.py b/openlp/core/ui/themewizard.py index 679157ef0..c68b9b47f 100644 --- a/openlp/core/ui/themewizard.py +++ b/openlp/core/ui/themewizard.py @@ -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) diff --git a/openlp/plugins/alerts/lib/alertsmanager.py b/openlp/plugins/alerts/lib/alertsmanager.py index 664dd2b5f..a4436393a 100644 --- a/openlp/plugins/alerts/lib/alertsmanager.py +++ b/openlp/plugins/alerts/lib/alertsmanager.py @@ -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): """ diff --git a/package.json b/package.json index b0ebf53ad..e8bd313e0 100644 --- a/package.json +++ b/package.json @@ -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 diff --git a/tests/js/test_display.js b/tests/js/test_display.js index 415cb249b..a50a8339e 100644 --- a/tests/js/test_display.js +++ b/tests/js/test_display.js @@ -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 = "

"; - 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 = "

"; 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; diff --git a/tests/js/test_display_init.js b/tests/js/test_display_init.js new file mode 100644 index 000000000..440a2b263 --- /dev/null +++ b/tests/js/test_display_init.js @@ -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'); + }); +}); \ No newline at end of file diff --git a/tests/openlp_core/display/test_window.py b/tests/openlp_core/display/test_window.py index f066311cb..fb8d486f7 100644 --- a/tests/openlp_core/display/test_window.py +++ b/tests/openlp_core/display/test_window.py @@ -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)