From bd0308f1b492b880ef0635e6f12af8da10fecb31 Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 2 Oct 2020 04:57:17 +0000 Subject: [PATCH] Ignore transition speed when no transitions Also add/fix tests --- openlp/core/api/versions/v1/core.py | 3 +- openlp/core/api/versions/v2/core.py | 3 +- openlp/core/display/html/display.js | 2 +- openlp/core/lib/__init__.py | 4 +- openlp/core/lib/serviceitem.py | 20 ++++- openlp/core/ui/printserviceform.py | 2 +- openlp/core/ui/slidecontroller.py | 46 +++++++++- tests/functional/openlp_core/lib/test_lib.py | 15 ++-- .../openlp_core/lib/test_serviceitem.py | 88 +++++++++++++++++++ .../openlp_core/ui/test_slidecontroller.py | 83 ++++++++++++++++- 10 files changed, 245 insertions(+), 21 deletions(-) diff --git a/openlp/core/api/versions/v1/core.py b/openlp/core/api/versions/v1/core.py index 90d1bcd2d..2230ba7ca 100644 --- a/openlp/core/api/versions/v1/core.py +++ b/openlp/core/api/versions/v1/core.py @@ -20,7 +20,6 @@ ########################################################################## from openlp.core.api.lib import old_auth, old_success_response from openlp.core.common.registry import Registry -from openlp.core.lib import image_to_byte from openlp.core.lib.plugin import PluginStatus, StringContent from openlp.core.state import State @@ -55,5 +54,5 @@ def plugin_list(): @core_views.route('/main/image') def main_image(): - img = 'data:image/png;base64,{}'.format(image_to_byte(Registry().get('live_controller').grab_maindisplay())) + img = 'data:image/jpeg;base64,{}'.format(Registry().get('live_controller').grab_maindisplay()) return jsonify({'slide_image': img}) diff --git a/openlp/core/api/versions/v2/core.py b/openlp/core/api/versions/v2/core.py index 650d58fb3..90ad755b6 100644 --- a/openlp/core/api/versions/v2/core.py +++ b/openlp/core/api/versions/v2/core.py @@ -21,7 +21,6 @@ import logging from openlp.core.api.lib import login_required from openlp.core.common.registry import Registry -from openlp.core.lib import image_to_byte from openlp.core.lib.plugin import PluginStatus, StringContent from openlp.core.state import State @@ -87,5 +86,5 @@ def login(): @core.route('/live-image') def main_image(): controller = Registry().get('live_controller') - img = 'data:image/png;base64,{}'.format(image_to_byte(controller.grab_maindisplay())) + img = 'data:image/jpeg;base64,{}'.format(controller.grab_maindisplay()) return jsonify({'binary_image': img}) diff --git a/openlp/core/display/html/display.js b/openlp/core/display/html/display.js index 260b191f7..c3e9f25b9 100644 --- a/openlp/core/display/html/display.js +++ b/openlp/core/display/html/display.js @@ -542,7 +542,7 @@ var Display = { section.setAttribute("data-background", bg_color); section.setAttribute("style", "height: 100%; width: 100%;"); var img = document.createElement('img'); - img.src = 'data:image/png;base64,' + image_data; + img.src = 'data:image/jpeg;base64,' + image_data; img.setAttribute("style", "height: 100%; width: 100%"); section.appendChild(img); Display._slidesContainer.appendChild(section); diff --git a/openlp/core/lib/__init__.py b/openlp/core/lib/__init__.py index 0b629da49..e0e1e2244 100644 --- a/openlp/core/lib/__init__.py +++ b/openlp/core/lib/__init__.py @@ -271,12 +271,12 @@ def image_to_byte(image, base_64=True): # use buffer to store pixmap into byteArray buffer = QtCore.QBuffer(byte_array) buffer.open(QtCore.QIODevice.WriteOnly) - image.save(buffer, "PNG") + image.save(buffer, "JPEG") log.debug('image_to_byte - end') if not base_64: return byte_array # convert to base64 encoding so does not get missed! - return bytes(byte_array.toBase64()).decode('utf-8') + return base64.b64encode(byte_array).decode('utf-8') def image_to_data_uri(image_path): diff --git a/openlp/core/lib/serviceitem.py b/openlp/core/lib/serviceitem.py index d00448610..d7cce51d6 100644 --- a/openlp/core/lib/serviceitem.py +++ b/openlp/core/lib/serviceitem.py @@ -42,7 +42,7 @@ from openlp.core.common.registry import Registry from openlp.core.display.render import remove_tags, render_tags, render_chords_for_printing from openlp.core.lib import ItemCapabilities from openlp.core.lib import create_thumb -from openlp.core.lib.theme import BackgroundType +from openlp.core.lib.theme import BackgroundType, TransitionSpeed from openlp.core.state import State from openlp.core.ui.icons import UiIcons from openlp.core.ui.media import parse_stream_path @@ -606,6 +606,24 @@ class ServiceItem(RegistryProperties): else: return self.slides[0]['title'] + def get_transition_delay(self): + """ + Returns a approximate time in seconds for how long it will take to switch slides + """ + delay = 1 + if self.is_capable(ItemCapabilities.ProvidesOwnDisplay): + delay = 0.5 + else: + theme = self.get_theme_data() + transition_speed = theme.display_slide_transition_speed + if theme.display_slide_transition is False or transition_speed == TransitionSpeed.Fast: + delay = 0.5 + elif transition_speed == TransitionSpeed.Normal: + delay = 1 + elif transition_speed == TransitionSpeed.Slow: + delay = 2 + return delay + def merge(self, other): """ Updates the unique_identifier with the value from the original one diff --git a/openlp/core/ui/printserviceform.py b/openlp/core/ui/printserviceform.py index 08080a11b..b1d23e3a1 100644 --- a/openlp/core/ui/printserviceform.py +++ b/openlp/core/ui/printserviceform.py @@ -208,7 +208,7 @@ class PrintServiceForm(QtWidgets.QDialog, Ui_PrintServiceDialog, RegistryPropert # Add the title of the service item. item_title = self._add_element('h2', parent=div, class_id='itemTitle') img = image_to_byte(item.icon.pixmap(20, 20).toImage()) - self._add_element('img', parent=item_title, attribute=('src', 'data:image/png;base64, ' + img)) + self._add_element('img', parent=item_title, attribute=('src', 'data:image/jpeg;base64, ' + img)) self._add_element('span', ' ' + html.escape(item.get_display_title()), item_title) if self.slide_text_check_box.isChecked(): # Add the text of the service item. diff --git a/openlp/core/ui/slidecontroller.py b/openlp/core/ui/slidecontroller.py index 028a01f8d..3c5b59dfa 100644 --- a/openlp/core/ui/slidecontroller.py +++ b/openlp/core/ui/slidecontroller.py @@ -35,6 +35,7 @@ from openlp.core.common.actions import ActionList, CategoryOrder from openlp.core.common.i18n import UiStrings, translate from openlp.core.common.mixins import LogMixin, RegistryProperties from openlp.core.common.registry import Registry, RegistryBase +from openlp.core.common.utils import wait_for from openlp.core.display.screens import ScreenList from openlp.core.display.window import DisplayWindow from openlp.core.lib import ServiceItemAction, image_to_byte @@ -274,6 +275,9 @@ class SlideController(QtWidgets.QWidget, LogMixin, RegistryProperties): self.controller_type = DisplayControllerType.Preview if self.is_live: self.controller_type = DisplayControllerType.Live + self.slide_changed_time = datetime.datetime.now() + self.fetching_screenshot = False + self.screen_capture = None # Hide Menu self.hide_menu = QtWidgets.QToolButton(self.toolbar) self.hide_menu.setObjectName('hide_menu') @@ -927,6 +931,13 @@ class SlideController(QtWidgets.QWidget, LogMixin, RegistryProperties): self.selected_row = 0 # take a copy not a link to the servicemanager copy. self.service_item = copy.copy(service_item) + if self.is_live: + # Reset screen capture before any api call can arrive + self.screen_capture = None + # If item transitions are on, make sure the delay is longer than the animation + self.slide_changed_time = datetime.datetime.now() + if self.settings.value('themes/item transitions') and self.service_item.get_transition_delay() < 1: + self.slide_changed_time += datetime.timedelta(seconds=0.5) if self.service_item.is_command() and not self.service_item.is_media(): Registry().execute( '{text}_start'.format(text=self.service_item.name.lower()), @@ -1213,7 +1224,10 @@ class SlideController(QtWidgets.QWidget, LogMixin, RegistryProperties): """ This updates the preview frame, for example after changing a slide or using *Blank to Theme*. """ - self.log_debug('update_preview {text} '.format(text=self.screens.current)) + self.log_debug('update_preview {text}'.format(text=self.screens.current)) + if self.is_live: + self.screen_capture = None + self.slide_changed_time = max(self.slide_changed_time, datetime.datetime.now()) if self.service_item and self.service_item.is_capable(ItemCapabilities.ProvidesOwnDisplay): if self.is_live: # If live, grab screen-cap of main display now @@ -1223,6 +1237,7 @@ class SlideController(QtWidgets.QWidget, LogMixin, RegistryProperties): else: # If not live, use the slide's thumbnail/icon instead image_path = Path(self.service_item.get_rendered_frame(self.selected_row)) + self.screen_capture = image_path self.preview_display.set_single_image('#000', image_path) else: self.preview_display.go_to_slide(self.selected_row) @@ -1231,14 +1246,16 @@ class SlideController(QtWidgets.QWidget, LogMixin, RegistryProperties): """ Gets an image of the display screen and updates the preview frame. """ - display_image = self.grab_maindisplay() + display_image = self._capture_maindisplay() base64_image = image_to_byte(display_image) + self.screen_capture = base64_image self.preview_display.set_single_image_data('#000', base64_image) - def grab_maindisplay(self): + def _capture_maindisplay(self): """ Creates an image of the current screen. """ + self.log_debug('_capture_maindisplay {text}'.format(text=self.screens.current)) win_id = QtWidgets.QApplication.desktop().winId() screen = QtWidgets.QApplication.primaryScreen() rect = ScreenList().current.display_geometry @@ -1246,6 +1263,29 @@ class SlideController(QtWidgets.QWidget, LogMixin, RegistryProperties): win_image.setDevicePixelRatio(self.preview_display.devicePixelRatio()) return win_image + def is_slide_loaded(self): + """ + Returns a boolean as to whether the slide should be fully visible. + Takes transition time into consideration. + """ + slide_delay_time = 1 + if self.service_item: + slide_delay_time = self.service_item.get_transition_delay() + slide_ready_time = self.slide_changed_time + datetime.timedelta(seconds=slide_delay_time) + return datetime.datetime.now() > slide_ready_time + + def grab_maindisplay(self): + """ + Gets the last taken screenshot + """ + wait_for(lambda: not self.fetching_screenshot) + if self.screen_capture is None: + self.fetching_screenshot = True + wait_for(self.is_slide_loaded) + self.screen_capture = image_to_byte(self._capture_maindisplay()) + self.fetching_screenshot = False + return self.screen_capture + def on_slide_selected_next_action(self, checked): """ Wrapper function from create_action so we can throw away the incorrect parameter diff --git a/tests/functional/openlp_core/lib/test_lib.py b/tests/functional/openlp_core/lib/test_lib.py index 6bae20572..f463e19c4 100644 --- a/tests/functional/openlp_core/lib/test_lib.py +++ b/tests/functional/openlp_core/lib/test_lib.py @@ -241,7 +241,7 @@ def test_image_to_byte(): MockedQtCore.QByteArray.assert_called_with() MockedQtCore.QBuffer.assert_called_with(mocked_byte_array) mocked_buffer.open.assert_called_with('writeonly') - mocked_image.save.assert_called_with(mocked_buffer, "PNG") + mocked_image.save.assert_called_with(mocked_buffer, "JPEG") assert mocked_byte_array.toBase64.called is False assert mocked_byte_array == result, 'The mocked out byte array should be returned' @@ -250,11 +250,12 @@ def test_image_to_byte_base_64(): """ Test the image_to_byte() function """ - with patch('openlp.core.lib.QtCore') as MockedQtCore: + with patch('openlp.core.lib.QtCore') as MockedQtCore, \ + patch('openlp.core.lib.base64.b64encode') as mocked_b64encode: # GIVEN: A set of mocked-out Qt classes - mocked_byte_array = MagicMock() + mocked_b64encode.side_effect = lambda x: MagicMock(decode=MagicMock(return_value='{} base64ified'.format(x))) + mocked_byte_array = "byte_array" MockedQtCore.QByteArray.return_value = mocked_byte_array - mocked_byte_array.toBase64.return_value = QtCore.QByteArray(b'base64mock') mocked_buffer = MagicMock() MockedQtCore.QBuffer.return_value = mocked_buffer MockedQtCore.QIODevice.WriteOnly = 'writeonly' @@ -267,9 +268,9 @@ def test_image_to_byte_base_64(): MockedQtCore.QByteArray.assert_called_with() MockedQtCore.QBuffer.assert_called_with(mocked_byte_array) mocked_buffer.open.assert_called_with('writeonly') - mocked_image.save.assert_called_with(mocked_buffer, "PNG") - mocked_byte_array.toBase64.assert_called_with() - assert 'base64mock' == result, 'The result should be the return value of the mocked out base64 method' + mocked_image.save.assert_called_with(mocked_buffer, "JPEG") + mocked_b64encode.assert_called_with(mocked_byte_array) + assert 'byte_array base64ified' == result, 'The result should be the return value of the mocked base64 method' def test_create_thumb_with_size(): diff --git a/tests/functional/openlp_core/lib/test_serviceitem.py b/tests/functional/openlp_core/lib/test_serviceitem.py index 14fafa739..14a38d392 100644 --- a/tests/functional/openlp_core/lib/test_serviceitem.py +++ b/tests/functional/openlp_core/lib/test_serviceitem.py @@ -31,6 +31,7 @@ from openlp.core.common.enum import ServiceItemType from openlp.core.common.registry import Registry from openlp.core.lib.formattingtags import FormattingTags from openlp.core.lib.serviceitem import ItemCapabilities, ServiceItem +from openlp.core.lib.theme import TransitionSpeed from tests.utils import convert_file_service_item from tests.utils.constants import RESOURCE_PATH @@ -572,6 +573,93 @@ def test_remove_capability(settings): assert ItemCapabilities.CanEdit not in service_item.capabilities, 'The capability should not be in the list' +def test_get_transition_delay_own_display(settings): + """ + Test the service item - get approx transition delay from theme + """ + # GIVEN: A service item with a theme and theme level set to global + service_item = ServiceItem(None) + service_item.add_capability(ItemCapabilities.ProvidesOwnDisplay) + service_item.theme = 'song_theme' + mocked_theme_manager = MagicMock() + mocked_theme_manager.global_theme = 'global_theme' + Registry().register('theme_manager', mocked_theme_manager) + settings.setValue('servicemanager/service theme', 'service_theme') + settings.setValue('themes/theme level', ThemeLevel.Global) + + # WHEN: Get theme data is run + delay = service_item.get_transition_delay() + + # THEN: theme should be 0.5s + assert delay == 0.5 + + +def test_get_transition_delay_no_transition(settings): + """ + Test the service item - get approx transition delay from theme + """ + # GIVEN: A service item with a theme and theme level set to global + service_item = ServiceItem(None) + mocked_theme_manager = MagicMock() + mocked_theme_manager.global_theme = 'global_theme' + mocked_theme_manager.get_theme_data = Mock(return_value=MagicMock(**{ + 'display_slide_transition': False, + 'display_slide_transition_speed': TransitionSpeed.Normal + })) + Registry().register('theme_manager', mocked_theme_manager) + settings.setValue('themes/theme level', ThemeLevel.Global) + + # WHEN: Get theme data is run + delay = service_item.get_transition_delay() + + # THEN: theme should be 0.5s + assert delay == 0.5 + + +def test_get_transition_delay_normal(settings): + """ + Test the service item - get approx transition delay from theme + """ + # GIVEN: A service item with a theme and theme level set to global + service_item = ServiceItem(None) + mocked_theme_manager = MagicMock() + mocked_theme_manager.global_theme = 'global_theme' + mocked_theme_manager.get_theme_data = Mock(return_value=MagicMock(**{ + 'display_slide_transition': True, + 'display_slide_transition_speed': TransitionSpeed.Normal + })) + Registry().register('theme_manager', mocked_theme_manager) + settings.setValue('themes/theme level', ThemeLevel.Global) + + # WHEN: Get theme data is run + delay = service_item.get_transition_delay() + + # THEN: theme should be 1s + assert delay == 1 + + +def test_get_transition_delay_slow(settings): + """ + Test the service item - get approx transition delay from theme + """ + # GIVEN: A service item with a theme and theme level set to global + service_item = ServiceItem(None) + mocked_theme_manager = MagicMock() + mocked_theme_manager.global_theme = 'global_theme' + mocked_theme_manager.get_theme_data = Mock(return_value=MagicMock(**{ + 'display_slide_transition': True, + 'display_slide_transition_speed': TransitionSpeed.Slow + })) + Registry().register('theme_manager', mocked_theme_manager) + settings.setValue('themes/theme level', ThemeLevel.Global) + + # WHEN: Get theme data is run + delay = service_item.get_transition_delay() + + # THEN: theme should be 2s + assert delay == 2 + + def test_to_dict_text_item(state_media, settings, service_item_env): """ Test that the to_dict() method returns the correct data for the service item diff --git a/tests/functional/openlp_core/ui/test_slidecontroller.py b/tests/functional/openlp_core/ui/test_slidecontroller.py index 2c92c1bbc..3461117ef 100644 --- a/tests/functional/openlp_core/ui/test_slidecontroller.py +++ b/tests/functional/openlp_core/ui/test_slidecontroller.py @@ -21,6 +21,8 @@ """ Package to test the openlp.core.ui.slidecontroller package. """ +import datetime + from unittest.mock import MagicMock, patch, sentinel from PyQt5 import QtCore, QtGui @@ -1011,6 +1013,7 @@ def test_update_preview_live(mocked_singleShot, registry): slide_controller.display_maindisplay = MagicMock() slide_controller.slide_preview = MagicMock() slide_controller.slide_count = 0 + slide_controller.slide_changed_time = datetime.datetime.now() # WHEN: update_preview is called slide_controller.update_preview() @@ -1142,7 +1145,7 @@ def test_display_maindisplay(mocked_image_to_byte, registry): """ # GIVEN: A mocked slide controller, with mocked functions slide_controller = SlideController(None) - slide_controller.grab_maindisplay = MagicMock(return_value='placeholder') + slide_controller._capture_maindisplay = MagicMock(return_value='placeholder') slide_controller.preview_display = MagicMock() mocked_image_to_byte.side_effect = lambda x: '{} bytified'.format(x) @@ -1150,10 +1153,86 @@ def test_display_maindisplay(mocked_image_to_byte, registry): slide_controller.display_maindisplay() # THEN: Should have grabbed the maindisplay and set to placeholder with a black background - slide_controller.grab_maindisplay.assert_called_once() + slide_controller._capture_maindisplay.assert_called_once() slide_controller.preview_display.set_single_image_data.assert_called_once_with('#000', 'placeholder bytified') +@patch(u'openlp.core.ui.slidecontroller.image_to_byte') +@patch(u'openlp.core.ui.slidecontroller.ScreenList') +@patch(u'openlp.core.ui.slidecontroller.QtWidgets.QApplication') +def test__capture_maindisplay(mocked_application, mocked_screenlist, mocked_image_to_byte, registry): + """ + Test the _capture_maindisplay method + """ + # GIVEN: A mocked slide controller, with mocked functions + slide_controller = SlideController(None) + mocked_display_geometry = MagicMock( + x=MagicMock(return_value=34), + y=MagicMock(return_value=67), + width=MagicMock(return_value=77), + height=MagicMock(return_value=42) + ) + mocked_screenlist_instance = MagicMock() + mocked_screenlist.return_value = mocked_screenlist_instance + mocked_screenlist_instance.current = MagicMock(display_geometry=mocked_display_geometry) + mocked_primary_screen = MagicMock() + mocked_application.primaryScreen = MagicMock(return_value=mocked_primary_screen) + mocked_application.desktop = MagicMock(return_value=MagicMock( + winId=MagicMock(return_value=23) + )) + slide_controller.preview_display = MagicMock() + + # WHEN: _capture_maindisplay is called + slide_controller._capture_maindisplay() + + # THEN: Screenshot should have been taken with correct winId and dimensions + mocked_primary_screen.grabWindow.assert_called_once_with(23, 34, 67, 77, 42) + + +@patch(u'openlp.core.ui.slidecontroller.image_to_byte') +def test_grab_maindisplay(mocked_image_to_byte, registry): + """ + Test the grab_maindisplay method + """ + # GIVEN: A mocked slide controller, with mocked functions + slide_controller = SlideController(None) + slide_controller._capture_maindisplay = MagicMock(return_value='placeholder') + slide_controller.preview_display = MagicMock() + slide_controller.fetching_screenshot = False + slide_controller.screen_capture = None + slide_controller.service_item = MagicMock(get_transition_delay=MagicMock(return_value=1)) + slide_controller.slide_changed_time = datetime.datetime.now() - datetime.timedelta(seconds=10) + mocked_image_to_byte.side_effect = lambda x: '{} bytified'.format(x) + + # WHEN: grab_maindisplay is called + grabbed_stuff = slide_controller.grab_maindisplay() + + # THEN: Should have grabbed the maindisplay and ran image_to_byte on it + slide_controller._capture_maindisplay.assert_called_once() + assert grabbed_stuff == 'placeholder bytified' + + +@patch(u'openlp.core.ui.slidecontroller.image_to_byte') +def test_grab_maindisplay_cached(mocked_image_to_byte, registry): + """ + Test the grab_maindisplay method with pre-cached screenshot + """ + # GIVEN: A mocked slide controller, with mocked functions + slide_controller = SlideController(None) + slide_controller._capture_maindisplay = MagicMock(return_value='placeholder') + slide_controller.preview_display = MagicMock() + slide_controller.fetching_screenshot = False + slide_controller.screen_capture = 'cached screen_capture' + mocked_image_to_byte.side_effect = lambda x: '{} bytified'.format(x) + + # WHEN: grab_maindisplay is called + grabbed_stuff = slide_controller.grab_maindisplay() + + # THEN: Should have not grabbed the maindisplay and returned the cached image + assert slide_controller._capture_maindisplay.call_count == 0 + assert grabbed_stuff == 'cached screen_capture' + + def test_paint_event_text_fits(): """ Test the paintEvent method when text fits the label