# -*- coding: utf-8 -*- ########################################################################## # OpenLP - Open Source Lyrics Projection # # ---------------------------------------------------------------------- # # Copyright (c) 2008-2023 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 . # ########################################################################## """ 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.plugin import StringContent from openlp.core.lib.ui import create_widget_action, 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 create_item_from_id(self, item_id): """ Create a media item from an item id. :param item_id: Id to make live """ Item = self.item_class if isinstance(item_id, (str, Path)): # Probably a file name item_data = self.manager.get_object_filtered(Item, Item.file_path == str(item_id)) else: item_data = item_id item = QtWidgets.QTreeWidgetItem() item.setData(0, QtCore.Qt.UserRole, item_data) return item 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) if self.has_edit_icon: create_widget_action( self.list_view, text=self.plugin.get_string(StringContent.Edit)['title'], icon=UiIcons().edit, triggers=self.on_edit_click) create_widget_action(self.list_view, separator=True) create_widget_action( self.list_view, 'listView{name}{preview}Item'.format(name=self.plugin.name.title(), preview=StringContent.Preview.title()), text=self.plugin.get_string(StringContent.Preview)['title'], icon=UiIcons().preview, can_shortcuts=True, triggers=self.on_preview_click) create_widget_action( self.list_view, 'listView{name}{live}Item'.format(name=self.plugin.name.title(), live=StringContent.Live.title()), text=self.plugin.get_string(StringContent.Live)['title'], icon=UiIcons().live, can_shortcuts=True, triggers=self.on_live_click) create_widget_action( self.list_view, 'listView{name}{service}Item'.format(name=self.plugin.name.title(), service=StringContent.Service.title()), can_shortcuts=True, text=self.plugin.get_string(StringContent.Service)['title'], icon=UiIcons().add, triggers=self.on_add_click) if self.add_to_service_item: create_widget_action(self.list_view, separator=True) create_widget_action( self.list_view, text=translate('OpenLP.MediaManagerItem', '&Add to selected Service Item'), icon=UiIcons().add, triggers=self.on_add_edit_click) create_widget_action(self.list_view, separator=True) if self.has_delete_icon: create_widget_action( self.list_view, 'listView{name}{delete}Item'.format(name=self.plugin.name.title(), delete=StringContent.Delete.title()), text=self.plugin.get_string(StringContent.Delete)['title'], icon=UiIcons().delete, can_shortcuts=True, triggers=self.on_delete_click) self.add_custom_context_actions() # Create the context menu and add all actions from the list_view. self.menu = QtWidgets.QMenu() self.menu.addActions(self.list_view.actions()) self.list_view.doubleClicked.connect(self.on_double_clicked) self.list_view.itemSelectionChanged.connect(self.on_selection_change) self.list_view.customContextMenuRequested.connect(self.context_menu) def add_custom_context_actions(self): """ Override this method to add custom context actions """ pass 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 or item.folder_id not in folder_items: 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.ilike('%' + 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)