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
This commit is contained in:
Raoul Snyman 2019-10-18 06:12:09 +00:00 committed by Tim Bentley
parent 362a775a55
commit 99d2ec7715
11 changed files with 281 additions and 36 deletions

2
.gitignore vendored
View File

@ -23,6 +23,8 @@
.pytest_cache
.venv
.vscode
.eggs
.venv
OpenLP.egg-info
\#*\#
__pycache__

View File

@ -32,6 +32,8 @@ sup {
}
#global-background {
background-size: cover;
background-position: 50% 50%;
display: block;
visibility: visible;
z-index: -1;

View File

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

View File

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

View File

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

View File

@ -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 <https://www.gnu.org/licenses/>. #
##########################################################################
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'))

View File

@ -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 <https://www.gnu.org/licenses/>. #
##########################################################################
"""
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)

View File

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ThemeProgressDialog</class>
<widget class="QDialog" name="ThemeProgressDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>306</height>
</rect>
</property>
<property name="windowTitle">
<string>Recreating Theme Thumbnails</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QFrame" name="frame">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>TextLabel</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QProgressBar" name="progressBar">
<property name="value">
<number>24</number>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

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

View File

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

View File

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