diff --git a/.bzrignore b/.bzrignore
index 9ae23569f..58338dd0e 100644
--- a/.bzrignore
+++ b/.bzrignore
@@ -1,57 +1,48 @@
*.*~
-*.~\?~
-\#*\#
-build
-.cache
-cover
-.coverage
-coverage
-.directory
-.vscode
-dist
*.dll
-documentation/build/doctrees
-documentation/build/html
*.e4*
-*eric[1-9]project
-.git
-env
-# Git files
-.gitignore
-htmlcov
-.idea
*.kate-swp
*.kdev4
-.kdev4
*.komodoproject
-.komodotools
-list
*.log*
*.nja
-openlp.cfg
-openlp/core/resources.py.old
-OpenLP.egg-info
-openlp.org 2.0.e4*
-openlp.pro
-openlp-test-projectordb.sqlite
*.orig
-output
*.pyc
-__pycache__
-.pylint.d
-.pytest_cache
*.qm
*.rej
-# Rejected diff's
-resources/innosetup/Output
-resources/windows/warnOpenLP.txt
*.ropeproject
-tags
-output
+*.~\?~
+*eric[1-9]project
+.cache
+.coverage
+.directory
+.git
+.gitignore
+.idea
+.kdev4
+.komodotools
+.pylint.d
+.pytest_cache
+.vscode
+OpenLP.egg-info
+\#*\#
+__pycache__
+build
+cover
+coverage
+dist
+env
htmlcov
+list
node_modules
openlp-test-projectordb.sqlite
+openlp.cfg
+openlp.pro
+openlp/core/resources.py.old
+openlp/plugins/presentations/lib/vendor/Pyro4
+openlp/plugins/presentations/lib/vendor/serpent.py
+output
package-lock.json
-.cache
+tags
test
tests.kdev4
diff --git a/openlp/core/common/path.py b/openlp/core/common/path.py
index 126555fd6..47517fb3b 100644
--- a/openlp/core/common/path.py
+++ b/openlp/core/common/path.py
@@ -78,6 +78,8 @@ def path_to_str(path=None):
:return: An empty string if :param:`path` is None, else a string representation of the :param:`path`
:rtype: str
"""
+ if isinstance(path, str):
+ return path
if not isinstance(path, Path) and path is not None:
raise TypeError('parameter \'path\' must be of type Path or NoneType')
if path is None:
diff --git a/openlp/core/display/render.py b/openlp/core/display/render.py
index 05b7b97fc..1097b5ac0 100644
--- a/openlp/core/display/render.py
+++ b/openlp/core/display/render.py
@@ -24,6 +24,7 @@ The :mod:`~openlp.display.render` module contains functions for rendering.
"""
import html
import logging
+import mako
import math
import os
import re
@@ -32,8 +33,10 @@ import time
from PyQt5 import QtWidgets, QtGui
from openlp.core.common import ThemeLevel
+from openlp.core.common.i18n import translate
from openlp.core.common.mixins import LogMixin, RegistryProperties
from openlp.core.common.registry import Registry, RegistryBase
+from openlp.core.common.settings import Settings
from openlp.core.display.screens import ScreenList
from openlp.core.display.window import DisplayWindow
from openlp.core.lib import ItemCapabilities
@@ -58,8 +61,10 @@ VERSE = 'The Lord said to {r}Noah{/r}: \n' \
'{r}C{/r}{b}h{/b}{bl}i{/bl}{y}l{/y}{g}d{/g}{pk}' \
'r{/pk}{o}e{/o}{pp}n{/pp} of the Lord\n'
VERSE_FOR_LINE_COUNT = '\n'.join(map(str, range(100)))
-TITLE = 'Arky Arky (Unknown)'
-FOOTER = ['Public Domain', 'CCLI 123456']
+TITLE = 'Arky Arky'
+AUTHOR = 'John Doe'
+FOOTER_COPYRIGHT = 'Public Domain'
+CCLI_NO = '123456'
def remove_tags(text, can_remove_chords=False):
@@ -425,7 +430,7 @@ def get_start_tags(raw_text):
return raw_text + ''.join(end_tags), ''.join(start_tags), ''.join(html_tags)
-class Renderer(RegistryBase, LogMixin, RegistryProperties, DisplayWindow):
+class ThemePreviewRenderer(LogMixin, DisplayWindow):
"""
A virtual display used for rendering thumbnails and other offscreen tasks
"""
@@ -435,24 +440,6 @@ class Renderer(RegistryBase, LogMixin, RegistryProperties, DisplayWindow):
"""
super().__init__(*args, **kwargs)
self.force_page = False
- for screen in ScreenList():
- if screen.is_display:
- self.setGeometry(screen.display_geometry.x(), screen.display_geometry.y(),
- screen.display_geometry.width(), screen.display_geometry.height())
- break
- # If the display is not show'ed and hidden like this webegine will not render
- self.show()
- self.hide()
- self.theme_height = 0
- self.theme_level = ThemeLevel.Global
-
- def set_theme_level(self, theme_level):
- """
- Sets the theme level.
-
- :param theme_level: The theme level to be used.
- """
- self.theme_level = theme_level
def calculate_line_count(self):
"""
@@ -466,7 +453,30 @@ class Renderer(RegistryBase, LogMixin, RegistryProperties, DisplayWindow):
"""
return self.run_javascript('Display.clearSlides();')
- def generate_preview(self, theme_data, force_page=False):
+ def generate_footer(self):
+ """
+ """
+ footer_template = Settings().value('songs/footer template')
+ # Keep this in sync with the list in songstab.py
+ vars = {
+ 'title': TITLE,
+ 'authors_none_label': translate('OpenLP.Ui', 'Written by'),
+ 'authors_words_label': translate('SongsPlugin.AuthorType', 'Words',
+ 'Author who wrote the lyrics of a song'),
+ 'authors_words': [AUTHOR],
+ 'copyright': FOOTER_COPYRIGHT,
+ 'ccli_license': Settings().value('core/ccli number'),
+ 'ccli_license_label': translate('SongsPlugin.MediaItem', 'CCLI License'),
+ 'ccli_number': CCLI_NO,
+ }
+ try:
+ footer_html = mako.template.Template(footer_template).render_unicode(**vars).replace('\n', '')
+ except mako.exceptions.SyntaxException:
+ log.error('Failed to render Song footer html:\n' + mako.exceptions.text_error_template().render())
+ footer_html = 'Dummy footer text'
+ return footer_html
+
+ def generate_preview(self, theme_data, force_page=False, generate_screenshot=True):
"""
Generate a preview of a theme.
@@ -479,14 +489,16 @@ class Renderer(RegistryBase, LogMixin, RegistryProperties, DisplayWindow):
if not self.force_page:
self.set_theme(theme_data)
self.theme_height = theme_data.font_main_height
- slides = self.format_slide(render_tags(VERSE), None)
+ slides = self.format_slide(VERSE, None)
verses = dict()
verses['title'] = TITLE
- verses['text'] = slides[0]
+ verses['text'] = render_tags(slides[0])
verses['verse'] = 'V1'
+ verses['footer'] = self.generate_footer()
self.load_verses([verses])
self.force_page = False
- return self.save_screenshot()
+ if generate_screenshot:
+ return self.save_screenshot()
self.force_page = False
return None
@@ -515,7 +527,7 @@ class Renderer(RegistryBase, LogMixin, RegistryProperties, DisplayWindow):
if item and item.is_capable(ItemCapabilities.CanWordSplit):
pages = self._paginate_slide_words(text.split('\n'), line_end)
# Songs and Custom
- elif item is None or item.is_capable(ItemCapabilities.CanSoftBreak):
+ elif item is None or (item and item.is_capable(ItemCapabilities.CanSoftBreak)):
pages = []
if '[---]' in text:
# Remove Overflow split if at start of the text
@@ -722,7 +734,8 @@ class Renderer(RegistryBase, LogMixin, RegistryProperties, DisplayWindow):
:param text: The text to check. It may contain HTML tags.
"""
self.clear_slides()
- self.run_javascript('Display.addTextSlide("v1", "{text}", "Dummy Footer");'.format(text=text), is_sync=True)
+ self.run_javascript('Display.addTextSlide("v1", "{text}", "Dummy Footer");'
+ .format(text=text.replace('"', '\\"')), is_sync=True)
does_text_fits = self.run_javascript('Display.doesContentFit();', is_sync=True)
return does_text_fits
@@ -745,3 +758,33 @@ class Renderer(RegistryBase, LogMixin, RegistryProperties, DisplayWindow):
pixmap.save(fname, ext)
else:
return pixmap
+
+
+class Renderer(RegistryBase, RegistryProperties, ThemePreviewRenderer):
+ """
+ A virtual display used for rendering thumbnails and other offscreen tasks
+ """
+ def __init__(self, *args, **kwargs):
+ """
+ Constructor
+ """
+ super().__init__(*args, **kwargs)
+ self.force_page = False
+ for screen in ScreenList():
+ if screen.is_display:
+ self.setGeometry(screen.display_geometry.x(), screen.display_geometry.y(),
+ screen.display_geometry.width(), screen.display_geometry.height())
+ break
+ # If the display is not show'ed and hidden like this webegine will not render
+ self.show()
+ self.hide()
+ self.theme_height = 0
+ self.theme_level = ThemeLevel.Global
+
+ def set_theme_level(self, theme_level):
+ """
+ Sets the theme level.
+
+ :param theme_level: The theme level to be used.
+ """
+ self.theme_level = theme_level
diff --git a/openlp/core/ui/media/mediacontroller.py b/openlp/core/ui/media/mediacontroller.py
index 27164a06a..4938c5f39 100644
--- a/openlp/core/ui/media/mediacontroller.py
+++ b/openlp/core/ui/media/mediacontroller.py
@@ -104,6 +104,8 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
State().update_pre_conditions('mediacontroller', True)
State().update_pre_conditions('media_live', True)
else:
+ if hasattr(self.main_window, 'splash') and self.main_window.splash.isVisible():
+ self.main_window.splash.hide()
State().missing_text('media_live', translate('OpenLP.SlideController',
'VLC or pymediainfo are missing, so you are unable to play any media'))
return True
diff --git a/openlp/core/ui/media/vlcplayer.py b/openlp/core/ui/media/vlcplayer.py
index 3904b63ae..244916d24 100644
--- a/openlp/core/ui/media/vlcplayer.py
+++ b/openlp/core/ui/media/vlcplayer.py
@@ -28,7 +28,6 @@ import os
import sys
import threading
from datetime import datetime
-import vlc
from PyQt5 import QtWidgets
@@ -62,25 +61,27 @@ def get_vlc():
:return: The "vlc" module, or None
"""
- if 'vlc' in sys.modules:
- # If VLC has already been imported, no need to do all the stuff below again
- is_vlc_available = False
+ # Import the VLC module if not already done
+ if 'vlc' not in sys.modules:
try:
- is_vlc_available = bool(sys.modules['vlc'].get_default_instance())
- except Exception:
- pass
- if is_vlc_available:
- return sys.modules['vlc']
- else:
+ import vlc # noqa module is not used directly, but is used via sys.modules['vlc']
+ except ImportError:
return None
- else:
- return vlc
+ # Verify that VLC is also loadable
+ is_vlc_available = False
+ try:
+ is_vlc_available = bool(sys.modules['vlc'].get_default_instance())
+ except Exception:
+ pass
+ if is_vlc_available:
+ return sys.modules['vlc']
+ return None
# On linux we need to initialise X threads, but not when running tests.
# This needs to happen on module load and not in get_vlc(), otherwise it can cause crashes on some DE on some setups
# (reported on Gnome3, Unity, Cinnamon, all GTK+ based) when using native filedialogs...
-if is_linux() and 'nose' not in sys.argv[0] and get_vlc():
+if is_linux() and 'pytest' not in sys.argv[0] and get_vlc():
try:
try:
x11 = ctypes.cdll.LoadLibrary('libX11.so.6')
diff --git a/openlp/core/ui/themeform.py b/openlp/core/ui/themeform.py
index f165d32dc..183993500 100644
--- a/openlp/core/ui/themeform.py
+++ b/openlp/core/ui/themeform.py
@@ -172,16 +172,14 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties):
if not event:
event = QtGui.QResizeEvent(self.size(), self.size())
QtWidgets.QWizard.resizeEvent(self, event)
- if hasattr(self, 'preview_page') and self.currentPage() == self.preview_page:
- frame_width = self.preview_box_label.lineWidth()
- pixmap_width = self.preview_area.width() - 2 * frame_width
- pixmap_height = self.preview_area.height() - 2 * frame_width
- aspect_ratio = float(pixmap_width) / pixmap_height
- if aspect_ratio < self.display_aspect_ratio:
- pixmap_height = int(pixmap_width / self.display_aspect_ratio + 0.5)
- else:
- pixmap_width = int(pixmap_height * self.display_aspect_ratio + 0.5)
- self.preview_box_label.setFixedSize(pixmap_width + 2 * frame_width, pixmap_height + 2 * frame_width)
+ try:
+ self.display_aspect_ratio = self.renderer.width() / self.renderer.height()
+ except ZeroDivisionError:
+ self.display_aspect_ratio = 1
+ # Make sure we don't resize before the widgets are actually created
+ if hasattr(self, 'preview_area_layout'):
+ self.preview_area_layout.set_aspect_ratio(self.display_aspect_ratio)
+ self.preview_box.set_scale(float(self.preview_box.width()) / self.renderer.width())
def validateCurrentPage(self):
"""
@@ -206,11 +204,17 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties):
self.setOption(QtWidgets.QWizard.HaveCustomButton1, enabled)
if self.page(page_id) == self.preview_page:
self.update_theme()
- frame = self.theme_manager.generate_image(self.theme)
- frame.setDevicePixelRatio(self.devicePixelRatio())
- self.preview_box_label.setPixmap(frame)
- self.display_aspect_ratio = float(frame.width()) / frame.height()
+ self.preview_box.set_theme(self.theme)
+ self.preview_box.clear_slides()
+ self.preview_box.set_scale(float(self.preview_box.width()) / self.renderer.width())
+ try:
+ self.display_aspect_ratio = self.renderer.width() / self.renderer.height()
+ except ZeroDivisionError:
+ self.display_aspect_ratio = 1
+ self.preview_area_layout.set_aspect_ratio(self.display_aspect_ratio)
self.resizeEvent()
+ self.preview_box.show()
+ self.preview_box.generate_preview(self.theme, False, False)
def on_custom_1_button_clicked(self, number):
"""
@@ -398,6 +402,7 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties):
Handle the display and state of the Preview page.
"""
self.setField('name', self.theme.theme_name)
+ self.preview_box.set_theme(self.theme)
def on_background_combo_box_current_index_changed(self, index):
"""
@@ -558,5 +563,5 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties):
source_path = self.theme.background_filename
if not self.edit_mode and not self.theme_manager.check_if_theme_exists(self.theme.theme_name):
return
- self.theme_manager.save_theme(self.theme, source_path, destination_path)
+ self.theme_manager.save_theme(self.theme, source_path, destination_path, self.preview_box.save_screenshot())
return QtWidgets.QDialog.accept(self)
diff --git a/openlp/core/ui/thememanager.py b/openlp/core/ui/thememanager.py
index fd003ed2e..46d067970 100644
--- a/openlp/core/ui/thememanager.py
+++ b/openlp/core/ui/thememanager.py
@@ -476,7 +476,7 @@ class ThemeManager(QtWidgets.QWidget, RegistryBase, Ui_ThemeManager, LogMixin, R
if not theme_paths:
theme = Theme()
theme.theme_name = UiStrings().Default
- self._write_theme(theme)
+ self.save_theme(theme)
Settings().setValue(self.settings_section + '/global theme', theme.theme_name)
self.application.set_normal_cursor()
@@ -639,24 +639,14 @@ class ThemeManager(QtWidgets.QWidget, RegistryBase, Ui_ThemeManager, LogMixin, R
return False
return True
- def save_theme(self, theme, image_source_path, image_destination_path):
- """
- Called by theme maintenance Dialog to save the theme and to trigger the reload of the theme list
-
- :param Theme theme: The theme data object.
- :param Path image_source_path: Where the theme image is currently located.
- :param Path image_destination_path: Where the Theme Image is to be saved to
- :rtype: None
- """
- self._write_theme(theme, image_source_path, image_destination_path)
-
- def _write_theme(self, theme, image_source_path=None, image_destination_path=None):
+ def save_theme(self, theme, image_source_path=None, image_destination_path=None, image=None):
"""
Writes the theme to the disk and handles the background image if necessary
:param Theme theme: The theme data object.
:param Path image_source_path: Where the theme image is currently located.
:param Path image_destination_path: Where the Theme Image is to be saved to
+ :param image: The example image of the theme. Optionally.
:rtype: None
"""
name = theme.theme_name
@@ -676,7 +666,15 @@ class ThemeManager(QtWidgets.QWidget, RegistryBase, Ui_ThemeManager, LogMixin, R
shutil.copyfile(image_source_path, image_destination_path)
except OSError:
self.log_exception('Failed to save theme image')
- self.generate_and_save_image(name, theme)
+ if image:
+ sample_path_name = self.theme_path / '{file_name}.png'.format(file_name=name)
+ if sample_path_name.exists():
+ sample_path_name.unlink()
+ image.save(str(sample_path_name), 'png')
+ thumb_path = self.thumb_path / '{name}.png'.format(name=name)
+ create_thumb(sample_path_name, thumb_path, False)
+ else:
+ self.generate_and_save_image(name, theme)
def generate_and_save_image(self, theme_name, theme):
"""
diff --git a/openlp/core/ui/themewizard.py b/openlp/core/ui/themewizard.py
index e6a7ac2c5..a60e0dc7f 100644
--- a/openlp/core/ui/themewizard.py
+++ b/openlp/core/ui/themewizard.py
@@ -31,6 +31,8 @@ from openlp.core.lib.ui import add_welcome_page, create_valign_selection_widgets
from openlp.core.ui.icons import UiIcons
from openlp.core.widgets.buttons import ColorButton
from openlp.core.widgets.edits import PathEdit
+from openlp.core.widgets.layouts import AspectRatioLayout
+from openlp.core.display.render import ThemePreviewRenderer
class Ui_ThemeWizard(object):
@@ -363,16 +365,13 @@ class Ui_ThemeWizard(object):
self.preview_layout.addLayout(self.theme_name_layout)
self.preview_area = QtWidgets.QWidget(self.preview_page)
self.preview_area.setObjectName('PreviewArea')
- self.preview_area_layout = QtWidgets.QGridLayout(self.preview_area)
- self.preview_area_layout.setContentsMargins(0, 0, 0, 0)
- self.preview_area_layout.setColumnStretch(0, 1)
- self.preview_area_layout.setRowStretch(0, 1)
- self.preview_area_layout.setObjectName('preview_area_layout')
- self.preview_box_label = QtWidgets.QLabel(self.preview_area)
- self.preview_box_label.setFrameShape(QtWidgets.QFrame.Box)
- self.preview_box_label.setScaledContents(True)
- self.preview_box_label.setObjectName('preview_box_label')
- self.preview_area_layout.addWidget(self.preview_box_label)
+ self.preview_area_layout = AspectRatioLayout(self.preview_area, 0.75) # Dummy ratio, will be update
+ self.preview_area_layout.margin = 8
+ self.preview_area_layout.setSpacing(0)
+ self.preview_area_layout.setObjectName('preview_web_layout')
+ self.preview_box = ThemePreviewRenderer(self)
+ self.preview_box.setObjectName('preview_box')
+ self.preview_area_layout.addWidget(self.preview_box)
self.preview_layout.addWidget(self.preview_area)
theme_wizard.addPage(self.preview_page)
self.retranslate_ui(theme_wizard)
diff --git a/openlp/plugins/media/lib/mediaitem.py b/openlp/plugins/media/lib/mediaitem.py
index 58994a909..30962967a 100644
--- a/openlp/plugins/media/lib/mediaitem.py
+++ b/openlp/plugins/media/lib/mediaitem.py
@@ -173,7 +173,7 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties):
item = self.list_view.currentItem()
if item is None:
return False
- filename = item.data(QtCore.Qt.UserRole)
+ filename = str(item.data(QtCore.Qt.UserRole))
# Special handling if the filename is a optical clip
if filename.startswith('optical:'):
(name, title, audio_track, subtitle_track, start, end, clip_name) = parse_optical_path(filename)
@@ -259,11 +259,12 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties):
# TODO needs to be fixed as no idea why this fails
# media.sort(key=lambda file_path: get_natural_key(file_path.name))
for track in media:
- track_info = QtCore.QFileInfo(track)
+ track_str = str(track)
+ track_info = QtCore.QFileInfo(track_str)
item_name = None
- if track.startswith('optical:'):
+ if track_str.startswith('optical:'):
# Handle optical based item
- (file_name, title, audio_track, subtitle_track, start, end, clip_name) = parse_optical_path(track)
+ (file_name, title, audio_track, subtitle_track, start, end, clip_name) = parse_optical_path(track_str)
item_name = QtWidgets.QListWidgetItem(clip_name)
item_name.setIcon(UiIcons().optical)
item_name.setData(QtCore.Qt.UserRole, track)
@@ -272,22 +273,22 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties):
end=format_milliseconds(end)))
elif not os.path.exists(track):
# File doesn't exist, mark as error.
- file_name = os.path.split(str(track))[1]
+ file_name = os.path.split(track_str)[1]
item_name = QtWidgets.QListWidgetItem(file_name)
item_name.setIcon(UiIcons().error)
item_name.setData(QtCore.Qt.UserRole, track)
- item_name.setToolTip(track)
+ item_name.setToolTip(track_str)
elif track_info.isFile():
# Normal media file handling.
- file_name = os.path.split(str(track))[1]
+ file_name = os.path.split(track_str)[1]
item_name = QtWidgets.QListWidgetItem(file_name)
search = file_name.split('.')[-1].lower()
- if '*.{text}'.format(text=search) in self.media_controller.audio_extensions_list:
+ if search in AUDIO_EXT:
item_name.setIcon(UiIcons().audio)
else:
item_name.setIcon(UiIcons().video)
item_name.setData(QtCore.Qt.UserRole, track)
- item_name.setToolTip(track)
+ item_name.setToolTip(track_str)
if item_name:
self.list_view.addItem(item_name)
diff --git a/openlp/plugins/presentations/lib/libreofficeserver.py b/openlp/plugins/presentations/lib/libreofficeserver.py
new file mode 100644
index 000000000..b3c0cd254
--- /dev/null
+++ b/openlp/plugins/presentations/lib/libreofficeserver.py
@@ -0,0 +1,431 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+##########################################################################
+# OpenLP - Open Source Lyrics Projection #
+# ---------------------------------------------------------------------- #
+# Copyright (c) 2008-2019 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 . #
+##########################################################################
+"""
+This module runs a Pyro4 server using LibreOffice's version of Python
+
+Please Note: This intentionally uses os.path over pathlib because we don't know which version of Python is shipped with
+the version of LibreOffice on the user's computer.
+"""
+from subprocess import Popen
+import sys
+import os
+import logging
+import time
+
+
+if sys.platform.startswith('darwin'):
+ # Only make the log file on OS X when running as a server
+ logfile = os.path.join(str(os.getenv('HOME')), 'Library', 'Application Support', 'openlp', 'libreofficeserver.log')
+ print('Setting up log file: {logfile}'.format(logfile=logfile))
+ logging.basicConfig(filename=logfile, level=logging.INFO)
+
+
+# Add the current directory to sys.path so that we can load the serializers
+sys.path.append(os.path.join(os.path.dirname(__file__)))
+# Add the vendor directory to sys.path so that we can load Pyro4
+sys.path.append(os.path.join(os.path.dirname(__file__), 'vendor'))
+
+from serializers import register_classes
+from Pyro4 import Daemon, expose
+
+try:
+ # Wrap these imports in a try so that we can run the tests on macOS
+ import uno
+ from com.sun.star.beans import PropertyValue
+ from com.sun.star.task import ErrorCodeIOException
+except ImportError:
+ # But they need to be defined for mocking
+ uno = None
+ PropertyValue = None
+ ErrorCodeIOException = Exception
+
+
+log = logging.getLogger(__name__)
+register_classes()
+
+
+class TextType(object):
+ """
+ Type Enumeration for Types of Text to request
+ """
+ Title = 0
+ SlideText = 1
+ Notes = 2
+
+
+class LibreOfficeException(Exception):
+ """
+ A specific exception for LO
+ """
+ pass
+
+
+@expose
+class LibreOfficeServer(object):
+ """
+ A Pyro4 server which controls LibreOffice
+ """
+ def __init__(self):
+ """
+ Set up the server
+ """
+ self._desktop = None
+ self._control = None
+ self._document = None
+ self._presentation = None
+ self._process = None
+ self._manager = None
+
+ def _create_property(self, name, value):
+ """
+ Create an OOo style property object which are passed into some Uno methods.
+ """
+ log.debug('create property')
+ property_object = PropertyValue()
+ property_object.Name = name
+ property_object.Value = value
+ return property_object
+
+ def _get_text_from_page(self, slide_no, text_type=TextType.SlideText):
+ """
+ Return any text extracted from the presentation page.
+
+ :param slide_no: The slide the notes are required for, starting at 1
+ :param notes: A boolean. If set the method searches the notes of the slide.
+ :param text_type: A TextType. Enumeration of the types of supported text.
+ """
+ text = ''
+ if TextType.Title <= text_type <= TextType.Notes:
+ pages = self._document.getDrawPages()
+ if 0 < slide_no <= pages.getCount():
+ page = pages.getByIndex(slide_no - 1)
+ if text_type == TextType.Notes:
+ page = page.getNotesPage()
+ for index in range(page.getCount()):
+ shape = page.getByIndex(index)
+ shape_type = shape.getShapeType()
+ if shape.supportsService('com.sun.star.drawing.Text'):
+ # if they requested title, make sure it is the title
+ if text_type != TextType.Title or shape_type == 'com.sun.star.presentation.TitleTextShape':
+ text += shape.getString() + '\n'
+ return text
+
+ def start_process(self):
+ """
+ Initialise Impress
+ """
+ uno_command = [
+ '/Applications/LibreOffice.app/Contents/MacOS/soffice',
+ '--nologo',
+ '--norestore',
+ '--minimized',
+ '--nodefault',
+ '--nofirststartwizard',
+ '--accept=pipe,name=openlp_maclo;urp;StarOffice.ServiceManager'
+ ]
+ self._process = Popen(uno_command)
+
+ @property
+ def desktop(self):
+ """
+ Set up an UNO desktop instance
+ """
+ if self._desktop is not None:
+ return self._desktop
+ uno_instance = None
+ context = uno.getComponentContext()
+ resolver = context.ServiceManager.createInstanceWithContext('com.sun.star.bridge.UnoUrlResolver', context)
+ loop = 0
+ while uno_instance is None and loop < 3:
+ try:
+ uno_instance = resolver.resolve('uno:pipe,name=openlp_maclo;urp;StarOffice.ComponentContext')
+ except Exception:
+ log.exception('Unable to find running instance, retrying...')
+ loop += 1
+ try:
+ self._manager = uno_instance.ServiceManager
+ log.debug('get UNO Desktop Openoffice - createInstanceWithContext - Desktop')
+ desktop = self._manager.createInstanceWithContext('com.sun.star.frame.Desktop', uno_instance)
+ if not desktop:
+ raise Exception('Failed to get UNO desktop')
+ self._desktop = desktop
+ return desktop
+ except Exception:
+ log.exception('Failed to get UNO desktop')
+ return None
+
+ def shutdown(self):
+ """
+ Shut down the server
+ """
+ can_kill = True
+ if hasattr(self, '_docs'):
+ while self._docs:
+ self._docs[0].close_presentation()
+ docs = self.desktop.getComponents()
+ count = 0
+ if docs.hasElements():
+ list_elements = docs.createEnumeration()
+ while list_elements.hasMoreElements():
+ doc = list_elements.nextElement()
+ if doc.getImplementationName() != 'com.sun.star.comp.framework.BackingComp':
+ count += 1
+ if count > 0:
+ log.debug('LibreOffice not terminated as docs are still open')
+ can_kill = False
+ else:
+ try:
+ self.desktop.terminate()
+ log.debug('LibreOffice killed')
+ except Exception:
+ log.exception('Failed to terminate LibreOffice')
+ if getattr(self, '_process') and can_kill:
+ self._process.kill()
+
+ def load_presentation(self, file_path, screen_number):
+ """
+ Load a presentation
+ """
+ self._file_path = file_path
+ url = uno.systemPathToFileUrl(file_path)
+ properties = (self._create_property('Hidden', True),)
+ self._document = None
+ loop_count = 0
+ while loop_count < 3:
+ try:
+ self._document = self.desktop.loadComponentFromURL(url, '_blank', 0, properties)
+ except Exception:
+ log.exception('Failed to load presentation {url}'.format(url=url))
+ if self._document:
+ break
+ time.sleep(0.5)
+ loop_count += 1
+ if loop_count == 3:
+ log.error('Looped too many times')
+ return False
+ self._presentation = self._document.getPresentation()
+ self._presentation.Display = screen_number
+ self._control = None
+ return True
+
+ def extract_thumbnails(self, temp_folder):
+ """
+ Create thumbnails for the presentation
+ """
+ thumbnails = []
+ thumb_dir_url = uno.systemPathToFileUrl(temp_folder)
+ properties = (self._create_property('FilterName', 'impress_png_Export'),)
+ pages = self._document.getDrawPages()
+ if not pages:
+ return []
+ if not os.path.isdir(temp_folder):
+ os.makedirs(temp_folder)
+ for index in range(pages.getCount()):
+ page = pages.getByIndex(index)
+ self._document.getCurrentController().setCurrentPage(page)
+ url_path = '{path}/{name}.png'.format(path=thumb_dir_url, name=str(index + 1))
+ path = os.path.join(temp_folder, str(index + 1) + '.png')
+ try:
+ self._document.storeToURL(url_path, properties)
+ thumbnails.append(path)
+ except ErrorCodeIOException as exception:
+ log.exception('ERROR! ErrorCodeIOException {error:d}'.format(error=exception.ErrCode))
+ except Exception:
+ log.exception('{path} - Unable to store openoffice preview'.format(path=path))
+ return thumbnails
+
+ def get_titles_and_notes(self):
+ """
+ Extract the titles and the notes from the slides.
+ """
+ titles = []
+ notes = []
+ pages = self._document.getDrawPages()
+ for slide_no in range(1, pages.getCount() + 1):
+ titles.append(self._get_text_from_page(slide_no, TextType.Title).replace('\n', ' ') + '\n')
+ note = self._get_text_from_page(slide_no, TextType.Notes)
+ if len(note) == 0:
+ note = ' '
+ notes.append(note)
+ return titles, notes
+
+ def close_presentation(self):
+ """
+ Close presentation and clean up objects.
+ """
+ log.debug('close Presentation LibreOffice')
+ if self._document:
+ if self._presentation:
+ try:
+ self._presentation.end()
+ self._presentation = None
+ self._document.dispose()
+ except Exception:
+ log.exception("Closing presentation failed")
+ self._document = None
+
+ def is_loaded(self):
+ """
+ Returns true if a presentation is loaded.
+ """
+ log.debug('is loaded LibreOffice')
+ if self._presentation is None or self._document is None:
+ log.debug("is_loaded: no presentation or document")
+ return False
+ try:
+ if self._document.getPresentation() is None:
+ log.debug("getPresentation failed to find a presentation")
+ return False
+ except Exception:
+ log.exception("getPresentation failed to find a presentation")
+ return False
+ return True
+
+ def is_active(self):
+ """
+ Returns true if a presentation is active and running.
+ """
+ log.debug('is active LibreOffice')
+ if not self.is_loaded():
+ return False
+ return self._control.isRunning() if self._control else False
+
+ def unblank_screen(self):
+ """
+ Unblanks the screen.
+ """
+ log.debug('unblank screen LibreOffice')
+ return self._control.resume()
+
+ def blank_screen(self):
+ """
+ Blanks the screen.
+ """
+ log.debug('blank screen LibreOffice')
+ self._control.blankScreen(0)
+
+ def is_blank(self):
+ """
+ Returns true if screen is blank.
+ """
+ log.debug('is blank LibreOffice')
+ if self._control and self._control.isRunning():
+ return self._control.isPaused()
+ else:
+ return False
+
+ def stop_presentation(self):
+ """
+ Stop the presentation, remove from screen.
+ """
+ log.debug('stop presentation LibreOffice')
+ self._presentation.end()
+ self._control = None
+
+ def start_presentation(self):
+ """
+ Start the presentation from the beginning.
+ """
+ log.debug('start presentation LibreOffice')
+ if self._control is None or not self._control.isRunning():
+ window = self._document.getCurrentController().getFrame().getContainerWindow()
+ window.setVisible(True)
+ self._presentation.start()
+ self._control = self._presentation.getController()
+ # start() returns before the Component is ready. Try for 15 seconds.
+ sleep_count = 1
+ while not self._control and sleep_count < 150:
+ time.sleep(0.1)
+ sleep_count += 1
+ self._control = self._presentation.getController()
+ window.setVisible(False)
+ else:
+ self._control.activate()
+ self.goto_slide(1)
+
+ def get_slide_number(self):
+ """
+ Return the current slide number on the screen, from 1.
+ """
+ return self._control.getCurrentSlideIndex() + 1
+
+ def get_slide_count(self):
+ """
+ Return the total number of slides.
+ """
+ return self._document.getDrawPages().getCount()
+
+ def goto_slide(self, slide_no):
+ """
+ Go to a specific slide (from 1).
+
+ :param slide_no: The slide the text is required for, starting at 1
+ """
+ self._control.gotoSlideIndex(slide_no - 1)
+
+ def next_step(self):
+ """
+ Triggers the next effect of slide on the running presentation.
+ """
+ is_paused = self._control.isPaused()
+ self._control.gotoNextEffect()
+ time.sleep(0.1)
+ if not is_paused and self._control.isPaused():
+ self._control.gotoPreviousEffect()
+
+ def previous_step(self):
+ """
+ Triggers the previous slide on the running presentation.
+ """
+ self._control.gotoPreviousEffect()
+
+ def get_slide_text(self, slide_no):
+ """
+ Returns the text on the slide.
+
+ :param slide_no: The slide the text is required for, starting at 1
+ """
+ return self._get_text_from_page(slide_no)
+
+ def get_slide_notes(self, slide_no):
+ """
+ Returns the text in the slide notes.
+
+ :param slide_no: The slide the notes are required for, starting at 1
+ """
+ return self._get_text_from_page(slide_no, TextType.Notes)
+
+
+def main():
+ """
+ The main function which runs the server
+ """
+ daemon = Daemon(host='localhost', port=4310)
+ daemon.register(LibreOfficeServer, 'openlp.libreofficeserver')
+ try:
+ daemon.requestLoop()
+ finally:
+ daemon.close()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/openlp/plugins/presentations/lib/maclocontroller.py b/openlp/plugins/presentations/lib/maclocontroller.py
new file mode 100644
index 000000000..10ba08e5e
--- /dev/null
+++ b/openlp/plugins/presentations/lib/maclocontroller.py
@@ -0,0 +1,266 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+##########################################################################
+# OpenLP - Open Source Lyrics Projection #
+# ---------------------------------------------------------------------- #
+# Copyright (c) 2008-2019 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 . #
+##########################################################################
+
+import logging
+from subprocess import Popen
+
+from Pyro4 import Proxy
+
+from openlp.core.common import delete_file, is_macosx
+from openlp.core.common.applocation import AppLocation
+from openlp.core.common.path import Path
+from openlp.core.common.registry import Registry
+from openlp.core.display.screens import ScreenList
+from openlp.plugins.presentations.lib.serializers import register_classes
+from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument
+
+
+LIBREOFFICE_PATH = Path('/Applications/LibreOffice.app')
+LIBREOFFICE_PYTHON = LIBREOFFICE_PATH / 'Contents' / 'Resources' / 'python'
+
+if is_macosx() and LIBREOFFICE_PATH.exists():
+ macuno_available = True
+else:
+ macuno_available = False
+
+
+log = logging.getLogger(__name__)
+register_classes()
+
+
+class MacLOController(PresentationController):
+ """
+ Class to control interactions with MacLO presentations on Mac OS X via Pyro4. It starts the Pyro4 nameserver,
+ starts the LibreOfficeServer, and then controls MacLO via Pyro4.
+ """
+ log.info('MacLOController loaded')
+
+ def __init__(self, plugin):
+ """
+ Initialise the class
+ """
+ log.debug('Initialising')
+ super(MacLOController, self).__init__(plugin, 'maclo', MacLODocument, 'Impress on macOS')
+ self.supports = ['odp']
+ self.also_supports = ['ppt', 'pps', 'pptx', 'ppsx', 'pptm']
+ self.server_process = None
+ self._client = None
+ self._start_server()
+
+ def _start_server(self):
+ """
+ Start a LibreOfficeServer
+ """
+ libreoffice_python = Path('/Applications/LibreOffice.app/Contents/Resources/python')
+ libreoffice_server = AppLocation.get_directory(AppLocation.PluginsDir).joinpath('presentations', 'lib',
+ 'libreofficeserver.py')
+ if libreoffice_python.exists():
+ self.server_process = Popen([str(libreoffice_python), str(libreoffice_server)])
+
+ @property
+ def client(self):
+ """
+ Set up a Pyro4 client so that we can talk to the LibreOfficeServer
+ """
+ if not self._client:
+ self._client = Proxy('PYRO:openlp.libreofficeserver@localhost:4310')
+ if not self._client._pyroConnection:
+ self._client._pyroReconnect()
+ return self._client
+
+ def check_available(self):
+ """
+ MacLO is able to run on this machine.
+ """
+ log.debug('check_available')
+ return macuno_available
+
+ def start_process(self):
+ """
+ Loads a running version of LibreOffice in the background. It is not displayed to the user but is available to
+ the UNO interface when required.
+ """
+ log.debug('Started automatically by the Pyro server')
+ self.client.start_process()
+
+ def kill(self):
+ """
+ Called at system exit to clean up any running presentations.
+ """
+ log.debug('Kill LibreOffice')
+ self.client.shutdown()
+ self.server_process.kill()
+
+
+class MacLODocument(PresentationDocument):
+ """
+ Class which holds information and controls a single presentation.
+ """
+
+ def __init__(self, controller, presentation):
+ """
+ Constructor, store information about the file and initialise.
+ """
+ log.debug('Init Presentation LibreOffice')
+ super(MacLODocument, self).__init__(controller, presentation)
+ self.client = controller.client
+
+ def load_presentation(self):
+ """
+ Tell the LibreOfficeServer to start the presentation.
+ """
+ log.debug('Load Presentation LibreOffice')
+ if not self.client.load_presentation(str(self.file_path), ScreenList().current.number + 1):
+ return False
+ self.create_thumbnails()
+ self.create_titles_and_notes()
+ return True
+
+ def create_thumbnails(self):
+ """
+ Create thumbnail images for presentation.
+ """
+ log.debug('create thumbnails LibreOffice')
+ if self.check_thumbnails():
+ return
+ temp_thumbnails = self.client.extract_thumbnails(str(self.get_temp_folder()))
+ for index, temp_thumb in enumerate(temp_thumbnails):
+ temp_thumb = Path(temp_thumb)
+ self.convert_thumbnail(temp_thumb, index + 1)
+ delete_file(temp_thumb)
+
+ def create_titles_and_notes(self):
+ """
+ Writes the list of titles (one per slide) to 'titles.txt' and the notes to 'slideNotes[x].txt'
+ in the thumbnails directory
+ """
+ titles, notes = self.client.get_titles_and_notes()
+ self.save_titles_and_notes(titles, notes)
+
+ def close_presentation(self):
+ """
+ Close presentation and clean up objects. Triggered by new object being added to SlideController or OpenLP being
+ shutdown.
+ """
+ log.debug('close Presentation LibreOffice')
+ self.client.close_presentation()
+ self.controller.remove_doc(self)
+
+ def is_loaded(self):
+ """
+ Returns true if a presentation is loaded.
+ """
+ log.debug('is loaded LibreOffice')
+ return self.client.is_loaded()
+
+ def is_active(self):
+ """
+ Returns true if a presentation is active and running.
+ """
+ log.debug('is active LibreOffice')
+ return self.client.is_active()
+
+ def unblank_screen(self):
+ """
+ Unblanks the screen.
+ """
+ log.debug('unblank screen LibreOffice')
+ return self.client.unblank_screen()
+
+ def blank_screen(self):
+ """
+ Blanks the screen.
+ """
+ log.debug('blank screen LibreOffice')
+ self.client.blank_screen()
+
+ def is_blank(self):
+ """
+ Returns true if screen is blank.
+ """
+ log.debug('is blank LibreOffice')
+ return self.client.is_blank()
+
+ def stop_presentation(self):
+ """
+ Stop the presentation, remove from screen.
+ """
+ log.debug('stop presentation LibreOffice')
+ self.client.stop_presentation()
+
+ def start_presentation(self):
+ """
+ Start the presentation from the beginning.
+ """
+ log.debug('start presentation LibreOffice')
+ self.client.start_presentation()
+ # Make sure impress doesn't steal focus, unless we're on a single screen setup
+ if len(ScreenList()) > 1:
+ Registry().get('main_window').activateWindow()
+
+ def get_slide_number(self):
+ """
+ Return the current slide number on the screen, from 1.
+ """
+ return self.client.get_slide_number()
+
+ def get_slide_count(self):
+ """
+ Return the total number of slides.
+ """
+ return self.client.get_slide_count()
+
+ def goto_slide(self, slide_no):
+ """
+ Go to a specific slide (from 1).
+
+ :param slide_no: The slide the text is required for, starting at 1
+ """
+ self.client.goto_slide(slide_no)
+
+ def next_step(self):
+ """
+ Triggers the next effect of slide on the running presentation.
+ """
+ self.client.next_step()
+
+ def previous_step(self):
+ """
+ Triggers the previous slide on the running presentation.
+ """
+ self.client.previous_step()
+
+ def get_slide_text(self, slide_no):
+ """
+ Returns the text on the slide.
+
+ :param slide_no: The slide the text is required for, starting at 1
+ """
+ return self.client.get_slide_text(slide_no)
+
+ def get_slide_notes(self, slide_no):
+ """
+ Returns the text in the slide notes.
+
+ :param slide_no: The slide the notes are required for, starting at 1
+ """
+ return self.client.get_slide_notes(slide_no)
diff --git a/openlp/plugins/presentations/lib/presentationcontroller.py b/openlp/plugins/presentations/lib/presentationcontroller.py
index ac25fa9a4..fa0284cea 100644
--- a/openlp/plugins/presentations/lib/presentationcontroller.py
+++ b/openlp/plugins/presentations/lib/presentationcontroller.py
@@ -412,7 +412,8 @@ class PresentationController(object):
"""
log.info('PresentationController loaded')
- def __init__(self, plugin=None, name='PresentationController', document_class=PresentationDocument):
+ def __init__(self, plugin=None, name='PresentationController', document_class=PresentationDocument,
+ display_name=None):
"""
This is the constructor for the presentationcontroller object. This provides an easy way for descendent plugins
@@ -432,6 +433,7 @@ class PresentationController(object):
self.docs = []
self.plugin = plugin
self.name = name
+ self.display_name = display_name if display_name is not None else name
self.document_class = document_class
self.settings_section = self.plugin.settings_section
self.available = None
diff --git a/openlp/plugins/presentations/lib/presentationtab.py b/openlp/plugins/presentations/lib/presentationtab.py
index 6a18fb330..05951a973 100644
--- a/openlp/plugins/presentations/lib/presentationtab.py
+++ b/openlp/plugins/presentations/lib/presentationtab.py
@@ -127,10 +127,10 @@ class PresentationTab(SettingsTab):
def set_controller_text(self, checkbox, controller):
if checkbox.isEnabled():
- checkbox.setText(controller.name)
+ checkbox.setText(controller.display_name)
else:
checkbox.setText(translate('PresentationPlugin.PresentationTab',
- '{name} (unavailable)').format(name=controller.name))
+ '{name} (unavailable)').format(name=controller.display_name))
def load(self):
"""
diff --git a/openlp/plugins/presentations/lib/serializers.py b/openlp/plugins/presentations/lib/serializers.py
new file mode 100644
index 000000000..6e9a69cb0
--- /dev/null
+++ b/openlp/plugins/presentations/lib/serializers.py
@@ -0,0 +1,52 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+##########################################################################
+# OpenLP - Open Source Lyrics Projection #
+# ---------------------------------------------------------------------- #
+# Copyright (c) 2008-2019 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 . #
+##########################################################################
+"""
+This module contains some helpers for serializing Path objects in Pyro4
+"""
+try:
+ from openlp.core.common.path import Path
+except ImportError:
+ from pathlib import Path
+
+from Pyro4.util import SerializerBase
+
+
+def path_class_to_dict(obj):
+ """
+ Serialize a Path object for Pyro4
+ """
+ return {
+ '__class__': 'Path',
+ 'parts': obj.parts
+ }
+
+
+def path_dict_to_class(classname, d):
+ return Path(d['parts'])
+
+
+def register_classes():
+ """
+ Register the serializers
+ """
+ SerializerBase.register_class_to_dict(Path, path_class_to_dict)
+ SerializerBase.register_dict_to_class('Path', path_dict_to_class)
diff --git a/openlp/plugins/presentations/lib/vendor/do_not_delete.txt b/openlp/plugins/presentations/lib/vendor/do_not_delete.txt
new file mode 100644
index 000000000..0c81c2425
--- /dev/null
+++ b/openlp/plugins/presentations/lib/vendor/do_not_delete.txt
@@ -0,0 +1,5 @@
+Vendor Directory
+================
+
+Do not delete this directory, it is used on Mac OS to place Pyro4 and serpent for use with Impress.
+
diff --git a/openlp/plugins/presentations/presentationplugin.py b/openlp/plugins/presentations/presentationplugin.py
index 320437c4c..64ff07611 100644
--- a/openlp/plugins/presentations/presentationplugin.py
+++ b/openlp/plugins/presentations/presentationplugin.py
@@ -28,13 +28,13 @@ import os
from PyQt5 import QtCore
-from openlp.core.state import State
from openlp.core.api.http import register_endpoint
from openlp.core.common import extension_loader
from openlp.core.common.i18n import translate
from openlp.core.common.settings import Settings
from openlp.core.lib import build_icon
from openlp.core.lib.plugin import Plugin, StringContent
+from openlp.core.state import State
from openlp.core.ui.icons import UiIcons
from openlp.plugins.presentations.endpoint import api_presentations_endpoint, presentations_endpoint
from openlp.plugins.presentations.lib.presentationcontroller import PresentationController
@@ -45,18 +45,20 @@ from openlp.plugins.presentations.lib.presentationtab import PresentationTab
log = logging.getLogger(__name__)
-__default_settings__ = {'presentations/override app': QtCore.Qt.Unchecked,
- 'presentations/enable_pdf_program': QtCore.Qt.Unchecked,
- 'presentations/pdf_program': None,
- 'presentations/Impress': QtCore.Qt.Checked,
- 'presentations/Powerpoint': QtCore.Qt.Checked,
- 'presentations/Pdf': QtCore.Qt.Checked,
- 'presentations/presentations files': [],
- 'presentations/thumbnail_scheme': '',
- 'presentations/powerpoint slide click advance': QtCore.Qt.Unchecked,
- 'presentations/powerpoint control window': QtCore.Qt.Unchecked,
- 'presentations/last directory': None
- }
+__default_settings__ = {
+ 'presentations/override app': QtCore.Qt.Unchecked,
+ 'presentations/enable_pdf_program': QtCore.Qt.Unchecked,
+ 'presentations/pdf_program': None,
+ 'presentations/maclo': QtCore.Qt.Checked,
+ 'presentations/Impress': QtCore.Qt.Checked,
+ 'presentations/Powerpoint': QtCore.Qt.Checked,
+ 'presentations/Pdf': QtCore.Qt.Checked,
+ 'presentations/presentations files': [],
+ 'presentations/thumbnail_scheme': '',
+ 'presentations/powerpoint slide click advance': QtCore.Qt.Unchecked,
+ 'presentations/powerpoint control window': QtCore.Qt.Unchecked,
+ 'presentations/last directory': None
+}
class PresentationPlugin(Plugin):
@@ -100,7 +102,7 @@ class PresentationPlugin(Plugin):
try:
self.controllers[controller].start_process()
except Exception:
- log.warning('Failed to start controller process')
+ log.exception('Failed to start controller process')
self.controllers[controller].available = False
self.media_item.build_file_mask_string()
diff --git a/openlp/plugins/songs/lib/songstab.py b/openlp/plugins/songs/lib/songstab.py
index 34b83a8e5..0abd6edda 100644
--- a/openlp/plugins/songs/lib/songstab.py
+++ b/openlp/plugins/songs/lib/songstab.py
@@ -88,7 +88,7 @@ class SongsTab(SettingsTab):
self.footer_group_box = QtWidgets.QGroupBox(self.left_column)
self.footer_group_box.setObjectName('footer_group_box')
self.footer_layout = QtWidgets.QVBoxLayout(self.footer_group_box)
- self.footer_layout.setObjectName('chords_layout')
+ self.footer_layout.setObjectName('footer_layout')
self.footer_info_label = QtWidgets.QLabel(self.footer_group_box)
self.footer_layout.addWidget(self.footer_info_label)
self.footer_placeholder_info = QtWidgets.QTextEdit(self.footer_group_box)
diff --git a/scripts/check_dependencies.py b/scripts/check_dependencies.py
index 19325fbb8..9ace75258 100755
--- a/scripts/check_dependencies.py
+++ b/scripts/check_dependencies.py
@@ -160,6 +160,8 @@ def check_module(mod, text='', indent=' '):
w('OK')
except ImportError:
w('FAIL')
+ except Exception:
+ w('ERROR')
w(os.linesep)
diff --git a/tests/functional/openlp_core/common/test_path.py b/tests/functional/openlp_core/common/test_path.py
index 5b983bc20..861022b7e 100644
--- a/tests/functional/openlp_core/common/test_path.py
+++ b/tests/functional/openlp_core/common/test_path.py
@@ -110,7 +110,18 @@ class TestPath(TestCase):
# WHEN: Calling `path_to_str` with an invalid Type
# THEN: A TypeError should have been raised
with self.assertRaises(TypeError):
- path_to_str(str())
+ path_to_str(57)
+
+ def test_path_to_str_wth_str(self):
+ """
+ Test that `path_to_str` just returns a str when given a str
+ """
+ # GIVEN: The `path_to_str` function
+ # WHEN: Calling `path_to_str` with a str
+ result = path_to_str('/usr/bin')
+
+ # THEN: The string should be returned
+ assert result == '/usr/bin'
def test_path_to_str_none(self):
"""
diff --git a/tests/functional/openlp_core/ui/test_thememanager.py b/tests/functional/openlp_core/ui/test_thememanager.py
index b4d50331b..3b015f238 100644
--- a/tests/functional/openlp_core/ui/test_thememanager.py
+++ b/tests/functional/openlp_core/ui/test_thememanager.py
@@ -83,7 +83,7 @@ class TestThemeManager(TestCase):
@patch('openlp.core.ui.thememanager.shutil')
@patch('openlp.core.ui.thememanager.create_paths')
- def test_write_theme_same_image(self, mocked_create_paths, mocked_shutil):
+ def test_save_theme_same_image(self, mocked_create_paths, mocked_shutil):
"""
Test that we don't try to overwrite a theme background image with itself
"""
@@ -98,16 +98,16 @@ class TestThemeManager(TestCase):
mocked_theme.extract_formatted_xml = MagicMock()
mocked_theme.extract_formatted_xml.return_value = 'fake_theme_xml'.encode()
- # WHEN: Calling _write_theme with path to the same image, but the path written slightly different
+ # WHEN: Calling save_theme with path to the same image, but the path written slightly different
file_path_1 = RESOURCE_PATH / 'church.jpg'
- theme_manager._write_theme(mocked_theme, file_path_1, file_path_1)
+ theme_manager.save_theme(mocked_theme, file_path_1, file_path_1)
# THEN: The mocked_copyfile should not have been called
assert mocked_shutil.copyfile.called is False, 'copyfile should not be called'
@patch('openlp.core.ui.thememanager.shutil')
@patch('openlp.core.ui.thememanager.create_paths')
- def test_write_theme_diff_images(self, mocked_create_paths, mocked_shutil):
+ def test_save_theme_diff_images(self, mocked_create_paths, mocked_shutil):
"""
Test that we do overwrite a theme background image when a new is submitted
"""
@@ -121,15 +121,15 @@ class TestThemeManager(TestCase):
mocked_theme.theme_name = 'themename'
mocked_theme.filename = "filename"
- # WHEN: Calling _write_theme with path to different images
+ # WHEN: Calling save_theme with path to different images
file_path_1 = RESOURCE_PATH / 'church.jpg'
file_path_2 = RESOURCE_PATH / 'church2.jpg'
- theme_manager._write_theme(mocked_theme, file_path_1, file_path_2)
+ theme_manager.save_theme(mocked_theme, file_path_1, file_path_2)
# THEN: The mocked_copyfile should not have been called
assert mocked_shutil.copyfile.called is True, 'copyfile should be called'
- def test_write_theme_special_char_name(self):
+ def test_save_theme_special_char_name(self):
"""
Test that we can save themes with special characters in the name
"""
@@ -142,8 +142,8 @@ class TestThemeManager(TestCase):
mocked_theme.theme_name = 'theme 愛 name'
mocked_theme.export_theme.return_value = "{}"
- # WHEN: Calling _write_theme with a theme with a name with special characters in it
- theme_manager._write_theme(mocked_theme)
+ # WHEN: Calling save_theme with a theme with a name with special characters in it
+ theme_manager.save_theme(mocked_theme)
# THEN: It should have been created
assert os.path.exists(os.path.join(self.temp_folder, 'theme 愛 name', 'theme 愛 name.json')) is True, \
diff --git a/tests/functional/openlp_plugins/presentations/test_libreofficeserver.py b/tests/functional/openlp_plugins/presentations/test_libreofficeserver.py
new file mode 100644
index 000000000..d747317d9
--- /dev/null
+++ b/tests/functional/openlp_plugins/presentations/test_libreofficeserver.py
@@ -0,0 +1,948 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+##########################################################################
+# OpenLP - Open Source Lyrics Projection #
+# ---------------------------------------------------------------------- #
+# Copyright (c) 2008-2019 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 . #
+##########################################################################
+"""
+Functional tests to test the LibreOffice Pyro server
+"""
+from unittest.mock import MagicMock, patch, call
+
+from openlp.plugins.presentations.lib.libreofficeserver import LibreOfficeServer, TextType, main
+
+
+def test_constructor():
+ """
+ Test the Constructor from the server
+ """
+ # GIVEN: No server
+ # WHEN: The server object is created
+ server = LibreOfficeServer()
+
+ # THEN: The server should have been set up correctly
+ assert server._control is None
+ # assert server._desktop is None
+ assert server._document is None
+ assert server._presentation is None
+ assert server._process is None
+
+
+@patch('openlp.plugins.presentations.lib.libreofficeserver.Popen')
+def test_start_process(MockedPopen):
+ """
+ Test that the correct command is issued to run LibreOffice
+ """
+ # GIVEN: A LOServer
+ mocked_process = MagicMock()
+ MockedPopen.return_value = mocked_process
+ server = LibreOfficeServer()
+
+ # WHEN: The start_process() method is run
+ server.start_process()
+
+ # THEN: The correct command line should run and the process should have started
+ MockedPopen.assert_called_with([
+ '/Applications/LibreOffice.app/Contents/MacOS/soffice',
+ '--nologo',
+ '--norestore',
+ '--minimized',
+ '--nodefault',
+ '--nofirststartwizard',
+ '--accept=pipe,name=openlp_maclo;urp;StarOffice.ServiceManager'
+ ])
+ assert server._process is mocked_process
+
+
+@patch('openlp.plugins.presentations.lib.libreofficeserver.uno')
+def test_desktop_already_has_desktop(mocked_uno):
+ """
+ Test that setup_desktop() exits early when there's already a desktop
+ """
+ # GIVEN: A LibreOfficeServer instance
+ server = LibreOfficeServer()
+ server._desktop = MagicMock()
+
+ # WHEN: the desktop property is called
+ desktop = server.desktop
+
+ # THEN: setup_desktop() exits early
+ assert desktop is server._desktop
+ assert server._manager is None
+
+
+@patch('openlp.plugins.presentations.lib.libreofficeserver.uno')
+def test_desktop_exception(mocked_uno):
+ """
+ Test that setting up the desktop works correctly when an exception occurs
+ """
+ # GIVEN: A LibreOfficeServer instance
+ server = LibreOfficeServer()
+ mocked_context = MagicMock()
+ mocked_resolver = MagicMock()
+ mocked_uno_instance = MagicMock()
+ MockedServiceManager = MagicMock()
+ mocked_uno.getComponentContext.return_value = mocked_context
+ mocked_context.ServiceManager.createInstanceWithContext.return_value = mocked_resolver
+ mocked_resolver.resolve.side_effect = [Exception, mocked_uno_instance]
+ mocked_uno_instance.ServiceManager = MockedServiceManager
+ MockedServiceManager.createInstanceWithContext.side_effect = Exception()
+
+ # WHEN: the desktop property is called
+ server.desktop
+
+ # THEN: A desktop object was created
+ mocked_uno.getComponentContext.assert_called_once_with()
+ mocked_context.ServiceManager.createInstanceWithContext.assert_called_once_with(
+ 'com.sun.star.bridge.UnoUrlResolver', mocked_context)
+ expected_calls = [
+ call('uno:pipe,name=openlp_maclo;urp;StarOffice.ComponentContext'),
+ call('uno:pipe,name=openlp_maclo;urp;StarOffice.ComponentContext')
+ ]
+ assert mocked_resolver.resolve.call_args_list == expected_calls
+ MockedServiceManager.createInstanceWithContext.assert_called_once_with(
+ 'com.sun.star.frame.Desktop', mocked_uno_instance)
+ assert server._manager is MockedServiceManager
+ assert server._desktop is None
+
+
+@patch('openlp.plugins.presentations.lib.libreofficeserver.uno')
+def test_desktop(mocked_uno):
+ """
+ Test that setting up the desktop works correctly
+ """
+ # GIVEN: A LibreOfficeServer instance
+ server = LibreOfficeServer()
+ mocked_context = MagicMock()
+ mocked_resolver = MagicMock()
+ mocked_uno_instance = MagicMock()
+ MockedServiceManager = MagicMock()
+ mocked_desktop = MagicMock()
+ mocked_uno.getComponentContext.return_value = mocked_context
+ mocked_context.ServiceManager.createInstanceWithContext.return_value = mocked_resolver
+ mocked_resolver.resolve.side_effect = [Exception, mocked_uno_instance]
+ mocked_uno_instance.ServiceManager = MockedServiceManager
+ MockedServiceManager.createInstanceWithContext.return_value = mocked_desktop
+
+ # WHEN: the desktop property is called
+ server.desktop
+
+ # THEN: A desktop object was created
+ mocked_uno.getComponentContext.assert_called_once_with()
+ mocked_context.ServiceManager.createInstanceWithContext.assert_called_once_with(
+ 'com.sun.star.bridge.UnoUrlResolver', mocked_context)
+ expected_calls = [
+ call('uno:pipe,name=openlp_maclo;urp;StarOffice.ComponentContext'),
+ call('uno:pipe,name=openlp_maclo;urp;StarOffice.ComponentContext')
+ ]
+ assert mocked_resolver.resolve.call_args_list == expected_calls
+ MockedServiceManager.createInstanceWithContext.assert_called_once_with(
+ 'com.sun.star.frame.Desktop', mocked_uno_instance)
+ assert server._manager is MockedServiceManager
+ assert server._desktop is mocked_desktop
+
+
+@patch('openlp.plugins.presentations.lib.libreofficeserver.PropertyValue')
+def test_create_property(MockedPropertyValue):
+ """
+ Test that the _create_property() method works correctly
+ """
+ # GIVEN: A server amnd property to set
+ server = LibreOfficeServer()
+ name = 'Hidden'
+ value = True
+
+ # WHEN: The _create_property() method is called
+ prop = server._create_property(name, value)
+
+ # THEN: The property should have the correct attributes
+ assert prop.Name == name
+ assert prop.Value == value
+
+
+def test_get_text_from_page_slide_text():
+ """
+ Test that the _get_text_from_page() method gives us nothing for slide text
+ """
+ # GIVEN: A LibreOfficeServer object and some mocked objects
+ text_type = TextType.SlideText
+ slide_no = 1
+ server = LibreOfficeServer()
+ server._document = MagicMock()
+ mocked_pages = MagicMock()
+ mocked_page = MagicMock()
+ mocked_shape = MagicMock()
+ server._document.getDrawPages.return_value = mocked_pages
+ mocked_pages.getCount.return_value = 1
+ mocked_pages.getByIndex.return_value = mocked_page
+ mocked_page.getByIndex.return_value = mocked_shape
+ mocked_shape.getShapeType.return_value = 'com.sun.star.presentation.TitleTextShape'
+ mocked_shape.supportsService.return_value = True
+ mocked_shape.getString.return_value = 'Page Text'
+
+ # WHEN: _get_text_from_page() is run for slide text
+ text = server._get_text_from_page(slide_no, text_type)
+
+ # THE: The text is correct
+ assert text == 'Page Text\n'
+
+
+def test_get_text_from_page_title():
+ """
+ Test that the _get_text_from_page() method gives us the text from the titles
+ """
+ # GIVEN: A LibreOfficeServer object and some mocked objects
+ text_type = TextType.Title
+ slide_no = 1
+ server = LibreOfficeServer()
+ server._document = MagicMock()
+ mocked_pages = MagicMock()
+ mocked_page = MagicMock()
+ mocked_shape = MagicMock()
+ server._document.getDrawPages.return_value = mocked_pages
+ mocked_pages.getCount.return_value = 1
+ mocked_pages.getByIndex.return_value = mocked_page
+ mocked_page.getByIndex.return_value = mocked_shape
+ mocked_shape.getShapeType.return_value = 'com.sun.star.presentation.TitleTextShape'
+ mocked_shape.supportsService.return_value = True
+ mocked_shape.getString.return_value = 'Page Title'
+
+ # WHEN: _get_text_from_page() is run for titles
+ text = server._get_text_from_page(slide_no, text_type)
+
+ # THEN: The text should be correct
+ assert text == 'Page Title\n'
+
+
+def test_get_text_from_page_notes():
+ """
+ Test that the _get_text_from_page() method gives us the text from the notes
+ """
+ # GIVEN: A LibreOfficeServer object and some mocked objects
+ text_type = TextType.Notes
+ slide_no = 1
+ server = LibreOfficeServer()
+ server._document = MagicMock()
+ mocked_pages = MagicMock()
+ mocked_page = MagicMock()
+ mocked_notes_page = MagicMock()
+ mocked_shape = MagicMock()
+ server._document.getDrawPages.return_value = mocked_pages
+ mocked_pages.getCount.return_value = 1
+ mocked_pages.getByIndex.return_value = mocked_page
+ mocked_page.getNotesPage.return_value = mocked_notes_page
+ mocked_notes_page.getByIndex.return_value = mocked_shape
+ mocked_shape.getShapeType.return_value = 'com.sun.star.presentation.TitleTextShape'
+ mocked_shape.supportsService.return_value = True
+ mocked_shape.getString.return_value = 'Page Notes'
+
+ # WHEN: _get_text_from_page() is run for titles
+ text = server._get_text_from_page(slide_no, text_type)
+
+ # THEN: The text should be correct
+ assert text == 'Page Notes\n'
+
+
+def test_shutdown_other_docs():
+ """
+ Test the shutdown method while other documents are open in LibreOffice
+ """
+ def close_docs():
+ server._docs = []
+
+ # GIVEN: An up an running LibreOfficeServer
+ server = LibreOfficeServer()
+ mocked_doc = MagicMock()
+ mocked_desktop = MagicMock()
+ mocked_docs = MagicMock()
+ mocked_list = MagicMock()
+ mocked_element_doc = MagicMock()
+ server._docs = [mocked_doc]
+ server._desktop = mocked_desktop
+ server._process = MagicMock()
+ mocked_doc.close_presentation.side_effect = close_docs
+ mocked_desktop.getComponents.return_value = mocked_docs
+ mocked_docs.hasElements.return_value = True
+ mocked_docs.createEnumeration.return_value = mocked_list
+ mocked_list.hasMoreElements.side_effect = [True, False]
+ mocked_list.nextElement.return_value = mocked_element_doc
+ mocked_element_doc.getImplementationName.side_effect = [
+ 'org.openlp.Nothing',
+ 'com.sun.star.comp.framework.BackingComp'
+ ]
+
+ # WHEN: shutdown() is called
+ server.shutdown()
+
+ # THEN: The right methods are called and everything works
+ mocked_doc.close_presentation.assert_called_once_with()
+ mocked_desktop.getComponents.assert_called_once_with()
+ mocked_docs.hasElements.assert_called_once_with()
+ mocked_docs.createEnumeration.assert_called_once_with()
+ assert mocked_list.hasMoreElements.call_count == 2
+ mocked_list.nextElement.assert_called_once_with()
+ mocked_element_doc.getImplementationName.assert_called_once_with()
+ assert mocked_desktop.terminate.call_count == 0
+ assert server._process.kill.call_count == 0
+
+
+def test_shutdown():
+ """
+ Test the shutdown method
+ """
+ def close_docs():
+ server._docs = []
+
+ # GIVEN: An up an running LibreOfficeServer
+ server = LibreOfficeServer()
+ mocked_doc = MagicMock()
+ mocked_desktop = MagicMock()
+ mocked_docs = MagicMock()
+ mocked_list = MagicMock()
+ mocked_element_doc = MagicMock()
+ server._docs = [mocked_doc]
+ server._desktop = mocked_desktop
+ server._process = MagicMock()
+ mocked_doc.close_presentation.side_effect = close_docs
+ mocked_desktop.getComponents.return_value = mocked_docs
+ mocked_docs.hasElements.return_value = True
+ mocked_docs.createEnumeration.return_value = mocked_list
+ mocked_list.hasMoreElements.side_effect = [True, False]
+ mocked_list.nextElement.return_value = mocked_element_doc
+ mocked_element_doc.getImplementationName.return_value = 'com.sun.star.comp.framework.BackingComp'
+
+ # WHEN: shutdown() is called
+ server.shutdown()
+
+ # THEN: The right methods are called and everything works
+ mocked_doc.close_presentation.assert_called_once_with()
+ mocked_desktop.getComponents.assert_called_once_with()
+ mocked_docs.hasElements.assert_called_once_with()
+ mocked_docs.createEnumeration.assert_called_once_with()
+ assert mocked_list.hasMoreElements.call_count == 2
+ mocked_list.nextElement.assert_called_once_with()
+ mocked_element_doc.getImplementationName.assert_called_once_with()
+ mocked_desktop.terminate.assert_called_once_with()
+ server._process.kill.assert_called_once_with()
+
+
+@patch('openlp.plugins.presentations.lib.libreofficeserver.uno')
+def test_load_presentation_exception(mocked_uno):
+ """
+ Test the load_presentation() method when an exception occurs
+ """
+ # GIVEN: A LibreOfficeServer object
+ presentation_file = '/path/to/presentation.odp'
+ screen_number = 1
+ server = LibreOfficeServer()
+ mocked_desktop = MagicMock()
+ mocked_uno.systemPathToFileUrl.side_effect = lambda x: x
+ server._desktop = mocked_desktop
+ mocked_desktop.loadComponentFromURL.side_effect = Exception()
+
+ # WHEN: load_presentation() is called
+ with patch.object(server, '_create_property') as mocked_create_property:
+ mocked_create_property.side_effect = lambda x, y: {x: y}
+ result = server.load_presentation(presentation_file, screen_number)
+
+ # THEN: A presentation is loaded
+ assert result is False
+
+
+@patch('openlp.plugins.presentations.lib.libreofficeserver.uno')
+def test_load_presentation(mocked_uno):
+ """
+ Test the load_presentation() method
+ """
+ # GIVEN: A LibreOfficeServer object
+ presentation_file = '/path/to/presentation.odp'
+ screen_number = 1
+ server = LibreOfficeServer()
+ mocked_desktop = MagicMock()
+ mocked_document = MagicMock()
+ mocked_presentation = MagicMock()
+ mocked_uno.systemPathToFileUrl.side_effect = lambda x: x
+ server._desktop = mocked_desktop
+ mocked_desktop.loadComponentFromURL.return_value = mocked_document
+ mocked_document.getPresentation.return_value = mocked_presentation
+
+ # WHEN: load_presentation() is called
+ with patch.object(server, '_create_property') as mocked_create_property:
+ mocked_create_property.side_effect = lambda x, y: {x: y}
+ result = server.load_presentation(presentation_file, screen_number)
+
+ # THEN: A presentation is loaded
+ assert result is True
+ mocked_uno.systemPathToFileUrl.assert_called_once_with(presentation_file)
+ mocked_create_property.assert_called_once_with('Hidden', True)
+ mocked_desktop.loadComponentFromURL.assert_called_once_with(
+ presentation_file, '_blank', 0, ({'Hidden': True},))
+ assert server._document is mocked_document
+ mocked_document.getPresentation.assert_called_once_with()
+ assert server._presentation is mocked_presentation
+ assert server._presentation.Display == screen_number
+ assert server._control is None
+
+
+@patch('openlp.plugins.presentations.lib.libreofficeserver.uno')
+def test_extract_thumbnails_no_pages(mocked_uno):
+ """
+ Test the extract_thumbnails() method when there are no pages
+ """
+ # GIVEN: A LibreOfficeServer instance
+ temp_folder = '/tmp'
+ server = LibreOfficeServer()
+ mocked_document = MagicMock()
+ server._document = mocked_document
+ mocked_uno.systemPathToFileUrl.side_effect = lambda x: x
+ mocked_document.getDrawPages.return_value = None
+
+ # WHEN: The extract_thumbnails() method is called
+ with patch.object(server, '_create_property') as mocked_create_property:
+ mocked_create_property.side_effect = lambda x, y: {x: y}
+ thumbnails = server.extract_thumbnails(temp_folder)
+
+ # THEN: Thumbnails have been extracted
+ mocked_uno.systemPathToFileUrl.assert_called_once_with(temp_folder)
+ mocked_create_property.assert_called_once_with('FilterName', 'impress_png_Export')
+ mocked_document.getDrawPages.assert_called_once_with()
+ assert thumbnails == []
+
+
+@patch('openlp.plugins.presentations.lib.libreofficeserver.uno')
+@patch('openlp.plugins.presentations.lib.libreofficeserver.os')
+def test_extract_thumbnails(mocked_os, mocked_uno):
+ """
+ Test the extract_thumbnails() method
+ """
+ # GIVEN: A LibreOfficeServer instance
+ temp_folder = '/tmp'
+ server = LibreOfficeServer()
+ mocked_document = MagicMock()
+ mocked_pages = MagicMock()
+ mocked_page_1 = MagicMock()
+ mocked_page_2 = MagicMock()
+ mocked_controller = MagicMock()
+ server._document = mocked_document
+ mocked_uno.systemPathToFileUrl.side_effect = lambda x: x
+ mocked_document.getDrawPages.return_value = mocked_pages
+ mocked_os.path.isdir.return_value = False
+ mocked_pages.getCount.return_value = 2
+ mocked_pages.getByIndex.side_effect = [mocked_page_1, mocked_page_2]
+ mocked_document.getCurrentController.return_value = mocked_controller
+ mocked_os.path.join.side_effect = lambda *x: '/'.join(x)
+
+ # WHEN: The extract_thumbnails() method is called
+ with patch.object(server, '_create_property') as mocked_create_property:
+ mocked_create_property.side_effect = lambda x, y: {x: y}
+ thumbnails = server.extract_thumbnails(temp_folder)
+
+ # THEN: Thumbnails have been extracted
+ mocked_uno.systemPathToFileUrl.assert_called_once_with(temp_folder)
+ mocked_create_property.assert_called_once_with('FilterName', 'impress_png_Export')
+ mocked_document.getDrawPages.assert_called_once_with()
+ mocked_pages.getCount.assert_called_once_with()
+ assert mocked_pages.getByIndex.call_args_list == [call(0), call(1)]
+ assert mocked_controller.setCurrentPage.call_args_list == \
+ [call(mocked_page_1), call(mocked_page_2)]
+ assert mocked_document.storeToURL.call_args_list == \
+ [call('/tmp/1.png', ({'FilterName': 'impress_png_Export'},)),
+ call('/tmp/2.png', ({'FilterName': 'impress_png_Export'},))]
+ assert thumbnails == ['/tmp/1.png', '/tmp/2.png']
+
+
+def test_get_titles_and_notes():
+ """
+ Test the get_titles_and_notes() method
+ """
+ # GIVEN: A LibreOfficeServer object and a bunch of mocks
+ server = LibreOfficeServer()
+ mocked_document = MagicMock()
+ mocked_pages = MagicMock()
+ server._document = mocked_document
+ mocked_document.getDrawPages.return_value = mocked_pages
+ mocked_pages.getCount.return_value = 2
+
+ # WHEN: get_titles_and_notes() is called
+ with patch.object(server, '_get_text_from_page') as mocked_get_text_from_page:
+ mocked_get_text_from_page.side_effect = [
+ 'OpenLP on Mac OS X',
+ '',
+ '',
+ 'Installing is a drag-and-drop affair'
+ ]
+ titles, notes = server.get_titles_and_notes()
+
+ # THEN: The right calls are made and the right stuff returned
+ mocked_document.getDrawPages.assert_called_once_with()
+ mocked_pages.getCount.assert_called_once_with()
+ assert mocked_get_text_from_page.call_count == 4
+ expected_calls = [
+ call(1, TextType.Title), call(1, TextType.Notes),
+ call(2, TextType.Title), call(2, TextType.Notes),
+ ]
+ assert mocked_get_text_from_page.call_args_list == expected_calls
+ assert titles == ['OpenLP on Mac OS X\n', '\n'], titles
+ assert notes == [' ', 'Installing is a drag-and-drop affair'], notes
+
+
+def test_close_presentation():
+ """
+ Test that closing the presentation cleans things up correctly
+ """
+ # GIVEN: A LibreOfficeServer instance and a bunch of mocks
+ server = LibreOfficeServer()
+ mocked_document = MagicMock()
+ mocked_presentation = MagicMock()
+ server._document = mocked_document
+ server._presentation = mocked_presentation
+
+ # WHEN: close_presentation() is called
+ server.close_presentation()
+
+ # THEN: The presentation and document should be closed
+ mocked_presentation.end.assert_called_once_with()
+ mocked_document.dispose.assert_called_once_with()
+ assert server._document is None
+ assert server._presentation is None
+
+
+def test_is_loaded_no_objects():
+ """
+ Test the is_loaded() method when there's no document or presentation
+ """
+ # GIVEN: A LibreOfficeServer instance and a bunch of mocks
+ server = LibreOfficeServer()
+
+ # WHEN: The is_loaded() method is called
+ result = server.is_loaded()
+
+ # THEN: The result should be false
+ assert result is False
+
+
+def test_is_loaded_no_presentation():
+ """
+ Test the is_loaded() method when there's no presentation
+ """
+ # GIVEN: A LibreOfficeServer instance and a bunch of mocks
+ server = LibreOfficeServer()
+ mocked_document = MagicMock()
+ server._document = mocked_document
+ server._presentation = MagicMock()
+ mocked_document.getPresentation.return_value = None
+
+ # WHEN: The is_loaded() method is called
+ result = server.is_loaded()
+
+ # THEN: The result should be false
+ assert result is False
+ mocked_document.getPresentation.assert_called_once_with()
+
+
+def test_is_loaded_exception():
+ """
+ Test the is_loaded() method when an exception is thrown
+ """
+ # GIVEN: A LibreOfficeServer instance and a bunch of mocks
+ server = LibreOfficeServer()
+ mocked_document = MagicMock()
+ server._document = mocked_document
+ server._presentation = MagicMock()
+ mocked_document.getPresentation.side_effect = Exception()
+
+ # WHEN: The is_loaded() method is called
+ result = server.is_loaded()
+
+ # THEN: The result should be false
+ assert result is False
+ mocked_document.getPresentation.assert_called_once_with()
+
+
+def test_is_loaded():
+ """
+ Test the is_loaded() method
+ """
+ # GIVEN: A LibreOfficeServer instance and a bunch of mocks
+ server = LibreOfficeServer()
+ mocked_document = MagicMock()
+ mocked_presentation = MagicMock()
+ server._document = mocked_document
+ server._presentation = mocked_presentation
+ mocked_document.getPresentation.return_value = mocked_presentation
+
+ # WHEN: The is_loaded() method is called
+ result = server.is_loaded()
+
+ # THEN: The result should be false
+ assert result is True
+ mocked_document.getPresentation.assert_called_once_with()
+
+
+def test_is_active_not_loaded():
+ """
+ Test is_active() when is_loaded() returns False
+ """
+ # GIVEN: A LibreOfficeServer instance and a bunch of mocks
+ server = LibreOfficeServer()
+
+ # WHEN: is_active() is called with is_loaded() returns False
+ with patch.object(server, 'is_loaded') as mocked_is_loaded:
+ mocked_is_loaded.return_value = False
+ result = server.is_active()
+
+ # THEN: It should have returned False
+ assert result is False
+
+
+def test_is_active_no_control():
+ """
+ Test is_active() when is_loaded() returns True but there's no control
+ """
+ # GIVEN: A LibreOfficeServer instance and a bunch of mocks
+ server = LibreOfficeServer()
+
+ # WHEN: is_active() is called with is_loaded() returns False
+ with patch.object(server, 'is_loaded') as mocked_is_loaded:
+ mocked_is_loaded.return_value = True
+ result = server.is_active()
+
+ # THEN: The result should be False
+ assert result is False
+ mocked_is_loaded.assert_called_once_with()
+
+
+def test_is_active():
+ """
+ Test is_active()
+ """
+ # GIVEN: A LibreOfficeServer instance and a bunch of mocks
+ server = LibreOfficeServer()
+ mocked_control = MagicMock()
+ server._control = mocked_control
+ mocked_control.isRunning.return_value = True
+
+ # WHEN: is_active() is called with is_loaded() returns False
+ with patch.object(server, 'is_loaded') as mocked_is_loaded:
+ mocked_is_loaded.return_value = True
+ result = server.is_active()
+
+ # THEN: The result should be False
+ assert result is True
+ mocked_is_loaded.assert_called_once_with()
+ mocked_control.isRunning.assert_called_once_with()
+
+
+def test_unblank_screen():
+ """
+ Test the unblank_screen() method
+ """
+ # GIVEN: A LibreOfficeServer instance and a bunch of mocks
+ server = LibreOfficeServer()
+ mocked_control = MagicMock()
+ server._control = mocked_control
+
+ # WHEN: unblank_screen() is run
+ server.unblank_screen()
+
+ # THEN: The resume method should have been called
+ mocked_control.resume.assert_called_once_with()
+
+
+def test_blank_screen():
+ """
+ Test the blank_screen() method
+ """
+ # GIVEN: A LibreOfficeServer instance and a bunch of mocks
+ server = LibreOfficeServer()
+ mocked_control = MagicMock()
+ server._control = mocked_control
+
+ # WHEN: blank_screen() is run
+ server.blank_screen()
+
+ # THEN: The resume method should have been called
+ mocked_control.blankScreen.assert_called_once_with(0)
+
+
+def test_is_blank_no_control():
+ """
+ Test the is_blank() method when there's no control
+ """
+ # GIVEN: A LibreOfficeServer instance and a bunch of mocks
+ server = LibreOfficeServer()
+
+ # WHEN: is_blank() is called
+ result = server.is_blank()
+
+ # THEN: It should have returned False
+ assert result is False
+
+
+def test_is_blank_control_is_running():
+ """
+ Test the is_blank() method when the control is running
+ """
+ # GIVEN: A LibreOfficeServer instance and a bunch of mocks
+ server = LibreOfficeServer()
+ mocked_control = MagicMock()
+ server._control = mocked_control
+ mocked_control.isRunning.return_value = True
+ mocked_control.isPaused.return_value = True
+
+ # WHEN: is_blank() is called
+ result = server.is_blank()
+
+ # THEN: It should have returned False
+ assert result is True
+ mocked_control.isRunning.assert_called_once_with()
+ mocked_control.isPaused.assert_called_once_with()
+
+
+def test_stop_presentation():
+ """
+ Test the stop_presentation() method
+ """
+ # GIVEN: A LibreOfficeServer instance and a mocked presentation
+ server = LibreOfficeServer()
+ mocked_presentation = MagicMock()
+ mocked_control = MagicMock()
+ server._presentation = mocked_presentation
+ server._control = mocked_control
+
+ # WHEN: stop_presentation() is called
+ server.stop_presentation()
+
+ # THEN: The presentation is ended and the control is removed
+ mocked_presentation.end.assert_called_once_with()
+ assert server._control is None
+
+
+@patch('openlp.plugins.presentations.lib.libreofficeserver.time.sleep')
+def test_start_presentation_no_control(mocked_sleep):
+ """
+ Test the start_presentation() method when there's no control
+ """
+ # GIVEN: A LibreOfficeServer instance and some mocks
+ server = LibreOfficeServer()
+ mocked_control = MagicMock()
+ mocked_document = MagicMock()
+ mocked_presentation = MagicMock()
+ mocked_controller = MagicMock()
+ mocked_frame = MagicMock()
+ mocked_window = MagicMock()
+ server._document = mocked_document
+ server._presentation = mocked_presentation
+ mocked_document.getCurrentController.return_value = mocked_controller
+ mocked_controller.getFrame.return_value = mocked_frame
+ mocked_frame.getContainerWindow.return_value = mocked_window
+ mocked_presentation.getController.side_effect = [None, mocked_control]
+
+ # WHEN: start_presentation() is called
+ server.start_presentation()
+
+ # THEN: The slide number should be correct
+ mocked_document.getCurrentController.assert_called_once_with()
+ mocked_controller.getFrame.assert_called_once_with()
+ mocked_frame.getContainerWindow.assert_called_once_with()
+ mocked_presentation.start.assert_called_once_with()
+ assert mocked_presentation.getController.call_count == 2
+ mocked_sleep.assert_called_once_with(0.1)
+ assert mocked_window.setVisible.call_args_list == [call(True), call(False)]
+ assert server._control is mocked_control
+
+
+def test_start_presentation():
+ """
+ Test the start_presentation() method when there's a control
+ """
+ # GIVEN: A LibreOfficeServer instance and some mocks
+ server = LibreOfficeServer()
+ mocked_control = MagicMock()
+ server._control = mocked_control
+
+ # WHEN: start_presentation() is called
+ with patch.object(server, 'goto_slide') as mocked_goto_slide:
+ server.start_presentation()
+
+ # THEN: The control should have been activated and the first slide selected
+ mocked_control.activate.assert_called_once_with()
+ mocked_goto_slide.assert_called_once_with(1)
+
+
+def test_get_slide_number():
+ """
+ Test the get_slide_number() method
+ """
+ # GIVEN: A LibreOfficeServer instance and some mocks
+ server = LibreOfficeServer()
+ mocked_control = MagicMock()
+ mocked_control.getCurrentSlideIndex.return_value = 3
+ server._control = mocked_control
+
+ # WHEN: get_slide_number() is called
+ result = server.get_slide_number()
+
+ # THEN: The slide number should be correct
+ assert result == 4
+
+
+def test_get_slide_count():
+ """
+ Test the get_slide_count() method
+ """
+ # GIVEN: A LibreOfficeServer instance and some mocks
+ server = LibreOfficeServer()
+ mocked_document = MagicMock()
+ mocked_pages = MagicMock()
+ server._document = mocked_document
+ mocked_document.getDrawPages.return_value = mocked_pages
+ mocked_pages.getCount.return_value = 2
+
+ # WHEN: get_slide_count() is called
+ result = server.get_slide_count()
+
+ # THEN: The slide count should be correct
+ assert result == 2
+
+
+def test_goto_slide():
+ """
+ Test the goto_slide() method
+ """
+ # GIVEN: A LibreOfficeServer instance and some mocks
+ server = LibreOfficeServer()
+ mocked_control = MagicMock()
+ server._control = mocked_control
+
+ # WHEN: goto_slide() is called
+ server.goto_slide(1)
+
+ # THEN: The slide number should be correct
+ mocked_control.gotoSlideIndex.assert_called_once_with(0)
+
+
+@patch('openlp.plugins.presentations.lib.libreofficeserver.time.sleep')
+def test_next_step_when_paused(mocked_sleep):
+ """
+ Test the next_step() method when paused
+ """
+ # GIVEN: A LibreOfficeServer instance and a mocked control
+ server = LibreOfficeServer()
+ mocked_control = MagicMock()
+ server._control = mocked_control
+ mocked_control.isPaused.side_effect = [False, True]
+
+ # WHEN: next_step() is called
+ server.next_step()
+
+ # THEN: The correct call should be made
+ mocked_control.gotoNextEffect.assert_called_once_with()
+ mocked_sleep.assert_called_once_with(0.1)
+ assert mocked_control.isPaused.call_count == 2
+ mocked_control.gotoPreviousEffect.assert_called_once_with()
+
+
+@patch('openlp.plugins.presentations.lib.libreofficeserver.time.sleep')
+def test_next_step(mocked_sleep):
+ """
+ Test the next_step() method when paused
+ """
+ # GIVEN: A LibreOfficeServer instance and a mocked control
+ server = LibreOfficeServer()
+ mocked_control = MagicMock()
+ server._control = mocked_control
+ mocked_control.isPaused.side_effect = [True, True]
+
+ # WHEN: next_step() is called
+ server.next_step()
+
+ # THEN: The correct call should be made
+ mocked_control.gotoNextEffect.assert_called_once_with()
+ mocked_sleep.assert_called_once_with(0.1)
+ assert mocked_control.isPaused.call_count == 1
+ assert mocked_control.gotoPreviousEffect.call_count == 0
+
+
+def test_previous_step():
+ """
+ Test the previous_step() method
+ """
+ # GIVEN: A LibreOfficeServer instance and a mocked control
+ server = LibreOfficeServer()
+ mocked_control = MagicMock()
+ server._control = mocked_control
+
+ # WHEN: previous_step() is called
+ server.previous_step()
+
+ # THEN: The correct call should be made
+ mocked_control.gotoPreviousEffect.assert_called_once_with()
+
+
+def test_get_slide_text():
+ """
+ Test the get_slide_text() method
+ """
+ # GIVEN: A LibreOfficeServer instance
+ server = LibreOfficeServer()
+
+ # WHEN: get_slide_text() is called for a particular slide
+ with patch.object(server, '_get_text_from_page') as mocked_get_text_from_page:
+ mocked_get_text_from_page.return_value = 'OpenLP on Mac OS X'
+ result = server.get_slide_text(5)
+
+ # THEN: The text should be returned
+ mocked_get_text_from_page.assert_called_once_with(5)
+ assert result == 'OpenLP on Mac OS X'
+
+
+def test_get_slide_notes():
+ """
+ Test the get_slide_notes() method
+ """
+ # GIVEN: A LibreOfficeServer instance
+ server = LibreOfficeServer()
+
+ # WHEN: get_slide_notes() is called for a particular slide
+ with patch.object(server, '_get_text_from_page') as mocked_get_text_from_page:
+ mocked_get_text_from_page.return_value = 'Installing is a drag-and-drop affair'
+ result = server.get_slide_notes(3)
+
+ # THEN: The text should be returned
+ mocked_get_text_from_page.assert_called_once_with(3, TextType.Notes)
+ assert result == 'Installing is a drag-and-drop affair'
+
+
+@patch('openlp.plugins.presentations.lib.libreofficeserver.Daemon')
+def test_main(MockedDaemon):
+ """
+ Test the main() function
+ """
+ # GIVEN: Mocked out Pyro objects
+ mocked_daemon = MagicMock()
+ MockedDaemon.return_value = mocked_daemon
+
+ # WHEN: main() is run
+ main()
+
+ # THEN: The correct calls are made
+ MockedDaemon.assert_called_once_with(host='localhost', port=4310)
+ mocked_daemon.register.assert_called_once_with(LibreOfficeServer, 'openlp.libreofficeserver')
+ mocked_daemon.requestLoop.assert_called_once_with()
+ mocked_daemon.close.assert_called_once_with()
diff --git a/tests/functional/openlp_plugins/presentations/test_maclocontroller.py b/tests/functional/openlp_plugins/presentations/test_maclocontroller.py
new file mode 100644
index 000000000..e53a0b576
--- /dev/null
+++ b/tests/functional/openlp_plugins/presentations/test_maclocontroller.py
@@ -0,0 +1,453 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+##########################################################################
+# OpenLP - Open Source Lyrics Projection #
+# ---------------------------------------------------------------------- #
+# Copyright (c) 2008-2019 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 . #
+##########################################################################
+"""
+Functional tests to test the Mac LibreOffice class and related methods.
+"""
+import shutil
+from tempfile import mkdtemp
+from unittest import TestCase
+from unittest.mock import MagicMock, patch, call
+
+from openlp.core.common.settings import Settings
+from openlp.core.common.path import Path
+from openlp.plugins.presentations.lib.maclocontroller import MacLOController, MacLODocument
+from openlp.plugins.presentations.presentationplugin import __default_settings__
+
+from tests.helpers.testmixin import TestMixin
+from tests.utils.constants import TEST_RESOURCES_PATH
+
+
+class TestMacLOController(TestCase, TestMixin):
+ """
+ Test the MacLOController Class
+ """
+
+ def setUp(self):
+ """
+ Set up the patches and mocks need for all tests.
+ """
+ self.setup_application()
+ self.build_settings()
+ self.mock_plugin = MagicMock()
+ self.temp_folder = mkdtemp()
+ self.mock_plugin.settings_section = self.temp_folder
+
+ def tearDown(self):
+ """
+ Stop the patches
+ """
+ self.destroy_settings()
+ shutil.rmtree(self.temp_folder)
+
+ @patch('openlp.plugins.presentations.lib.maclocontroller.MacLOController._start_server')
+ def test_constructor(self, mocked_start_server):
+ """
+ Test the Constructor from the MacLOController
+ """
+ # GIVEN: No presentation controller
+ controller = None
+
+ # WHEN: The presentation controller object is created
+ controller = MacLOController(plugin=self.mock_plugin)
+
+ # THEN: The name of the presentation controller should be correct
+ assert controller.name == 'maclo', \
+ 'The name of the presentation controller should be correct'
+ assert controller.display_name == 'Impress on macOS', \
+ 'The display name of the presentation controller should be correct'
+ mocked_start_server.assert_called_once_with()
+
+ @patch('openlp.plugins.presentations.lib.maclocontroller.MacLOController._start_server')
+ @patch('openlp.plugins.presentations.lib.maclocontroller.Proxy')
+ def test_client(self, MockedProxy, mocked_start_server):
+ """
+ Test the client property of the Controller
+ """
+ # GIVEN: A controller without a client and a mocked out Pyro
+ controller = MacLOController(plugin=self.mock_plugin)
+ mocked_client = MagicMock()
+ MockedProxy.return_value = mocked_client
+ mocked_client._pyroConnection = None
+
+ # WHEN: the client property is called the first time
+ client = controller.client
+
+ # THEN: a client is created
+ assert client == mocked_client
+ MockedProxy.assert_called_once_with('PYRO:openlp.libreofficeserver@localhost:4310')
+ mocked_client._pyroReconnect.assert_called_once_with()
+
+ @patch('openlp.plugins.presentations.lib.maclocontroller.MacLOController._start_server')
+ def test_check_available(self, mocked_start_server):
+ """
+ Test the check_available() method
+ """
+ from openlp.plugins.presentations.lib.maclocontroller import macuno_available
+
+ # GIVEN: A controller
+ controller = MacLOController(plugin=self.mock_plugin)
+
+ # WHEN: check_available() is run
+ result = controller.check_available()
+
+ # THEN: it should return false
+ assert result == macuno_available
+
+ @patch('openlp.plugins.presentations.lib.maclocontroller.MacLOController._start_server')
+ def test_start_process(self, mocked_start_server):
+ """
+ Test the start_process() method
+ """
+ # GIVEN: A controller and a client
+ controller = MacLOController(plugin=self.mock_plugin)
+ controller._client = MagicMock()
+
+ # WHEN: start_process() is called
+ controller.start_process()
+
+ # THEN: The client's start_process() should have been called
+ controller._client.start_process.assert_called_once_with()
+
+ @patch('openlp.plugins.presentations.lib.maclocontroller.MacLOController._start_server')
+ def test_kill(self, mocked_start_server):
+ """
+ Test the kill() method
+ """
+ # GIVEN: A controller and a client
+ controller = MacLOController(plugin=self.mock_plugin)
+ controller._client = MagicMock()
+ controller.server_process = MagicMock()
+
+ # WHEN: start_process() is called
+ controller.kill()
+
+ # THEN: The client's start_process() should have been called
+ controller._client.shutdown.assert_called_once_with()
+ controller.server_process.kill.assert_called_once_with()
+
+
+class TestMacLODocument(TestCase):
+ """
+ Test the MacLODocument Class
+ """
+ def setUp(self):
+ mocked_plugin = MagicMock()
+ mocked_plugin.settings_section = 'presentations'
+ Settings().extend_default_settings(__default_settings__)
+ self.file_name = Path(TEST_RESOURCES_PATH) / 'presentations' / 'test.odp'
+ self.mocked_client = MagicMock()
+ with patch('openlp.plugins.presentations.lib.maclocontroller.MacLOController._start_server'):
+ self.controller = MacLOController(mocked_plugin)
+ self.controller._client = self.mocked_client
+ self.document = MacLODocument(self.controller, self.file_name)
+
+ @patch('openlp.plugins.presentations.lib.maclocontroller.ScreenList')
+ def test_load_presentation_cannot_load(self, MockedScreenList):
+ """
+ Test the load_presentation() method when the server can't load the presentation
+ """
+ # GIVEN: A document and a mocked client
+ mocked_screen_list = MagicMock()
+ MockedScreenList.return_value = mocked_screen_list
+ mocked_screen_list.current.number = 0
+ self.mocked_client.load_presentation.return_value = False
+
+ # WHEN: load_presentation() is called
+ result = self.document.load_presentation()
+
+ # THEN: Stuff should work right
+ self.mocked_client.load_presentation.assert_called_once_with(str(self.file_name), 1)
+ assert result is False
+
+ @patch('openlp.plugins.presentations.lib.maclocontroller.ScreenList')
+ def test_load_presentation(self, MockedScreenList):
+ """
+ Test the load_presentation() method
+ """
+ # GIVEN: A document and a mocked client
+ mocked_screen_list = MagicMock()
+ MockedScreenList.return_value = mocked_screen_list
+ mocked_screen_list.current.number = 0
+ self.mocked_client.load_presentation.return_value = True
+
+ # WHEN: load_presentation() is called
+ with patch.object(self.document, 'create_thumbnails') as mocked_create_thumbnails, \
+ patch.object(self.document, 'create_titles_and_notes') as mocked_create_titles_and_notes:
+ result = self.document.load_presentation()
+
+ # THEN: Stuff should work right
+ self.mocked_client.load_presentation.assert_called_once_with(str(self.file_name), 1)
+ mocked_create_thumbnails.assert_called_once_with()
+ mocked_create_titles_and_notes.assert_called_once_with()
+ assert result is True
+
+ def test_create_thumbnails_already_exist(self):
+ """
+ Test the create_thumbnails() method when thumbnails already exist
+ """
+ # GIVEN: thumbnails that exist and a mocked client
+ self.document.check_thumbnails = MagicMock(return_value=True)
+
+ # WHEN: create_thumbnails() is called
+ self.document.create_thumbnails()
+
+ # THEN: The method should exit early
+ assert self.mocked_client.extract_thumbnails.call_count == 0
+
+ @patch('openlp.plugins.presentations.lib.maclocontroller.delete_file')
+ def test_create_thumbnails(self, mocked_delete_file):
+ """
+ Test the create_thumbnails() method
+ """
+ # GIVEN: thumbnails that don't exist and a mocked client
+ self.document.check_thumbnails = MagicMock(return_value=False)
+ self.mocked_client.extract_thumbnails.return_value = ['thumb1.png', 'thumb2.png']
+
+ # WHEN: create_thumbnails() is called
+ with patch.object(self.document, 'convert_thumbnail') as mocked_convert_thumbnail, \
+ patch.object(self.document, 'get_temp_folder') as mocked_get_temp_folder:
+ mocked_get_temp_folder.return_value = 'temp'
+ self.document.create_thumbnails()
+
+ # THEN: The method should complete successfully
+ self.mocked_client.extract_thumbnails.assert_called_once_with('temp')
+ assert mocked_convert_thumbnail.call_args_list == [
+ call(Path('thumb1.png'), 1), call(Path('thumb2.png'), 2)]
+ assert mocked_delete_file.call_args_list == [call(Path('thumb1.png')), call(Path('thumb2.png'))]
+
+ def test_create_titles_and_notes(self):
+ """
+ Test create_titles_and_notes() method
+ """
+ # GIVEN: mocked client and mocked save_titles_and_notes() method
+ self.mocked_client.get_titles_and_notes.return_value = ('OpenLP', 'This is a note')
+
+ # WHEN: create_titles_and_notes() is called
+ with patch.object(self.document, 'save_titles_and_notes') as mocked_save_titles_and_notes:
+ self.document.create_titles_and_notes()
+
+ # THEN save_titles_and_notes should have been called
+ self.mocked_client.get_titles_and_notes.assert_called_once_with()
+ mocked_save_titles_and_notes.assert_called_once_with('OpenLP', 'This is a note')
+
+ def test_close_presentation(self):
+ """
+ Test the close_presentation() method
+ """
+ # GIVEN: A mocked client and mocked remove_doc() method
+ # WHEN: close_presentation() is called
+ with patch.object(self.controller, 'remove_doc') as mocked_remove_doc:
+ self.document.close_presentation()
+
+ # THEN: The presentation should have been closed
+ self.mocked_client.close_presentation.assert_called_once_with()
+ mocked_remove_doc.assert_called_once_with(self.document)
+
+ def test_is_loaded(self):
+ """
+ Test the is_loaded() method
+ """
+ # GIVEN: A mocked client
+ self.mocked_client.is_loaded.return_value = True
+
+ # WHEN: is_loaded() is called
+ result = self.document.is_loaded()
+
+ # THEN: Then the result should be correct
+ assert result is True
+
+ def test_is_active(self):
+ """
+ Test the is_active() method
+ """
+ # GIVEN: A mocked client
+ self.mocked_client.is_active.return_value = True
+
+ # WHEN: is_active() is called
+ result = self.document.is_active()
+
+ # THEN: Then the result should be correct
+ assert result is True
+
+ def test_unblank_screen(self):
+ """
+ Test the unblank_screen() method
+ """
+ # GIVEN: A mocked client
+ self.mocked_client.unblank_screen.return_value = True
+
+ # WHEN: unblank_screen() is called
+ result = self.document.unblank_screen()
+
+ # THEN: Then the result should be correct
+ self.mocked_client.unblank_screen.assert_called_once_with()
+ assert result is True
+
+ def test_blank_screen(self):
+ """
+ Test the blank_screen() method
+ """
+ # GIVEN: A mocked client
+ self.mocked_client.blank_screen.return_value = True
+
+ # WHEN: blank_screen() is called
+ self.document.blank_screen()
+
+ # THEN: Then the result should be correct
+ self.mocked_client.blank_screen.assert_called_once_with()
+
+ def test_is_blank(self):
+ """
+ Test the is_blank() method
+ """
+ # GIVEN: A mocked client
+ self.mocked_client.is_blank.return_value = True
+
+ # WHEN: is_blank() is called
+ result = self.document.is_blank()
+
+ # THEN: Then the result should be correct
+ assert result is True
+
+ def test_stop_presentation(self):
+ """
+ Test the stop_presentation() method
+ """
+ # GIVEN: A mocked client
+ self.mocked_client.stop_presentation.return_value = True
+
+ # WHEN: stop_presentation() is called
+ self.document.stop_presentation()
+
+ # THEN: Then the result should be correct
+ self.mocked_client.stop_presentation.assert_called_once_with()
+
+ @patch('openlp.plugins.presentations.lib.maclocontroller.ScreenList')
+ @patch('openlp.plugins.presentations.lib.maclocontroller.Registry')
+ def test_start_presentation(self, MockedRegistry, MockedScreenList):
+ """
+ Test the start_presentation() method
+ """
+ # GIVEN: a mocked client, and multiple screens
+ mocked_screen_list = MagicMock()
+ mocked_screen_list.__len__.return_value = 2
+ mocked_registry = MagicMock()
+ mocked_main_window = MagicMock()
+ MockedScreenList.return_value = mocked_screen_list
+ MockedRegistry.return_value = mocked_registry
+ mocked_screen_list.screen_list = [0, 1]
+ mocked_registry.get.return_value = mocked_main_window
+
+ # WHEN: start_presentation() is called
+ self.document.start_presentation()
+
+ # THEN: The presentation should be started
+ self.mocked_client.start_presentation.assert_called_once_with()
+ mocked_registry.get.assert_called_once_with('main_window')
+ mocked_main_window.activateWindow.assert_called_once_with()
+
+ def test_get_slide_number(self):
+ """
+ Test the get_slide_number() method
+ """
+ # GIVEN: A mocked client
+ self.mocked_client.get_slide_number.return_value = 5
+
+ # WHEN: get_slide_number() is called
+ result = self.document.get_slide_number()
+
+ # THEN: Then the result should be correct
+ assert result == 5
+
+ def test_get_slide_count(self):
+ """
+ Test the get_slide_count() method
+ """
+ # GIVEN: A mocked client
+ self.mocked_client.get_slide_count.return_value = 8
+
+ # WHEN: get_slide_count() is called
+ result = self.document.get_slide_count()
+
+ # THEN: Then the result should be correct
+ assert result == 8
+
+ def test_goto_slide(self):
+ """
+ Test the goto_slide() method
+ """
+ # GIVEN: A mocked client
+ # WHEN: goto_slide() is called
+ self.document.goto_slide(3)
+
+ # THEN: Then the result should be correct
+ self.mocked_client.goto_slide.assert_called_once_with(3)
+
+ def test_next_step(self):
+ """
+ Test the next_step() method
+ """
+ # GIVEN: A mocked client
+ # WHEN: next_step() is called
+ self.document.next_step()
+
+ # THEN: Then the result should be correct
+ self.mocked_client.next_step.assert_called_once_with()
+
+ def test_previous_step(self):
+ """
+ Test the previous_step() method
+ """
+ # GIVEN: A mocked client
+ # WHEN: previous_step() is called
+ self.document.previous_step()
+
+ # THEN: Then the result should be correct
+ self.mocked_client.previous_step.assert_called_once_with()
+
+ def test_get_slide_text(self):
+ """
+ Test the get_slide_text() method
+ """
+ # GIVEN: A mocked client
+ self.mocked_client.get_slide_text.return_value = 'Some slide text'
+
+ # WHEN: get_slide_text() is called
+ result = self.document.get_slide_text(1)
+
+ # THEN: Then the result should be correct
+ self.mocked_client.get_slide_text.assert_called_once_with(1)
+ assert result == 'Some slide text'
+
+ def test_get_slide_notes(self):
+ """
+ Test the get_slide_notes() method
+ """
+ # GIVEN: A mocked client
+ self.mocked_client.get_slide_notes.return_value = 'This is a note'
+
+ # WHEN: get_slide_notes() is called
+ result = self.document.get_slide_notes(2)
+
+ # THEN: Then the result should be correct
+ self.mocked_client.get_slide_notes.assert_called_once_with(2)
+ assert result == 'This is a note'
diff --git a/tests/openlp_core/ui/test_themeform.py b/tests/openlp_core/ui/test_themeform.py
index 87a5f3bf6..02a7d0589 100644
--- a/tests/openlp_core/ui/test_themeform.py
+++ b/tests/openlp_core/ui/test_themeform.py
@@ -23,6 +23,7 @@
Interface tests to test the ThemeWizard class and related methods.
"""
from unittest import TestCase
+from unittest.mock import patch
from openlp.core.common.registry import Registry
from openlp.core.ui.themeform import ThemeForm
@@ -39,7 +40,8 @@ class TestThemeManager(TestCase, TestMixin):
"""
Registry.create()
- def test_create_theme_wizard(self):
+ @patch('openlp.core.display.window.QtWidgets.QVBoxLayout')
+ def test_create_theme_wizard(self, mocked_qvboxlayout):
"""
Test creating a ThemeForm instance
"""