Ignore transition speed when no transitions

Also add/fix tests
This commit is contained in:
Daniel 2020-10-02 04:57:17 +00:00 committed by Raoul Snyman
parent f09ce2fed0
commit bd0308f1b4
10 changed files with 245 additions and 21 deletions

View File

@ -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})

View File

@ -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})

View File

@ -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);

View File

@ -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):

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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():

View File

@ -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

View File

@ -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