From 99d2ec77153a1ff5b51ffd01cc440ef065f495bf Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Fri, 18 Oct 2019 06:12:09 +0000 Subject: [PATCH] Create a Theme Preview dialog, plus some theme background fixes. - Update .gitignore to ignore virtualenvs and eggs directory - Create a dialog to generate theme previews/thumbnails - Use theme preview generation dialog all over - Create a test for the new method - Update existing theme manager tests - Skip Bible HTTP tests when in GitLab CI - Make theme backgrounds scale and centred --- .gitignore | 2 + openlp/core/display/html/display.css | 2 + openlp/core/display/render.py | 18 ++++- openlp/core/display/window.py | 14 ++-- openlp/core/ui/thememanager.py | 48 +++++++----- openlp/core/ui/themeprogressdialog.py | 75 +++++++++++++++++++ openlp/core/ui/themeprogressform.py | 64 ++++++++++++++++ resources/forms/themeprogressdialog.ui | 54 +++++++++++++ .../openlp_core/ui/test_thememanager.py | 35 +++++++-- .../openlp_core/ui/test_thememanager.py | 3 +- .../openlp_plugins/bibles/test_lib_http.py | 2 +- 11 files changed, 281 insertions(+), 36 deletions(-) create mode 100644 openlp/core/ui/themeprogressdialog.py create mode 100644 openlp/core/ui/themeprogressform.py create mode 100644 resources/forms/themeprogressdialog.ui diff --git a/.gitignore b/.gitignore index 39e4703d3..81724a356 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,8 @@ .pytest_cache .venv .vscode +.eggs +.venv OpenLP.egg-info \#*\# __pycache__ diff --git a/openlp/core/display/html/display.css b/openlp/core/display/html/display.css index d1af3a902..be87359be 100644 --- a/openlp/core/display/html/display.css +++ b/openlp/core/display/html/display.css @@ -32,6 +32,8 @@ sup { } #global-background { + background-size: cover; + background-position: 50% 50%; display: block; visibility: visible; z-index: -1; diff --git a/openlp/core/display/render.py b/openlp/core/display/render.py index 4068e8bf1..22222de5b 100644 --- a/openlp/core/display/render.py +++ b/openlp/core/display/render.py @@ -486,6 +486,17 @@ class ThemePreviewRenderer(LogMixin, DisplayWindow): footer_html = 'Dummy footer text' return footer_html + def _wait_and_process(self, delay): + """ + Wait while allowing things to process + + :param delay: The amount of time in seconds to delay, can be a float + """ + end_time = time.time() + delay + app = Registry().get('application') + while time.time() < end_time: + app.process_events() + def generate_preview(self, theme_data, force_page=False, generate_screenshot=True): """ Generate a preview of a theme. @@ -498,7 +509,7 @@ class ThemePreviewRenderer(LogMixin, DisplayWindow): # save value for use in format_slide self.force_page = force_page if not self.force_page: - self.set_theme(theme_data) + self.set_theme(theme_data, is_sync=True) self.theme_height = theme_data.font_main_height slides = self.format_slide(VERSE, None) verses = dict() @@ -506,10 +517,11 @@ class ThemePreviewRenderer(LogMixin, DisplayWindow): verses['text'] = render_tags(slides[0]) verses['verse'] = 'V1' verses['footer'] = self.generate_footer() - self.load_verses([verses]) + self.load_verses([verses], is_sync=True) + self._wait_and_process(1) self.force_page = False if generate_screenshot: - return self.save_screenshot() + return self.grab() self.force_page = False return None diff --git a/openlp/core/display/window.py b/openlp/core/display/window.py index c1e5ee8f7..f5a53b57d 100644 --- a/openlp/core/display/window.py +++ b/openlp/core/display/window.py @@ -104,7 +104,7 @@ class DisplayWindow(QtWidgets.QWidget): """ This is a window to show the output """ - def __init__(self, parent=None, screen=None): + def __init__(self, parent=None, screen=None, can_show_startup_screen=True): """ Create the display window """ @@ -112,6 +112,7 @@ class DisplayWindow(QtWidgets.QWidget): # Need to import this inline to get around a QtWebEngine issue from openlp.core.display.webengine import WebEngineView self._is_initialised = False + self._can_show_startup_screen = can_show_startup_screen self._fbo = None self.setWindowTitle(translate('OpenLP.DisplayWindow', 'Display Window')) self.setWindowFlags(QtCore.Qt.FramelessWindowHint | QtCore.Qt.Tool | QtCore.Qt.WindowStaysOnTopHint) @@ -199,7 +200,8 @@ class DisplayWindow(QtWidgets.QWidget): """ self.run_javascript('Display.init();') self._is_initialised = True - self.set_startup_screen() + if self._can_show_startup_screen: + self.set_startup_screen() # Make sure the scale is set if it was attempted set before init if self.scale != 1: self.set_scale(self.scale) @@ -239,12 +241,12 @@ class DisplayWindow(QtWidgets.QWidget): """ self.run_javascript('Display.goToSlide("{verse}");'.format(verse=verse)) - def load_verses(self, verses): + 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)) + self.run_javascript('Display.setTextSlides({verses});'.format(verses=json_verses), is_sync=is_sync) def load_images(self, images): """ @@ -324,7 +326,7 @@ class DisplayWindow(QtWidgets.QWidget): else: return pixmap - def set_theme(self, theme): + def set_theme(self, theme, is_sync=False): """ Set the theme of the display """ @@ -336,7 +338,7 @@ class DisplayWindow(QtWidgets.QWidget): exported_theme = theme_copy.export_theme(is_js=True) else: exported_theme = theme.export_theme(is_js=True) - self.run_javascript('Display.setTheme({theme});'.format(theme=exported_theme)) + self.run_javascript('Display.setTheme({theme});'.format(theme=exported_theme), is_sync=is_sync) def get_video_types(self): """ diff --git a/openlp/core/ui/thememanager.py b/openlp/core/ui/thememanager.py index d3810587f..ca31466e2 100644 --- a/openlp/core/ui/thememanager.py +++ b/openlp/core/ui/thememanager.py @@ -44,6 +44,7 @@ from openlp.core.lib.ui import create_widget_action, critical_error_message_box from openlp.core.ui.filerenameform import FileRenameForm from openlp.core.ui.icons import UiIcons from openlp.core.ui.themeform import ThemeForm +from openlp.core.ui.themeprogressform import ThemeProgressForm from openlp.core.widgets.dialogs import FileDialog from openlp.core.widgets.toolbar import OpenLPToolbar @@ -148,6 +149,7 @@ class ThemeManager(QtWidgets.QWidget, RegistryBase, Ui_ThemeManager, LogMixin, R process the bootstrap initialise setup request """ self.setup_ui(self) + self.progress_form = ThemeProgressForm(self) self.global_theme = Settings().value(self.settings_section + '/global theme') self.build_theme_path() self.load_first_time_themes() @@ -364,7 +366,7 @@ class ThemeManager(QtWidgets.QWidget, RegistryBase, Ui_ThemeManager, LogMixin, R row = self.theme_list_widget.row(item) self.theme_list_widget.takeItem(row) self.delete_theme(theme) - self.renderer.set_theme(item.data(QtCore.Qt.UserRole)) + # self.renderer.set_theme(self.get_theme_data(item.data(QtCore.Qt.UserRole))) # As we do not reload the themes, push out the change. Reload the # list as the internal lists and events need to be triggered. self._push_themes() @@ -455,9 +457,11 @@ class ThemeManager(QtWidgets.QWidget, RegistryBase, Ui_ThemeManager, LogMixin, R if not file_paths: return self.application.set_busy_cursor() + new_themes = [] for file_path in file_paths: - self.unzip_theme(file_path, self.theme_path) + new_themes.append(self.unzip_theme(file_path, self.theme_path)) Settings().setValue(self.settings_section + '/last directory import', file_path.parent) + self.update_preview_images(new_themes) self.load_themes() self.application.set_normal_cursor() @@ -467,9 +471,10 @@ class ThemeManager(QtWidgets.QWidget, RegistryBase, Ui_ThemeManager, LogMixin, R """ self.application.set_busy_cursor() theme_paths = AppLocation.get_files(self.settings_section, '.otz') + new_themes = [] for theme_path in theme_paths: theme_path = self.theme_path / theme_path - self.unzip_theme(theme_path, self.theme_path) + new_themes.append(self.unzip_theme(theme_path, self.theme_path)) delete_file(theme_path) theme_paths = AppLocation.get_files(self.settings_section, '.png') # No themes have been found so create one @@ -478,6 +483,8 @@ class ThemeManager(QtWidgets.QWidget, RegistryBase, Ui_ThemeManager, LogMixin, R theme.theme_name = UiStrings().Default self.save_theme(theme) Settings().setValue(self.settings_section + '/global theme', theme.theme_name) + new_themes = [theme.theme_name] + self.update_preview_images(new_themes) self.application.set_normal_cursor() def load_themes(self): @@ -619,10 +626,12 @@ class ThemeManager(QtWidgets.QWidget, RegistryBase, Ui_ThemeManager, LogMixin, R # As all files are closed, we can create the Theme. if file_xml: if json_theme: - theme = self._create_theme_from_json(file_xml, self.theme_path) + self._create_theme_from_json(file_xml, self.theme_path) else: - theme = self._create_theme_from_xml(file_xml, self.theme_path) - self.generate_and_save_image(theme_name, theme) + self._create_theme_from_xml(file_xml, self.theme_path) + return theme_name + else: + return None def check_if_theme_exists(self, theme_name): """ @@ -674,32 +683,31 @@ class ThemeManager(QtWidgets.QWidget, RegistryBase, Ui_ThemeManager, LogMixin, R thumb_path = self.thumb_path / '{name}.png'.format(name=name) create_thumb(sample_path_name, thumb_path, False) else: - self.generate_and_save_image(name, theme) + self.update_preview_images([name]) - def generate_and_save_image(self, theme_name, theme): + def save_preview(self, theme_name, preview_pixmap): """ - Generate and save a preview image - - :param str theme_name: The name of the theme. - :param theme: The theme data object. + Save the preview QPixmap object to a file """ - frame = self.generate_image(theme) sample_path_name = self.theme_path / '{file_name}.png'.format(file_name=theme_name) if sample_path_name.exists(): sample_path_name.unlink() - frame.save(str(sample_path_name), 'png') + preview_pixmap.save(str(sample_path_name), 'png') thumb_path = self.thumb_path / '{name}.png'.format(name=theme_name) create_thumb(sample_path_name, thumb_path, False) - def update_preview_images(self): + def update_preview_images(self, theme_list=None): """ Called to update the themes' preview images. """ - self.main_window.display_progress_bar(len(self.theme_list)) - for theme in self.theme_list: - self.main_window.increment_progress_bar() - self.generate_and_save_image(theme, self.get_theme_data(theme)) - self.main_window.finished_progress_bar() + theme_list = theme_list or self.theme_list + self.progress_form.theme_list = theme_list + self.progress_form.show() + for theme_name in theme_list: + theme_data = self.get_theme_data(theme_name) + preview_pixmap = self.progress_form.get_preview(theme_name, theme_data) + self.save_preview(theme_name, preview_pixmap) + self.progress_form.close() self.load_themes() def generate_image(self, theme_data, force_page=False): diff --git a/openlp/core/ui/themeprogressdialog.py b/openlp/core/ui/themeprogressdialog.py new file mode 100644 index 000000000..87db0cff5 --- /dev/null +++ b/openlp/core/ui/themeprogressdialog.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- + +########################################################################## +# OpenLP - Open Source Lyrics Projection # +# ---------------------------------------------------------------------- # +# Copyright (c) 2008-2019 OpenLP Developers # +# ---------------------------------------------------------------------- # +# This program is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see . # +########################################################################## +from PyQt5 import QtCore, QtWidgets + +from openlp.core.common.i18n import translate +from openlp.core.display.render import ThemePreviewRenderer +from openlp.core.ui.icons import UiIcons +from openlp.core.widgets.layouts import AspectRatioLayout + + +class UiThemeProgressDialog(object): + """ + The GUI widgets for the ThemeProgressDialog + """ + + def setup_ui(self, theme_progress_dialog): + """ + Set up the UI for the dialog. + + :param theme_progress_dialog: The QDialog object to set up. + """ + theme_progress_dialog.setObjectName('theme_progress_dialog') + theme_progress_dialog.setWindowIcon(UiIcons().main_icon) + theme_progress_dialog.resize(400, 306) + self.theme_progress_layout = QtWidgets.QVBoxLayout(theme_progress_dialog) + self.theme_progress_layout.setObjectName('theme_progress_layout') + self.preview_area = QtWidgets.QWidget(theme_progress_dialog) + self.preview_area.setObjectName('PreviewArea') + self.theme_preview_layout = AspectRatioLayout(self.preview_area, 0.75) # Dummy ratio, will be update + 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.setObjectName('theme_display') + self.theme_preview_layout.addWidget(self.theme_display) + self.theme_progress_layout.addWidget(self.preview_area) + self.label = QtWidgets.QLabel(theme_progress_dialog) + self.label.setAlignment(QtCore.Qt.AlignCenter) + self.label.setObjectName('label') + self.theme_progress_layout.addWidget(self.label) + self.progress_bar = QtWidgets.QProgressBar(theme_progress_dialog) + self.progress_bar.setProperty('value', 24) + self.progress_bar.setObjectName('progress_bar') + self.theme_progress_layout.addWidget(self.progress_bar) + self.theme_display.show() + + self.retranslate_ui(theme_progress_dialog) + QtCore.QMetaObject.connectSlotsByName(theme_progress_dialog) + + def retranslate_ui(self, theme_progress_dialog): + """ + Dynamically translate the UI. + + :param about_dialog: The QDialog object to translate + """ + theme_progress_dialog.setWindowTitle(translate('OpenLP.Themes', 'Recreating Theme Thumbnails')) + self.label.setText(translate('OpenLP.Themes', 'TextLabel')) diff --git a/openlp/core/ui/themeprogressform.py b/openlp/core/ui/themeprogressform.py new file mode 100644 index 000000000..4d61357a3 --- /dev/null +++ b/openlp/core/ui/themeprogressform.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- + +########################################################################## +# OpenLP - Open Source Lyrics Projection # +# ---------------------------------------------------------------------- # +# Copyright (c) 2008-2019 OpenLP Developers # +# ---------------------------------------------------------------------- # +# This program is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see . # +########################################################################## +""" +The theme regeneration progress dialog +""" +from PyQt5 import QtWidgets + +from openlp.core.display.screens import ScreenList +from openlp.core.ui.themeprogressdialog import UiThemeProgressDialog +from openlp.core.common.mixins import RegistryProperties + + +class ThemeProgressForm(QtWidgets.QDialog, UiThemeProgressDialog, RegistryProperties): + """ + The theme regeneration progress dialog + """ + def __init__(self, parent=None): + super().__init__(parent) + self.setup_ui(self) + self._theme_list = [] + self.progress_bar.setMinimum(0) + self.progress_bar.setMaximum(0) + self.progress_bar.setValue(0) + try: + screens = ScreenList() + self.ratio = screens.current.display_geometry.width() / screens.current.display_geometry.height() + except ZeroDivisionError: + self.ratio = 16 / 9 + self.theme_preview_layout.aspect_ratio = self.ratio + + def get_preview(self, theme_name, theme_data): + self.label.setText(theme_name) + self.progress_bar.setValue(self.progress_bar.value() + 1) + self.theme_display.set_scale(float(self.theme_display.width()) / self.renderer.width()) + return self.theme_display.generate_preview(theme_data, generate_screenshot=True) + + def _get_theme_list(self): + """Property getter""" + return self._theme_list + + def _set_theme_list(self, value): + """Property setter""" + self._theme_list = value + self.progress_bar.setMaximum(len(self._theme_list)) + + theme_list = property(_get_theme_list, _set_theme_list) diff --git a/resources/forms/themeprogressdialog.ui b/resources/forms/themeprogressdialog.ui new file mode 100644 index 000000000..458e993f2 --- /dev/null +++ b/resources/forms/themeprogressdialog.ui @@ -0,0 +1,54 @@ + + + ThemeProgressDialog + + + + 0 + 0 + 400 + 306 + + + + Recreating Theme Thumbnails + + + + + + + 0 + 0 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + TextLabel + + + Qt::AlignCenter + + + + + + + 24 + + + + + + + + diff --git a/tests/functional/openlp_core/ui/test_thememanager.py b/tests/functional/openlp_core/ui/test_thememanager.py index 0c8a96aa2..7f89d8099 100644 --- a/tests/functional/openlp_core/ui/test_thememanager.py +++ b/tests/functional/openlp_core/ui/test_thememanager.py @@ -26,7 +26,7 @@ import shutil from pathlib import Path from tempfile import mkdtemp from unittest import TestCase -from unittest.mock import ANY, MagicMock, patch +from unittest.mock import ANY, MagicMock, patch, call from PyQt5 import QtWidgets @@ -90,7 +90,7 @@ class TestThemeManager(TestCase): # theme, create_paths and thememanager-attributes. theme_manager = ThemeManager(None) theme_manager.old_background_image = None - theme_manager.generate_and_save_image = MagicMock() + theme_manager.update_preview_images = MagicMock() theme_manager.theme_path = MagicMock() mocked_theme = MagicMock() mocked_theme.theme_name = 'themename' @@ -114,7 +114,7 @@ class TestThemeManager(TestCase): # theme, create_paths and thememanager-attributes. theme_manager = ThemeManager(None) theme_manager.old_background_image = None - theme_manager.generate_and_save_image = MagicMock() + theme_manager.update_preview_images = MagicMock() theme_manager.theme_path = MagicMock() mocked_theme = MagicMock() mocked_theme.theme_name = 'themename' @@ -135,7 +135,7 @@ class TestThemeManager(TestCase): # GIVEN: A new theme manager instance, with mocked theme and thememanager-attributes. theme_manager = ThemeManager(None) theme_manager.old_background_image = None - theme_manager.generate_and_save_image = MagicMock() + theme_manager.update_preview_images = MagicMock() theme_manager.theme_path = Path(self.temp_folder) mocked_theme = MagicMock() mocked_theme.theme_name = 'theme 愛 name' @@ -195,7 +195,7 @@ class TestThemeManager(TestCase): as mocked_critical_error_message_box: theme_manager = ThemeManager(None) theme_manager._create_theme_from_xml = MagicMock() - theme_manager.generate_and_save_image = MagicMock() + theme_manager.update_preview_images = MagicMock() theme_manager.theme_path = None folder_path = Path(mkdtemp()) theme_file_path = RESOURCE_PATH / 'themes' / 'Moss_on_tree.otz' @@ -227,3 +227,28 @@ class TestThemeManager(TestCase): # THEN: The critical_error_message_box should have been called assert mocked_critical_error_message_box.call_count == 1, 'Should have been called once' + + def test_update_preview_images(self): + """ + Test that the update_preview_images() method works correctly + """ + # GIVEN: A ThemeManager + theme_manager = ThemeManager(None) + theme_manager.save_preview = MagicMock() + theme_manager.get_theme_data = MagicMock(return_value='theme_data') + theme_manager.progress_form = MagicMock(**{'get_preview.return_value': 'preview'}) + theme_manager.load_themes = MagicMock() + theme_list = ['Default', 'Test'] + + # WHEN: ThemeManager.update_preview_images() is called + theme_manager.update_preview_images(theme_list) + + # THEN: Things should work right + assert theme_manager.progress_form.theme_list == theme_list + theme_manager.progress_form.show.assert_called_once_with() + assert theme_manager.get_theme_data.call_args_list == [call('Default'), call('Test')] + assert theme_manager.progress_form.get_preview.call_args_list == [call('Default', 'theme_data'), + call('Test', 'theme_data')] + assert theme_manager.save_preview.call_args_list == [call('Default', 'preview'), call('Test', 'preview')] + theme_manager.progress_form.close.assert_called_once_with() + theme_manager.load_themes.assert_called_once_with() diff --git a/tests/interfaces/openlp_core/ui/test_thememanager.py b/tests/interfaces/openlp_core/ui/test_thememanager.py index 0c7883d48..a5b5a88e5 100644 --- a/tests/interfaces/openlp_core/ui/test_thememanager.py +++ b/tests/interfaces/openlp_core/ui/test_thememanager.py @@ -63,7 +63,8 @@ class TestThemeManager(TestCase, TestMixin): Settings().setValue('themes/global theme', 'my_theme') # WHEN: the initialisation is run - self.theme_manager.bootstrap_initialise() + with patch('openlp.core.ui.thememanager.ThemeProgressForm'): + self.theme_manager.bootstrap_initialise() # THEN: self.theme_manager.setup_ui.assert_called_once_with(self.theme_manager) diff --git a/tests/interfaces/openlp_plugins/bibles/test_lib_http.py b/tests/interfaces/openlp_plugins/bibles/test_lib_http.py index bd81fdda8..0fa4a1f12 100644 --- a/tests/interfaces/openlp_plugins/bibles/test_lib_http.py +++ b/tests/interfaces/openlp_plugins/bibles/test_lib_http.py @@ -29,7 +29,7 @@ from openlp.core.common.registry import Registry from openlp.plugins.bibles.lib.importers.http import BGExtract, BSExtract, CWExtract -@skipIf(os.environ.get('JENKINS_URL'), 'Skip Bible HTTP tests to prevent Jenkins from being blacklisted') +@skipIf(os.environ.get('GITLAB_CI'), 'Skip Bible HTTP tests to prevent GitLab CI from being blacklisted') class TestBibleHTTP(TestCase): def setUp(self):