openlp/openlp/core/ui/thememanager.py

842 lines
40 KiB
Python
Raw Normal View History

2009-09-13 15:14:45 +00:00
# -*- coding: utf-8 -*-
2019-04-13 13:00:22 +00:00
##########################################################################
# OpenLP - Open Source Lyrics Projection #
# ---------------------------------------------------------------------- #
2022-02-01 10:10:57 +00:00
# Copyright (c) 2008-2022 OpenLP Developers #
2019-04-13 13:00:22 +00:00
# ---------------------------------------------------------------------- #
# 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/>. #
##########################################################################
2013-02-01 21:34:23 +00:00
"""
The Theme Manager manages adding, deleteing and modifying of themes.
"""
2009-09-13 15:14:45 +00:00
import os
import shutil
2009-09-13 15:14:45 +00:00
import zipfile
from pathlib import Path
2018-10-02 04:39:42 +00:00
from xml.etree.ElementTree import XML, ElementTree
2017-09-26 16:39:13 +00:00
2019-04-21 12:48:23 +00:00
from PyQt5 import QtCore, QtWidgets
2009-09-13 15:14:45 +00:00
2019-07-31 21:01:22 +00:00
from openlp.core.state import State
2017-10-07 07:05:07 +00:00
from openlp.core.common import delete_file
from openlp.core.common.applocation import AppLocation
2018-10-02 04:39:42 +00:00
from openlp.core.common.i18n import UiStrings, get_locale_key, translate
2017-10-23 22:09:57 +00:00
from openlp.core.common.mixins import LogMixin, RegistryProperties
from openlp.core.common.path import create_paths
2017-10-23 22:09:57 +00:00
from openlp.core.common.registry import Registry, RegistryBase
from openlp.core.common.utils import wait_for
2019-04-21 12:48:23 +00:00
from openlp.core.lib import build_icon, check_item_selected, create_thumb, get_text_file_string, validate_thumb
from openlp.core.lib.exceptions import ValidationError
2019-04-21 12:48:23 +00:00
from openlp.core.lib.theme import Theme
2018-10-02 04:39:42 +00:00
from openlp.core.lib.ui import create_widget_action, critical_error_message_box
from openlp.core.ui.filerenameform import FileRenameForm
2018-10-02 04:39:42 +00:00
from openlp.core.ui.icons import UiIcons
from openlp.core.ui.themeform import ThemeForm
from openlp.core.ui.themeprogressform import ThemeProgressForm
2017-10-23 22:09:57 +00:00
from openlp.core.widgets.dialogs import FileDialog
from openlp.core.widgets.toolbar import OpenLPToolbar
2009-09-13 15:14:45 +00:00
2010-02-27 15:31:23 +00:00
2013-12-31 20:29:03 +00:00
class Ui_ThemeManager(object):
2009-09-13 15:14:45 +00:00
"""
2013-12-31 20:29:03 +00:00
UI part of the Theme Manager
2009-09-13 15:14:45 +00:00
"""
2013-12-31 20:29:03 +00:00
def setup_ui(self, widget):
2013-02-01 21:34:23 +00:00
"""
2013-12-31 20:29:03 +00:00
Define the UI
2020-05-05 18:17:21 +00:00
:param widget: The screen object the dialog is to be attached to.
2013-02-01 21:34:23 +00:00
"""
# start with the layout
2015-11-07 00:49:40 +00:00
self.layout = QtWidgets.QVBoxLayout(widget)
self.layout.setSpacing(0)
2015-11-07 00:49:40 +00:00
self.layout.setContentsMargins(0, 0, 0, 0)
2013-08-31 18:17:38 +00:00
self.layout.setObjectName('layout')
2013-12-31 21:02:35 +00:00
self.toolbar = OpenLPToolbar(widget)
2013-08-31 18:17:38 +00:00
self.toolbar.setObjectName('toolbar')
self.toolbar.add_toolbar_action('newTheme',
2018-04-07 20:31:54 +00:00
text=UiStrings().NewTheme, icon=UiIcons().new,
2013-12-23 07:03:56 +00:00
tooltip=translate('OpenLP.ThemeManager', 'Create a new theme.'),
triggers=self.on_add_theme)
2013-08-31 18:17:38 +00:00
self.toolbar.add_toolbar_action('editTheme',
2013-12-23 07:03:56 +00:00
text=translate('OpenLP.ThemeManager', 'Edit Theme'),
2018-04-07 20:31:54 +00:00
icon=UiIcons().edit,
2013-12-23 07:03:56 +00:00
tooltip=translate('OpenLP.ThemeManager', 'Edit a theme.'),
triggers=self.on_edit_theme)
2013-08-31 18:17:38 +00:00
self.delete_toolbar_action = self.toolbar.add_toolbar_action('delete_theme',
2013-12-23 07:03:56 +00:00
text=translate('OpenLP.ThemeManager',
'Delete Theme'),
2018-04-07 20:31:54 +00:00
icon=UiIcons().delete,
2013-12-23 07:03:56 +00:00
tooltip=translate('OpenLP.ThemeManager',
'Delete a theme.'),
triggers=self.on_delete_theme)
self.toolbar.addSeparator()
2013-08-31 18:17:38 +00:00
self.toolbar.add_toolbar_action('importTheme',
2013-12-23 07:03:56 +00:00
text=translate('OpenLP.ThemeManager', 'Import Theme'),
2018-04-21 05:47:20 +00:00
icon=UiIcons().download,
2013-12-23 07:03:56 +00:00
tooltip=translate('OpenLP.ThemeManager', 'Import a theme.'),
triggers=self.on_import_theme)
2013-08-31 18:17:38 +00:00
self.toolbar.add_toolbar_action('exportTheme',
2013-12-23 07:03:56 +00:00
text=translate('OpenLP.ThemeManager', 'Export Theme'),
2018-04-21 05:47:20 +00:00
icon=UiIcons().upload,
2013-12-23 07:03:56 +00:00
tooltip=translate('OpenLP.ThemeManager', 'Export a theme.'),
triggers=self.on_export_theme)
self.layout.addWidget(self.toolbar)
2015-11-07 00:49:40 +00:00
self.theme_widget = QtWidgets.QWidgetAction(self.toolbar)
2013-08-31 18:17:38 +00:00
self.theme_widget.setObjectName('theme_widget')
# create theme manager list
2015-11-07 00:49:40 +00:00
self.theme_list_widget = QtWidgets.QListWidget(widget)
self.theme_list_widget.setAlternatingRowColors(True)
self.theme_list_widget.setIconSize(QtCore.QSize(88, 50))
self.theme_list_widget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
2013-08-31 18:17:38 +00:00
self.theme_list_widget.setObjectName('theme_list_widget')
self.layout.addWidget(self.theme_list_widget)
2013-02-14 21:50:10 +00:00
self.theme_list_widget.customContextMenuRequested.connect(self.context_menu)
# build the context menu
2015-11-07 00:49:40 +00:00
self.menu = QtWidgets.QMenu()
self.edit_action = create_widget_action(self.menu,
2013-12-23 07:03:56 +00:00
text=translate('OpenLP.ThemeManager', '&Edit Theme'),
2018-04-07 20:31:54 +00:00
icon=UiIcons().edit, triggers=self.on_edit_theme)
self.copy_action = create_widget_action(self.menu,
2013-12-23 07:03:56 +00:00
text=translate('OpenLP.ThemeManager', '&Copy Theme'),
2018-04-07 20:31:54 +00:00
icon=UiIcons().copy, triggers=self.on_copy_theme)
self.rename_action = create_widget_action(self.menu,
2013-12-23 07:03:56 +00:00
text=translate('OpenLP.ThemeManager', '&Rename Theme'),
2018-04-07 20:31:54 +00:00
icon=UiIcons().edit, triggers=self.on_rename_theme)
self.delete_action = create_widget_action(self.menu,
2013-12-23 07:03:56 +00:00
text=translate('OpenLP.ThemeManager', '&Delete Theme'),
2018-04-07 20:31:54 +00:00
icon=UiIcons().delete, triggers=self.on_delete_theme)
self.menu.addSeparator()
self.global_action = create_widget_action(self.menu,
2013-12-23 07:03:56 +00:00
text=translate('OpenLP.ThemeManager', 'Set As &Global Default'),
2018-04-07 20:31:54 +00:00
icon=UiIcons().default,
2013-12-23 07:03:56 +00:00
triggers=self.change_global_from_screen)
2013-12-24 06:15:41 +00:00
self.export_action = create_widget_action(self.menu,
text=translate('OpenLP.ThemeManager', '&Export Theme'),
2018-04-07 20:31:54 +00:00
icon=UiIcons().upload, triggers=self.on_export_theme)
2010-12-27 10:18:09 +00:00
# Signals
2013-02-14 21:50:10 +00:00
self.theme_list_widget.doubleClicked.connect(self.change_global_from_screen)
self.theme_list_widget.currentItemChanged.connect(self.check_list_state)
2013-12-31 20:29:03 +00:00
2020-04-01 18:26:57 +00:00
Registry().register_function('get_theme_names', self.get_theme_names)
2013-12-31 20:29:03 +00:00
2017-10-23 22:09:57 +00:00
class ThemeManager(QtWidgets.QWidget, RegistryBase, Ui_ThemeManager, LogMixin, RegistryProperties):
2013-12-31 20:29:03 +00:00
"""
Manages the orders of Theme.
"""
2022-01-16 13:15:09 +00:00
# These signals are used by the web api to update the theme on the ui thread
2020-10-28 04:06:56 +00:00
theme_update_global = QtCore.pyqtSignal()
2022-01-16 13:15:09 +00:00
theme_level_updated = QtCore.pyqtSignal()
2020-10-28 04:06:56 +00:00
2013-12-31 20:29:03 +00:00
def __init__(self, parent=None):
"""
Constructor
"""
super(ThemeManager, self).__init__(parent)
2010-12-27 10:18:09 +00:00
# Variables
self._theme_list = {}
2017-09-26 16:39:13 +00:00
self.old_background_image_path = None
Registry().register_function('config_screen_changed', self.screen_changed)
2009-09-13 15:14:45 +00:00
def get_global_theme(self):
return self.get_theme_data(self.global_theme)
def get_theme_data(self, theme_name):
"""
Gets a theme given a name, returns the default theme if missing
"""
theme = Theme()
if theme_name in self._theme_list:
theme = self._theme_list[theme_name]
else:
self.log_warning('Missing requested theme data for "{}", using default theme'.format(theme_name))
return theme
2013-12-23 07:03:56 +00:00
def bootstrap_initialise(self):
"""
process the bootstrap initialise setup request
"""
2013-12-31 20:29:03 +00:00
self.setup_ui(self)
self.global_theme = self.settings.value('themes/global theme')
2013-12-23 07:03:56 +00:00
self.build_theme_path()
2022-01-16 13:15:09 +00:00
Registry().register_function('reload_global_theme', self.on_update_global_theme)
# These signals are used by the web api to update the theme on the ui thread
self.theme_level_updated.connect(self.on_theme_level_updated)
self.theme_update_global.connect(self.on_update_global_theme)
2013-12-23 07:03:56 +00:00
def bootstrap_post_set_up(self):
"""
process the bootstrap post setup request
"""
self.progress_form = ThemeProgressForm(self)
2013-12-31 20:29:03 +00:00
self.theme_form = ThemeForm(self)
2017-09-26 16:39:13 +00:00
self.theme_form.path = self.theme_path
2013-12-31 20:29:03 +00:00
self.file_rename_form = FileRenameForm()
self.upgrade_themes() # TODO: Can be removed when upgrade path from OpenLP 2.4 no longer needed
2014-01-04 20:58:18 +00:00
self.load_themes()
2013-12-23 07:03:56 +00:00
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()
2017-09-26 16:39:13 +00:00
def upgrade_themes(self):
"""
Upgrade the xml files to json.
:rtype: None
"""
xml_file_paths = [p for p in AppLocation.get_section_data_path('themes').glob('*/*.xml')]
# Exit early if there are no themes to upgrade
if not xml_file_paths:
return
# Wait for 2 seconds to allow some other things to start processing first
wait_for(lambda: False, timeout=1)
2017-09-26 16:39:13 +00:00
for xml_file_path in xml_file_paths:
theme_data = get_text_file_string(xml_file_path)
theme = self._create_theme_from_xml(theme_data, self.theme_path)
self.save_theme(theme)
2017-09-26 16:39:13 +00:00
xml_file_path.unlink()
2013-12-31 20:29:03 +00:00
def build_theme_path(self):
"""
Set up the theme path variables
2017-09-26 16:39:13 +00:00
:rtype: None
2013-12-31 20:29:03 +00:00
"""
self.theme_path = AppLocation.get_section_data_path('themes')
2017-09-26 16:39:13 +00:00
self.thumb_path = self.theme_path / 'thumbnails'
2017-10-07 07:05:07 +00:00
create_paths(self.theme_path, self.thumb_path)
2013-12-31 20:29:03 +00:00
2013-12-23 07:03:56 +00:00
def check_list_state(self, item, field=None):
"""
If Default theme selected remove delete button.
2013-12-23 07:03:56 +00:00
Note for some reason a dummy field is required. Nothing is passed!
2014-01-01 09:33:07 +00:00
:param field:
:param item: Service Item to process
"""
if item is None:
return
2012-05-19 15:10:05 +00:00
real_theme_name = item.data(QtCore.Qt.UserRole)
2012-05-17 18:57:01 +00:00
theme_name = item.text()
2011-01-11 17:44:13 +00:00
# If default theme restrict actions
if real_theme_name == theme_name:
2013-03-16 11:05:52 +00:00
self.delete_toolbar_action.setVisible(True)
else:
2013-03-16 11:05:52 +00:00
self.delete_toolbar_action.setVisible(False)
def context_menu(self, point):
2010-12-28 10:56:19 +00:00
"""
2014-01-01 09:33:07 +00:00
Build the Right Click Context menu and set state depending on the type of theme.
:param point: The position of the mouse so the correct item can be found.
2010-12-28 10:56:19 +00:00
"""
item = self.theme_list_widget.itemAt(point)
if item is None:
return
2012-05-19 15:10:05 +00:00
real_theme_name = item.data(QtCore.Qt.UserRole)
2013-08-31 18:17:38 +00:00
theme_name = str(item.text())
2012-06-27 17:00:02 +00:00
visible = real_theme_name == theme_name
self.delete_action.setVisible(visible)
self.rename_action.setVisible(visible)
self.global_action.setVisible(visible)
2015-11-07 00:49:40 +00:00
self.menu.exec(self.theme_list_widget.mapToGlobal(point))
2022-01-16 13:15:09 +00:00
def on_update_global_theme(self):
2010-06-10 01:57:59 +00:00
"""
2022-01-16 13:15:09 +00:00
Update the global theme to the theme set in the settings.
2010-06-10 01:57:59 +00:00
"""
self.global_theme = self.settings.value('themes/global theme')
2022-01-16 13:15:09 +00:00
self.log_debug('on_update_global_theme {text}'.format(text=self.global_theme))
2013-02-02 21:16:42 +00:00
for count in range(0, self.theme_list_widget.count()):
2010-08-28 15:49:51 +00:00
# reset the old name
item = self.theme_list_widget.item(count)
old_name = item.text()
2012-05-19 15:10:05 +00:00
new_name = item.data(QtCore.Qt.UserRole)
if old_name != new_name:
self.theme_list_widget.item(count).setText(new_name)
2010-08-28 15:49:51 +00:00
# Set the new name
2013-03-11 19:11:46 +00:00
if self.global_theme == new_name:
2016-05-20 16:22:06 +00:00
name = translate('OpenLP.ThemeManager', '{text} (default)').format(text=new_name)
self.theme_list_widget.item(count).setText(name)
2013-03-16 11:05:52 +00:00
self.delete_toolbar_action.setVisible(item not in self.theme_list_widget.selectedItems())
2022-01-16 13:15:09 +00:00
Registry().execute('theme_change_global')
def on_theme_level_updated(self):
"""
Update the theme level, called from web controller.
"""
Registry().execute('theme_level_changed')
2009-09-13 15:14:45 +00:00
2013-02-05 21:42:15 +00:00
def change_global_from_screen(self, index=-1):
2010-06-10 01:57:59 +00:00
"""
2014-01-01 09:33:07 +00:00
Change the global theme when a theme is double clicked upon in the Theme Manager list.
:param index:
2010-06-10 01:57:59 +00:00
"""
selected_row = self.theme_list_widget.currentRow()
2013-02-02 21:16:42 +00:00
for count in range(0, self.theme_list_widget.count()):
item = self.theme_list_widget.item(count)
old_name = item.text()
2010-08-28 15:49:51 +00:00
# reset the old name
2012-05-19 15:10:05 +00:00
if old_name != item.data(QtCore.Qt.UserRole):
self.theme_list_widget.item(count).setText(item.data(QtCore.Qt.UserRole))
2010-08-28 15:49:51 +00:00
# Set the new name
2009-10-10 18:36:58 +00:00
if count == selected_row:
self.global_theme = self.theme_list_widget.item(count).text()
2016-05-20 16:22:06 +00:00
name = translate('OpenLP.ThemeManager', '{text} (default)').format(text=self.global_theme)
self.theme_list_widget.item(count).setText(name)
self.settings.setValue('themes/global theme', self.global_theme)
2022-01-16 13:15:09 +00:00
Registry().execute('theme_change_global')
2009-09-13 15:14:45 +00:00
2013-12-23 07:03:56 +00:00
def on_add_theme(self, field=None):
2010-06-10 01:57:59 +00:00
"""
2014-01-01 09:33:07 +00:00
Loads a new theme with the default settings and then launches the theme editing form for the user to make
their customisations.
:param field:
2010-06-10 01:57:59 +00:00
"""
2017-05-13 07:47:22 +00:00
theme = Theme()
2013-03-16 11:05:52 +00:00
self.theme_form.theme = theme
2015-11-07 00:49:40 +00:00
self.theme_form.exec()
self.load_themes()
2009-09-13 15:14:45 +00:00
2013-12-23 07:03:56 +00:00
def on_rename_theme(self, field=None):
2010-09-26 06:20:24 +00:00
"""
Renames an existing theme to a new name
2014-01-01 09:33:07 +00:00
:param field:
2010-09-26 06:20:24 +00:00
"""
2012-12-29 15:25:29 +00:00
if self._validate_theme_action(translate('OpenLP.ThemeManager', 'You must select a theme to rename.'),
2013-12-23 07:03:56 +00:00
translate('OpenLP.ThemeManager', 'Rename Confirmation'),
translate('OpenLP.ThemeManager', 'Rename {theme_name} theme?'), False, False):
item = self.theme_list_widget.currentItem()
2012-05-19 15:10:05 +00:00
old_theme_name = item.data(QtCore.Qt.UserRole)
2013-03-16 11:05:52 +00:00
self.file_rename_form.file_name_edit.setText(old_theme_name)
2015-11-07 00:49:40 +00:00
if self.file_rename_form.exec():
2013-03-16 11:05:52 +00:00
new_theme_name = self.file_rename_form.file_name_edit.text()
if old_theme_name == new_theme_name:
return
if self.check_if_theme_exists(new_theme_name):
old_theme_data = self.get_theme_data(old_theme_name)
2013-03-16 11:05:52 +00:00
self.clone_theme_data(old_theme_data, new_theme_name)
self.delete_theme(old_theme_name)
2019-07-31 21:01:22 +00:00
for plugin in State().list_plugins():
2013-02-19 21:23:56 +00:00
if plugin.uses_theme(old_theme_name):
plugin.rename_theme(old_theme_name, new_theme_name)
self.renderer.set_theme(self.get_theme_data(new_theme_name))
self.load_themes()
2010-09-26 06:20:24 +00:00
2013-12-23 07:03:56 +00:00
def on_copy_theme(self, field=None):
2010-09-26 06:20:24 +00:00
"""
Copies an existing theme to a new name
2014-01-01 09:33:07 +00:00
:param field:
2010-09-26 06:20:24 +00:00
"""
item = self.theme_list_widget.currentItem()
2012-05-19 15:10:05 +00:00
old_theme_name = item.data(QtCore.Qt.UserRole)
2013-03-16 11:05:52 +00:00
self.file_rename_form.file_name_edit.setText(translate('OpenLP.ThemeManager',
2016-05-20 16:22:06 +00:00
'Copy of {name}',
'Copy of <theme name>').format(name=old_theme_name))
2015-11-07 00:49:40 +00:00
if self.file_rename_form.exec(True):
2013-03-16 11:05:52 +00:00
new_theme_name = self.file_rename_form.file_name_edit.text()
if self.check_if_theme_exists(new_theme_name):
theme_data = self.get_theme_data(old_theme_name)
2013-03-16 11:05:52 +00:00
self.clone_theme_data(theme_data, new_theme_name)
2010-09-26 07:39:50 +00:00
2013-03-16 11:05:52 +00:00
def clone_theme_data(self, theme_data, new_theme_name):
2010-09-26 07:39:50 +00:00
"""
2010-10-03 07:42:02 +00:00
Takes a theme and makes a new copy of it as well as saving it.
2014-01-01 09:33:07 +00:00
2017-09-26 16:39:13 +00:00
:param Theme theme_data: The theme to be used
:param str new_theme_name: The new theme name of the theme
:rtype: None
2010-09-26 07:39:50 +00:00
"""
old_background = None
2016-04-30 15:40:23 +00:00
if theme_data.background_type == 'image' or theme_data.background_type == 'video':
old_background = theme_data.background_filename
theme_data.background_filename = self.theme_path / new_theme_name / theme_data.background_filename.name
theme_data.theme_name = new_theme_name
2017-09-26 16:39:13 +00:00
theme_data.extend_image_filename(self.theme_path)
self.save_theme(theme_data, background_override=old_background)
self.update_preview_images([new_theme_name])
self.load_themes()
2010-09-26 06:20:24 +00:00
2013-12-23 07:03:56 +00:00
def on_edit_theme(self, field=None):
2010-06-10 01:57:59 +00:00
"""
Loads the settings for the theme that is to be edited and launches the
theme editing form so the user can make their changes.
2014-01-01 09:33:07 +00:00
:param field:
2010-06-10 01:57:59 +00:00
"""
if check_item_selected(self.theme_list_widget,
2013-12-23 07:03:56 +00:00
translate('OpenLP.ThemeManager', 'You must select a theme to edit.')):
item = self.theme_list_widget.currentItem()
theme = self.get_theme_data(item.data(QtCore.Qt.UserRole))
2016-04-30 15:40:23 +00:00
if theme.background_type == 'image' or theme.background_type == 'video':
2017-09-26 16:39:13 +00:00
self.old_background_image_path = theme.background_filename
2013-03-16 11:05:52 +00:00
self.theme_form.theme = theme
2015-11-07 00:49:40 +00:00
self.theme_form.exec(True)
2017-09-26 16:39:13 +00:00
self.old_background_image_path = None
2019-02-13 20:54:35 +00:00
self.renderer.set_theme(theme)
self.load_themes()
2009-09-13 15:14:45 +00:00
2013-12-23 07:03:56 +00:00
def on_delete_theme(self, field=None):
2010-06-10 01:57:59 +00:00
"""
2014-01-01 09:33:07 +00:00
Delete a theme triggered by the UI.
:param field:
2010-06-10 01:57:59 +00:00
"""
2012-12-29 15:25:29 +00:00
if self._validate_theme_action(translate('OpenLP.ThemeManager', 'You must select a theme to delete.'),
translate('OpenLP.ThemeManager', 'Delete Confirmation'),
translate('OpenLP.ThemeManager', 'Delete {theme_name} theme?')):
item = self.theme_list_widget.currentItem()
2012-05-17 18:57:01 +00:00
theme = item.text()
row = self.theme_list_widget.row(item)
self.theme_list_widget.takeItem(row)
self.delete_theme(theme)
# self.renderer.set_theme(self.get_theme_data(item.data(QtCore.Qt.UserRole)))
2011-02-05 20:10:08 +00:00
# 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()
def delete_theme(self, theme):
"""
Delete a theme.
2014-01-01 09:33:07 +00:00
:param theme: The theme to delete.
"""
self._theme_list.pop(theme)
2016-05-20 16:22:06 +00:00
thumb = '{name}.png'.format(name=theme)
2017-09-26 16:39:13 +00:00
delete_file(self.theme_path / thumb)
delete_file(self.thumb_path / thumb)
try:
shutil.rmtree(self.theme_path / theme)
2017-09-26 16:39:13 +00:00
except OSError:
2016-05-20 16:22:06 +00:00
self.log_exception('Error deleting theme {name}'.format(name=theme))
2009-09-13 15:14:45 +00:00
2017-09-26 16:39:13 +00:00
def on_export_theme(self, checked=None):
"""
2017-09-26 16:39:13 +00:00
Export the theme to a zip file
:param bool checked: Sent by the QAction.triggered signal. It's not used in this method.
:rtype: None
"""
item = self.theme_list_widget.currentItem()
if item is None:
2012-12-29 15:25:29 +00:00
critical_error_message_box(message=translate('OpenLP.ThemeManager', 'You have not selected a theme.'))
return
2017-09-26 16:39:13 +00:00
theme_name = item.data(QtCore.Qt.UserRole)
export_path, filter_used = \
FileDialog.getSaveFileName(self.main_window,
2017-09-26 17:02:56 +00:00
translate('OpenLP.ThemeManager',
'Save Theme - ({name})').format(name=theme_name),
self.settings.value('themes/last directory export'),
2017-09-26 17:02:56 +00:00
translate('OpenLP.ThemeManager', 'OpenLP Themes (*.otz)'),
translate('OpenLP.ThemeManager', 'OpenLP Themes (*.otz)'))
2013-02-03 19:23:12 +00:00
self.application.set_busy_cursor()
if export_path:
self.settings.setValue('themes/last directory export', export_path.parent)
2017-09-26 16:39:13 +00:00
if self._export_theme(export_path.with_suffix('.otz'), theme_name):
2015-11-07 00:49:40 +00:00
QtWidgets.QMessageBox.information(self,
translate('OpenLP.ThemeManager', 'Theme Exported'),
translate('OpenLP.ThemeManager',
'Your theme has been successfully exported.'))
2013-02-03 19:23:12 +00:00
self.application.set_normal_cursor()
2009-09-13 15:14:45 +00:00
2017-09-26 16:39:13 +00:00
def _export_theme(self, theme_path, theme_name):
2014-06-23 13:51:56 +00:00
"""
Create the zipfile with the theme contents.
2017-09-26 16:39:13 +00:00
:param Path theme_path: Location where the zip file will be placed
2017-09-26 16:39:13 +00:00
:param str theme_name: The name of the theme to be exported
:return: The success of creating the zip file
:rtype: bool
2014-06-23 13:51:56 +00:00
"""
try:
2019-03-10 21:01:39 +00:00
with zipfile.ZipFile(theme_path, 'w') as theme_zip:
2017-09-26 16:39:13 +00:00
source_path = self.theme_path / theme_name
for file_path in source_path.iterdir():
2019-03-10 21:01:39 +00:00
theme_zip.write(file_path, Path(theme_name, file_path.name))
return True
except OSError as ose:
self.log_exception('Export Theme Failed')
critical_error_message_box(translate('OpenLP.ThemeManager', 'Theme Export Failed'),
2017-09-26 16:39:13 +00:00
translate('OpenLP.ThemeManager',
2018-03-16 06:05:18 +00:00
'The {theme_name} export failed because this error occurred: {err}')
.format(theme_name=theme_name, err=ose.strerror))
2017-09-26 16:39:13 +00:00
if theme_path.exists():
shutil.rmtree(theme_path, ignore_errors=True)
return False
2014-06-23 13:51:56 +00:00
2017-09-26 16:39:13 +00:00
def on_import_theme(self, checked=None):
2010-06-10 01:57:59 +00:00
"""
2013-03-16 11:05:52 +00:00
Opens a file dialog to select the theme file(s) to import before attempting to extract OpenLP themes from
those files. This process will only load version 2 themes.
2017-09-26 16:39:13 +00:00
:param bool checked: Sent by the QAction.triggered signal. It's not used in this method.
:rtype: None
2010-06-10 01:57:59 +00:00
"""
2017-09-26 16:39:13 +00:00
file_paths, filter_used = FileDialog.getOpenFileNames(
2017-08-07 20:50:01 +00:00
self,
translate('OpenLP.ThemeManager', 'Select Theme Import File'),
self.settings.value('themes/last directory import'),
2017-08-07 20:50:01 +00:00
translate('OpenLP.ThemeManager', 'OpenLP Themes (*.otz)'))
self.log_info('New Themes {file_paths}'.format(file_paths=file_paths))
if not file_paths:
return
2013-02-03 19:23:12 +00:00
self.application.set_busy_cursor()
new_themes = []
2017-08-07 20:50:01 +00:00
for file_path in file_paths:
new_themes.append(self.unzip_theme(file_path))
self.settings.setValue('themes/last directory import', file_path.parent)
self.update_preview_images(new_themes)
2013-02-03 19:23:12 +00:00
self.application.set_normal_cursor()
2009-09-13 15:14:45 +00:00
2013-02-19 19:50:14 +00:00
def load_first_time_themes(self):
"""
Imports any themes on start up and makes sure there is at least one theme
"""
self.application.set_busy_cursor()
theme_paths = AppLocation.get_files('themes', '.otz')
new_themes = []
2017-09-26 16:39:13 +00:00
for theme_path in theme_paths:
theme_path = self.theme_path / theme_path
new_themes.append(self.unzip_theme(theme_path))
2017-09-26 16:39:13 +00:00
delete_file(theme_path)
2013-02-19 19:50:14 +00:00
# No themes have been found so create one
2017-09-26 16:39:13 +00:00
if not theme_paths:
2017-05-13 07:47:22 +00:00
theme = Theme()
2013-02-19 19:50:14 +00:00
theme.theme_name = UiStrings().Default
self.save_theme(theme)
self.settings.setValue('themes/global theme', theme.theme_name)
new_themes = [theme.theme_name]
if new_themes:
self.update_preview_images(new_themes)
2013-02-19 19:50:14 +00:00
self.application.set_normal_cursor()
def load_themes(self):
2009-09-13 15:14:45 +00:00
"""
2013-10-23 19:25:46 +00:00
Loads the theme lists and triggers updates across the whole system using direct calls or core functions and
events for the plugins.
2009-09-13 15:14:45 +00:00
The plugins will call back in to get the real list if they want it.
"""
self._theme_list.clear()
self.theme_list_widget.clear()
files = AppLocation.get_files('themes', '/*.json')
# Sort the themes by its name considering language specific
2013-08-31 18:17:38 +00:00
files.sort(key=lambda file_name: get_locale_key(str(file_name)))
2011-03-20 07:37:44 +00:00
# now process the file list of png files
2017-09-26 16:39:13 +00:00
for file in files:
2011-03-20 07:37:44 +00:00
# check to see file is in theme root directory
2017-09-26 16:39:13 +00:00
theme_path = self.theme_path / file
if theme_path.exists():
text_name = theme_path.stem
if text_name == self.global_theme:
2016-05-20 16:22:06 +00:00
name = translate('OpenLP.ThemeManager', '{name} (default)').format(name=text_name)
2011-03-20 07:37:44 +00:00
else:
name = text_name
2017-11-18 11:23:15 +00:00
thumb_path = self.thumb_path / '{name}.png'.format(name=text_name)
2015-11-07 00:49:40 +00:00
item_name = QtWidgets.QListWidgetItem(name)
2017-11-18 11:23:15 +00:00
if validate_thumb(theme_path, thumb_path):
icon = build_icon(thumb_path)
2011-03-20 07:37:44 +00:00
else:
2017-11-18 11:23:15 +00:00
icon = create_thumb(theme_path, thumb_path)
2011-03-20 07:37:44 +00:00
item_name.setIcon(icon)
2012-05-17 15:13:09 +00:00
item_name.setData(QtCore.Qt.UserRole, text_name)
self.theme_list_widget.addItem(item_name)
self._theme_list[text_name] = self._get_theme_data(text_name)
self._push_themes()
2009-09-13 15:14:45 +00:00
def _push_themes(self):
2010-06-10 01:57:59 +00:00
"""
Notify listeners that the theme list has been updated
"""
Registry().execute('theme_update_list', self.get_theme_names())
2009-09-13 15:14:45 +00:00
def get_theme_names(self):
2010-06-10 01:57:59 +00:00
"""
Return the list of loaded themes
"""
return [theme_name for theme_name in self._theme_list]
2009-09-13 15:14:45 +00:00
def _get_theme_data(self, theme_name):
2010-06-10 01:57:59 +00:00
"""
2017-09-26 16:39:13 +00:00
Returns a theme object from a JSON file
2010-06-10 01:57:59 +00:00
2017-09-26 16:39:13 +00:00
:param str theme_name: Name of the theme to load from file
:return: The theme object.
:rtype: Theme
2010-06-10 01:57:59 +00:00
"""
2017-09-26 16:39:13 +00:00
theme_name = str(theme_name)
theme_file_path = self.theme_path / theme_name / '{file_name}.json'.format(file_name=theme_name)
theme_data = get_text_file_string(theme_file_path)
2017-05-13 08:22:48 +00:00
if not theme_data:
2013-12-23 07:03:56 +00:00
self.log_debug('No theme data - using default theme')
2017-05-13 07:47:22 +00:00
return Theme()
theme_object = self._create_theme_from_json(theme_data, self.theme_path)
theme_object.set_default_header_footer()
return theme_object
2009-09-13 15:14:45 +00:00
def over_write_message_box(self, theme_name):
"""
2013-02-01 21:34:23 +00:00
Display a warning box to the user that a theme already exists
2014-01-11 17:52:01 +00:00
2014-01-01 09:33:07 +00:00
:param theme_name: Name of the theme.
2015-09-08 19:13:59 +00:00
:return: Confirm if the theme is to be overwritten.
"""
2015-11-07 00:49:40 +00:00
ret = QtWidgets.QMessageBox.question(self, translate('OpenLP.ThemeManager', 'Theme Already Exists'),
translate('OpenLP.ThemeManager',
2016-05-20 16:22:06 +00:00
'Theme {name} already exists. '
'Do you want to replace it?').format(name=theme_name),
defaultButton=QtWidgets.QMessageBox.No)
2015-11-07 00:49:40 +00:00
return ret == QtWidgets.QMessageBox.Yes
2009-09-13 15:14:45 +00:00
def unzip_theme(self, file_path):
2009-09-13 15:14:45 +00:00
"""
2014-01-01 09:33:07 +00:00
Unzip the theme, remove the preview file if stored. Generate a new preview file. Check the XML theme version
and upgrade if necessary.
:param Path file_path:
2009-09-13 15:14:45 +00:00
"""
2017-09-26 16:39:13 +00:00
self.log_debug('Unzipping theme {name}'.format(name=file_path))
2012-03-10 08:22:52 +00:00
file_xml = None
2012-06-15 16:19:46 +00:00
abort_import = True
2017-05-30 13:55:39 +00:00
json_theme = False
theme_name = ""
try:
2019-03-10 21:01:39 +00:00
with zipfile.ZipFile(file_path) as theme_zip:
2017-09-26 16:39:13 +00:00
json_file = [name for name in theme_zip.namelist() if os.path.splitext(name)[1].lower() == '.json']
if len(json_file) != 1:
# TODO: remove XML handling after once the upgrade path from 2.4 is no longer required
2017-09-26 16:39:13 +00:00
xml_file = [name for name in theme_zip.namelist() if os.path.splitext(name)[1].lower() == '.xml']
if len(xml_file) != 1:
self.log_error('Theme contains "{val:d}" theme files'.format(val=len(xml_file)))
raise ValidationError
xml_tree = ElementTree(element=XML(theme_zip.read(xml_file[0]))).getroot()
theme_version = xml_tree.get('version', default=None)
if not theme_version or float(theme_version) < 2.0:
self.log_error('Theme version is less than 2.0')
raise ValidationError
theme_name = xml_tree.find('name').text.strip()
else:
new_theme = Theme()
new_theme.load_theme(theme_zip.read(json_file[0]).decode("utf-8"))
theme_name = new_theme.theme_name
json_theme = True
theme_folder = self.theme_path / theme_name
2017-09-26 16:39:13 +00:00
if theme_folder.exists() and not self.over_write_message_box(theme_name):
abort_import = True
return
else:
2017-09-26 16:39:13 +00:00
abort_import = False
for zipped_file in theme_zip.namelist():
zipped_file_rel_path = Path(zipped_file)
split_name = zipped_file_rel_path.parts
if split_name[-1] == '' or len(split_name) == 1:
# is directory or preview file
continue
full_name = self.theme_path / zipped_file_rel_path
2017-10-07 07:05:07 +00:00
create_paths(full_name.parent)
2017-09-26 16:39:13 +00:00
if zipped_file_rel_path.suffix.lower() == '.xml' or zipped_file_rel_path.suffix.lower() == '.json':
file_xml = str(theme_zip.read(zipped_file), 'utf-8')
with full_name.open('w', encoding='utf-8') as out_file:
out_file.write(file_xml)
else:
with full_name.open('wb') as out_file:
out_file.write(theme_zip.read(zipped_file))
except (OSError, ValidationError, zipfile.BadZipFile):
2017-09-26 16:39:13 +00:00
self.log_exception('Importing theme from zip failed {name}'.format(name=file_path))
critical_error_message_box(
translate('OpenLP.ThemeManager', 'Import Error'),
2019-08-18 09:15:51 +00:00
translate('OpenLP.ThemeManager', 'There was a problem importing {file_name}.\n\nIt is corrupt, '
'inaccessible or not a valid theme.').format(file_name=file_path))
finally:
2012-03-10 08:22:52 +00:00
if not abort_import:
# TODO: remove XML handling after once the upgrade path from 2.4 is no longer required
# As all files are closed, upgrade theme (xml to json) if needed.
if file_xml and not json_theme:
theme_path = self.theme_path / theme_name
xml_file_paths = theme_path.glob('*.xml')
for xml_file_path in xml_file_paths:
xml_file_path.unlink()
theme = self._create_theme_from_xml(file_xml, self.theme_path)
self.save_theme(theme)
return theme_name
else:
return None
2009-09-13 15:14:45 +00:00
def check_if_theme_exists(self, theme_name):
2009-09-13 15:14:45 +00:00
"""
2010-12-27 10:18:09 +00:00
Check if theme already exists and displays error message
2010-06-10 01:57:59 +00:00
2017-09-26 16:39:13 +00:00
:param str theme_name: Name of the Theme to test
2015-09-08 19:13:59 +00:00
:return: True or False if theme exists
2017-09-26 16:39:13 +00:00
:rtype: bool
2010-12-27 10:18:09 +00:00
"""
2017-09-26 16:39:13 +00:00
if (self.theme_path / theme_name).exists():
critical_error_message_box(
2011-01-15 19:24:50 +00:00
translate('OpenLP.ThemeManager', 'Validation Error'),
2012-12-29 15:25:29 +00:00
translate('OpenLP.ThemeManager', 'A theme with this name already exists.'))
2010-12-27 10:18:09 +00:00
return False
return True
2009-09-13 15:14:45 +00:00
def save_theme(self, theme, background_override=None):
"""
Writes the theme to the disk and including the background image and thumbnail if necessary
2014-01-01 09:33:07 +00:00
2017-09-26 16:39:13 +00:00
:param Theme theme: The theme data object.
:param background_override: Background to use rather than background_source. Optionally.
2017-09-26 16:39:13 +00:00
:rtype: None
"""
2010-11-05 19:20:41 +00:00
name = theme.theme_name
2017-09-26 16:39:13 +00:00
theme_pretty = theme.export_theme(self.theme_path)
theme_dir = self.theme_path / name
2017-10-07 07:05:07 +00:00
create_paths(theme_dir)
2017-09-26 16:39:13 +00:00
theme_path = theme_dir / '{file_name}.json'.format(file_name=name)
2010-12-27 10:18:09 +00:00
try:
2019-02-13 20:54:35 +00:00
theme_path.write_text(theme_pretty)
except OSError:
2013-12-23 07:03:56 +00:00
self.log_exception('Saving theme to file failed')
if theme.background_source and theme.background_filename and theme.background_type != 'stream':
background_file = background_override
# Use theme source image if override doesn't exist
if not background_file or not background_file.exists():
background_file = theme.background_source
if self.old_background_image_path and theme.background_filename != self.old_background_image_path:
2017-09-26 16:39:13 +00:00
delete_file(self.old_background_image_path)
if not background_file.exists():
self.log_warning('Background does not exist, retaining cached background')
elif background_file != theme.background_filename:
2017-09-26 16:39:13 +00:00
try:
shutil.copyfile(background_file, theme.background_filename)
except OSError:
2017-09-26 16:39:13 +00:00
self.log_exception('Failed to save theme image')
2009-09-13 15:14:45 +00:00
def save_preview(self, theme_name, preview_pixmap):
"""
Save the preview QPixmap object to a file
"""
2017-09-26 16:39:13 +00:00
sample_path_name = self.theme_path / '{file_name}.png'.format(file_name=theme_name)
if sample_path_name.exists():
sample_path_name.unlink()
preview_pixmap.save(str(sample_path_name), 'png')
2017-09-26 16:39:13 +00:00
thumb_path = self.thumb_path / '{name}.png'.format(name=theme_name)
2017-11-18 11:23:15 +00:00
create_thumb(sample_path_name, thumb_path, False)
2009-09-13 15:14:45 +00:00
def update_preview_images(self, theme_name_list=None):
"""
Called to update the themes' preview images.
:param theme_name_list: A list of theme names in the theme data folder
"""
theme_name_list = theme_name_list or self.get_theme_names()
self.progress_form.theme_list = theme_name_list
self.progress_form.show()
for theme_name in theme_name_list:
theme_data = self._get_theme_data(theme_name)
preview_pixmap = self.progress_form.get_preview(theme_name, theme_data)
if preview_pixmap is None:
break
self.save_preview(theme_name, preview_pixmap)
self.progress_form.close()
self.load_themes()
2013-10-23 19:25:46 +00:00
def generate_image(self, theme_data, force_page=False):
2009-09-13 15:14:45 +00:00
"""
2011-03-28 18:56:39 +00:00
Call the renderer to build a Sample Image
2014-01-01 09:33:07 +00:00
:param theme_data: The theme to generated a preview for.
2017-11-20 21:57:34 +00:00
:param force_page: Flag to tell message lines per page need to be generated.
2017-11-18 11:23:15 +00:00
:rtype: QtGui.QPixmap
2009-09-13 15:14:45 +00:00
"""
2013-10-23 19:25:46 +00:00
return self.renderer.generate_preview(theme_data, force_page)
2009-09-13 15:14:45 +00:00
2017-05-13 07:47:22 +00:00
@staticmethod
def _create_theme_from_xml(theme_xml, image_path):
2010-06-10 19:45:02 +00:00
"""
Return a theme object using information parsed from XML
2014-01-01 09:33:07 +00:00
:param theme_xml: The Theme data object.
:param Path image_path: Where the theme image is stored
2015-09-08 19:13:59 +00:00
:return: Theme data.
2017-09-26 16:39:13 +00:00
:rtype: Theme
2010-06-10 19:45:02 +00:00
"""
2017-05-13 07:47:22 +00:00
theme = Theme()
theme.parse(theme_xml)
2014-01-01 09:33:07 +00:00
theme.extend_image_filename(image_path)
2009-11-06 02:12:56 +00:00
return theme
2017-09-26 16:39:13 +00:00
def _create_theme_from_json(self, theme_json, image_path):
2017-05-13 07:47:22 +00:00
"""
Return a theme object using information parsed from JSON
:param theme_json: The Theme data object.
:param Path image_path: Where the theme image is stored
2017-05-13 07:47:22 +00:00
:return: Theme data.
2017-09-26 16:39:13 +00:00
:rtype: Theme
2017-05-13 07:47:22 +00:00
"""
theme = Theme()
2017-09-26 16:39:13 +00:00
theme.load_theme(theme_json, self.theme_path)
2017-05-13 07:47:22 +00:00
theme.extend_image_filename(image_path)
return theme
2013-10-23 19:25:46 +00:00
def _validate_theme_action(self, select_text, confirm_title, confirm_text, test_plugin=True, confirm=True):
"""
2013-12-24 06:15:41 +00:00
Check to see if theme has been selected and the destructive action is allowed.
2014-01-01 09:33:07 +00:00
:param select_text: Text for message box if no item selected.
:param confirm_title: Confirm message title to be displayed.
:param confirm_text: Confirm message text to be displayed.
:param test_plugin: Do we check the plugins for theme usage.
:param confirm: Do we display a confirm box before run checks.
2015-09-08 19:13:59 +00:00
:return: True or False depending on the validity.
"""
self.global_theme = self.settings.value('themes/global theme')
if check_item_selected(self.theme_list_widget, select_text):
item = self.theme_list_widget.currentItem()
2012-05-17 18:57:01 +00:00
theme = item.text()
# confirm deletion
2011-02-05 17:31:13 +00:00
if confirm:
2015-11-07 00:49:40 +00:00
answer = QtWidgets.QMessageBox.question(
self, confirm_title, confirm_text.format(theme_name=theme),
defaultButton=QtWidgets.QMessageBox.No)
2015-11-07 00:49:40 +00:00
if answer == QtWidgets.QMessageBox.No:
2011-02-05 17:31:13 +00:00
return False
# should be the same unless default
2012-05-19 15:10:05 +00:00
if theme != item.data(QtCore.Qt.UserRole):
critical_error_message_box(
2012-12-29 15:25:29 +00:00
message=translate('OpenLP.ThemeManager', 'You are unable to delete the default theme.'))
return False
2010-12-27 10:18:09 +00:00
# check for use in the system else where.
2013-10-23 19:25:46 +00:00
if test_plugin:
2015-10-16 16:30:13 +00:00
plugin_usage = ""
2019-07-31 21:01:22 +00:00
for plugin in State().list_plugins():
2015-10-16 16:30:13 +00:00
used_count = plugin.uses_theme(theme)
if used_count:
2016-05-20 16:22:06 +00:00
plugin_usage = "{plug}{text}".format(plug=plugin_usage,
text=(translate('OpenLP.ThemeManager',
'{count} time(s) by {plugin}'
2021-04-25 05:15:37 +00:00
).format(count=used_count,
2016-05-20 16:22:06 +00:00
plugin=plugin.name)))
plugin_usage = "{text}\n".format(text=plugin_usage)
2015-10-16 16:30:13 +00:00
if plugin_usage:
critical_error_message_box(translate('OpenLP.ThemeManager', 'Unable to delete theme'),
2016-05-20 16:22:06 +00:00
translate('OpenLP.ThemeManager',
'Theme is currently used \n\n{text}'
).format(text=plugin_usage))
2015-10-16 16:30:13 +00:00
return False
return True
2014-03-20 19:10:31 +00:00
return False