diff --git a/openlp/core/lib/__init__.py b/openlp/core/lib/__init__.py index 08b675000..0babbc0d1 100644 --- a/openlp/core/lib/__init__.py +++ b/openlp/core/lib/__init__.py @@ -29,9 +29,10 @@ import os import re import math -from PyQt5 import QtCore, QtGui, Qt, QtWidgets +from PyQt5 import QtCore, QtGui, QtWidgets from openlp.core.common import translate +from openlp.core.common.path import Path log = logging.getLogger(__name__ + '.__init__') @@ -125,10 +126,11 @@ def build_icon(icon): Build a QIcon instance from an existing QIcon, a resource location, or a physical file location. If the icon is a QIcon instance, that icon is simply returned. If not, it builds a QIcon instance from the resource or file name. - :param icon: - The icon to build. This can be a QIcon, a resource string in the form ``:/resource/file.png``, or a file - location like ``/path/to/file.png``. However, the **recommended** way is to specify a resource string. + :param QtGui.QIcon | Path | QtGui.QIcon | str icon: + The icon to build. This can be a QIcon, a resource string in the form ``:/resource/file.png``, or a file path + location like ``Path(/path/to/file.png)``. However, the **recommended** way is to specify a resource string. :return: The build icon. + :rtype: QtGui.QIcon """ if isinstance(icon, QtGui.QIcon): return icon @@ -136,6 +138,8 @@ def build_icon(icon): button_icon = QtGui.QIcon() if isinstance(icon, str): pix_map = QtGui.QPixmap(icon) + elif isinstance(icon, Path): + pix_map = QtGui.QPixmap(str(icon)) elif isinstance(icon, QtGui.QImage): pix_map = QtGui.QPixmap.fromImage(icon) if pix_map: @@ -221,10 +225,12 @@ def validate_thumb(file_path, thumb_path): :param thumb_path: The path to the thumb. :return: True, False if the image has changed since the thumb was created. """ - if not os.path.exists(thumb_path): + file_path = Path(file_path) + thumb_path = Path(thumb_path) + if not thumb_path.exists(): return False - image_date = os.stat(file_path).st_mtime - thumb_date = os.stat(thumb_path).st_mtime + image_date = file_path.stat().st_mtime + thumb_date = thumb_path.stat().st_mtime return image_date <= thumb_date diff --git a/openlp/core/lib/mediamanageritem.py b/openlp/core/lib/mediamanageritem.py index 59a0e4e33..1c7a5b4ef 100644 --- a/openlp/core/lib/mediamanageritem.py +++ b/openlp/core/lib/mediamanageritem.py @@ -359,10 +359,8 @@ class MediaManagerItem(QtWidgets.QWidget, RegistryProperties): :param files: The files to be loaded. :param target_group: The QTreeWidgetItem of the group that will be the parent of the added files """ - names = [] full_list = [] for count in range(self.list_view.count()): - names.append(self.list_view.item(count).text()) full_list.append(self.list_view.item(count).data(QtCore.Qt.UserRole)) duplicates_found = False files_added = False diff --git a/openlp/core/lib/shutil.py b/openlp/core/lib/shutil.py index d2f6ae34d..44dea590a 100755 --- a/openlp/core/lib/shutil.py +++ b/openlp/core/lib/shutil.py @@ -95,3 +95,20 @@ def rmtree(*args, **kwargs): args, kwargs = replace_params(args, kwargs, ((0, 'path', path_to_str),)) return shutil.rmtree(*args, **kwargs) +# TODO: Test and tidy +def which(*args, **kwargs): + """ + Wraps :func:shutil.rmtree` so that we can accept Path objects. + + :param openlp.core.common.path.Path path: Takes a Path object which is then converted to a str object + :return: Passes the return from :func:`shutil.rmtree` back + :rtype: None + + See the following link for more information on the other parameters: + https://docs.python.org/3/library/shutil.html#shutil.rmtree + """ + + file_name = shutil.which(*args, **kwargs) + if file_name: + return str_to_path(file_name) + return None diff --git a/openlp/plugins/presentations/lib/impresscontroller.py b/openlp/plugins/presentations/lib/impresscontroller.py index 7699946a0..472e07801 100644 --- a/openlp/plugins/presentations/lib/impresscontroller.py +++ b/openlp/plugins/presentations/lib/impresscontroller.py @@ -32,11 +32,14 @@ # http://nxsy.org/comparing-documents-with-openoffice-and-python import logging -import os import time -from openlp.core.common import is_win, Registry, delete_file -from openlp.core.common.path import Path +from PyQt5 import QtCore + +from openlp.core.common import Registry, delete_file, get_uno_command, get_uno_instance, is_win +from openlp.core.lib import ScreenList +from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument, \ + TextType if is_win(): from win32com.client import Dispatch @@ -55,14 +58,6 @@ else: except ImportError: uno_available = False -from PyQt5 import QtCore - -from openlp.core.lib import ScreenList -from openlp.core.common import get_uno_command, get_uno_instance -from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument, \ - TextType - - log = logging.getLogger(__name__) @@ -203,12 +198,15 @@ class ImpressDocument(PresentationDocument): Class which holds information and controls a single presentation. """ - def __init__(self, controller, presentation): + def __init__(self, controller, document_path): """ Constructor, store information about the file and initialise. + + :param openlp.core.common.path.Path document_path: File path for the document to load + :rtype: None """ log.debug('Init Presentation OpenOffice') - super(ImpressDocument, self).__init__(controller, presentation) + super().__init__(controller, document_path) self.document = None self.presentation = None self.control = None @@ -225,10 +223,10 @@ class ImpressDocument(PresentationDocument): if desktop is None: self.controller.start_process() desktop = self.controller.get_com_desktop() - url = 'file:///' + self.file_path.replace('\\', '/').replace(':', '|').replace(' ', '%20') + url = self.file_path.as_uri() else: desktop = self.controller.get_uno_desktop() - url = uno.systemPathToFileUrl(self.file_path) + url = uno.systemPathToFileUrl(str(self.file_path)) if desktop is None: return False self.desktop = desktop @@ -254,11 +252,11 @@ class ImpressDocument(PresentationDocument): log.debug('create thumbnails OpenOffice') if self.check_thumbnails(): return + temp_folder_path = self.get_temp_folder() if is_win(): - thumb_dir_url = 'file:///' + str(self.get_temp_folder()).replace('\\', '/') \ - .replace(':', '|').replace(' ', '%20') + thumb_dir_url = temp_folder_path.as_uri() else: - thumb_dir_url = uno.systemPathToFileUrl(str(self.get_temp_folder())) + thumb_dir_url = uno.systemPathToFileUrl(str(temp_folder_path)) properties = [] properties.append(self.create_property('FilterName', 'impress_png_Export')) properties = tuple(properties) @@ -266,17 +264,16 @@ class ImpressDocument(PresentationDocument): pages = doc.getDrawPages() if not pages: return - temp_folder_path = self.get_temp_folder() - if not temp_folder_path.isdir(): - temp_folder_path.mkdir() + if not temp_folder_path.is_dir(): + temp_folder_path.mkdir(parents=True) for index in range(pages.getCount()): page = pages.getByIndex(index) doc.getCurrentController().setCurrentPage(page) - url_path = '{path}/{name}.png'.format(path=thumb_dir_url, name=str(index + 1)) - path = temp_folder_path / '{number).png'.format(number=index + 1) + url_path = '{path}/{name:d}.png'.format(path=thumb_dir_url, name=index + 1) + path = temp_folder_path / '{number:d}.png'.format(number=index + 1) try: doc.storeToURL(url_path, properties) - self.convert_thumbnail(str(path), index + 1) + self.convert_thumbnail(path, index + 1) delete_file(path) except ErrorCodeIOException as exception: log.exception('ERROR! ErrorCodeIOException {error:d}'.format(error=exception.ErrCode)) diff --git a/openlp/plugins/presentations/lib/mediaitem.py b/openlp/plugins/presentations/lib/mediaitem.py index 275279e15..aa5bfc0d6 100644 --- a/openlp/plugins/presentations/lib/mediaitem.py +++ b/openlp/plugins/presentations/lib/mediaitem.py @@ -19,15 +19,13 @@ # with this program; if not, write to the Free Software Foundation, Inc., 59 # # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### - import logging -import os from PyQt5 import QtCore, QtGui, QtWidgets from openlp.core.common import Registry, Settings, UiStrings, translate from openlp.core.common.languagemanager import get_locale_key -from openlp.core.common.path import path_to_str +from openlp.core.common.path import Path, path_to_str, str_to_path from openlp.core.lib import MediaManagerItem, ItemCapabilities, ServiceItemContext,\ build_icon, check_item_selected, create_thumb, validate_thumb from openlp.core.lib.ui import critical_error_message_box, create_horizontal_adjusting_combo_box @@ -128,7 +126,7 @@ class PresentationMediaItem(MediaManagerItem): """ self.list_view.setIconSize(QtCore.QSize(88, 50)) file_paths = Settings().value(self.settings_section + '/presentations files') - self.load_list([path_to_str(file) for file in file_paths], initial_load=True) + self.load_list([path_to_str(path) for path in file_paths], initial_load=True) self.populate_display_types() def populate_display_types(self): @@ -152,54 +150,57 @@ class PresentationMediaItem(MediaManagerItem): else: self.presentation_widget.hide() - def load_list(self, files, target_group=None, initial_load=False): + 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[openlp.core.common.path.Path] file_paths: List of file paths to add to the media manager. """ - current_list = self.get_file_list() - titles = [file_path.name for file_path in current_list] + file_paths = [str_to_path(filename) for filename in file_paths] + 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(files)) + self.main_window.display_progress_bar(len(file_paths)) # Sort the presentations by its filename considering language specific characters. - files.sort(key=lambda filename: get_locale_key(os.path.split(str(filename))[1])) - for file in files: + file_paths.sort(key=lambda file_path: get_locale_key(file_path.name)) + for file_path in file_paths: if not initial_load: self.main_window.increment_progress_bar() - if current_list.count(file) > 0: + if current_paths.count(file_path) > 0: continue - filename = os.path.split(file)[1] - if not os.path.exists(file): - item_name = QtWidgets.QListWidgetItem(filename) + file_name = file_path.name + if not file_path.exists(): + item_name = QtWidgets.QListWidgetItem(file_name) item_name.setIcon(build_icon(ERROR_IMAGE)) - item_name.setData(QtCore.Qt.UserRole, file) - item_name.setToolTip(file) + item_name.setData(QtCore.Qt.UserRole, path_to_str(file_path)) + item_name.setToolTip(str(file_path)) self.list_view.addItem(item_name) else: - if titles.count(filename) > 0: + 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(filename) + controller_name = self.find_controller_by_type(file_path) if controller_name: controller = self.controllers[controller_name] - doc = controller.add_document(file) - thumb = str(doc.get_thumbnail_folder() / 'icon.png') - preview = doc.get_thumbnail_path(1, True) - if not preview and not initial_load: + 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 = doc.get_thumbnail_path(1, True) + preview_path = doc.get_thumbnail_path(1, True) doc.close_presentation() - if not (preview and os.path.exists(preview)): + if not (preview_path and preview_path.exists()): icon = build_icon(':/general/general_delete.png') else: - if validate_thumb(preview, thumb): - icon = build_icon(thumb) + if validate_thumb(preview_path, thumbnail_path): + icon = build_icon(thumbnail_path) else: - icon = create_thumb(preview, thumb) + icon = create_thumb(str(preview_path), str(thumbnail_path)) else: if initial_load: icon = build_icon(':/general/general_delete.png') @@ -208,10 +209,10 @@ class PresentationMediaItem(MediaManagerItem): translate('PresentationPlugin.MediaItem', 'This type of presentation is not supported.')) continue - item_name = QtWidgets.QListWidgetItem(filename) - item_name.setData(QtCore.Qt.UserRole, file) + item_name = QtWidgets.QListWidgetItem(file_name) + item_name.setData(QtCore.Qt.UserRole, path_to_str(file_path)) item_name.setIcon(icon) - item_name.setToolTip(file) + item_name.setToolTip(str(file_path)) self.list_view.addItem(item_name) if not initial_load: self.main_window.finished_progress_bar() @@ -228,8 +229,8 @@ class PresentationMediaItem(MediaManagerItem): self.application.set_busy_cursor() self.main_window.display_progress_bar(len(row_list)) for item in items: - filepath = str(item.data(QtCore.Qt.UserRole)) - self.clean_up_thumbnails(filepath) + file_path = str_to_path(item.data(QtCore.Qt.UserRole)) + self.clean_up_thumbnails(file_path) self.main_window.increment_progress_bar() self.main_window.finished_progress_bar() for row in row_list: @@ -237,30 +238,29 @@ class PresentationMediaItem(MediaManagerItem): Settings().setValue(self.settings_section + '/presentations files', self.get_file_list()) self.application.set_normal_cursor() - def clean_up_thumbnails(self, filepath, clean_for_update=False): + def clean_up_thumbnails(self, file_path, clean_for_update=False): """ Clean up the files created such as thumbnails - :param filepath: File path of the presention to clean up after - :param clean_for_update: Only clean thumbnails if update is needed - :return: None + :param openlp.core.common.path.Path file_path: File path of the presention to clean up after + :param bool clean_for_update: Only clean thumbnails if update is needed + :rtype: None """ for cidx in self.controllers: - root, file_ext = os.path.splitext(filepath) - file_ext = file_ext[1:] + 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(filepath) + 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 os.path.exists(filepath) or os.path.getmtime( - thumb_path) < os.path.getmtime(filepath): + 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 generate_slide_data(self, service_item, item=None, xml_version=False, remote=False, - context=ServiceItemContext.Service, presentation_file=None): + context=ServiceItemContext.Service, file_path=None): """ Generate the slide data. Needs to be implemented by the plugin. @@ -276,10 +276,9 @@ class PresentationMediaItem(MediaManagerItem): items = self.list_view.selectedItems() if len(items) > 1: return False - filename = presentation_file - if filename is None: - filename = items[0].data(QtCore.Qt.UserRole) - file_type = os.path.splitext(filename.lower())[1][1:] + if file_path is None: + file_path = str_to_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) @@ -292,29 +291,28 @@ class PresentationMediaItem(MediaManagerItem): # force a nonexistent theme service_item.theme = -1 for bitem in items: - filename = presentation_file - if filename is None: - filename = bitem.data(QtCore.Qt.UserRole) - (path, name) = os.path.split(filename) - service_item.title = name - if os.path.exists(filename): - processor = self.find_controller_by_type(filename) + if file_path is None: + file_path = str_to_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(filename) + 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(): + not (doc.get_temp_folder() / 'mainslide001.png').is_file(): doc.load_presentation() i = 1 - image = str(doc.get_temp_folder() / 'mainslide{number:0>3d}.png'.format(number=i)) - thumbnail = str(doc.get_thumbnail_folder() / 'slide{number:d}.png'.format(number=i)) - while os.path.isfile(image): - service_item.add_from_image(image, name, thumbnail=thumbnail) + 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(str(image_path), file_name, thumbnail=str(thumbnail_path)) i += 1 - image = str(doc.get_temp_folder() / 'mainslide{number:0>3d}.png'.format(number=i)) - thumbnail = str(doc.get_thumbnail_folder() / 'slide{number:d}.png'.format(number=i)) + 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() return True @@ -324,34 +322,34 @@ class PresentationMediaItem(MediaManagerItem): critical_error_message_box(translate('PresentationPlugin.MediaItem', 'Missing Presentation'), translate('PresentationPlugin.MediaItem', 'The presentation {name} no longer exists.' - ).format(name=filename)) + ).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: - filename = bitem.data(QtCore.Qt.UserRole) - (path, name) = os.path.split(filename) - service_item.title = name - if os.path.exists(filename): + file_path = str_to_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(filename) + 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(filename) + doc = controller.add_document(file_path) if doc.get_thumbnail_path(1, True) is None: doc.load_presentation() i = 1 - img = doc.get_thumbnail_path(i, True) - if img: + 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 img: + while thumbnail_path: # Use title and note if available title = '' if titles and len(titles) >= i: @@ -359,9 +357,9 @@ class PresentationMediaItem(MediaManagerItem): note = '' if notes and len(notes) >= i: note = notes[i - 1] - service_item.add_from_command(path, name, img, title, note) + service_item.add_from_command(str(path), file_name, str(thumbnail_path), title, note) i += 1 - img = doc.get_thumbnail_path(i, True) + thumbnail_path = doc.get_thumbnail_path(i, True) doc.close_presentation() return True else: @@ -371,7 +369,7 @@ class PresentationMediaItem(MediaManagerItem): 'Missing Presentation'), translate('PresentationPlugin.MediaItem', 'The presentation {name} is incomplete, ' - 'please reload.').format(name=filename)) + 'please reload.').format(name=file_path)) return False else: # File is no longer present @@ -379,18 +377,20 @@ class PresentationMediaItem(MediaManagerItem): critical_error_message_box(translate('PresentationPlugin.MediaItem', 'Missing Presentation'), translate('PresentationPlugin.MediaItem', 'The presentation {name} no longer exists.' - ).format(name=filename)) + ).format(name=file_path)) return False - def find_controller_by_type(self, filename): + 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 filename: The file name + :param openlp.core.common.path.Path file_path: The file path + :return: The default application controller for this file type, or None if not supported + :rtype: PresentationController """ - file_type = os.path.splitext(filename)[1][1:] + file_type = file_path.suffix[1:] if not file_type: return None for controller in self.controllers: diff --git a/openlp/plugins/presentations/lib/messagelistener.py b/openlp/plugins/presentations/lib/messagelistener.py index 8e5de3e2d..5ad46d0fe 100644 --- a/openlp/plugins/presentations/lib/messagelistener.py +++ b/openlp/plugins/presentations/lib/messagelistener.py @@ -19,16 +19,15 @@ # with this program; if not, write to the Free Software Foundation, Inc., 59 # # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### - -import logging import copy -import os +import logging from PyQt5 import QtCore from openlp.core.common import Registry, Settings -from openlp.core.ui import HideMode +from openlp.core.common.path import Path from openlp.core.lib import ServiceItemContext +from openlp.core.ui import HideMode from openlp.plugins.presentations.lib.pdfcontroller import PDF_CONTROLLER_FILETYPES log = logging.getLogger(__name__) @@ -325,21 +324,25 @@ class MessageListener(object): is_live = message[1] item = message[0] hide_mode = message[2] - file = item.get_frame_path() + file_path = Path(item.get_frame_path()) self.handler = item.processor # When starting presentation from the servicemanager we convert # PDF/XPS/OXPS-serviceitems into image-serviceitems. When started from the mediamanager # the conversion has already been done at this point. - file_type = os.path.splitext(file.lower())[1][1:] + file_type = file_path.suffix.lower()[1:] if file_type in PDF_CONTROLLER_FILETYPES: - log.debug('Converting from pdf/xps/oxps to images for serviceitem with file {name}'.format(name=file)) + log.debug('Converting from pdf/xps/oxps to images for serviceitem with file {name}'.format(name=file_path)) # Create a copy of the original item, and then clear the original item so it can be filled with images item_cpy = copy.copy(item) item.__init__(None) if is_live: - self.media_item.generate_slide_data(item, item_cpy, False, False, ServiceItemContext.Live, file) + # TODO: To Path object + self.media_item.generate_slide_data(item, item_cpy, False, False, ServiceItemContext.Live, + str(file_path)) else: - self.media_item.generate_slide_data(item, item_cpy, False, False, ServiceItemContext.Preview, file) + # TODO: To Path object + self.media_item.generate_slide_data(item, item_cpy, False, False, ServiceItemContext.Preview, + str(file_path)) # Some of the original serviceitem attributes is needed in the new serviceitem item.footer = item_cpy.footer item.from_service = item_cpy.from_service @@ -352,13 +355,13 @@ class MessageListener(object): self.handler = None else: if self.handler == self.media_item.automatic: - self.handler = self.media_item.find_controller_by_type(file) + self.handler = self.media_item.find_controller_by_type(file_path) if not self.handler: return else: - # the saved handler is not present so need to use one based on file suffix. + # the saved handler is not present so need to use one based on file_path suffix. if not self.controllers[self.handler].available: - self.handler = self.media_item.find_controller_by_type(file) + self.handler = self.media_item.find_controller_by_type(file_path) if not self.handler: return if is_live: @@ -370,7 +373,7 @@ class MessageListener(object): if self.handler is None: self.controller = controller else: - controller.add_handler(self.controllers[self.handler], file, hide_mode, message[3]) + controller.add_handler(self.controllers[self.handler], file_path, hide_mode, message[3]) self.timer.start() def slide(self, message): diff --git a/openlp/plugins/presentations/lib/pdfcontroller.py b/openlp/plugins/presentations/lib/pdfcontroller.py index f80ad94ba..9f4aa1b4f 100644 --- a/openlp/plugins/presentations/lib/pdfcontroller.py +++ b/openlp/plugins/presentations/lib/pdfcontroller.py @@ -23,13 +23,13 @@ import os import logging import re -from shutil import which from subprocess import check_output, CalledProcessError from openlp.core.common import AppLocation, check_binary_exists from openlp.core.common import Settings, is_win from openlp.core.common.path import Path, path_to_str from openlp.core.lib import ScreenList +from openlp.core.lib.shutil import which from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument if is_win(): @@ -66,11 +66,12 @@ class PdfController(PresentationController): Function that checks whether a binary is either ghostscript or mudraw or neither. Is also used from presentationtab.py - :param program_path:The full path to the binary to check. + :param openlp.core.common.path.Path program_path: The full path to the binary to check. :return: Type of the binary, 'gs' if ghostscript, 'mudraw' if mudraw, None if invalid. + :rtype: str | None """ program_type = None - runlog = check_binary_exists(Path(program_path)) + runlog = check_binary_exists(program_path) # Analyse the output to see it the program is mudraw, ghostscript or neither for line in runlog.splitlines(): decoded_line = line.decode() @@ -107,30 +108,29 @@ class PdfController(PresentationController): :return: True if program to open PDF-files was found, otherwise False. """ log.debug('check_installed Pdf') - self.mudrawbin = '' - self.mutoolbin = '' - self.gsbin = '' + self.mudrawbin = None + self.mutoolbin = None + self.gsbin = None self.also_supports = [] # Use the user defined program if given if Settings().value('presentations/enable_pdf_program'): - pdf_program = path_to_str(Settings().value('presentations/pdf_program')) - program_type = self.process_check_binary(pdf_program) + program_path = Settings().value('presentations/pdf_program') + program_type = self.process_check_binary(program_path) if program_type == 'gs': - self.gsbin = pdf_program + self.gsbin = program_path elif program_type == 'mudraw': - self.mudrawbin = pdf_program + self.mudrawbin = program_path elif program_type == 'mutool': - self.mutoolbin = pdf_program + self.mutoolbin = program_path else: # Fallback to autodetection - application_path = str(AppLocation.get_directory(AppLocation.AppDir)) + application_path = AppLocation.get_directory(AppLocation.AppDir) if is_win(): # for windows we only accept mudraw.exe or mutool.exe in the base folder - application_path = str(AppLocation.get_directory(AppLocation.AppDir)) - if os.path.isfile(os.path.join(application_path, 'mudraw.exe')): - self.mudrawbin = os.path.join(application_path, 'mudraw.exe') - elif os.path.isfile(os.path.join(application_path, 'mutool.exe')): - self.mutoolbin = os.path.join(application_path, 'mutool.exe') + if (application_path / 'mudraw.exe').is_file(): + self.mudrawbin = application_path / 'mudraw.exe' + elif (application_path / 'mutool.exe').is_file(): + self.mutoolbin = application_path / 'mutool.exe' else: DEVNULL = open(os.devnull, 'wb') # First try to find mudraw @@ -143,11 +143,11 @@ class PdfController(PresentationController): self.gsbin = which('gs') # Last option: check if mudraw or mutool is placed in OpenLP base folder if not self.mudrawbin and not self.mutoolbin and not self.gsbin: - application_path = str(AppLocation.get_directory(AppLocation.AppDir)) - if os.path.isfile(os.path.join(application_path, 'mudraw')): - self.mudrawbin = os.path.join(application_path, 'mudraw') - elif os.path.isfile(os.path.join(application_path, 'mutool')): - self.mutoolbin = os.path.join(application_path, 'mutool') + application_path = AppLocation.get_directory(AppLocation.AppDir) + if (application_path / 'mudraw').is_file(): + self.mudrawbin = application_path / 'mudraw' + elif (application_path / 'mutool').is_file(): + self.mutoolbin = application_path / 'mutool' if self.mudrawbin or self.mutoolbin: self.also_supports = ['xps', 'oxps'] return True @@ -172,12 +172,15 @@ class PdfDocument(PresentationDocument): image-serviceitem on the fly and present as such. Therefore some of the 'playback' functions is not implemented. """ - def __init__(self, controller, presentation): + def __init__(self, controller, document_path): """ Constructor, store information about the file and initialise. + + :param openlp.core.common.path.Path document_path: Path to the document to load + :rtype: None """ log.debug('Init Presentation Pdf') - PresentationDocument.__init__(self, controller, presentation) + super().__init__(controller, document_path) self.presentation = None self.blanked = False self.hidden = False @@ -200,13 +203,13 @@ class PdfDocument(PresentationDocument): :return: The resolution dpi to be used. """ # Use a postscript script to get size of the pdf. It is assumed that all pages have same size - gs_resolution_script = str(AppLocation.get_directory( - AppLocation.PluginsDir)) + '/presentations/lib/ghostscript_get_resolution.ps' + gs_resolution_script = AppLocation.get_directory( + AppLocation.PluginsDir) / 'presentations' / 'lib' / 'ghostscript_get_resolution.ps' # Run the script on the pdf to get the size runlog = [] try: - runlog = check_output([self.controller.gsbin, '-dNOPAUSE', '-dNODISPLAY', '-dBATCH', - '-sFile=' + self.file_path, gs_resolution_script], + runlog = check_output([str(self.controller.gsbin), '-dNOPAUSE', '-dNODISPLAY', '-dBATCH', + '-sFile={file_path}'.format(file_path=self.file_path), str(gs_resolution_script)], startupinfo=self.startupinfo) except CalledProcessError as e: log.debug(' '.join(e.cmd)) @@ -246,7 +249,7 @@ class PdfDocument(PresentationDocument): created_files = sorted(temp_dir_path.glob('*')) for image_path in created_files: if image_path.is_file(): - self.image_files.append(str(image_path)) + self.image_files.append(image_path) self.num_pages = len(self.image_files) return True size = ScreenList().current['size'] @@ -258,27 +261,27 @@ class PdfDocument(PresentationDocument): # The %03d in the file name is handled by each binary if self.controller.mudrawbin: log.debug('loading presentation using mudraw') - runlog = check_output([self.controller.mudrawbin, '-w', str(size.width()), '-h', str(size.height()), - '-o', str(temp_dir_path / 'mainslide%03d.png'), self.file_path], + runlog = check_output([str(self.controller.mudrawbin), '-w', str(size.width()), '-h', str(size.height()), + '-o', str(temp_dir_path / 'mainslide%03d.png'), str(self.file_path)], startupinfo=self.startupinfo) elif self.controller.mutoolbin: log.debug('loading presentation using mutool') - runlog = check_output([self.controller.mutoolbin, 'draw', '-w', str(size.width()), '-h', - str(size.height()), - '-o', str(temp_dir_path / 'mainslide%03d.png'), self.file_path], + runlog = check_output([str(self.controller.mutoolbin), 'draw', '-w', str(size.width()), + '-h', str(size.height()), '-o', str(temp_dir_path / 'mainslide%03d.png'), + str(self.file_path)], startupinfo=self.startupinfo) elif self.controller.gsbin: log.debug('loading presentation using gs') resolution = self.gs_get_resolution(size) - runlog = check_output([self.controller.gsbin, '-dSAFER', '-dNOPAUSE', '-dBATCH', '-sDEVICE=png16m', - '-r' + str(resolution), '-dTextAlphaBits=4', '-dGraphicsAlphaBits=4', - '-sOutputFile=' + str(temp_dir_path / 'mainslide%03d.png'), - self.file_path], startupinfo=self.startupinfo) + runlog = check_output([str(self.controller.gsbin), '-dSAFER', '-dNOPAUSE', '-dBATCH', '-sDEVICE=png16m', + '-r{res}'.format(res=resolution), '-dTextAlphaBits=4', '-dGraphicsAlphaBits=4', + '-sOutputFile={output}'.format(output=temp_dir_path / 'mainslide%03d.png'), + str(self.file_path)], startupinfo=self.startupinfo) created_files = sorted(temp_dir_path.glob('*')) for image_path in created_files: if image_path.is_file(): - self.image_files.append(str(image_path)) - except Exception: + self.image_files.append(image_path) + except Exception as e: log.exception(runlog) return False self.num_pages = len(self.image_files) diff --git a/openlp/plugins/presentations/lib/powerpointcontroller.py b/openlp/plugins/presentations/lib/powerpointcontroller.py index 1014db851..fa253ffda 100644 --- a/openlp/plugins/presentations/lib/powerpointcontroller.py +++ b/openlp/plugins/presentations/lib/powerpointcontroller.py @@ -120,15 +120,16 @@ class PowerpointDocument(PresentationDocument): Class which holds information and controls a single presentation. """ - def __init__(self, controller, presentation): + def __init__(self, controller, document_path): """ Constructor, store information about the file and initialise. :param controller: - :param presentation: + :param openlp.core.common.path.Path document_path: Path to the document to load + :rtype: None """ log.debug('Init Presentation Powerpoint') - super(PowerpointDocument, self).__init__(controller, presentation) + super().__init__(controller, document_path) self.presentation = None self.index_map = {} self.slide_count = 0 @@ -145,7 +146,7 @@ class PowerpointDocument(PresentationDocument): try: if not self.controller.process: self.controller.start_process() - self.controller.process.Presentations.Open(os.path.normpath(self.file_path), False, False, False) + self.controller.process.Presentations.Open(str(self.file_path), False, False, False) self.presentation = self.controller.process.Presentations(self.controller.process.Presentations.Count) self.create_thumbnails() self.create_titles_and_notes() @@ -363,9 +364,8 @@ class PowerpointDocument(PresentationDocument): width=size.width(), horizontal=(right - left))) log.debug('window title: {title}'.format(title=window_title)) - filename_root, filename_ext = os.path.splitext(os.path.basename(self.file_path)) if size.y() == top and size.height() == (bottom - top) and size.x() == left and \ - size.width() == (right - left) and filename_root in window_title: + size.width() == (right - left) and self.file_path.stem in window_title: log.debug('Found a match and will save the handle') self.presentation_hwnd = hwnd # Stop powerpoint from flashing in the taskbar diff --git a/openlp/plugins/presentations/lib/pptviewcontroller.py b/openlp/plugins/presentations/lib/pptviewcontroller.py index c936fe65c..547636026 100644 --- a/openlp/plugins/presentations/lib/pptviewcontroller.py +++ b/openlp/plugins/presentations/lib/pptviewcontroller.py @@ -85,9 +85,9 @@ class PptviewController(PresentationController): if self.process: return log.debug('start PPTView') - dll_path = os.path.join(str(AppLocation.get_directory(AppLocation.AppDir)), - 'plugins', 'presentations', 'lib', 'pptviewlib', 'pptviewlib.dll') - self.process = cdll.LoadLibrary(dll_path) + dll_path = AppLocation.get_directory(AppLocation.AppDir) \ + / 'plugins' / 'presentations' / 'lib' / 'pptviewlib' / 'pptviewlib.dll' + self.process = cdll.LoadLibrary(str(dll_path)) if log.isEnabledFor(logging.DEBUG): self.process.SetDebug(1) @@ -104,12 +104,15 @@ class PptviewDocument(PresentationDocument): """ Class which holds information and controls a single presentation. """ - def __init__(self, controller, presentation): + def __init__(self, controller, document_path): """ Constructor, store information about the file and initialise. + + :param openlp.core.common.path.Path document_path: File path to the document to load + :rtype: None """ log.debug('Init Presentation PowerPoint') - super(PptviewDocument, self).__init__(controller, presentation) + super().__init__(controller, document_path) self.presentation = None self.ppt_id = None self.blanked = False @@ -121,17 +124,16 @@ class PptviewDocument(PresentationDocument): the background PptView task started earlier. """ log.debug('LoadPresentation') - temp_dir_path = self.get_temp_folder() + temp_path = self.get_temp_folder() size = ScreenList().current['size'] rect = RECT(size.x(), size.y(), size.right(), size.bottom()) - self.file_path = os.path.normpath(self.file_path) - preview_path = temp_dir_path / 'slide' + preview_path = temp_path / 'slide' # Ensure that the paths are null terminated - byte_file_path = self.file_path.encode('utf-16-le') + b'\0' - preview_file_name = str(preview_path).encode('utf-16-le') + b'\0' - if not temp_dir_path: - temp_dir_path.mkdir(parents=True) - self.ppt_id = self.controller.process.OpenPPT(byte_file_path, None, rect, preview_file_name) + file_path_utf16 = str(self.file_path).encode('utf-16-le') + b'\0' + preview_path_utf16 = str(preview_path).encode('utf-16-le') + b'\0' + if not temp_path.is_dir(): + temp_path.mkdir(parents=True) + self.ppt_id = self.controller.process.OpenPPT(file_path_utf16, None, rect, preview_path_utf16) if self.ppt_id >= 0: self.create_thumbnails() self.stop_presentation() @@ -148,7 +150,7 @@ class PptviewDocument(PresentationDocument): return log.debug('create_thumbnails proceeding') for idx in range(self.get_slide_count()): - path = '{folder}\\slide{index}.bmp'.format(folder=self.get_temp_folder(), index=str(idx + 1)) + path = self.get_temp_folder() / 'slide{index:d}.bmp'.format(index=idx + 1) self.convert_thumbnail(path, idx + 1) def create_titles_and_notes(self): @@ -161,13 +163,12 @@ class PptviewDocument(PresentationDocument): """ titles = None notes = None - filename = os.path.normpath(self.file_path) # let's make sure we have a valid zipped presentation - if os.path.exists(filename) and zipfile.is_zipfile(filename): + if self.file_path.exists() and zipfile.is_zipfile(str(self.file_path)): namespaces = {"p": "http://schemas.openxmlformats.org/presentationml/2006/main", "a": "http://schemas.openxmlformats.org/drawingml/2006/main"} # open the file - with zipfile.ZipFile(filename) as zip_file: + with zipfile.ZipFile(str(self.file_path)) as zip_file: # find the presentation.xml to get the slide count with zip_file.open('ppt/presentation.xml') as pres: tree = ElementTree.parse(pres) diff --git a/openlp/plugins/presentations/lib/presentationcontroller.py b/openlp/plugins/presentations/lib/presentationcontroller.py index af665bb55..13a759a5e 100644 --- a/openlp/plugins/presentations/lib/presentationcontroller.py +++ b/openlp/plugins/presentations/lib/presentationcontroller.py @@ -19,10 +19,7 @@ # with this program; if not, write to the Free Software Foundation, Inc., 59 # # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### - import logging -import os -import shutil from PyQt5 import QtCore @@ -87,19 +84,26 @@ class PresentationDocument(object): Returns a path to an image containing a preview for the requested slide """ - def __init__(self, controller, name): + def __init__(self, controller, document_path): """ Constructor for the PresentationController class + + :param controller: + :param openlp.core.common.path.Path document_path: Path to the document to load. + :rtype: None """ self.controller = controller - self._setup(name) + self._setup(document_path) - def _setup(self, name): + def _setup(self, document_path): """ Run some initial setup. This method is separate from __init__ in order to mock it out in tests. + + :param openlp.core.common.path.Path document_path: Path to the document to load. + :rtype: None """ self.slide_number = 0 - self.file_path = name + self.file_path = document_path check_directory_exists(self.get_thumbnail_folder()) def load_presentation(self): @@ -126,12 +130,6 @@ class PresentationDocument(object): except OSError: log.exception('Failed to delete presentation controller files') - def get_file_name(self): - """ - Return just the filename of the presentation, without the directory - """ - return os.path.split(self.file_path)[1] - def get_thumbnail_folder(self): """ The location where thumbnail images will be stored @@ -141,9 +139,9 @@ class PresentationDocument(object): """ # TODO: If statement can be removed when the upgrade path from 2.0.x to 2.2.x is no longer needed if Settings().value('presentations/thumbnail_scheme') == 'md5': - folder = md5_hash(self.file_path.encode('utf-8')) + folder = md5_hash(bytes(self.file_path)) else: - folder = self.get_file_name() + folder = self.file_path.name return Path(self.controller.thumbnail_folder, folder) def get_temp_folder(self): @@ -155,19 +153,22 @@ class PresentationDocument(object): """ # TODO: If statement can be removed when the upgrade path from 2.0.x to 2.2.x is no longer needed if Settings().value('presentations/thumbnail_scheme') == 'md5': - folder = md5_hash(self.file_path.encode('utf-8')) + folder = md5_hash(bytes(self.file_path)) else: - folder = self.get_file_name() + folder = self.file_path.name return Path(self.controller.temp_folder, folder) def check_thumbnails(self): """ - Returns ``True`` if the thumbnail images exist and are more recent than the powerpoint file. + Check that the last thumbnail image exists and is valid and are more recent than the powerpoint file. + + :return: If the thumbnail is valid + :rtype: bool """ - last_image = self.get_thumbnail_path(self.get_slide_count(), True) - if not (last_image and os.path.isfile(last_image)): + last_image_path = self.get_thumbnail_path(self.get_slide_count(), True) + if not (last_image_path and last_image_path.is_file()): return False - return validate_thumb(self.file_path, last_image) + return validate_thumb(self.file_path, last_image_path) def close_presentation(self): """ @@ -250,24 +251,28 @@ class PresentationDocument(object): """ pass - def convert_thumbnail(self, file, idx): + def convert_thumbnail(self, image_path, index): """ Convert the slide image the application made to a scaled 360px height .png image. + + :param openlp.core.common.path.Path image_path: Path to the image to create a thumb nail of + :param int index: The index of the slide to create the thumbnail for. + :rtype: None """ if self.check_thumbnails(): return - if os.path.isfile(file): - thumb_path = self.get_thumbnail_path(idx, False) - create_thumb(file, thumb_path, False, QtCore.QSize(-1, 360)) + if image_path.is_file(): + thumb_path = self.get_thumbnail_path(index, False) + create_thumb(str(image_path), str(thumb_path), False, QtCore.QSize(-1, 360)) - def get_thumbnail_path(self, slide_no, check_exists=True): + def get_thumbnail_path(self, slide_no, check_exists=False): """ Returns an image path containing a preview for the requested slide :param int slide_no: The slide an image is required for, starting at 1 :param bool check_exists: Check if the generated path exists :return: The path, or None if the :param:`check_exists` is True and the file does not exist - :rtype: openlp.core.common.path.Path, None + :rtype: openlp.core.common.path.Path | None """ path = self.get_thumbnail_folder() / (self.controller.thumbnail_prefix + str(slide_no) + '.png') if path.is_file() or not check_exists: @@ -313,43 +318,38 @@ class PresentationDocument(object): Reads the titles from the titles file and the notes files and returns the content in two lists """ - titles = [] notes = [] - titles_file = str(self.get_thumbnail_folder() / 'titles.txt') - if os.path.exists(titles_file): - try: - with open(titles_file, encoding='utf-8') as fi: - titles = fi.read().splitlines() - except: - log.exception('Failed to open/read existing titles file') - titles = [] + titles_path = self.get_thumbnail_folder() / 'titles.txt' + try: + titles = titles_path.read_text().splitlines() + except: + log.exception('Failed to open/read existing titles file') + titles = [] for slide_no, title in enumerate(titles, 1): - notes_file = str(self.get_thumbnail_folder() / 'slideNotes{number:d}.txt'.format(number=slide_no)) - note = '' - if os.path.exists(notes_file): - try: - with open(notes_file, encoding='utf-8') as fn: - note = fn.read() - except: - log.exception('Failed to open/read notes file') - note = '' + notes_path = self.get_thumbnail_folder() / 'slideNotes{number:d}.txt'.format(number=slide_no) + try: + note = notes_path.read_text() + except: + log.exception('Failed to open/read notes file') + note = '' notes.append(note) return titles, notes def save_titles_and_notes(self, titles, notes): """ - Performs the actual persisting of titles to the titles.txt - and notes to the slideNote%.txt + Performs the actual persisting of titles to the titles.txt and notes to the slideNote%.txt + + :param list[str] titles: The titles to save + :param list[str] notes: The notes to save + :rtype: None """ if titles: titles_path = self.get_thumbnail_folder() / 'titles.txt' - with titles_path.open(mode='wt', encoding='utf-8') as fo: - fo.writelines(titles) + titles_path.write_text('\n'.join(titles)) if notes: for slide_no, note in enumerate(notes, 1): notes_path = self.get_thumbnail_folder() / 'slideNotes{number:d}.txt'.format(number=slide_no) - with notes_path.open(mode='wt', encoding='utf-8') as fn: - fn.write(note) + notes_path.write_text(note) class PresentationController(object): @@ -426,12 +426,11 @@ class PresentationController(object): self.document_class = document_class self.settings_section = self.plugin.settings_section self.available = None - self.temp_folder = os.path.join(str(AppLocation.get_section_data_path(self.settings_section)), name) - self.thumbnail_folder = os.path.join( - str(AppLocation.get_section_data_path(self.settings_section)), 'thumbnails') + self.temp_folder = AppLocation.get_section_data_path(self.settings_section) / name + self.thumbnail_folder = AppLocation.get_section_data_path(self.settings_section) / 'thumbnails' self.thumbnail_prefix = 'slide' - check_directory_exists(Path(self.thumbnail_folder)) - check_directory_exists(Path(self.temp_folder)) + check_directory_exists(self.thumbnail_folder) + check_directory_exists(self.temp_folder) def enabled(self): """ @@ -466,11 +465,15 @@ class PresentationController(object): log.debug('Kill') self.close_presentation() - def add_document(self, name): + def add_document(self, document_path): """ Called when a new presentation document is opened. + + :param openlp.core.common.path.Path document_path: Path to the document to load + :return: The document + :rtype: PresentationDocument """ - document = self.document_class(self, name) + document = self.document_class(self, document_path) self.docs.append(document) return document diff --git a/openlp/plugins/presentations/lib/presentationtab.py b/openlp/plugins/presentations/lib/presentationtab.py index 3e92827c8..ca9ceacbc 100644 --- a/openlp/plugins/presentations/lib/presentationtab.py +++ b/openlp/plugins/presentations/lib/presentationtab.py @@ -38,7 +38,6 @@ class PresentationTab(SettingsTab): """ Constructor """ - self.parent = parent self.controllers = controllers super(PresentationTab, self).__init__(parent, title, visible_title, icon_path) self.activated = False @@ -194,7 +193,7 @@ class PresentationTab(SettingsTab): pdf_program_path = self.program_path_edit.path enable_pdf_program = self.pdf_program_check_box.checkState() # If the given program is blank disable using the program - if not pdf_program_path: + if pdf_program_path is None: enable_pdf_program = 0 if pdf_program_path != Settings().value(self.settings_section + '/pdf_program'): Settings().setValue(self.settings_section + '/pdf_program', pdf_program_path) @@ -220,9 +219,11 @@ class PresentationTab(SettingsTab): def on_program_path_edit_path_changed(self, new_path): """ - Select the mudraw or ghostscript binary that should be used. + Handle the `pathEditChanged` signal from program_path_edit + + :param openlp.core.common.path.Path new_path: File path to the new program + :rtype: None """ - new_path = path_to_str(new_path) if new_path: if not PdfController.process_check_binary(new_path): critical_error_message_box(UiStrings().Error, diff --git a/tests/functional/openlp_plugins/presentations/test_presentationcontroller.py b/tests/functional/openlp_plugins/presentations/test_presentationcontroller.py index 1bbd29522..c5e6d3df3 100644 --- a/tests/functional/openlp_plugins/presentations/test_presentationcontroller.py +++ b/tests/functional/openlp_plugins/presentations/test_presentationcontroller.py @@ -244,20 +244,3 @@ class TestPresentationDocument(TestCase): # THEN: load_presentation should return false self.assertFalse(result, "PresentationDocument.load_presentation should return false.") - - def test_get_file_name(self): - """ - Test the PresentationDocument.get_file_name method. - """ - - # GIVEN: A mocked os.path.split which returns a list, an instance of PresentationDocument and - # arbitary file_path. - self.mock_os.path.split.return_value = ['directory', 'file.ext'] - instance = PresentationDocument(self.mock_controller, 'Name') - instance.file_path = 'filepath' - - # WHEN: Calling get_file_name - result = instance.get_file_name() - - # THEN: get_file_name should return 'file.ext' - self.assertEqual(result, 'file.ext')