forked from openlp/openlp
Raoul Snyman
4d3ade20c7
- Make a derivative MediaManagerItem class for generic folder support - Make add and choose folder dialogs based on image plugin dialogs - Implement folder and item mixins to get db models "for free" - Implement database layer for media plugin - Implement database layer for presentations plugins - Refactor media plugin to inherit from FolderLibraryItem - Refactor presentations plugin to inherit from FolderLibraryItem - Migrate media files from settings to database - Migrate presentations files from settings to database - Convert the load icons in the media plugin into a dropdown - Add new tests for MediaMediaItem.get_list() - Closes #165 - Closes #223 - Closes #224 - Closes #582
388 lines
18 KiB
Python
388 lines
18 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
##########################################################################
|
|
# OpenLP - Open Source Lyrics Projection #
|
|
# ---------------------------------------------------------------------- #
|
|
# Copyright (c) 2008-2021 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/>. #
|
|
##########################################################################
|
|
"""
|
|
Provides additional classes for working in the library
|
|
"""
|
|
import os
|
|
from pathlib import Path
|
|
|
|
from PyQt5 import QtCore, QtWidgets
|
|
|
|
from openlp.core.common import sha256_file_hash
|
|
from openlp.core.common.i18n import UiStrings, get_natural_key, translate
|
|
from openlp.core.lib import check_item_selected
|
|
from openlp.core.lib.mediamanageritem import MediaManagerItem
|
|
from openlp.core.lib.ui import critical_error_message_box
|
|
from openlp.core.ui.folders import AddFolderForm, ChooseFolderForm
|
|
from openlp.core.ui.icons import UiIcons
|
|
from openlp.core.widgets.views import TreeWidgetWithDnD
|
|
|
|
|
|
class FolderLibraryItem(MediaManagerItem):
|
|
"""
|
|
This is a custom MediaManagerItem subclass with support for folders
|
|
"""
|
|
def __init__(self, parent, plugin, folder_class, item_class):
|
|
super(FolderLibraryItem, self).__init__(parent, plugin)
|
|
self.manager = self.plugin.manager
|
|
self.choose_folder_form = ChooseFolderForm(self, self.manager, folder_class)
|
|
self.add_folder_form = AddFolderForm(self, self.manager, folder_class)
|
|
self.folder_class = folder_class
|
|
self.item_class = item_class
|
|
|
|
@property
|
|
def current_folder(self):
|
|
"""
|
|
Returns the currently active folder, or None
|
|
"""
|
|
selected_items = self.list_view.selectedItems()
|
|
selected_folder = None
|
|
if selected_items:
|
|
selected_item = selected_items[0]
|
|
if isinstance(selected_item.data(0, QtCore.Qt.UserRole), self.item_class):
|
|
selected_item = selected_item.parent()
|
|
if isinstance(selected_item, QtWidgets.QTreeWidgetItem) and \
|
|
isinstance(selected_item.data(0, QtCore.Qt.UserRole), self.folder_class):
|
|
selected_folder = selected_item.data(0, QtCore.Qt.UserRole)
|
|
return selected_folder
|
|
|
|
def retranslate_ui(self):
|
|
"""
|
|
This method is called automatically to provide OpenLP with the opportunity to translate the ``MediaManagerItem``
|
|
to another language.
|
|
"""
|
|
self.add_folder_action.setText(UiStrings().AddFolder)
|
|
self.add_folder_action.setToolTip(UiStrings().AddFolderDot)
|
|
|
|
def on_add_folder_click(self):
|
|
"""
|
|
Called to add a new folder
|
|
"""
|
|
# Syntactic sugar, plus a minor performance optimisation
|
|
Item = self.item_class
|
|
|
|
if self.add_folder_form.exec(show_top_level_folder=True, selected_folder=self.current_folder):
|
|
new_folder = self.add_folder_form.new_folder
|
|
if self.manager.save_object(new_folder):
|
|
self.load_list(self.manager.get_all_objects(Item, order_by_ref=Item.file_path))
|
|
self.expand_folder(new_folder.id)
|
|
else:
|
|
critical_error_message_box(
|
|
message=translate('OpenLP.FolderLibraryItem', 'Could not add the new folder.'))
|
|
|
|
def on_delete_click(self):
|
|
"""
|
|
Remove an item from the list.
|
|
"""
|
|
# Syntactic sugar, plus a minor performance optimisation
|
|
Folder, Item = self.folder_class, self.item_class
|
|
|
|
# Turn off auto preview triggers.
|
|
self.list_view.blockSignals(True)
|
|
if check_item_selected(self.list_view,
|
|
translate('OpenLP.FolderLibraryItem', 'You must select an item or folder to delete.')):
|
|
tree_item_list = self.list_view.selectedItems()
|
|
self.application.set_busy_cursor()
|
|
self.main_window.display_progress_bar(len(tree_item_list))
|
|
for tree_item in tree_item_list:
|
|
if not tree_item:
|
|
self.main_window.increment_progress_bar()
|
|
continue
|
|
item = tree_item.data(0, QtCore.Qt.UserRole)
|
|
if isinstance(item, Item):
|
|
self.delete_item(item)
|
|
if not item.folder_id:
|
|
self.list_view.takeTopLevelItem(self.list_view.indexOfTopLevelItem(tree_item))
|
|
else:
|
|
tree_item.parent().removeChild(tree_item)
|
|
self.manager.delete_object(Item, item.id)
|
|
elif isinstance(item, Folder):
|
|
if QtWidgets.QMessageBox.question(
|
|
self.list_view.parent(),
|
|
translate('OpenLP.FolderLibraryItem', 'Remove folder'),
|
|
translate('OpenLP.FolderLibraryItem',
|
|
'Are you sure you want to remove "{name}" and everything in it?'
|
|
).format(name=item.name)
|
|
) == QtWidgets.QMessageBox.Yes:
|
|
self.recursively_delete_folder(item)
|
|
self.manager.delete_object(Folder, item.id)
|
|
if item.parent_id is None:
|
|
self.list_view.takeTopLevelItem(self.list_view.indexOfTopLevelItem(tree_item))
|
|
else:
|
|
tree_item.parent().removeChild(tree_item)
|
|
self.main_window.increment_progress_bar()
|
|
self.main_window.finished_progress_bar()
|
|
self.application.set_normal_cursor()
|
|
self.list_view.blockSignals(False)
|
|
|
|
def add_list_view_to_toolbar(self):
|
|
"""
|
|
Creates the main widget for listing items.
|
|
"""
|
|
# Add the List widget
|
|
self.list_view = TreeWidgetWithDnD(self, self.plugin.name)
|
|
self.list_view.setObjectName('{name}TreeView'.format(name=self.plugin.name))
|
|
# Add to pageLayout
|
|
self.page_layout.addWidget(self.list_view)
|
|
# define and add the context menu
|
|
self.list_view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
|
|
|
def add_middle_header_bar(self):
|
|
"""
|
|
Add buttons after the main buttons
|
|
"""
|
|
self.add_folder_action = self.toolbar.add_toolbar_action(
|
|
'add_folder_action', icon=UiIcons().folder, triggers=self.on_add_folder_click)
|
|
|
|
def add_sub_folders(self, folder_list, parent_id=None):
|
|
"""
|
|
Recursively add subfolders to the given parent folder in a QTreeWidget.
|
|
|
|
:param folder_list: The List object that contains all QTreeWidgetItems.
|
|
:param parent_folder_id: The ID of the folder that will be added recursively.
|
|
"""
|
|
# Syntactic sugar, plus a minor performance optimisation
|
|
Folder = self.folder_class
|
|
|
|
folders = self.manager.get_all_objects(Folder, Folder.parent_id == parent_id)
|
|
folders.sort(key=lambda folder_object: get_natural_key(folder_object.name))
|
|
folder_icon = UiIcons().folder
|
|
for folder in folders:
|
|
folder_item = QtWidgets.QTreeWidgetItem()
|
|
folder_item.setText(0, folder.name)
|
|
folder_item.setData(0, QtCore.Qt.UserRole, folder)
|
|
folder_item.setIcon(0, folder_icon)
|
|
if parent_id is None:
|
|
self.list_view.addTopLevelItem(folder_item)
|
|
else:
|
|
folder_list[parent_id].addChild(folder_item)
|
|
folder_list[folder.id] = folder_item
|
|
self.add_sub_folders(folder_list, folder.id)
|
|
|
|
def expand_folder(self, folder_id, root_item=None):
|
|
"""
|
|
Expand folders in the widget recursively.
|
|
|
|
:param folder_id: The ID of the folder that will be expanded.
|
|
:param root_item: This option is only used for recursion purposes.
|
|
"""
|
|
return_value = False
|
|
if root_item is None:
|
|
root_item = self.list_view.invisibleRootItem()
|
|
for i in range(root_item.childCount()):
|
|
child = root_item.child(i)
|
|
if self.expand_folder(folder_id, child):
|
|
child.setExpanded(True)
|
|
return_value = True
|
|
if isinstance(root_item.data(0, QtCore.Qt.UserRole), self.folder_class):
|
|
if root_item.data(0, QtCore.Qt.UserRole).id == folder_id:
|
|
return True
|
|
return return_value
|
|
|
|
def recursively_delete_folder(self, folder):
|
|
"""
|
|
Recursively deletes a folder and all folders and items in it.
|
|
|
|
:param folder: The Folder instance of the folder that will be deleted.
|
|
"""
|
|
# Syntactic sugar, plus a minor performance optimisation
|
|
Folder, Item = self.folder_class, self.item_class
|
|
|
|
items = self.manager.get_all_objects(Item, Item.folder_id == folder.id)
|
|
for item in items:
|
|
self.delete_item(item)
|
|
self.manager.delete_object(Item, item.id)
|
|
folders = self.manager.get_all_objects(Folder, Folder.parent_id == folder.id)
|
|
for child in folders:
|
|
self.recursively_delete_folder(child)
|
|
self.manager.delete_object(Folder, child.id)
|
|
|
|
def file_to_item(self, filename):
|
|
"""
|
|
This method allows the media item to convert a string filename into an item class
|
|
|
|
Override this method to customise your plugin's loading method
|
|
"""
|
|
if isinstance(filename, Path):
|
|
name = filename.name
|
|
filename = str(filename)
|
|
else:
|
|
name = os.path.basename(filename)
|
|
item = self.item_class(name=name, file_path=filename)
|
|
self.manager.save_object(item)
|
|
return item
|
|
|
|
def load_item(self, item, is_initial_load=False):
|
|
"""
|
|
This method allows the media item to set up the QTreeWidgetItem the way it wants
|
|
"""
|
|
raise NotImplementedError('load_item needs to be implemented by the descendant class')
|
|
|
|
def delete_item(self, item):
|
|
"""
|
|
This method allows the media item to delete the Item
|
|
"""
|
|
raise NotImplementedError('delete_item needs to be implemented by the descendant class')
|
|
|
|
def load_list(self, items, is_initial_load=False, target_folder=None):
|
|
"""
|
|
Load the list of items into the tree view
|
|
|
|
:param items: The items to load
|
|
:param target_folder: The folder to load the items into
|
|
"""
|
|
if not is_initial_load:
|
|
self.application.set_busy_cursor()
|
|
self.main_window.display_progress_bar(len(items))
|
|
self.list_view.clear()
|
|
# Load the list of folders and add them to the tree view
|
|
folder_items = {}
|
|
self.add_sub_folders(folder_items, parent_id=None)
|
|
if target_folder is not None:
|
|
self.expand_folder(target_folder.id)
|
|
# Convert any filenames to items
|
|
for counter, filename in enumerate(items):
|
|
if isinstance(filename, (Path, str)):
|
|
items[counter] = self.file_to_item(filename)
|
|
# Sort the files by the filename
|
|
items.sort(key=lambda item: get_natural_key(item if isinstance(item, str) else item.file_path))
|
|
for item in items:
|
|
self.log_debug('Loading item: {name}'.format(name=item.file_path))
|
|
tree_item = self.load_item(item, is_initial_load)
|
|
if not tree_item:
|
|
continue
|
|
elif not item.folder_id:
|
|
self.list_view.addTopLevelItem(tree_item)
|
|
else:
|
|
folder_items[item.folder_id].addChild(tree_item)
|
|
if not is_initial_load:
|
|
self.main_window.increment_progress_bar()
|
|
if not is_initial_load:
|
|
self.main_window.finished_progress_bar()
|
|
self.application.set_normal_cursor()
|
|
|
|
def format_search_result(self, item):
|
|
"""
|
|
Format an item for the search results. The default implementation simply returns
|
|
[item.file_path, item.file_path]
|
|
|
|
:param Item item: An Item to be formatted
|
|
:return list[str, str]: A list of two items containing the full path and a pretty name
|
|
"""
|
|
return [item.file_path, item.file_path]
|
|
|
|
def search(self, string, show_error):
|
|
"""
|
|
Performs a search for items containing ``string``
|
|
|
|
:param string: String to be displayed
|
|
:param show_error: Should the error be shown (True)
|
|
:return: The search result.
|
|
"""
|
|
string = string.lower()
|
|
items = self.manager.get_all_objects(self.item_class, self.item_class.file_path.match(string))
|
|
return [self.format_search_result(item) for item in items]
|
|
|
|
def validate_and_load(self, file_paths, target_folder=None):
|
|
"""
|
|
Process a list for files either from the File Dialog or from Drag and Drop.
|
|
This method is overloaded from MediaManagerItem.
|
|
|
|
:param list[Path] file_paths: A List of paths to be loaded
|
|
:param target_group: The QTreeWidgetItem of the group that will be the parent of the added files
|
|
"""
|
|
self.application.set_normal_cursor()
|
|
if target_folder:
|
|
target_folder = target_folder.data(0, QtCore.Qt.UserRole)
|
|
elif self.current_folder:
|
|
target_folder = self.current_folder
|
|
if not target_folder and self.choose_folder_form.exec() == QtWidgets.QDialog.Accepted:
|
|
target_folder = self.choose_folder_form.folder
|
|
if self.choose_folder_form.is_new_folder:
|
|
self.manager.save_object(target_folder)
|
|
existing_files = [item.file_path for item in self.manager.get_all_objects(self.item_class)]
|
|
# Convert file paths to items
|
|
for file_path in file_paths:
|
|
if file_paths.count(file_path) > 1 or existing_files.count(file_path) > 0:
|
|
# If a file already exists in the items or has been selected twice, show an error message
|
|
critical_error_message_box(translate('OpenLP.FolderLibraryItem', 'File Exists'),
|
|
translate('OpenLP.FolderLibraryItem',
|
|
'An item with that filename already exists.'))
|
|
continue
|
|
|
|
self.log_debug('Adding new item: {name}'.format(name=file_path))
|
|
item = self.item_class(name=str(file_path), file_path=str(file_path))
|
|
if isinstance(file_path, Path) and file_path.exists():
|
|
item.file_hash = sha256_file_hash(file_path)
|
|
if target_folder:
|
|
item.folder_id = target_folder.id
|
|
self.manager.save_object(item)
|
|
self.main_window.increment_progress_bar()
|
|
self.load_list(self.manager.get_all_objects(self.item_class, order_by_ref=self.item_class.file_path),
|
|
target_folder=target_folder)
|
|
last_dir = Path(file_paths[0]).parent
|
|
self.settings.setValue(self.settings_section + '/last directory', last_dir)
|
|
|
|
def dnd_move_internal(self, target):
|
|
"""
|
|
Handle drag-and-drop moving of items within the media manager
|
|
|
|
:param target: This contains the QTreeWidget that is the target of the DnD action
|
|
"""
|
|
items_to_move = self.list_view.selectedItems()
|
|
# Determine group to move images to
|
|
target_folder = target
|
|
if target_folder is not None and isinstance(target_folder.data(0, QtCore.Qt.UserRole), self.item_class):
|
|
target_folder = target.parent()
|
|
# Move to toplevel
|
|
if target_folder is None:
|
|
target_folder = self.list_view.invisibleRootItem()
|
|
target_folder.setData(0, QtCore.Qt.UserRole, self.folder_class())
|
|
target_folder.data(0, QtCore.Qt.UserRole).id = 0
|
|
# Move images in the treeview
|
|
items_to_save = []
|
|
for item in items_to_move:
|
|
if isinstance(item.data(0, QtCore.Qt.UserRole), self.item_class):
|
|
if isinstance(item.parent(), QtWidgets.QTreeWidgetItem):
|
|
item.parent().removeChild(item)
|
|
else:
|
|
self.list_view.invisibleRootItem().removeChild(item)
|
|
target_folder.addChild(item)
|
|
item.setSelected(True)
|
|
item_data = item.data(0, QtCore.Qt.UserRole)
|
|
item_data.folder_id = target_folder.data(0, QtCore.Qt.UserRole).id
|
|
items_to_save.append(item_data)
|
|
target_folder.setExpanded(True)
|
|
# Update the folder ID's of the items in the database
|
|
self.manager.save_objects(items_to_save)
|
|
# Sort the target folder
|
|
sort_folders = []
|
|
sort_items = []
|
|
for item in target_folder.takeChildren():
|
|
if isinstance(item.data(0, QtCore.Qt.UserRole), self.folder_class):
|
|
sort_folders.append(item)
|
|
if isinstance(item.data(0, QtCore.Qt.UserRole), self.item_class):
|
|
sort_items.append(item)
|
|
sort_folders.sort(key=lambda item: get_natural_key(item.text(0)))
|
|
target_folder.addChildren(sort_folders)
|
|
sort_items.sort(key=lambda item: get_natural_key(item.text(0)))
|
|
target_folder.addChildren(sort_items)
|