forked from openlp/openlp
Merge branch 'dynamic_themes' into 'master'
Dynamic themes See merge request openlp/openlp!200
This commit is contained in:
commit
28a7840b9f
@ -334,6 +334,7 @@ class Settings(QtCore.QSettings):
|
||||
'themes/last directory import': None,
|
||||
'themes/theme level': ThemeLevel.Global,
|
||||
'themes/item transitions': False,
|
||||
'themes/hot reload': False,
|
||||
'user interface/live panel': True,
|
||||
'user interface/live splitter geometry': QtCore.QByteArray(),
|
||||
'user interface/lock panel': True,
|
||||
|
@ -301,6 +301,7 @@ class Theme(object):
|
||||
json_path = AppLocation.get_directory(AppLocation.AppDir) / 'core' / 'lib' / 'json' / 'theme.json'
|
||||
jsn = get_text_file_string(json_path)
|
||||
self.load_theme(jsn)
|
||||
self.set_default_header_footer()
|
||||
self.background_filename = None
|
||||
self.background_source = None
|
||||
self.version = 2
|
||||
@ -334,13 +335,18 @@ class Theme(object):
|
||||
|
||||
def set_default_header_footer(self):
|
||||
"""
|
||||
Set the header and footer size into the current primary screen.
|
||||
10 px on each side is removed to allow for a border.
|
||||
Set the default header and footer size to match the current primary screen.
|
||||
Obeys theme override variables.
|
||||
"""
|
||||
self.set_default_header()
|
||||
self.set_default_footer()
|
||||
if not self.font_main_override:
|
||||
self.set_default_header()
|
||||
if not self.font_footer_override:
|
||||
self.set_default_footer()
|
||||
|
||||
def set_default_header(self):
|
||||
"""
|
||||
Sets the default header position and size, ignores font_main_override
|
||||
"""
|
||||
current_screen_geometry = ScreenList().current.display_geometry
|
||||
self.font_main_x = 10
|
||||
self.font_main_y = 0
|
||||
@ -348,6 +354,9 @@ class Theme(object):
|
||||
self.font_main_height = current_screen_geometry.height() * 9 / 10
|
||||
|
||||
def set_default_footer(self):
|
||||
"""
|
||||
Sets the default footer position and size, ignores font_footer_override
|
||||
"""
|
||||
current_screen_geometry = ScreenList().current.display_geometry
|
||||
self.font_footer_x = 10
|
||||
self.font_footer_y = current_screen_geometry.height() * 9 / 10
|
||||
|
@ -1355,7 +1355,7 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
|
||||
"""
|
||||
self.service_theme = self.theme_combo_box.currentText()
|
||||
self.settings.setValue('servicemanager/service theme', self.service_theme)
|
||||
self.regenerate_service_items(True)
|
||||
Registry().execute('theme_update_service')
|
||||
|
||||
def theme_change(self):
|
||||
"""
|
||||
|
@ -502,6 +502,10 @@ class SlideController(QtWidgets.QWidget, LogMixin, RegistryProperties):
|
||||
self.mediacontroller_live_stop.connect(self.media_controller.on_media_stop)
|
||||
else:
|
||||
getattr(self, 'slidecontroller_preview_clear').connect(self.on_clear)
|
||||
# Update the theme whenever global or service theme updated
|
||||
# theme_update_list catches changes to themes AND if the global theme changes
|
||||
Registry().register_function('theme_update_list', self.theme_updated)
|
||||
Registry().register_function('theme_update_service', self.theme_updated)
|
||||
|
||||
def new_song_menu(self):
|
||||
"""
|
||||
@ -868,6 +872,16 @@ class SlideController(QtWidgets.QWidget, LogMixin, RegistryProperties):
|
||||
for display in self.displays:
|
||||
display.set_background_image(bg_color, image_path)
|
||||
|
||||
def theme_updated(self, var=None):
|
||||
"""
|
||||
Reloads the service item
|
||||
|
||||
:param var: Unused but needed to catch the theme_update_list event
|
||||
"""
|
||||
if self.service_item and self.settings.value('themes/hot reload'):
|
||||
slide_num = self.preview_widget.current_slide_number()
|
||||
self._process_item(self.service_item, slide_num)
|
||||
|
||||
def reload_theme(self):
|
||||
"""
|
||||
Reload the theme on displays.
|
||||
|
@ -144,6 +144,7 @@ class ThemeManager(QtWidgets.QWidget, RegistryBase, Ui_ThemeManager, LogMixin, R
|
||||
# Variables
|
||||
self._theme_list = {}
|
||||
self.old_background_image_path = None
|
||||
Registry().register_function('config_screen_changed', self.screen_changed)
|
||||
|
||||
def get_global_theme(self):
|
||||
return self.get_theme_data(self.global_theme)
|
||||
@ -184,6 +185,14 @@ class ThemeManager(QtWidgets.QWidget, RegistryBase, Ui_ThemeManager, LogMixin, R
|
||||
self.load_themes()
|
||||
Registry().register_function('theme_update_global', self.change_global_from_tab)
|
||||
|
||||
def screen_changed(self):
|
||||
"""
|
||||
Update the default theme location and size for when screen size changed
|
||||
"""
|
||||
for theme_name in self._theme_list:
|
||||
theme_object = self._theme_list[theme_name]
|
||||
theme_object.set_default_header_footer()
|
||||
|
||||
def upgrade_themes(self):
|
||||
"""
|
||||
Upgrade the xml files to json.
|
||||
@ -292,7 +301,6 @@ class ThemeManager(QtWidgets.QWidget, RegistryBase, Ui_ThemeManager, LogMixin, R
|
||||
:param field:
|
||||
"""
|
||||
theme = Theme()
|
||||
theme.set_default_header_footer()
|
||||
self.theme_form.theme = theme
|
||||
self.theme_form.exec()
|
||||
self.load_themes()
|
||||
@ -566,7 +574,9 @@ class ThemeManager(QtWidgets.QWidget, RegistryBase, Ui_ThemeManager, LogMixin, R
|
||||
if not theme_data:
|
||||
self.log_debug('No theme data - using default theme')
|
||||
return Theme()
|
||||
return self._create_theme_from_json(theme_data, self.theme_path)
|
||||
theme_object = self._create_theme_from_json(theme_data, self.theme_path)
|
||||
theme_object.set_default_header_footer()
|
||||
return theme_object
|
||||
|
||||
def over_write_message_box(self, theme_name):
|
||||
"""
|
||||
@ -675,7 +685,6 @@ class ThemeManager(QtWidgets.QWidget, RegistryBase, Ui_ThemeManager, LogMixin, R
|
||||
Writes the theme to the disk and including the background image and thumbnail if necessary
|
||||
|
||||
:param Theme theme: The theme data object.
|
||||
:param image: The theme thumbnail. Optionally.
|
||||
:param background_override: Background to use rather than background_source. Optionally.
|
||||
:rtype: None
|
||||
"""
|
||||
|
@ -69,6 +69,9 @@ class ThemesTab(SettingsTab):
|
||||
self.item_transitions_check_box = QtWidgets.QCheckBox(self.universal_group_box)
|
||||
self.item_transitions_check_box.setObjectName('item_transitions_check_box')
|
||||
self.universal_group_box_layout.addWidget(self.item_transitions_check_box)
|
||||
self.theme_hot_reload = QtWidgets.QCheckBox(self.universal_group_box)
|
||||
self.theme_hot_reload.setObjectName('theme_hot_reload')
|
||||
self.universal_group_box_layout.addWidget(self.theme_hot_reload)
|
||||
self.left_layout.addWidget(self.universal_group_box)
|
||||
self.left_layout.addStretch()
|
||||
self.level_group_box = QtWidgets.QGroupBox(self.right_column)
|
||||
@ -115,6 +118,7 @@ class ThemesTab(SettingsTab):
|
||||
self.global_group_box.setTitle(translate('OpenLP.ThemesTab', 'Global Theme'))
|
||||
self.universal_group_box.setTitle(translate('OpenLP.ThemesTab', 'Universal Settings'))
|
||||
self.item_transitions_check_box.setText(translate('OpenLP.ThemesTab', '&Transition between service items'))
|
||||
self.theme_hot_reload.setText(translate('OpenLP.ThemesTab', '&Reload live theme when changed'))
|
||||
self.level_group_box.setTitle(translate('OpenLP.ThemesTab', 'Theme Level'))
|
||||
self.song_level_radio_button.setText(translate('OpenLP.ThemesTab', 'S&ong Level'))
|
||||
self.song_level_label.setText(
|
||||
@ -138,6 +142,7 @@ class ThemesTab(SettingsTab):
|
||||
self.theme_level = self.settings.value('themes/theme level')
|
||||
self.global_theme = self.settings.value('themes/global theme')
|
||||
self.item_transitions_check_box.setChecked(self.settings.value('themes/item transitions'))
|
||||
self.theme_hot_reload.setChecked(self.settings.value('themes/hot reload'))
|
||||
if self.theme_level == ThemeLevel.Global:
|
||||
self.global_level_radio_button.setChecked(True)
|
||||
elif self.theme_level == ThemeLevel.Service:
|
||||
@ -152,6 +157,7 @@ class ThemesTab(SettingsTab):
|
||||
self.settings.setValue('themes/theme level', self.theme_level)
|
||||
self.settings.setValue('themes/global theme', self.global_theme)
|
||||
self.settings.setValue('themes/item transitions', self.item_transitions_check_box.isChecked())
|
||||
self.settings.setValue('themes/hot reload', self.theme_hot_reload.isChecked())
|
||||
self.renderer.set_theme_level(self.theme_level)
|
||||
if self.tab_visited:
|
||||
self.settings_form.register_post_process('theme_update_global')
|
||||
|
@ -23,6 +23,7 @@ Package to test the openlp.core.display.window package.
|
||||
"""
|
||||
import sys
|
||||
import time
|
||||
import pytest
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
@ -37,6 +38,18 @@ from openlp.core.lib.theme import Theme
|
||||
from openlp.core.ui import HideMode
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_geometry():
|
||||
mocked_screen = MagicMock()
|
||||
mocked_screen.display_geometry = QtCore.QRect(10, 20, 400, 600)
|
||||
mocked_screenlist = MagicMock()
|
||||
mocked_screenlist.current = mocked_screen
|
||||
screenlist_patcher = patch('openlp.core.lib.theme.ScreenList', return_value=mocked_screenlist)
|
||||
screenlist_patcher.start()
|
||||
yield
|
||||
screenlist_patcher.stop()
|
||||
|
||||
|
||||
@patch('openlp.core.display.window.QtWidgets.QVBoxLayout')
|
||||
@patch('openlp.core.display.webengine.WebEngineView')
|
||||
def test_x11_override_on(mocked_webengine, mocked_addWidget, mock_settings):
|
||||
@ -209,7 +222,7 @@ def test_run_javascript_sync_no_wait(mock_time, mocked_webengine, mocked_addWidg
|
||||
|
||||
@patch('openlp.core.display.window.QtWidgets.QVBoxLayout')
|
||||
@patch('openlp.core.display.webengine.WebEngineView')
|
||||
def test_set_theme_is_display_video(mocked_webengine, mocked_addWidget, mock_settings):
|
||||
def test_set_theme_is_display_video(mocked_webengine, mocked_addWidget, mock_settings, mock_geometry):
|
||||
"""
|
||||
Test the set_theme function
|
||||
"""
|
||||
@ -233,7 +246,7 @@ def test_set_theme_is_display_video(mocked_webengine, mocked_addWidget, mock_set
|
||||
|
||||
@patch('openlp.core.display.window.QtWidgets.QVBoxLayout')
|
||||
@patch('openlp.core.display.webengine.WebEngineView')
|
||||
def test_set_theme_not_display_video(mocked_webengine, mocked_addWidget, mock_settings):
|
||||
def test_set_theme_not_display_video(mocked_webengine, mocked_addWidget, mock_settings, mock_geometry):
|
||||
"""
|
||||
Test the set_theme function
|
||||
"""
|
||||
@ -264,7 +277,7 @@ def test_set_theme_not_display_video(mocked_webengine, mocked_addWidget, mock_se
|
||||
|
||||
@patch('openlp.core.display.window.QtWidgets.QVBoxLayout')
|
||||
@patch('openlp.core.display.webengine.WebEngineView')
|
||||
def test_set_theme_not_display_live(mocked_webengine, mocked_addWidget, mock_settings):
|
||||
def test_set_theme_not_display_live(mocked_webengine, mocked_addWidget, mock_settings, mock_geometry):
|
||||
"""
|
||||
Test the set_theme function
|
||||
"""
|
||||
|
@ -21,12 +21,26 @@
|
||||
"""
|
||||
Package to test the openlp.core.lib.theme package.
|
||||
"""
|
||||
import pytest
|
||||
from PyQt5 import QtCore
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from openlp.core.lib.theme import BackgroundType, BackgroundGradientType, TransitionType, TransitionSpeed, Theme
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_geometry():
|
||||
mocked_screen = MagicMock()
|
||||
mocked_screen.display_geometry = QtCore.QRect(10, 20, 400, 600)
|
||||
mocked_screenlist = MagicMock()
|
||||
mocked_screenlist.current = mocked_screen
|
||||
screenlist_patcher = patch('openlp.core.lib.theme.ScreenList', return_value=mocked_screenlist)
|
||||
screenlist_patcher.start()
|
||||
yield
|
||||
screenlist_patcher.stop()
|
||||
|
||||
|
||||
def test_background_type_to_string():
|
||||
"""
|
||||
Test the to_string method of :class:`BackgroundType`
|
||||
@ -183,7 +197,7 @@ def test_transition_speed_from_string():
|
||||
assert TransitionSpeed.from_string(transition_speed_slow) == TransitionSpeed.Slow
|
||||
|
||||
|
||||
def test_new_theme():
|
||||
def test_new_theme(mock_geometry):
|
||||
"""
|
||||
Test the Theme constructor
|
||||
"""
|
||||
@ -195,7 +209,7 @@ def test_new_theme():
|
||||
check_theme(default_theme)
|
||||
|
||||
|
||||
def test_expand_json():
|
||||
def test_expand_json(mock_geometry):
|
||||
"""
|
||||
Test the expand_json method
|
||||
"""
|
||||
@ -226,7 +240,7 @@ def test_expand_json():
|
||||
check_theme(theme)
|
||||
|
||||
|
||||
def test_extend_image_filename():
|
||||
def test_extend_image_filename(mock_geometry):
|
||||
"""
|
||||
Test the extend_image_filename method
|
||||
"""
|
||||
@ -246,7 +260,7 @@ def test_extend_image_filename():
|
||||
assert 'MyBeautifulTheme' == theme.theme_name
|
||||
|
||||
|
||||
def test_save_retrieve():
|
||||
def test_save_retrieve(mock_geometry):
|
||||
"""
|
||||
Load a dummy theme, save it and reload it
|
||||
"""
|
||||
@ -260,16 +274,31 @@ def test_save_retrieve():
|
||||
check_theme(lt)
|
||||
|
||||
|
||||
@patch('openlp.core.display.screens.ScreenList.current')
|
||||
def test_set_default_header_footer_overridden(mock_geometry):
|
||||
"""
|
||||
Check that when the theme header and footer locations are overridden, the defaults are not set
|
||||
"""
|
||||
# GIVEN: A theme with the overrides on
|
||||
theme = Theme()
|
||||
theme.set_default_header = MagicMock()
|
||||
theme.set_default_footer = MagicMock()
|
||||
theme.font_main_override = True
|
||||
theme.font_footer_override = True
|
||||
|
||||
# WHEN: set_default_header_footer is called
|
||||
theme.set_default_header_footer()
|
||||
|
||||
# THEN: Neither header or footer default fns should have been called
|
||||
assert theme.set_default_header.call_count == 0
|
||||
assert theme.set_default_footer.call_count == 0
|
||||
|
||||
|
||||
def test_set_default_footer(mock_geometry):
|
||||
"""
|
||||
Test the set_default_footer function sets the footer back to default
|
||||
(reletive to the screen)
|
||||
"""
|
||||
# GIVEN: A screen geometry object and a Theme footer with a strange area
|
||||
mock_geometry.display_geometry = MagicMock()
|
||||
mock_geometry.display_geometry.height.return_value = 600
|
||||
mock_geometry.display_geometry.width.return_value = 400
|
||||
theme = Theme()
|
||||
theme.font_main_x = 20
|
||||
theme.font_footer_x = 207
|
||||
@ -288,16 +317,12 @@ def test_set_default_footer(mock_geometry):
|
||||
assert theme.font_footer_height == 60, 'height should have been reset to (screen_size_height / 10)'
|
||||
|
||||
|
||||
@patch('openlp.core.display.screens.ScreenList.current')
|
||||
def test_set_default_header(mock_geometry):
|
||||
"""
|
||||
Test the set_default_header function sets the header back to default
|
||||
(reletive to the screen)
|
||||
"""
|
||||
# GIVEN: A screen geometry object and a Theme header with a strange area
|
||||
mock_geometry.display_geometry = MagicMock()
|
||||
mock_geometry.display_geometry.height.return_value = 600
|
||||
mock_geometry.display_geometry.width.return_value = 400
|
||||
theme = Theme()
|
||||
theme.font_footer_x = 200
|
||||
theme.font_main_x = 687
|
||||
@ -316,14 +341,12 @@ def test_set_default_header(mock_geometry):
|
||||
assert theme.font_main_height == 540, 'height should have been reset to (screen_size_height * 9 / 10)'
|
||||
|
||||
|
||||
@patch('openlp.core.display.screens.ScreenList.current')
|
||||
def test_set_default_header_footer(mock_geometry):
|
||||
"""
|
||||
Test the set_default_header_footer function sets the header and footer back to default
|
||||
(reletive to the screen)
|
||||
"""
|
||||
# GIVEN: A screen geometry object and a Theme header with a strange area
|
||||
mock_geometry.display_geometry = MagicMock()
|
||||
theme = Theme()
|
||||
theme.font_footer_x = 200
|
||||
theme.font_main_x = 687
|
||||
|
@ -155,7 +155,7 @@ def test_mainwindow_configuration(main_window):
|
||||
'authentication_token', 'settings_form', 'service_manager', 'theme_manager',
|
||||
'projector_manager']
|
||||
expected_functions_list = ['bootstrap_initialise', 'bootstrap_post_set_up', 'bootstrap_completion',
|
||||
'theme_update_global', 'config_screen_changed']
|
||||
'config_screen_changed', 'theme_update_global']
|
||||
assert list(Registry().service_list.keys()) == expected_service_list, \
|
||||
'The service list should have been {}'.format(Registry().service_list.keys())
|
||||
assert list(Registry().functions_list.keys()) == expected_functions_list, \
|
||||
|
@ -21,7 +21,7 @@
|
||||
"""
|
||||
Package to test the openlp.core.ui.slidecontroller package.
|
||||
"""
|
||||
from unittest.mock import MagicMock, patch
|
||||
from unittest.mock import MagicMock, patch, sentinel
|
||||
|
||||
from PyQt5 import QtCore, QtGui
|
||||
|
||||
@ -697,6 +697,79 @@ def test_on_slide_selected_index_service_item_not_command(mocked_execute, regist
|
||||
mocked_slide_selected.assert_called_once_with()
|
||||
|
||||
|
||||
def test_set_background_image(registry):
|
||||
"""
|
||||
Test that the display and preview background are set
|
||||
"""
|
||||
# GIVEN: A slide controller
|
||||
slide_controller = SlideController(None)
|
||||
slide_controller.preview_display = MagicMock()
|
||||
mock_display = MagicMock()
|
||||
slide_controller.displays = [mock_display]
|
||||
|
||||
# WHEN: set_background_image is called
|
||||
slide_controller.set_background_image(sentinel.colour, sentinel.image)
|
||||
|
||||
# THEN: The preview and main display are called with the new colour and image
|
||||
slide_controller.preview_display.set_background_image.assert_called_once_with(sentinel.colour, sentinel.image)
|
||||
mock_display.set_background_image.assert_called_once_with(sentinel.colour, sentinel.image)
|
||||
|
||||
|
||||
def test_theme_updated(mock_settings):
|
||||
"""
|
||||
Test that the theme_updated function updates the service if hot reload is on
|
||||
"""
|
||||
# GIVEN: A slide controller and settings return true
|
||||
slide_controller = SlideController(None)
|
||||
slide_controller.service_item = sentinel.service_item
|
||||
slide_controller._process_item = MagicMock()
|
||||
slide_controller.preview_widget = MagicMock()
|
||||
slide_controller.preview_widget.current_slide_number.return_value = 14
|
||||
mock_settings.value.return_value = True
|
||||
|
||||
# WHEN: theme_updated is called
|
||||
slide_controller.theme_updated()
|
||||
|
||||
# THEN: process_item is called with the current service_item and slide number
|
||||
slide_controller._process_item.assert_called_once_with(sentinel.service_item, 14)
|
||||
|
||||
|
||||
def test_theme_updated_no_reload(mock_settings):
|
||||
"""
|
||||
Test that the theme_updated function does not update the service if hot reload is off
|
||||
"""
|
||||
# GIVEN: A slide controller and settings return false
|
||||
slide_controller = SlideController(None)
|
||||
slide_controller.service_item = sentinel.service_item
|
||||
slide_controller._process_item = MagicMock()
|
||||
slide_controller.preview_widget = MagicMock()
|
||||
mock_settings.value.return_value = False
|
||||
|
||||
# WHEN: theme_updated is called
|
||||
slide_controller.theme_updated()
|
||||
|
||||
# THEN: process_item is not called
|
||||
assert slide_controller._process_item.call_count == 0
|
||||
|
||||
|
||||
def test_reload_theme(mock_settings):
|
||||
"""
|
||||
Test that the reload_theme function triggers the reload_theme function for the displays
|
||||
"""
|
||||
# GIVEN: A slide controller and mocked displays
|
||||
slide_controller = SlideController(None)
|
||||
slide_controller.preview_display = MagicMock()
|
||||
mock_display = MagicMock()
|
||||
slide_controller.displays = [mock_display]
|
||||
|
||||
# WHEN: reload_theme is called
|
||||
slide_controller.reload_theme()
|
||||
|
||||
# THEN: reload_theme is called with the preview and main display
|
||||
slide_controller.preview_display.reload_theme.assert_called_once_with()
|
||||
mock_display.reload_theme.assert_called_once_with()
|
||||
|
||||
|
||||
@patch.object(Registry, 'execute')
|
||||
def test_process_item(mocked_execute, registry):
|
||||
"""
|
||||
|
Loading…
Reference in New Issue
Block a user