mirror of https://gitlab.com/openlp/openlp.git
462 lines
24 KiB
Python
462 lines
24 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
##########################################################################
|
|
# OpenLP - Open Source Lyrics Projection #
|
|
# ---------------------------------------------------------------------- #
|
|
# Copyright (c) 2008-2020 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/>. #
|
|
##########################################################################
|
|
import logging
|
|
import os
|
|
import shutil
|
|
|
|
from PyQt5 import QtCore, QtWidgets
|
|
from pathlib import Path
|
|
|
|
from openlp.core.common import sha256_file_hash
|
|
from openlp.core.common.i18n import UiStrings, get_natural_key, translate
|
|
from openlp.core.common.path import path_to_str
|
|
from openlp.core.common.registry import Registry
|
|
from openlp.core.lib import ServiceItemContext, build_icon, check_item_selected, create_thumb, validate_thumb
|
|
from openlp.core.lib.mediamanageritem import MediaManagerItem
|
|
from openlp.core.lib.serviceitem import ItemCapabilities
|
|
from openlp.core.lib.ui import create_horizontal_adjusting_combo_box, critical_error_message_box
|
|
from openlp.core.ui.icons import UiIcons
|
|
from openlp.plugins.presentations.lib.messagelistener import MessageListener
|
|
from openlp.plugins.presentations.lib.pdfcontroller import PDF_CONTROLLER_FILETYPES
|
|
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
class PresentationMediaItem(MediaManagerItem):
|
|
"""
|
|
This is the Presentation media manager item for Presentation Items. It can present files using Openoffice and
|
|
Powerpoint
|
|
"""
|
|
presentations_go_live = QtCore.pyqtSignal(list)
|
|
presentations_add_to_service = QtCore.pyqtSignal(list)
|
|
log.info('Presentations Media Item loaded')
|
|
|
|
def __init__(self, parent, plugin, controllers):
|
|
"""
|
|
Constructor. Setup defaults
|
|
"""
|
|
self.icon_path = 'presentations/presentation'
|
|
self.controllers = controllers
|
|
super(PresentationMediaItem, self).__init__(parent, plugin)
|
|
|
|
def retranslate_ui(self):
|
|
"""
|
|
The name of the plugin media displayed in UI
|
|
"""
|
|
self.on_new_prompt = translate('PresentationPlugin.MediaItem', 'Select Presentation(s)')
|
|
self.automatic = translate('PresentationPlugin.MediaItem', 'Automatic')
|
|
self.display_type_label.setText(translate('PresentationPlugin.MediaItem', 'Present using:'))
|
|
|
|
def setup_item(self):
|
|
"""
|
|
Do some additional setup.
|
|
"""
|
|
self.presentations_go_live.connect(self.go_live_remote)
|
|
self.presentations_add_to_service.connect(self.add_to_service_remote)
|
|
self.message_listener = MessageListener(self)
|
|
self.has_search = True
|
|
self.single_service_item = False
|
|
Registry().register_function('mediaitem_presentation_rebuild', self.populate_display_types)
|
|
Registry().register_function('mediaitem_suffixes', self.build_file_mask_string)
|
|
# Allow DnD from the desktop
|
|
self.list_view.activateDnD()
|
|
|
|
def build_file_mask_string(self):
|
|
"""
|
|
Build the list of file extensions to be used in the Open file dialog.
|
|
"""
|
|
file_type_string = ''
|
|
for controller in self.controllers:
|
|
if self.controllers[controller].enabled():
|
|
file_types = self.controllers[controller].supports + self.controllers[controller].also_supports
|
|
for file_type in file_types:
|
|
if file_type not in file_type_string:
|
|
file_type_string += '*.{text} '.format(text=file_type)
|
|
file_type_string = file_type_string.strip()
|
|
self.service_manager.supported_suffixes(file_type_string.split(' '))
|
|
self.on_new_file_masks = translate('PresentationPlugin.MediaItem',
|
|
'Presentations ({text})').format(text=file_type_string)
|
|
|
|
def required_icons(self):
|
|
"""
|
|
Set which icons the media manager tab should show.
|
|
"""
|
|
MediaManagerItem.required_icons(self)
|
|
self.has_file_icon = True
|
|
self.has_new_icon = False
|
|
self.has_edit_icon = False
|
|
|
|
def add_end_header_bar(self):
|
|
"""
|
|
Display custom media manager items for presentations.
|
|
"""
|
|
self.presentation_widget = QtWidgets.QWidget(self)
|
|
self.presentation_widget.setObjectName('presentation_widget')
|
|
self.display_layout = QtWidgets.QFormLayout(self.presentation_widget)
|
|
self.display_layout.setContentsMargins(self.display_layout.spacing(), self.display_layout.spacing(),
|
|
self.display_layout.spacing(), self.display_layout.spacing())
|
|
self.display_layout.setObjectName('display_layout')
|
|
self.display_type_label = QtWidgets.QLabel(self.presentation_widget)
|
|
self.display_type_label.setObjectName('display_type_label')
|
|
self.display_type_combo_box = create_horizontal_adjusting_combo_box(self.presentation_widget,
|
|
'display_type_combo_box')
|
|
self.display_type_label.setBuddy(self.display_type_combo_box)
|
|
self.display_layout.addRow(self.display_type_label, self.display_type_combo_box)
|
|
# Add the Presentation widget to the page layout.
|
|
self.page_layout.addWidget(self.presentation_widget)
|
|
|
|
def initialise(self):
|
|
"""
|
|
Populate the media manager tab
|
|
"""
|
|
self.list_view.setIconSize(QtCore.QSize(88, 50))
|
|
file_paths = self.settings.value(self.settings_section + '/presentations files')
|
|
self.load_list(file_paths, initial_load=True)
|
|
self.populate_display_types()
|
|
|
|
def populate_display_types(self):
|
|
"""
|
|
Load the combobox with the enabled presentation controllers, allowing user to select a specific app if settings
|
|
allow.
|
|
"""
|
|
self.display_type_combo_box.clear()
|
|
for item in self.controllers:
|
|
# For PDF reload backend, since it can have changed
|
|
if self.controllers[item].name == 'Pdf':
|
|
self.controllers[item].check_available()
|
|
# load the drop down selection
|
|
if self.controllers[item].enabled():
|
|
self.display_type_combo_box.addItem(item)
|
|
if self.display_type_combo_box.count() > 1:
|
|
self.display_type_combo_box.insertItem(0, self.automatic, userData='automatic')
|
|
self.display_type_combo_box.setCurrentIndex(0)
|
|
if self.settings.value(self.settings_section + '/override app') == QtCore.Qt.Checked:
|
|
self.presentation_widget.show()
|
|
else:
|
|
self.presentation_widget.hide()
|
|
|
|
def load_list(self, file_paths, target_group=None, initial_load=False):
|
|
"""
|
|
Add presentations into the media manager. This is called both on initial load of the plugin to populate with
|
|
existing files, and when the user adds new files via the media manager.
|
|
|
|
:param list[pathlib.Path] file_paths: List of file paths to add to the media manager.
|
|
"""
|
|
current_paths = self.get_file_list()
|
|
titles = [file_path.name for file_path in current_paths]
|
|
self.application.set_busy_cursor()
|
|
if not initial_load:
|
|
self.main_window.display_progress_bar(len(file_paths))
|
|
# Sort the presentations by its filename considering language specific characters.
|
|
file_paths.sort(key=lambda file_path: get_natural_key(file_path.name))
|
|
for file_path in file_paths:
|
|
if not initial_load:
|
|
self.main_window.increment_progress_bar()
|
|
if current_paths.count(file_path) > 0:
|
|
continue
|
|
file_name = file_path.name
|
|
if not file_path.exists():
|
|
item_name = QtWidgets.QListWidgetItem(file_name)
|
|
item_name.setIcon(UiIcons().delete)
|
|
item_name.setData(QtCore.Qt.UserRole, file_path)
|
|
item_name.setToolTip(str(file_path))
|
|
self.list_view.addItem(item_name)
|
|
else:
|
|
if titles.count(file_name) > 0:
|
|
if not initial_load:
|
|
critical_error_message_box(translate('PresentationPlugin.MediaItem', 'File Exists'),
|
|
translate('PresentationPlugin.MediaItem',
|
|
'A presentation with that filename already exists.'))
|
|
continue
|
|
controller_name = self.find_controller_by_type(file_path)
|
|
if controller_name:
|
|
controller = self.controllers[controller_name]
|
|
doc = controller.add_document(file_path)
|
|
thumbnail_path = doc.get_thumbnail_folder() / 'icon.png'
|
|
preview_path = doc.get_thumbnail_path(1, True)
|
|
if not preview_path and not initial_load:
|
|
doc.load_presentation()
|
|
preview_path = doc.get_thumbnail_path(1, True)
|
|
doc.close_presentation()
|
|
if not (preview_path and preview_path.exists()):
|
|
icon = UiIcons().delete
|
|
else:
|
|
if validate_thumb(preview_path, thumbnail_path):
|
|
icon = build_icon(thumbnail_path)
|
|
else:
|
|
icon = create_thumb(preview_path, thumbnail_path)
|
|
else:
|
|
if initial_load:
|
|
icon = UiIcons().delete
|
|
else:
|
|
critical_error_message_box(UiStrings().UnsupportedFile,
|
|
translate('PresentationPlugin.MediaItem',
|
|
'This type of presentation is not supported.'))
|
|
continue
|
|
item_name = QtWidgets.QListWidgetItem(file_name)
|
|
item_name.setData(QtCore.Qt.UserRole, file_path)
|
|
item_name.setIcon(icon)
|
|
item_name.setToolTip(str(file_path))
|
|
self.list_view.addItem(item_name)
|
|
if not initial_load:
|
|
self.main_window.finished_progress_bar()
|
|
self.application.set_normal_cursor()
|
|
|
|
def on_delete_click(self):
|
|
"""
|
|
Remove a presentation item from the list.
|
|
"""
|
|
if check_item_selected(self.list_view, UiStrings().SelectDelete):
|
|
items = self.list_view.selectedIndexes()
|
|
row_list = [item.row() for item in items]
|
|
row_list.sort(reverse=True)
|
|
self.application.set_busy_cursor()
|
|
self.main_window.display_progress_bar(len(row_list))
|
|
for item in items:
|
|
self.clean_up_thumbnails(item.data(QtCore.Qt.UserRole))
|
|
self.main_window.increment_progress_bar()
|
|
self.main_window.finished_progress_bar()
|
|
for row in row_list:
|
|
self.list_view.takeItem(row)
|
|
self.settings.setValue(self.settings_section + '/presentations files', self.get_file_list())
|
|
self.application.set_normal_cursor()
|
|
|
|
def clean_up_thumbnails(self, file_path, clean_for_update=False):
|
|
"""
|
|
Clean up the files created such as thumbnails
|
|
|
|
:param pathlib.Path file_path: File path of the presentation to clean up after
|
|
:param bool clean_for_update: Only clean thumbnails if update is needed
|
|
:rtype: None
|
|
"""
|
|
for cidx in self.controllers:
|
|
if not self.controllers[cidx].enabled():
|
|
# skip presentation controllers that are not enabled
|
|
continue
|
|
file_ext = file_path.suffix[1:]
|
|
if file_ext in self.controllers[cidx].supports or file_ext in self.controllers[cidx].also_supports:
|
|
doc = self.controllers[cidx].add_document(file_path)
|
|
if clean_for_update:
|
|
thumb_path = doc.get_thumbnail_path(1, True)
|
|
if not thumb_path or not file_path.exists() or \
|
|
thumb_path.stat().st_mtime < file_path.stat().st_mtime:
|
|
doc.presentation_deleted()
|
|
else:
|
|
doc.presentation_deleted()
|
|
doc.close_presentation()
|
|
|
|
def update_thumbnail_scheme(self, file_path):
|
|
"""
|
|
Update the thumbnail folder naming scheme to the new sha256 based one.
|
|
"""
|
|
# TODO: Can be removed when the upgrade path to OpenLP 3.0 is no longer needed, also ensure code in
|
|
# PresentationDocument.get_thumbnail_folder and PresentationDocument.get_temp_folder is removed
|
|
for cidx in self.controllers:
|
|
if not self.controllers[cidx].enabled():
|
|
# skip presentation controllers that are not enabled
|
|
continue
|
|
file_ext = file_path.suffix[1:]
|
|
if file_ext in self.controllers[cidx].supports or file_ext in self.controllers[cidx].also_supports:
|
|
doc = self.controllers[cidx].add_document(file_path)
|
|
# Check if the file actually exists
|
|
if file_path.exists():
|
|
thumb_path = doc.get_thumbnail_folder()
|
|
hash = sha256_file_hash(file_path)
|
|
# Rename the thumbnail folder so that it uses the sha256 naming scheme
|
|
if thumb_path.exists():
|
|
new_folder = Path(os.path.split(thumb_path)[0]) / hash
|
|
log.info('Moved thumbnails from {md5} to {sha256}'.format(md5=str(thumb_path),
|
|
sha256=str(new_folder)))
|
|
shutil.move(thumb_path, new_folder)
|
|
# Rename the data folder, if one exists
|
|
old_folder = doc.get_temp_folder()
|
|
if old_folder.exists():
|
|
new_folder = Path(os.path.split(old_folder)[0]) / hash
|
|
log.info('Moved data from {md5} to {sha256}'.format(md5=str(old_folder),
|
|
sha256=str(new_folder)))
|
|
shutil.move(old_folder, new_folder)
|
|
|
|
def generate_slide_data(self, service_item, *, item=None, remote=False, context=ServiceItemContext.Service,
|
|
file_path=None, **kwargs):
|
|
"""
|
|
Generate the slide data. Needs to be implemented by the plugin.
|
|
|
|
:param service_item: The service item to be built on
|
|
:param item: The Song item to be used
|
|
:param remote: Triggered from remote
|
|
:param context: Why is it being generated
|
|
:param file_path: Path for the file to be processes
|
|
:param kwargs: Consume other unused args specified by the base implementation, but not use by this one.
|
|
"""
|
|
if item:
|
|
items = [item]
|
|
else:
|
|
items = self.list_view.selectedItems()
|
|
if len(items) > 1:
|
|
return False
|
|
if file_path is None:
|
|
file_path = Path(items[0].data(QtCore.Qt.UserRole))
|
|
file_type = file_path.suffix.lower()[1:]
|
|
if not self.display_type_combo_box.currentText():
|
|
return False
|
|
service_item.add_capability(ItemCapabilities.CanEditTitle)
|
|
if file_type in PDF_CONTROLLER_FILETYPES and context != ServiceItemContext.Service:
|
|
service_item.add_capability(ItemCapabilities.CanMaintain)
|
|
service_item.add_capability(ItemCapabilities.CanPreview)
|
|
service_item.add_capability(ItemCapabilities.CanLoop)
|
|
service_item.add_capability(ItemCapabilities.CanAppend)
|
|
service_item.name = 'images'
|
|
# force a nonexistent theme
|
|
service_item.theme = -1
|
|
for bitem in items:
|
|
if file_path is None:
|
|
file_path = bitem.data(QtCore.Qt.UserRole)
|
|
path, file_name = file_path.parent, file_path.name
|
|
service_item.title = file_name
|
|
if file_path.exists():
|
|
processor = self.find_controller_by_type(file_path)
|
|
if not processor:
|
|
return False
|
|
controller = self.controllers[processor]
|
|
service_item.processor = None
|
|
doc = controller.add_document(file_path)
|
|
if doc.get_thumbnail_path(1, True) is None or \
|
|
not (doc.get_temp_folder() / 'mainslide001.png').is_file():
|
|
doc.load_presentation()
|
|
i = 1
|
|
image_path = doc.get_temp_folder() / 'mainslide{number:0>3d}.png'.format(number=i)
|
|
thumbnail_path = doc.get_thumbnail_folder() / 'slide{number:d}.png'.format(number=i)
|
|
while image_path.is_file():
|
|
service_item.add_from_image(image_path, file_name, thumbnail=thumbnail_path)
|
|
i += 1
|
|
image_path = doc.get_temp_folder() / 'mainslide{number:0>3d}.png'.format(number=i)
|
|
thumbnail_path = doc.get_thumbnail_folder() / 'slide{number:d}.png'.format(number=i)
|
|
service_item.add_capability(ItemCapabilities.HasThumbnails)
|
|
doc.close_presentation()
|
|
service_item.validate_item()
|
|
return True
|
|
else:
|
|
# File is no longer present
|
|
if not remote:
|
|
critical_error_message_box(translate('PresentationPlugin.MediaItem', 'Missing Presentation'),
|
|
translate('PresentationPlugin.MediaItem',
|
|
'The presentation {name} no longer exists.'
|
|
).format(name=file_path))
|
|
return False
|
|
else:
|
|
service_item.processor = self.display_type_combo_box.currentText()
|
|
service_item.add_capability(ItemCapabilities.ProvidesOwnDisplay)
|
|
for bitem in items:
|
|
file_path = Path(bitem.data(QtCore.Qt.UserRole))
|
|
path, file_name = file_path.parent, file_path.name
|
|
service_item.title = file_name
|
|
if file_path.exists():
|
|
if self.display_type_combo_box.itemData(self.display_type_combo_box.currentIndex()) == 'automatic':
|
|
service_item.processor = self.find_controller_by_type(file_path)
|
|
if not service_item.processor:
|
|
return False
|
|
controller = self.controllers[service_item.processor]
|
|
doc = controller.add_document(file_path)
|
|
if doc.get_thumbnail_path(1, True) is None:
|
|
doc.load_presentation()
|
|
i = 1
|
|
thumbnail_path = doc.get_thumbnail_path(i, True)
|
|
if thumbnail_path:
|
|
# Get titles and notes
|
|
titles, notes = doc.get_titles_and_notes()
|
|
service_item.add_capability(ItemCapabilities.HasDisplayTitle)
|
|
if notes.count('') != len(notes):
|
|
service_item.add_capability(ItemCapabilities.HasNotes)
|
|
service_item.add_capability(ItemCapabilities.HasThumbnails)
|
|
while thumbnail_path:
|
|
# Use title and note if available
|
|
title = ''
|
|
if titles and len(titles) >= i:
|
|
title = titles[i - 1]
|
|
note = ''
|
|
if notes and len(notes) >= i:
|
|
note = notes[i - 1]
|
|
service_item.add_from_command(str(path), file_name, thumbnail_path, title, note,
|
|
doc.get_sha256_file_hash())
|
|
i += 1
|
|
thumbnail_path = doc.get_thumbnail_path(i, True)
|
|
doc.close_presentation()
|
|
service_item.validate_item()
|
|
return True
|
|
else:
|
|
# File is no longer present
|
|
if not remote:
|
|
critical_error_message_box(translate('PresentationPlugin.MediaItem',
|
|
'Missing Presentation'),
|
|
translate('PresentationPlugin.MediaItem',
|
|
'The presentation {name} is incomplete, '
|
|
'please reload.').format(name=file_path))
|
|
return False
|
|
else:
|
|
# File is no longer present
|
|
if not remote:
|
|
critical_error_message_box(translate('PresentationPlugin.MediaItem', 'Missing Presentation'),
|
|
translate('PresentationPlugin.MediaItem',
|
|
'The presentation {name} no longer exists.'
|
|
).format(name=file_path))
|
|
return False
|
|
|
|
def find_controller_by_type(self, file_path):
|
|
"""
|
|
Determine the default application controller to use for the selected file type. This is used if "Automatic" is
|
|
set as the preferred controller. Find the first (alphabetic) enabled controller which "supports" the extension.
|
|
If none found, then look for a controller which "also supports" it instead.
|
|
|
|
:param pathlib.Path file_path: The file path
|
|
:return: The default application controller for this file type, or None if not supported
|
|
:rtype: PresentationController
|
|
"""
|
|
file_type = file_path.suffix[1:]
|
|
if not file_type:
|
|
return None
|
|
for controller in self.controllers:
|
|
if self.controllers[controller].enabled():
|
|
if file_type in self.controllers[controller].supports:
|
|
return controller
|
|
for controller in self.controllers:
|
|
if self.controllers[controller].enabled():
|
|
if file_type in self.controllers[controller].also_supports:
|
|
return controller
|
|
return None
|
|
|
|
def search(self, string, show_error):
|
|
"""
|
|
Search in files
|
|
|
|
:param string: name to be found
|
|
:param show_error: not used
|
|
:return:
|
|
"""
|
|
file_paths = self.settings.value(self.settings_section + '/presentations files')
|
|
results = []
|
|
string = string.lower()
|
|
for file_path in file_paths:
|
|
file_name = file_path.name
|
|
if file_name.lower().find(string) > -1:
|
|
results.append([path_to_str(file_path), file_name])
|
|
return results
|