openlp-core team mailing list archive
-
openlp-core team
-
Mailing list archive
-
Message #34149
[Merge] lp:~tomasgroth/openlp/fix-theme-thumb into lp:openlp
Tomas Groth has proposed merging lp:~tomasgroth/openlp/fix-theme-thumb into lp:openlp.
Commit message:
Add a webengine view for previewing themes.
Made VLC loading more robust.
A few minor fixes.
Requested reviews:
Raoul Snyman (raoul-snyman)
Tim Bentley (trb143)
For more details, see:
https://code.launchpad.net/~tomasgroth/openlp/fix-theme-thumb/+merge/368572
--
Your team OpenLP Core is subscribed to branch lp:openlp.
=== modified file 'openlp/core/display/render.py'
--- openlp/core/display/render.py 2019-04-13 13:00:22 +0000
+++ openlp/core/display/render.py 2019-06-07 21:06:40 +0000
@@ -24,6 +24,7 @@
"""
import html
import logging
+import mako
import math
import os
import re
@@ -32,8 +33,10 @@
from PyQt5 import QtWidgets, QtGui
from openlp.core.common import ThemeLevel
+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.settings import Settings
from openlp.core.display.screens import ScreenList
from openlp.core.display.window import DisplayWindow
from openlp.core.lib import ItemCapabilities
@@ -58,8 +61,10 @@
'{r}C{/r}{b}h{/b}{bl}i{/bl}{y}l{/y}{g}d{/g}{pk}' \
'r{/pk}{o}e{/o}{pp}n{/pp} of the Lord\n'
VERSE_FOR_LINE_COUNT = '\n'.join(map(str, range(100)))
-TITLE = 'Arky Arky (Unknown)'
-FOOTER = ['Public Domain', 'CCLI 123456']
+TITLE = 'Arky Arky'
+AUTHOR = 'John Doe'
+FOOTER_COPYRIGHT = 'Public Domain'
+CCLI_NO = '123456'
def remove_tags(text, can_remove_chords=False):
@@ -425,7 +430,7 @@
return raw_text + ''.join(end_tags), ''.join(start_tags), ''.join(html_tags)
-class Renderer(RegistryBase, LogMixin, RegistryProperties, DisplayWindow):
+class ThemePreviewRenderer(LogMixin, DisplayWindow):
"""
A virtual display used for rendering thumbnails and other offscreen tasks
"""
@@ -435,24 +440,6 @@
"""
super().__init__(*args, **kwargs)
self.force_page = False
- for screen in ScreenList():
- if screen.is_display:
- self.setGeometry(screen.display_geometry.x(), screen.display_geometry.y(),
- screen.display_geometry.width(), screen.display_geometry.height())
- break
- # If the display is not show'ed and hidden like this webegine will not render
- self.show()
- self.hide()
- self.theme_height = 0
- self.theme_level = ThemeLevel.Global
-
- def set_theme_level(self, theme_level):
- """
- Sets the theme level.
-
- :param theme_level: The theme level to be used.
- """
- self.theme_level = theme_level
def calculate_line_count(self):
"""
@@ -466,7 +453,30 @@
"""
return self.run_javascript('Display.clearSlides();')
- def generate_preview(self, theme_data, force_page=False):
+ def generate_footer(self):
+ """
+ """
+ footer_template = Settings().value('songs/footer template')
+ # Keep this in sync with the list in songstab.py
+ vars = {
+ 'title': TITLE,
+ 'authors_none_label': translate('OpenLP.Ui', 'Written by'),
+ 'authors_words_label': translate('SongsPlugin.AuthorType', 'Words',
+ 'Author who wrote the lyrics of a song'),
+ 'authors_words': [AUTHOR],
+ 'copyright': FOOTER_COPYRIGHT,
+ 'ccli_license': Settings().value('core/ccli number'),
+ 'ccli_license_label': translate('SongsPlugin.MediaItem', 'CCLI License'),
+ 'ccli_number': CCLI_NO,
+ }
+ try:
+ footer_html = mako.template.Template(footer_template).render_unicode(**vars).replace('\n', '')
+ except mako.exceptions.SyntaxException:
+ log.error('Failed to render Song footer html:\n' + mako.exceptions.text_error_template().render())
+ footer_html = 'Dummy footer text'
+ return footer_html
+
+ def generate_preview(self, theme_data, force_page=False, generate_screenshot=True):
"""
Generate a preview of a theme.
@@ -479,14 +489,16 @@
if not self.force_page:
self.set_theme(theme_data)
self.theme_height = theme_data.font_main_height
- slides = self.format_slide(render_tags(VERSE), None)
+ slides = self.format_slide(VERSE, None)
verses = dict()
verses['title'] = TITLE
- verses['text'] = slides[0]
+ verses['text'] = render_tags(slides[0])
verses['verse'] = 'V1'
+ verses['footer'] = self.generate_footer()
self.load_verses([verses])
self.force_page = False
- return self.save_screenshot()
+ if generate_screenshot:
+ return self.save_screenshot()
self.force_page = False
return None
@@ -515,7 +527,7 @@
if item and item.is_capable(ItemCapabilities.CanWordSplit):
pages = self._paginate_slide_words(text.split('\n'), line_end)
# Songs and Custom
- elif item is None or item.is_capable(ItemCapabilities.CanSoftBreak):
+ elif item is None or (item and item.is_capable(ItemCapabilities.CanSoftBreak)):
pages = []
if '[---]' in text:
# Remove Overflow split if at start of the text
@@ -722,7 +734,8 @@
:param text: The text to check. It may contain HTML tags.
"""
self.clear_slides()
- self.run_javascript('Display.addTextSlide("v1", "{text}", "Dummy Footer");'.format(text=text), is_sync=True)
+ self.run_javascript('Display.addTextSlide("v1", "{text}", "Dummy Footer");'
+ .format(text=text.replace('"', '\\"')), is_sync=True)
does_text_fits = self.run_javascript('Display.doesContentFit();', is_sync=True)
return does_text_fits
@@ -745,3 +758,33 @@
pixmap.save(fname, ext)
else:
return pixmap
+
+
+class Renderer(RegistryBase, RegistryProperties, ThemePreviewRenderer):
+ """
+ A virtual display used for rendering thumbnails and other offscreen tasks
+ """
+ def __init__(self, *args, **kwargs):
+ """
+ Constructor
+ """
+ super().__init__(*args, **kwargs)
+ self.force_page = False
+ for screen in ScreenList():
+ if screen.is_display:
+ self.setGeometry(screen.display_geometry.x(), screen.display_geometry.y(),
+ screen.display_geometry.width(), screen.display_geometry.height())
+ break
+ # If the display is not show'ed and hidden like this webegine will not render
+ self.show()
+ self.hide()
+ self.theme_height = 0
+ self.theme_level = ThemeLevel.Global
+
+ def set_theme_level(self, theme_level):
+ """
+ Sets the theme level.
+
+ :param theme_level: The theme level to be used.
+ """
+ self.theme_level = theme_level
=== modified file 'openlp/core/ui/media/vlcplayer.py'
--- openlp/core/ui/media/vlcplayer.py 2019-05-31 20:19:15 +0000
+++ openlp/core/ui/media/vlcplayer.py 2019-06-07 21:06:40 +0000
@@ -28,7 +28,6 @@
import sys
import threading
from datetime import datetime
-import vlc
from PyQt5 import QtWidgets
@@ -62,25 +61,27 @@
:return: The "vlc" module, or None
"""
- if 'vlc' in sys.modules:
- # If VLC has already been imported, no need to do all the stuff below again
- is_vlc_available = False
+ # Import the VLC module if not already done
+ if 'vlc' not in sys.modules:
try:
- is_vlc_available = bool(sys.modules['vlc'].get_default_instance())
- except Exception:
- pass
- if is_vlc_available:
- return sys.modules['vlc']
- else:
+ import vlc
+ except ImportError:
return None
- else:
- return vlc
+ # Verify that VLC is also loadable
+ is_vlc_available = False
+ try:
+ is_vlc_available = bool(sys.modules['vlc'].get_default_instance())
+ except Exception:
+ pass
+ if is_vlc_available:
+ return sys.modules['vlc']
+ return None
# On linux we need to initialise X threads, but not when running tests.
# This needs to happen on module load and not in get_vlc(), otherwise it can cause crashes on some DE on some setups
# (reported on Gnome3, Unity, Cinnamon, all GTK+ based) when using native filedialogs...
-if is_linux() and 'nose' not in sys.argv[0] and get_vlc():
+if is_linux() and 'pytest' not in sys.argv[0] and get_vlc():
try:
try:
x11 = ctypes.cdll.LoadLibrary('libX11.so.6')
=== modified file 'openlp/core/ui/themeform.py'
--- openlp/core/ui/themeform.py 2019-05-26 20:53:54 +0000
+++ openlp/core/ui/themeform.py 2019-06-07 21:06:40 +0000
@@ -172,16 +172,14 @@
if not event:
event = QtGui.QResizeEvent(self.size(), self.size())
QtWidgets.QWizard.resizeEvent(self, event)
- if hasattr(self, 'preview_page') and self.currentPage() == self.preview_page:
- frame_width = self.preview_box_label.lineWidth()
- pixmap_width = self.preview_area.width() - 2 * frame_width
- pixmap_height = self.preview_area.height() - 2 * frame_width
- aspect_ratio = float(pixmap_width) / pixmap_height
- if aspect_ratio < self.display_aspect_ratio:
- pixmap_height = int(pixmap_width / self.display_aspect_ratio + 0.5)
- else:
- pixmap_width = int(pixmap_height * self.display_aspect_ratio + 0.5)
- self.preview_box_label.setFixedSize(pixmap_width + 2 * frame_width, pixmap_height + 2 * frame_width)
+ try:
+ self.display_aspect_ratio = self.renderer.width() / self.renderer.height()
+ except ZeroDivisionError:
+ self.display_aspect_ratio = 1
+ # Make sure we don't resize before the widgets are actually created
+ if hasattr(self, 'preview_area_layout'):
+ self.preview_area_layout.set_aspect_ratio(self.display_aspect_ratio)
+ self.preview_box.set_scale(float(self.preview_box.width()) / self.renderer.width())
def validateCurrentPage(self):
"""
@@ -206,11 +204,17 @@
self.setOption(QtWidgets.QWizard.HaveCustomButton1, enabled)
if self.page(page_id) == self.preview_page:
self.update_theme()
- frame = self.theme_manager.generate_image(self.theme)
- frame.setDevicePixelRatio(self.devicePixelRatio())
- self.preview_box_label.setPixmap(frame)
- self.display_aspect_ratio = float(frame.width()) / frame.height()
+ self.preview_box.set_theme(self.theme)
+ self.preview_box.clear_slides()
+ self.preview_box.set_scale(float(self.preview_box.width()) / self.renderer.width())
+ try:
+ self.display_aspect_ratio = self.renderer.width() / self.renderer.height()
+ except ZeroDivisionError:
+ self.display_aspect_ratio = 1
+ self.preview_area_layout.set_aspect_ratio(self.display_aspect_ratio)
self.resizeEvent()
+ self.preview_box.show()
+ self.preview_box.generate_preview(self.theme, False, False)
def on_custom_1_button_clicked(self, number):
"""
@@ -398,6 +402,7 @@
Handle the display and state of the Preview page.
"""
self.setField('name', self.theme.theme_name)
+ self.preview_box.set_theme(self.theme)
def on_background_combo_box_current_index_changed(self, index):
"""
@@ -558,5 +563,5 @@
source_path = self.theme.background_filename
if not self.edit_mode and not self.theme_manager.check_if_theme_exists(self.theme.theme_name):
return
- self.theme_manager.save_theme(self.theme, source_path, destination_path)
+ self.theme_manager.save_theme(self.theme, source_path, destination_path, self.preview_box.save_screenshot())
return QtWidgets.QDialog.accept(self)
=== modified file 'openlp/core/ui/thememanager.py'
--- openlp/core/ui/thememanager.py 2019-05-22 06:47:00 +0000
+++ openlp/core/ui/thememanager.py 2019-06-07 21:06:40 +0000
@@ -476,7 +476,7 @@
if not theme_paths:
theme = Theme()
theme.theme_name = UiStrings().Default
- self._write_theme(theme)
+ self.save_theme(theme)
Settings().setValue(self.settings_section + '/global theme', theme.theme_name)
self.application.set_normal_cursor()
@@ -639,24 +639,14 @@
return False
return True
- def save_theme(self, theme, image_source_path, image_destination_path):
- """
- Called by theme maintenance Dialog to save the theme and to trigger the reload of the theme list
-
- :param Theme theme: The theme data object.
- :param Path image_source_path: Where the theme image is currently located.
- :param Path image_destination_path: Where the Theme Image is to be saved to
- :rtype: None
- """
- self._write_theme(theme, image_source_path, image_destination_path)
-
- def _write_theme(self, theme, image_source_path=None, image_destination_path=None):
+ def save_theme(self, theme, image_source_path=None, image_destination_path=None, image=None):
"""
Writes the theme to the disk and handles the background image if necessary
:param Theme theme: The theme data object.
:param Path image_source_path: Where the theme image is currently located.
:param Path image_destination_path: Where the Theme Image is to be saved to
+ :param image: The example image of the theme. Optionally.
:rtype: None
"""
name = theme.theme_name
@@ -676,7 +666,15 @@
shutil.copyfile(image_source_path, image_destination_path)
except OSError:
self.log_exception('Failed to save theme image')
- self.generate_and_save_image(name, theme)
+ if image:
+ sample_path_name = self.theme_path / '{file_name}.png'.format(file_name=name)
+ if sample_path_name.exists():
+ sample_path_name.unlink()
+ image.save(str(sample_path_name), 'png')
+ 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)
def generate_and_save_image(self, theme_name, theme):
"""
=== modified file 'openlp/core/ui/themewizard.py'
--- openlp/core/ui/themewizard.py 2019-04-13 13:00:22 +0000
+++ openlp/core/ui/themewizard.py 2019-06-07 21:06:40 +0000
@@ -31,6 +31,8 @@
from openlp.core.ui.icons import UiIcons
from openlp.core.widgets.buttons import ColorButton
from openlp.core.widgets.edits import PathEdit
+from openlp.core.widgets.layouts import AspectRatioLayout
+from openlp.core.display.render import ThemePreviewRenderer
class Ui_ThemeWizard(object):
@@ -363,16 +365,13 @@
self.preview_layout.addLayout(self.theme_name_layout)
self.preview_area = QtWidgets.QWidget(self.preview_page)
self.preview_area.setObjectName('PreviewArea')
- self.preview_area_layout = QtWidgets.QGridLayout(self.preview_area)
- self.preview_area_layout.setContentsMargins(0, 0, 0, 0)
- self.preview_area_layout.setColumnStretch(0, 1)
- self.preview_area_layout.setRowStretch(0, 1)
- self.preview_area_layout.setObjectName('preview_area_layout')
- self.preview_box_label = QtWidgets.QLabel(self.preview_area)
- self.preview_box_label.setFrameShape(QtWidgets.QFrame.Box)
- self.preview_box_label.setScaledContents(True)
- self.preview_box_label.setObjectName('preview_box_label')
- self.preview_area_layout.addWidget(self.preview_box_label)
+ self.preview_area_layout = AspectRatioLayout(self.preview_area, 0.75) # Dummy ratio, will be update
+ self.preview_area_layout.margin = 8
+ self.preview_area_layout.setSpacing(0)
+ self.preview_area_layout.setObjectName('preview_web_layout')
+ self.preview_box = ThemePreviewRenderer(self)
+ self.preview_box.setObjectName('preview_box')
+ self.preview_area_layout.addWidget(self.preview_box)
self.preview_layout.addWidget(self.preview_area)
theme_wizard.addPage(self.preview_page)
self.retranslate_ui(theme_wizard)
=== modified file 'openlp/plugins/media/lib/mediaitem.py'
--- openlp/plugins/media/lib/mediaitem.py 2019-06-01 06:59:45 +0000
+++ openlp/plugins/media/lib/mediaitem.py 2019-06-07 21:06:40 +0000
@@ -173,7 +173,7 @@
item = self.list_view.currentItem()
if item is None:
return False
- filename = item.data(QtCore.Qt.UserRole)
+ filename = str(item.data(QtCore.Qt.UserRole))
# Special handling if the filename is a optical clip
if filename.startswith('optical:'):
(name, title, audio_track, subtitle_track, start, end, clip_name) = parse_optical_path(filename)
@@ -259,11 +259,12 @@
# TODO needs to be fixed as no idea why this fails
# media.sort(key=lambda file_path: get_natural_key(file_path.name))
for track in media:
- track_info = QtCore.QFileInfo(track)
+ track_str = str(track)
+ track_info = QtCore.QFileInfo(track_str)
item_name = None
- if track.startswith('optical:'):
+ if track_str.startswith('optical:'):
# Handle optical based item
- (file_name, title, audio_track, subtitle_track, start, end, clip_name) = parse_optical_path(track)
+ (file_name, title, audio_track, subtitle_track, start, end, clip_name) = parse_optical_path(track_str)
item_name = QtWidgets.QListWidgetItem(clip_name)
item_name.setIcon(UiIcons().optical)
item_name.setData(QtCore.Qt.UserRole, track)
@@ -272,22 +273,22 @@
end=format_milliseconds(end)))
elif not os.path.exists(track):
# File doesn't exist, mark as error.
- file_name = os.path.split(str(track))[1]
+ file_name = os.path.split(track_str)[1]
item_name = QtWidgets.QListWidgetItem(file_name)
item_name.setIcon(UiIcons().error)
item_name.setData(QtCore.Qt.UserRole, track)
- item_name.setToolTip(track)
+ item_name.setToolTip(track_str)
elif track_info.isFile():
# Normal media file handling.
- file_name = os.path.split(str(track))[1]
+ file_name = os.path.split(track_str)[1]
item_name = QtWidgets.QListWidgetItem(file_name)
search = file_name.split('.')[-1].lower()
- if '*.{text}'.format(text=search) in self.media_controller.audio_extensions_list:
+ if search in AUDIO_EXT:
item_name.setIcon(UiIcons().audio)
else:
item_name.setIcon(UiIcons().video)
item_name.setData(QtCore.Qt.UserRole, track)
- item_name.setToolTip(track)
+ item_name.setToolTip(track_str)
if item_name:
self.list_view.addItem(item_name)
=== modified file 'openlp/plugins/songs/lib/songstab.py'
--- openlp/plugins/songs/lib/songstab.py 2019-04-13 13:00:22 +0000
+++ openlp/plugins/songs/lib/songstab.py 2019-06-07 21:06:40 +0000
@@ -88,7 +88,7 @@
self.footer_group_box = QtWidgets.QGroupBox(self.left_column)
self.footer_group_box.setObjectName('footer_group_box')
self.footer_layout = QtWidgets.QVBoxLayout(self.footer_group_box)
- self.footer_layout.setObjectName('chords_layout')
+ self.footer_layout.setObjectName('footer_layout')
self.footer_info_label = QtWidgets.QLabel(self.footer_group_box)
self.footer_layout.addWidget(self.footer_info_label)
self.footer_placeholder_info = QtWidgets.QTextEdit(self.footer_group_box)
=== modified file 'tests/functional/openlp_core/ui/test_thememanager.py'
--- tests/functional/openlp_core/ui/test_thememanager.py 2019-05-22 06:47:00 +0000
+++ tests/functional/openlp_core/ui/test_thememanager.py 2019-06-07 21:06:40 +0000
@@ -83,7 +83,7 @@
@patch('openlp.core.ui.thememanager.shutil')
@patch('openlp.core.ui.thememanager.create_paths')
- def test_write_theme_same_image(self, mocked_create_paths, mocked_shutil):
+ def test_save_theme_same_image(self, mocked_create_paths, mocked_shutil):
"""
Test that we don't try to overwrite a theme background image with itself
"""
@@ -98,16 +98,16 @@
mocked_theme.extract_formatted_xml = MagicMock()
mocked_theme.extract_formatted_xml.return_value = 'fake_theme_xml'.encode()
- # WHEN: Calling _write_theme with path to the same image, but the path written slightly different
+ # WHEN: Calling save_theme with path to the same image, but the path written slightly different
file_path_1 = RESOURCE_PATH / 'church.jpg'
- theme_manager._write_theme(mocked_theme, file_path_1, file_path_1)
+ theme_manager.save_theme(mocked_theme, file_path_1, file_path_1)
# THEN: The mocked_copyfile should not have been called
assert mocked_shutil.copyfile.called is False, 'copyfile should not be called'
@patch('openlp.core.ui.thememanager.shutil')
@patch('openlp.core.ui.thememanager.create_paths')
- def test_write_theme_diff_images(self, mocked_create_paths, mocked_shutil):
+ def test_save_theme_diff_images(self, mocked_create_paths, mocked_shutil):
"""
Test that we do overwrite a theme background image when a new is submitted
"""
@@ -121,15 +121,15 @@
mocked_theme.theme_name = 'themename'
mocked_theme.filename = "filename"
- # WHEN: Calling _write_theme with path to different images
+ # WHEN: Calling save_theme with path to different images
file_path_1 = RESOURCE_PATH / 'church.jpg'
file_path_2 = RESOURCE_PATH / 'church2.jpg'
- theme_manager._write_theme(mocked_theme, file_path_1, file_path_2)
+ theme_manager.save_theme(mocked_theme, file_path_1, file_path_2)
# THEN: The mocked_copyfile should not have been called
assert mocked_shutil.copyfile.called is True, 'copyfile should be called'
- def test_write_theme_special_char_name(self):
+ def test_save_theme_special_char_name(self):
"""
Test that we can save themes with special characters in the name
"""
@@ -142,8 +142,8 @@
mocked_theme.theme_name = 'theme 愛 name'
mocked_theme.export_theme.return_value = "{}"
- # WHEN: Calling _write_theme with a theme with a name with special characters in it
- theme_manager._write_theme(mocked_theme)
+ # WHEN: Calling save_theme with a theme with a name with special characters in it
+ theme_manager.save_theme(mocked_theme)
# THEN: It should have been created
assert os.path.exists(os.path.join(self.temp_folder, 'theme 愛 name', 'theme 愛 name.json')) is True, \
Follow ups