diff --git a/openlp.py b/openlp.py index 8a53fe965..5d507606d 100755 --- a/openlp.py +++ b/openlp.py @@ -36,10 +36,9 @@ if __name__ == '__main__': """ Instantiate and run the application. """ - # Mac OS X passes arguments like '-psn_XXXX' to gui application. - # This argument is process serial number. However, this causes - # conflict with other OpenLP arguments. Since we do not use this - # argument we can delete it to avoid any potential conflicts. + # Mac OS X passes arguments like '-psn_XXXX' to the application. This argument is actually a process serial number. + # However, this causes a conflict with other OpenLP arguments. Since we do not use this argument we can delete it + # to avoid any potential conflicts. if sys.platform.startswith('darwin'): sys.argv = [x for x in sys.argv if not x.startswith('-psn')] main() diff --git a/openlp/core/__init__.py b/openlp/core/__init__.py index 32ef5c5a3..cb9105797 100644 --- a/openlp/core/__init__.py +++ b/openlp/core/__init__.py @@ -59,7 +59,7 @@ __all__ = ['OpenLP', 'main'] log = logging.getLogger() -NT_REPAIR_STYLESHEET = """ +WIN_REPAIR_STYLESHEET = """ QMainWindow::separator { border: none; @@ -127,7 +127,7 @@ class OpenLP(OpenLPMixin, QtGui.QApplication): 'QTableWidget, QListWidget, QTreeWidget {alternate-background-color: ' + base_color.name() + ';}\n' application_stylesheet += alternate_rows_repair_stylesheet if is_win(): - application_stylesheet += NT_REPAIR_STYLESHEET + application_stylesheet += WIN_REPAIR_STYLESHEET if application_stylesheet: self.setStyleSheet(application_stylesheet) show_splash = Settings().value('core/show splash') diff --git a/openlp/core/common/openlpmixin.py b/openlp/core/common/openlpmixin.py index 1c7fe7d5a..3e8a8926a 100644 --- a/openlp/core/common/openlpmixin.py +++ b/openlp/core/common/openlpmixin.py @@ -33,6 +33,7 @@ import logging import inspect from openlp.core.common import trace_error_handler + DO_NOT_TRACE_EVENTS = ['timerEvent', 'paintEvent', 'drag_enter_event', 'drop_event', 'on_controller_size_changed', 'preview_size_changed', 'resizeEvent'] @@ -41,11 +42,8 @@ class OpenLPMixin(object): """ Base Calling object for OpenLP classes. """ - def __init__(self, parent): - try: - super(OpenLPMixin, self).__init__(parent) - except TypeError: - super(OpenLPMixin, self).__init__() + def __init__(self, *args, **kwargs): + super(OpenLPMixin, self).__init__(*args, **kwargs) self.logger = logging.getLogger("%s.%s" % (self.__module__, self.__class__.__name__)) if self.logger.getEffectiveLevel() == logging.DEBUG: for name, m in inspect.getmembers(self, inspect.ismethod): diff --git a/openlp/core/lib/__init__.py b/openlp/core/lib/__init__.py index a7cba33a0..b85070445 100644 --- a/openlp/core/lib/__init__.py +++ b/openlp/core/lib/__init__.py @@ -145,11 +145,13 @@ def build_icon(icon): return button_icon -def image_to_byte(image): +def image_to_byte(image, base_64=True): """ Resize an image to fit on the current screen for the web and returns it as a byte stream. :param image: The image to converted. + :param base_64: If True returns the image as Base64 bytes, otherwise the image is returned as a byte array. + To preserve original intention, this defaults to True """ log.debug('image_to_byte - start') byte_array = QtCore.QByteArray() @@ -158,6 +160,8 @@ def image_to_byte(image): buffie.open(QtCore.QIODevice.WriteOnly) image.save(buffie, "PNG") log.debug('image_to_byte - end') + if not base_64: + return byte_array # convert to base64 encoding so does not get missed! return bytes(byte_array.toBase64()).decode('utf-8') diff --git a/openlp/core/lib/imagemanager.py b/openlp/core/lib/imagemanager.py index cba393815..46fab10d2 100644 --- a/openlp/core/lib/imagemanager.py +++ b/openlp/core/lib/imagemanager.py @@ -106,7 +106,7 @@ class Image(object): """ secondary_priority = 0 - def __init__(self, path, source, background): + def __init__(self, path, source, background, width=-1, height=-1): """ Create an image for the :class:`ImageManager`'s cache. @@ -115,7 +115,8 @@ class Image(object): :class:`~openlp.core.lib.ImageSource` class. :param background: A ``QtGui.QColor`` object specifying the colour to be used to fill the gabs if the image's ratio does not match with the display ratio. - + :param width: The width of the image, defaults to -1 meaning that the screen width will be used. + :param height: The height of the image, defaults to -1 meaning that the screen height will be used. """ self.path = path self.image = None @@ -124,6 +125,8 @@ class Image(object): self.source = source self.background = background self.timestamp = 0 + self.width = width + self.height = height # FIXME: We assume that the path exist. The caller has to take care that it exists! if os.path.exists(path): self.timestamp = os.stat(path).st_mtime @@ -210,13 +213,13 @@ class ImageManager(QtCore.QObject): image.background = background self._reset_image(image) - def update_image_border(self, path, source, background): + def update_image_border(self, path, source, background, width=-1, height=-1): """ Border has changed so update the image affected. """ log.debug('update_image_border') # Mark the image as dirty for a rebuild by setting the image and byte stream to None. - image = self._cache[(path, source)] + image = self._cache[(path, source, width, height)] if image.source == source: image.background = background self._reset_image(image) @@ -237,12 +240,12 @@ class ImageManager(QtCore.QObject): if not self.image_thread.isRunning(): self.image_thread.start() - def get_image(self, path, source): + def get_image(self, path, source, width=-1, height=-1): """ Return the ``QImage`` from the cache. If not present wait for the background thread to process it. """ log.debug('getImage %s' % path) - image = self._cache[(path, source)] + image = self._cache[(path, source, width, height)] if image.image is None: self._conversion_queue.modify_priority(image, Priority.High) # make sure we are running and if not give it a kick @@ -257,12 +260,12 @@ class ImageManager(QtCore.QObject): self._conversion_queue.modify_priority(image, Priority.Low) return image.image - def get_image_bytes(self, path, source): + def get_image_bytes(self, path, source, width=-1, height=-1): """ Returns the byte string for an image. If not present wait for the background thread to process it. """ log.debug('get_image_bytes %s' % path) - image = self._cache[(path, source)] + image = self._cache[(path, source, width, height)] if image.image_bytes is None: self._conversion_queue.modify_priority(image, Priority.Urgent) # make sure we are running and if not give it a kick @@ -272,14 +275,14 @@ class ImageManager(QtCore.QObject): time.sleep(0.1) return image.image_bytes - def add_image(self, path, source, background): + def add_image(self, path, source, background, width=-1, height=-1): """ Add image to cache if it is not already there. """ log.debug('add_image %s' % path) - if not (path, source) in self._cache: - image = Image(path, source, background) - self._cache[(path, source)] = image + if not (path, source, width, height) in self._cache: + image = Image(path, source, background, width, height) + self._cache[(path, source, width, height)] = image self._conversion_queue.put((image.priority, image.secondary_priority, image)) # Check if the there are any images with the same path and check if the timestamp has changed. for image in list(self._cache.values()): @@ -308,7 +311,10 @@ class ImageManager(QtCore.QObject): image = self._conversion_queue.get()[2] # Generate the QImage for the image. if image.image is None: - image.image = resize_image(image.path, self.width, self.height, image.background) + # Let's see if the image was requested with specific dimensions + width = self.width if image.width == -1 else image.width + height = self.height if image.height == -1 else image.height + image.image = resize_image(image.path, width, height, image.background) # Set the priority to Lowest and stop here as we need to process more important images first. if image.priority == Priority.Normal: self._conversion_queue.modify_priority(image, Priority.Lowest) diff --git a/openlp/core/lib/serviceitem.py b/openlp/core/lib/serviceitem.py index ecd6ca5bd..4e7ff032a 100644 --- a/openlp/core/lib/serviceitem.py +++ b/openlp/core/lib/serviceitem.py @@ -39,8 +39,8 @@ import uuid from PyQt4 import QtGui -from openlp.core.common import RegistryProperties, Settings, translate -from openlp.core.lib import ImageSource, build_icon, clean_tags, expand_tags +from openlp.core.common import RegistryProperties, Settings, translate, AppLocation +from openlp.core.lib import ImageSource, build_icon, clean_tags, expand_tags, create_thumb log = logging.getLogger(__name__) @@ -112,7 +112,17 @@ class ItemCapabilities(object): The capability to edit the title of the item ``IsOptical`` - .Determines is the service_item is based on an optical device + Determines is the service_item is based on an optical device + + ``HasDisplayTitle`` + The item contains 'displaytitle' on every frame which should be + preferred over 'title' when displaying the item + + ``HasNotes`` + The item contains 'notes' + + ``HasThumbnails`` + The item has related thumbnails available """ CanPreview = 1 @@ -133,6 +143,9 @@ class ItemCapabilities(object): CanAutoStartForLive = 16 CanEditTitle = 17 IsOptical = 18 + HasDisplayTitle = 19 + HasNotes = 20 + HasThumbnails = 21 class ServiceItem(RegistryProperties): @@ -272,18 +285,22 @@ class ServiceItem(RegistryProperties): self.raw_footer = [] self.foot_text = '
'.join([_f for _f in self.raw_footer if _f]) - def add_from_image(self, path, title, background=None): + def add_from_image(self, path, title, background=None, thumbnail=None): """ Add an image slide to the service item. :param path: The directory in which the image file is located. :param title: A title for the slide in the service item. :param background: + :param thumbnail: Optional alternative thumbnail, used for remote thumbnails. """ if background: self.image_border = background self.service_item_type = ServiceItemType.Image - self._raw_frames.append({'title': title, 'path': path}) + if not thumbnail: + self._raw_frames.append({'title': title, 'path': path}) + else: + self._raw_frames.append({'title': title, 'path': path, 'image': thumbnail}) self.image_manager.add_image(path, ImageSource.ImagePlugin, self.image_border) self._new_item() @@ -301,16 +318,22 @@ class ServiceItem(RegistryProperties): self._raw_frames.append({'title': title, 'raw_slide': raw_slide, 'verseTag': verse_tag}) self._new_item() - def add_from_command(self, path, file_name, image): + def add_from_command(self, path, file_name, image, display_title=None, notes=None): """ Add a slide from a command. :param path: The title of the slide in the service item. :param file_name: The title of the slide in the service item. :param image: The command of/for the slide. + :param display_title: Title to show in gui/webinterface, optional. + :param notes: Notes to show in the webinteface, optional. """ self.service_item_type = ServiceItemType.Command - self._raw_frames.append({'title': file_name, 'image': image, 'path': path}) + # If the item should have a display title but this frame doesn't have one, we make one up + if self.is_capable(ItemCapabilities.HasDisplayTitle) and not display_title: + display_title = translate('OpenLP.ServiceItem', '[slide %d]') % (len(self._raw_frames) + 1) + self._raw_frames.append({'title': file_name, 'image': image, 'path': path, + 'display_title': display_title, 'notes': notes}) self._new_item() def get_service_repr(self, lite_save): @@ -354,7 +377,8 @@ class ServiceItem(RegistryProperties): service_data = [slide['title'] for slide in self._raw_frames] elif self.service_item_type == ServiceItemType.Command: for slide in self._raw_frames: - service_data.append({'title': slide['title'], 'image': slide['image'], 'path': slide['path']}) + service_data.append({'title': slide['title'], 'image': slide['image'], 'path': slide['path'], + 'display_title': slide['display_title'], 'notes': slide['notes']}) return {'header': service_header, 'data': service_data} def set_from_service(self, service_item, path=None): @@ -425,7 +449,8 @@ class ServiceItem(RegistryProperties): self.add_from_command(text_image['path'], text_image['title'], text_image['image']) elif path: self.has_original_files = False - self.add_from_command(path, text_image['title'], text_image['image']) + self.add_from_command(path, text_image['title'], text_image['image'], + text_image.get('display_title', ''), text_image.get('notes', '')) else: self.add_from_command(text_image['path'], text_image['title'], text_image['image']) self._new_item() diff --git a/openlp/core/lib/ui.py b/openlp/core/lib/ui.py index cbc35e28d..af4b263d3 100644 --- a/openlp/core/lib/ui.py +++ b/openlp/core/lib/ui.py @@ -33,7 +33,7 @@ import logging from PyQt4 import QtCore, QtGui -from openlp.core.common import Registry, UiStrings, translate +from openlp.core.common import Registry, UiStrings, translate, is_macosx from openlp.core.lib import build_icon from openlp.core.utils.actions import ActionList @@ -247,6 +247,8 @@ def create_action(parent, name, **kwargs): """ action = QtGui.QAction(parent) action.setObjectName(name) + if is_macosx(): + action.setIconVisibleInMenu(False) if kwargs.get('text'): action.setText(kwargs.pop('text')) if kwargs.get('icon'): diff --git a/openlp/core/ui/firsttimeform.py b/openlp/core/ui/firsttimeform.py index d7c16f0d3..8599c8d35 100644 --- a/openlp/core/ui/firsttimeform.py +++ b/openlp/core/ui/firsttimeform.py @@ -387,17 +387,21 @@ class FirstTimeForm(QtGui.QWizard, Ui_FirstTimeWizard, RegistryProperties): self.progress_bar.setValue(self.progress_bar.maximum()) if self.has_run_wizard: self.progress_label.setText(translate('OpenLP.FirstTimeWizard', - 'Download complete. Click the finish button to return to OpenLP.')) + 'Download complete. Click the %s button to return to OpenLP.') % + self.buttonText(QtGui.QWizard.FinishButton)) else: self.progress_label.setText(translate('OpenLP.FirstTimeWizard', - 'Download complete. Click the finish button to start OpenLP.')) + 'Download complete. Click the %s button to start OpenLP.') % + self.buttonText(QtGui.QWizard.FinishButton)) else: if self.has_run_wizard: self.progress_label.setText(translate('OpenLP.FirstTimeWizard', - 'Click the finish button to return to OpenLP.')) + 'Click the %s button to return to OpenLP.') % + self.buttonText(QtGui.QWizard.FinishButton)) else: self.progress_label.setText(translate('OpenLP.FirstTimeWizard', - 'Click the finish button to start OpenLP.')) + 'Click the %s button to start OpenLP.') % + self.buttonText(QtGui.QWizard.FinishButton)) self.finish_button.setVisible(True) self.finish_button.setEnabled(True) self.cancel_button.setVisible(False) diff --git a/openlp/core/ui/firsttimewizard.py b/openlp/core/ui/firsttimewizard.py index ff1675ff5..c5098eda6 100644 --- a/openlp/core/ui/firsttimewizard.py +++ b/openlp/core/ui/firsttimewizard.py @@ -31,9 +31,7 @@ The UI widgets for the first time wizard. """ from PyQt4 import QtCore, QtGui -import sys - -from openlp.core.common import translate +from openlp.core.common import translate, is_macosx from openlp.core.lib import build_icon from openlp.core.lib.ui import add_welcome_page @@ -64,9 +62,12 @@ class Ui_FirstTimeWizard(object): first_time_wizard.setWindowIcon(build_icon(u':/icon/openlp-logo.svg')) first_time_wizard.resize(550, 386) first_time_wizard.setModal(True) - first_time_wizard.setWizardStyle(QtGui.QWizard.ModernStyle) first_time_wizard.setOptions(QtGui.QWizard.IndependentPages | QtGui.QWizard.NoBackButtonOnStartPage | QtGui.QWizard.NoBackButtonOnLastPage | QtGui.QWizard.HaveCustomButton1) + if is_macosx(): + first_time_wizard.setPixmap(QtGui.QWizard.BackgroundPixmap, + QtGui.QPixmap(':/wizards/openlp-osx-wizard.png')) + first_time_wizard.resize(634, 386) self.finish_button = self.button(QtGui.QWizard.FinishButton) self.no_internet_finish_button = self.button(QtGui.QWizard.CustomButton1) self.cancel_button = self.button(QtGui.QWizard.CancelButton) @@ -212,7 +213,8 @@ class Ui_FirstTimeWizard(object): translate('OpenLP.FirstTimeWizard', 'Welcome to the First Time Wizard')) self.information_label.setText( translate('OpenLP.FirstTimeWizard', 'This wizard will help you to configure OpenLP for initial use. ' - 'Click the next button below to start.')) + 'Click the %s button below to start.') % + self.buttonText(QtGui.QWizard.NextButton)) self.plugin_page.setTitle(translate('OpenLP.FirstTimeWizard', 'Activate required Plugins')) self.plugin_page.setSubTitle(translate('OpenLP.FirstTimeWizard', 'Select the Plugins you wish to use. ')) self.songs_check_box.setText(translate('OpenLP.FirstTimeWizard', 'Songs')) @@ -236,7 +238,7 @@ class Ui_FirstTimeWizard(object): 'wizard by selecting "Tools/Re-run First Time Wizard" from OpenLP.') self.cancelWizardText = translate('OpenLP.FirstTimeWizard', '\n\nTo cancel the First Time Wizard completely (and not start OpenLP), ' - 'click the Cancel button now.') + 'click the %s button now.') % self.buttonText(QtGui.QWizard.CancelButton) self.songs_page.setTitle(translate('OpenLP.FirstTimeWizard', 'Sample Songs')) self.songs_page.setSubTitle(translate('OpenLP.FirstTimeWizard', 'Select and download public domain songs.')) self.bibles_page.setTitle(translate('OpenLP.FirstTimeWizard', 'Sample Bibles')) diff --git a/openlp/core/ui/formattingtagform.py b/openlp/core/ui/formattingtagform.py index 4f3d5d251..96e25c27d 100644 --- a/openlp/core/ui/formattingtagform.py +++ b/openlp/core/ui/formattingtagform.py @@ -60,6 +60,12 @@ class FormattingTagForm(QtGui.QDialog, Ui_FormattingTagDialog, FormattingTagCont """ super(FormattingTagForm, self).__init__(parent) self.setupUi(self) + self._setup() + + def _setup(self): + """ + Set up the class. This method is mocked out by the tests. + """ self.services = FormattingTagController() self.tag_table_widget.itemSelectionChanged.connect(self.on_row_selected) self.new_button.clicked.connect(self.on_new_clicked) diff --git a/openlp/core/ui/listpreviewwidget.py b/openlp/core/ui/listpreviewwidget.py index 831eb182b..3909c6a31 100644 --- a/openlp/core/ui/listpreviewwidget.py +++ b/openlp/core/ui/listpreviewwidget.py @@ -94,8 +94,8 @@ class ListPreviewWidget(QtGui.QTableWidget, RegistryProperties): Displays the given slide. """ self.service_item = service_item - self.clear() self.setRowCount(0) + self.clear() self.setColumnWidth(0, width) row = 0 text = [] diff --git a/openlp/core/ui/mainwindow.py b/openlp/core/ui/mainwindow.py index 1a6b688c7..77a903c5f 100644 --- a/openlp/core/ui/mainwindow.py +++ b/openlp/core/ui/mainwindow.py @@ -92,6 +92,8 @@ class Ui_MainWindow(object): main_window.setObjectName('MainWindow') main_window.setWindowIcon(build_icon(':/icon/openlp-logo.svg')) main_window.setDockNestingEnabled(True) + if is_macosx(): + main_window.setDocumentMode(True) # Set up the main container, which contains all the other form widgets. self.main_content = QtGui.QWidget(main_window) self.main_content.setObjectName('main_content') @@ -118,10 +120,12 @@ class Ui_MainWindow(object): self.recent_files_menu = QtGui.QMenu(self.file_menu) self.recent_files_menu.setObjectName('recentFilesMenu') self.file_import_menu = QtGui.QMenu(self.file_menu) - self.file_import_menu.setIcon(build_icon(u':/general/general_import.png')) + if not is_macosx(): + self.file_import_menu.setIcon(build_icon(u':/general/general_import.png')) self.file_import_menu.setObjectName('file_import_menu') self.file_export_menu = QtGui.QMenu(self.file_menu) - self.file_export_menu.setIcon(build_icon(u':/general/general_export.png')) + if not is_macosx(): + self.file_export_menu.setIcon(build_icon(u':/general/general_export.png')) self.file_export_menu.setObjectName('file_export_menu') # View Menu self.view_menu = QtGui.QMenu(self.menu_bar) diff --git a/openlp/core/ui/servicemanager.py b/openlp/core/ui/servicemanager.py index 369137694..d0ac1560e 100644 --- a/openlp/core/ui/servicemanager.py +++ b/openlp/core/ui/servicemanager.py @@ -1281,7 +1281,11 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ServiceManage # Add the children to their parent tree_widget_item. for count, frame in enumerate(service_item_from_item.get_frames()): child = QtGui.QTreeWidgetItem(tree_widget_item) - text = frame['title'].replace('\n', ' ') + # prefer to use a display_title + if service_item_from_item.is_capable(ItemCapabilities.HasDisplayTitle): + text = frame['display_title'].replace('\n', ' ') + else: + text = frame['title'].replace('\n', ' ') child.setText(0, text[:40]) child.setData(0, QtCore.Qt.UserRole, count) if service_item == item_count: diff --git a/openlp/core/ui/slidecontroller.py b/openlp/core/ui/slidecontroller.py index 44c28deb6..b12438679 100644 --- a/openlp/core/ui/slidecontroller.py +++ b/openlp/core/ui/slidecontroller.py @@ -873,6 +873,7 @@ class SlideController(DisplayController, RegistryProperties): if self.service_item.is_command(): Registry().execute('%s_slide' % self.service_item.name.lower(), [self.service_item, self.is_live, index]) self.update_preview() + self.selected_row = index else: self.preview_widget.change_slide(index) self.slide_selected() @@ -1042,8 +1043,8 @@ class SlideController(DisplayController, RegistryProperties): self.display.image(to_display) # reset the store used to display first image self.service_item.bg_image_bytes = None - self.update_preview() self.selected_row = row + self.update_preview() self.preview_widget.change_slide(row) self.display.setFocus() @@ -1055,6 +1056,7 @@ class SlideController(DisplayController, RegistryProperties): """ self.preview_widget.change_slide(row) self.update_preview() + self.selected_row = row def update_preview(self): """ diff --git a/openlp/core/ui/themewizard.py b/openlp/core/ui/themewizard.py index bda52c807..50200313f 100644 --- a/openlp/core/ui/themewizard.py +++ b/openlp/core/ui/themewizard.py @@ -31,7 +31,7 @@ The Create/Edit theme wizard """ from PyQt4 import QtCore, QtGui -from openlp.core.common import UiStrings, translate +from openlp.core.common import UiStrings, translate, is_macosx from openlp.core.lib import build_icon from openlp.core.lib.theme import HorizontalType, BackgroundType, BackgroundGradientType from openlp.core.lib.ui import add_welcome_page, create_valign_selection_widgets @@ -41,19 +41,21 @@ class Ui_ThemeWizard(object): """ The Create/Edit theme wizard """ - def setupUi(self, themeWizard): + def setupUi(self, theme_wizard): """ Set up the UI """ - themeWizard.setObjectName('OpenLP.ThemeWizard') - themeWizard.setWindowIcon(build_icon(u':/icon/openlp-logo.svg')) - themeWizard.setModal(True) - themeWizard.setWizardStyle(QtGui.QWizard.ModernStyle) - themeWizard.setOptions(QtGui.QWizard.IndependentPages | - QtGui.QWizard.NoBackButtonOnStartPage | QtGui.QWizard.HaveCustomButton1) + theme_wizard.setObjectName('OpenLP.ThemeWizard') + theme_wizard.setWindowIcon(build_icon(u':/icon/openlp-logo.svg')) + theme_wizard.setModal(True) + theme_wizard.setOptions(QtGui.QWizard.IndependentPages | + QtGui.QWizard.NoBackButtonOnStartPage | QtGui.QWizard.HaveCustomButton1) + if is_macosx(): + theme_wizard.setPixmap(QtGui.QWizard.BackgroundPixmap, QtGui.QPixmap(':/wizards/openlp-osx-wizard.png')) + theme_wizard.resize(646, 400) self.spacer = QtGui.QSpacerItem(10, 0, QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Minimum) # Welcome Page - add_welcome_page(themeWizard, ':/wizards/wizard_createtheme.bmp') + add_welcome_page(theme_wizard, ':/wizards/wizard_createtheme.bmp') # Background Page self.background_page = QtGui.QWizardPage() self.background_page.setObjectName('background_page') @@ -137,7 +139,7 @@ class Ui_ThemeWizard(object): self.transparent_layout.setObjectName('Transparent_layout') self.background_stack.addWidget(self.transparent_widget) self.background_layout.addLayout(self.background_stack) - themeWizard.addPage(self.background_page) + theme_wizard.addPage(self.background_page) # Main Area Page self.main_area_page = QtGui.QWizardPage() self.main_area_page.setObjectName('main_area_page') @@ -218,7 +220,7 @@ class Ui_ThemeWizard(object): self.shadow_size_spin_box.setObjectName('shadow_size_spin_box') self.shadow_layout.addWidget(self.shadow_size_spin_box) self.main_area_layout.addRow(self.shadow_check_box, self.shadow_layout) - themeWizard.addPage(self.main_area_page) + theme_wizard.addPage(self.main_area_page) # Footer Area Page self.footer_area_page = QtGui.QWizardPage() self.footer_area_page.setObjectName('footer_area_page') @@ -242,7 +244,7 @@ class Ui_ThemeWizard(object): self.footer_size_spin_box.setObjectName('FooterSizeSpinBox') self.footer_area_layout.addRow(self.footer_size_label, self.footer_size_spin_box) self.footer_area_layout.setItem(3, QtGui.QFormLayout.LabelRole, self.spacer) - themeWizard.addPage(self.footer_area_page) + theme_wizard.addPage(self.footer_area_page) # Alignment Page self.alignment_page = QtGui.QWizardPage() self.alignment_page.setObjectName('alignment_page') @@ -264,7 +266,7 @@ class Ui_ThemeWizard(object): self.transitions_check_box.setObjectName('transitions_check_box') self.alignment_layout.addRow(self.transitions_label, self.transitions_check_box) self.alignment_layout.setItem(3, QtGui.QFormLayout.LabelRole, self.spacer) - themeWizard.addPage(self.alignment_page) + theme_wizard.addPage(self.alignment_page) # Area Position Page self.area_position_page = QtGui.QWizardPage() self.area_position_page.setObjectName('area_position_page') @@ -334,7 +336,7 @@ class Ui_ThemeWizard(object): self.footer_height_spin_box.setObjectName('footer_height_spin_box') self.footer_position_layout.addRow(self.footer_height_label, self.footer_height_spin_box) self.area_position_layout.addWidget(self.footer_position_group_box) - themeWizard.addPage(self.area_position_page) + theme_wizard.addPage(self.area_position_page) # Preview Page self.preview_page = QtGui.QWizardPage() self.preview_page.setObjectName('preview_page') @@ -362,8 +364,8 @@ class Ui_ThemeWizard(object): self.preview_box_label.setObjectName('preview_box_label') self.preview_area_layout.addWidget(self.preview_box_label) self.preview_layout.addWidget(self.preview_area) - themeWizard.addPage(self.preview_page) - self.retranslateUi(themeWizard) + theme_wizard.addPage(self.preview_page) + self.retranslateUi(theme_wizard) QtCore.QObject.connect(self.background_combo_box, QtCore.SIGNAL('currentIndexChanged(int)'), self.background_stack, QtCore.SLOT('setCurrentIndex(int)')) QtCore.QObject.connect(self.outline_check_box, QtCore.SIGNAL('toggled(bool)'), self.outline_color_button, @@ -391,11 +393,11 @@ class Ui_ThemeWizard(object): QtCore.QObject.connect(self.footer_position_check_box, QtCore.SIGNAL('toggled(bool)'), self.footer_height_spin_box, QtCore.SLOT('setDisabled(bool)')) - def retranslateUi(self, themeWizard): + def retranslateUi(self, theme_wizard): """ Translate the UI on the fly """ - themeWizard.setWindowTitle(translate('OpenLP.ThemeWizard', 'Theme Wizard')) + theme_wizard.setWindowTitle(translate('OpenLP.ThemeWizard', 'Theme Wizard')) self.title_label.setText('%s' % translate('OpenLP.ThemeWizard', 'Welcome to the Theme Wizard')) self.information_label.setText( @@ -484,8 +486,8 @@ class Ui_ThemeWizard(object): self.footer_height_label.setText(translate('OpenLP.ThemeWizard', 'Height:')) self.footer_height_spin_box.setSuffix(translate('OpenLP.ThemeWizard', 'px')) self.footer_position_check_box.setText(translate('OpenLP.ThemeWizard', 'Use default location')) - themeWizard.setOption(QtGui.QWizard.HaveCustomButton1, False) - themeWizard.setButtonText(QtGui.QWizard.CustomButton1, translate('OpenLP.ThemeWizard', 'Layout Preview')) + theme_wizard.setOption(QtGui.QWizard.HaveCustomButton1, False) + theme_wizard.setButtonText(QtGui.QWizard.CustomButton1, translate('OpenLP.ThemeWizard', 'Layout Preview')) self.preview_page.setTitle(translate('OpenLP.ThemeWizard', 'Preview and Save')) self.preview_page.setSubTitle(translate('OpenLP.ThemeWizard', 'Preview the theme and save it.')) self.theme_name_label.setText(translate('OpenLP.ThemeWizard', 'Theme name:')) diff --git a/openlp/core/ui/wizard.py b/openlp/core/ui/wizard.py index 23bc0a9e1..7199d1742 100644 --- a/openlp/core/ui/wizard.py +++ b/openlp/core/ui/wizard.py @@ -34,7 +34,7 @@ import os from PyQt4 import QtGui -from openlp.core.common import Registry, RegistryProperties, Settings, UiStrings, translate +from openlp.core.common import Registry, RegistryProperties, Settings, UiStrings, translate, is_macosx from openlp.core.lib import build_icon from openlp.core.lib.ui import add_welcome_page @@ -121,9 +121,10 @@ class OpenLPWizard(QtGui.QWizard, RegistryProperties): """ self.setWindowIcon(build_icon(u':/icon/openlp-logo.svg')) self.setModal(True) - self.setWizardStyle(QtGui.QWizard.ModernStyle) self.setOptions(QtGui.QWizard.IndependentPages | QtGui.QWizard.NoBackButtonOnStartPage | QtGui.QWizard.NoBackButtonOnLastPage) + if is_macosx(): + self.setPixmap(QtGui.QWizard.BackgroundPixmap, QtGui.QPixmap(':/wizards/openlp-osx-wizard.png')) add_welcome_page(self, image) self.add_custom_pages() if self.with_progress_page: diff --git a/openlp/plugins/bibles/lib/manager.py b/openlp/plugins/bibles/lib/manager.py index 7dab8b3d7..436a7e34b 100644 --- a/openlp/plugins/bibles/lib/manager.py +++ b/openlp/plugins/bibles/lib/manager.py @@ -85,7 +85,7 @@ class BibleFormat(object): BibleFormat.CSV, BibleFormat.OpenSong, BibleFormat.WebDownload, - BibleFormar.Zefania, + BibleFormat.Zefania, ] diff --git a/openlp/plugins/images/lib/mediaitem.py b/openlp/plugins/images/lib/mediaitem.py index 36df55dac..d5892c250 100644 --- a/openlp/plugins/images/lib/mediaitem.py +++ b/openlp/plugins/images/lib/mediaitem.py @@ -551,6 +551,7 @@ class ImageMediaItem(MediaManagerItem): service_item.add_capability(ItemCapabilities.CanLoop) service_item.add_capability(ItemCapabilities.CanAppend) service_item.add_capability(ItemCapabilities.CanEditTitle) + service_item.add_capability(ItemCapabilities.HasThumbnails) # force a nonexistent theme service_item.theme = -1 missing_items_file_names = [] @@ -589,7 +590,7 @@ class ImageMediaItem(MediaManagerItem): # Continue with the existing images. for filename in images_file_names: name = os.path.split(filename)[1] - service_item.add_from_image(filename, name, background) + service_item.add_from_image(filename, name, background, os.path.join(self.service_path, name)) return True def check_group_exists(self, new_group): diff --git a/openlp/plugins/media/forms/mediaclipselectorform.py b/openlp/plugins/media/forms/mediaclipselectorform.py index 28d37f32e..d63e8a8bb 100644 --- a/openlp/plugins/media/forms/mediaclipselectorform.py +++ b/openlp/plugins/media/forms/mediaclipselectorform.py @@ -28,31 +28,29 @@ ############################################################################### import os -if os.name == 'nt': - from win32com.client import Dispatch - import string -import sys - -if sys.platform.startswith('linux'): - import dbus import logging import re from time import sleep from datetime import datetime - from PyQt4 import QtCore, QtGui -from openlp.core.common import translate +from openlp.core.common import translate, is_win, is_linux, is_macosx, RegistryProperties from openlp.plugins.media.forms.mediaclipselectordialog import Ui_MediaClipSelector from openlp.core.lib.ui import critical_error_message_box -from openlp.core.ui.media import format_milliseconds + +if is_win(): + from win32com.client import Dispatch + +if is_linux(): + import dbus + try: from openlp.core.ui.media.vendor import vlc except (ImportError, NameError, NotImplementedError): pass except OSError as e: - if sys.platform.startswith('win'): + if is_win(): if not isinstance(e, WindowsError) and e.winerror != 126: raise else: @@ -61,7 +59,7 @@ except OSError as e: log = logging.getLogger(__name__) -class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): +class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector, RegistryProperties): """ Class to manage the clip selection """ @@ -144,9 +142,9 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): # You have to give the id of the QFrame (or similar object) # to vlc, different platforms have different functions for this. win_id = int(self.preview_frame.winId()) - if sys.platform == "win32": + if is_win(): self.vlc_media_player.set_hwnd(win_id) - elif sys.platform == "darwin": + elif is_macosx(): # We have to use 'set_nsobject' since Qt4 on OSX uses Cocoa # framework and not the old Carbon. self.vlc_media_player.set_nsobject(win_id) @@ -190,7 +188,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): self.audio_cd = True self.titles_combo_box.setDisabled(False) self.titles_combo_box.setCurrentIndex(0) - self.on_title_combo_box_currentIndexChanged(0) + self.on_titles_combo_box_currentIndexChanged(0) return True @@ -203,18 +201,21 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): """ log.debug('on_load_disc_button_clicked') self.disable_all() + self.application.set_busy_cursor() path = self.media_path_combobox.currentText() # Check if given path is non-empty and exists before starting VLC if not path: log.debug('no given path') critical_error_message_box(message=translate('MediaPlugin.MediaClipSelectorForm', 'No path was given')) self.toggle_disable_load_media(False) + self.application.set_normal_cursor() return if not os.path.exists(path): log.debug('Given path does not exists') critical_error_message_box(message=translate('MediaPlugin.MediaClipSelectorForm', 'Given path does not exists')) self.toggle_disable_load_media(False) + self.application.set_normal_cursor() return # VLC behaves a bit differently on windows and linux when loading, which creates problems when trying to # detect if we're dealing with a DVD or CD, so we use different loading approaches depending on the OS. @@ -231,6 +232,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): critical_error_message_box(message=translate('MediaPlugin.MediaClipSelectorForm', 'An error happened during initialization of VLC player')) self.toggle_disable_load_media(False) + self.application.set_normal_cursor() return # put the media in the media player self.vlc_media_player.set_media(self.vlc_media) @@ -241,6 +243,8 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): critical_error_message_box(message=translate('MediaPlugin.MediaClipSelectorForm', 'VLC player failed playing the media')) self.toggle_disable_load_media(False) + self.application.set_normal_cursor() + self.vlc_media_player.audio_set_mute(False) return self.vlc_media_player.audio_set_mute(True) if not self.media_state_wait(vlc.State.Playing): @@ -249,23 +253,32 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): critical_error_message_box(message=translate('MediaPlugin.MediaClipSelectorForm', 'VLC player failed playing the media')) self.toggle_disable_load_media(False) + self.application.set_normal_cursor() + self.vlc_media_player.audio_set_mute(False) return - self.vlc_media_player.audio_set_mute(True) + # pause + self.vlc_media_player.set_time(0) + self.vlc_media_player.set_pause(1) + self.media_state_wait(vlc.State.Paused) + self.toggle_disable_load_media(False) + self.application.set_normal_cursor() + self.vlc_media_player.audio_set_mute(False) if not self.audio_cd: + # Temporarily disable signals + self.blockSignals(True) # Get titles, insert in combobox titles = self.vlc_media_player.video_get_title_description() self.titles_combo_box.clear() for title in titles: self.titles_combo_box.addItem(title[1].decode(), title[0]) + # Re-enable signals + self.blockSignals(False) # Main title is usually title #1 if len(titles) > 1: self.titles_combo_box.setCurrentIndex(1) - else: - self.titles_combo_box.setCurrentIndex(0) # Enable audio track combobox if anything is in it if len(titles) > 0: self.titles_combo_box.setDisabled(False) - self.toggle_disable_load_media(False) log.debug('load_disc_button end - vlc_media_player state: %s' % self.vlc_media_player.get_state()) @QtCore.pyqtSlot(bool) @@ -378,6 +391,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): if not self.vlc_media_player: log.error('vlc_media_player was None') return + self.application.set_busy_cursor() if self.audio_cd: self.vlc_media = self.audio_cd_tracks.item_at_index(index) self.vlc_media_player.set_media(self.vlc_media) @@ -385,14 +399,14 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): self.vlc_media_player.play() if not self.media_state_wait(vlc.State.Playing): log.error('Could not start playing audio cd, needed to get track info') + self.application.set_normal_cursor() return self.vlc_media_player.audio_set_mute(True) - # Sleep 1 second to make sure VLC has the needed metadata - sleep(1) # pause self.vlc_media_player.set_time(0) self.vlc_media_player.set_pause(1) self.vlc_media_player.audio_set_mute(False) + self.application.set_normal_cursor() self.toggle_disable_player(False) else: self.vlc_media_player.set_title(index) @@ -400,13 +414,13 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): self.vlc_media_player.play() if not self.media_state_wait(vlc.State.Playing): log.error('Could not start playing dvd, needed to get track info') + self.application.set_normal_cursor() return self.vlc_media_player.audio_set_mute(True) - # Sleep 1 second to make sure VLC has the needed metadata - sleep(1) - self.vlc_media_player.set_time(0) - # Get audio tracks, insert in combobox + # Get audio tracks audio_tracks = self.vlc_media_player.audio_get_track_description() + log.debug('number of audio tracks: %d' % len(audio_tracks)) + # Clear the audio track combobox, insert new tracks self.audio_tracks_combobox.clear() for audio_track in audio_tracks: self.audio_tracks_combobox.addItem(audio_track[1].decode(), audio_track[0]) @@ -447,6 +461,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): self.vlc_media_player.set_pause(1) loop_count += 1 log.debug('titles_combo_box end - vlc_media_player state: %s' % self.vlc_media_player.get_state()) + self.application.set_normal_cursor() @QtCore.pyqtSlot(int) def on_audio_tracks_combobox_currentIndexChanged(self, index): @@ -535,7 +550,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): """ Saves the current media and trackinfo as a clip to the mediamanager """ - log.debug('in on_save_button_clicked') + log.debug('in MediaClipSelectorForm.accept') start_time = self.start_position_edit.time() start_time_ms = start_time.hour() * 60 * 60 * 1000 + \ start_time.minute() * 60 * 1000 + \ @@ -550,10 +565,23 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): path = self.media_path_combobox.currentText() optical = '' if self.audio_cd: + # Check for load problems + if start_time_ms is None or end_time_ms is None or title is None: + critical_error_message_box(translate('MediaPlugin.MediaClipSelectorForm', 'CD not loaded correctly'), + translate('MediaPlugin.MediaClipSelectorForm', + 'The CD was not loaded correctly, please re-load and try again.')) + return optical = 'optical:%d:-1:-1:%d:%d:' % (title, start_time_ms, end_time_ms) else: audio_track = self.audio_tracks_combobox.itemData(self.audio_tracks_combobox.currentIndex()) subtitle_track = self.subtitle_tracks_combobox.itemData(self.subtitle_tracks_combobox.currentIndex()) + # Check for load problems + if start_time_ms is None or end_time_ms is None or title is None or audio_track is None\ + or subtitle_track is None: + critical_error_message_box(translate('MediaPlugin.MediaClipSelectorForm', 'DVD not loaded correctly'), + translate('MediaPlugin.MediaClipSelectorForm', + 'The DVD was not loaded correctly, please re-load and try again.')) + return optical = 'optical:%d:%d:%d:%d:%d:' % (title, audio_track, subtitle_track, start_time_ms, end_time_ms) # Ask for an alternative name for the mediaclip while True: @@ -595,7 +623,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): while media_state != self.vlc_media_player.get_state(): if self.vlc_media_player.get_state() == vlc.State.Error: return False - if (datetime.now() - start).seconds > 30: + if (datetime.now() - start).seconds > 15: return False return True @@ -606,7 +634,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): """ # Clear list first self.media_path_combobox.clear() - if os.name == 'nt': + if is_win(): # use win api to find optical drives fso = Dispatch('scripting.filesystemobject') for drive in fso.Drives: @@ -614,7 +642,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): # if type is 4, it is a cd-rom drive if drive.DriveType == 4: self.media_path_combobox.addItem('%s:\\' % drive.DriveLetter) - elif sys.platform.startswith('linux'): + elif is_linux(): # Get disc devices from dbus and find the ones that are optical bus = dbus.SystemBus() try: @@ -646,7 +674,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): if chr(c) != '\x00': block_file += chr(c) self.media_path_combobox.addItem(block_file) - elif sys.platform.startswith('darwin'): + elif is_macosx(): # Look for DVD folders in devices to find optical devices volumes = os.listdir('/Volumes') candidates = list() diff --git a/openlp/plugins/presentations/lib/impresscontroller.py b/openlp/plugins/presentations/lib/impresscontroller.py index d032b9161..a6e52411b 100644 --- a/openlp/plugins/presentations/lib/impresscontroller.py +++ b/openlp/plugins/presentations/lib/impresscontroller.py @@ -65,7 +65,7 @@ from PyQt4 import QtCore from openlp.core.lib import ScreenList from openlp.core.utils import delete_file, get_uno_command, get_uno_instance -from .presentationcontroller import PresentationController, PresentationDocument +from .presentationcontroller import PresentationController, PresentationDocument, TextType log = logging.getLogger(__name__) @@ -257,6 +257,7 @@ class ImpressDocument(PresentationDocument): self.presentation.Display = ScreenList().current['number'] + 1 self.control = None self.create_thumbnails() + self.create_titles_and_notes() return True def create_thumbnails(self): @@ -450,22 +451,44 @@ class ImpressDocument(PresentationDocument): :param slide_no: The slide the notes are required for, starting at 1 """ - return self.__get_text_from_page(slide_no, True) + return self.__get_text_from_page(slide_no, TextType.Notes) - def __get_text_from_page(self, slide_no, notes=False): + 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 = '' - pages = self.document.getDrawPages() - page = pages.getByIndex(slide_no - 1) - if notes: - page = page.getNotesPage() - for index in range(page.getCount()): - shape = page.getByIndex(index) - if shape.supportsService("com.sun.star.drawing.Text"): - text += shape.getString() + '\n' + 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 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 = [] + 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) + self.save_titles_and_notes(titles, notes) diff --git a/openlp/plugins/presentations/lib/mediaitem.py b/openlp/plugins/presentations/lib/mediaitem.py index 5b503d50f..5467d117d 100644 --- a/openlp/plugins/presentations/lib/mediaitem.py +++ b/openlp/plugins/presentations/lib/mediaitem.py @@ -288,13 +288,14 @@ class PresentationMediaItem(MediaManagerItem): os.path.join(doc.get_temp_folder(), 'mainslide001.png')): doc.load_presentation() i = 1 - image_file = 'mainslide%03d.png' % i - image = os.path.join(doc.get_temp_folder(), image_file) + image = os.path.join(doc.get_temp_folder(), 'mainslide%03d.png' % i) + thumbnail = os.path.join(doc.get_thumbnail_folder(), 'slide%d.png' % i) while os.path.isfile(image): - service_item.add_from_image(image, name) + service_item.add_from_image(image, name, thumbnail=thumbnail) i += 1 - image_file = 'mainslide%03d.png' % i - image = os.path.join(doc.get_temp_folder(), image_file) + image = os.path.join(doc.get_temp_folder(), 'mainslide%03d.png' % i) + thumbnail = os.path.join(doc.get_thumbnail_folder(), 'slide%d.png' % i) + service_item.add_capability(ItemCapabilities.HasThumbnails) doc.close_presentation() return True else: @@ -323,8 +324,21 @@ class PresentationMediaItem(MediaManagerItem): i = 1 img = doc.get_thumbnail_path(i, True) if img: + # Get titles and notes + titles, notes = doc.get_titles_and_notes() + service_item.add_capability(ItemCapabilities.HasDisplayTitle) + if notes.count('') != len(notes): + service_item.add_capability(ItemCapabilities.HasNotes) + service_item.add_capability(ItemCapabilities.HasThumbnails) while img: - service_item.add_from_command(path, name, img) + # Use title and note if available + title = '' + if titles and len(titles) >= i: + title = titles[i - 1] + note = '' + if notes and len(notes) >= i: + note = notes[i - 1] + service_item.add_from_command(path, name, img, title, note) i += 1 img = doc.get_thumbnail_path(i, True) doc.close_presentation() diff --git a/openlp/plugins/presentations/lib/powerpointcontroller.py b/openlp/plugins/presentations/lib/powerpointcontroller.py index f42e4f814..cf0f5bb99 100644 --- a/openlp/plugins/presentations/lib/powerpointcontroller.py +++ b/openlp/plugins/presentations/lib/powerpointcontroller.py @@ -27,7 +27,7 @@ # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### """ -This modul is for controlling powerpiont. PPT API documentation: +This module is for controlling powerpoint. PPT API documentation: `http://msdn.microsoft.com/en-us/library/aa269321(office.10).aspx`_ """ import os @@ -37,16 +37,17 @@ from openlp.core.common import is_win if is_win(): from win32com.client import Dispatch + import win32com import winreg import win32ui import pywintypes from openlp.core.lib import ScreenList +from openlp.core.common import Registry from openlp.core.lib.ui import UiStrings, critical_error_message_box, translate from openlp.core.common import trace_error_handler from .presentationcontroller import PresentationController, PresentationDocument - log = logging.getLogger(__name__) @@ -136,6 +137,7 @@ class PowerpointDocument(PresentationDocument): self.controller.process.Presentations.Open(self.file_path, False, False, True) self.presentation = self.controller.process.Presentations(self.controller.process.Presentations.Count) self.create_thumbnails() + self.create_titles_and_notes() # Powerpoint 2013 pops up when loading a file, so we minimize it again if self.presentation.Application.Version == u'15.0': try: @@ -392,6 +394,28 @@ class PowerpointDocument(PresentationDocument): """ return _get_text_from_shapes(self.presentation.Slides(slide_no).NotesPage.Shapes) + 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 = [] + for slide in self.presentation.Slides: + try: + text = slide.Shapes.Title.TextFrame.TextRange.Text + except Exception as e: + log.exception(e) + text = '' + titles.append(text.replace('\n', ' ').replace('\x0b', ' ') + '\n') + note = _get_text_from_shapes(slide.NotesPage.Shapes) + if len(note) == 0: + note = ' ' + notes.append(note) + self.save_titles_and_notes(titles, notes) + def show_error_msg(self): """ Stop presentation and display an error message. @@ -410,8 +434,8 @@ def _get_text_from_shapes(shapes): :param shapes: A set of shapes to search for text. """ text = '' - for index in range(shapes.Count): - shape = shapes(index + 1) - if shape.HasTextFrame: - text += shape.TextFrame.TextRange.Text + '\n' + for shape in shapes: + if shape.PlaceholderFormat.Type == 2: # 2 from is enum PpPlaceholderType.ppPlaceholderBody + if shape.HasTextFrame and shape.TextFrame.HasText: + text += shape.TextFrame.TextRange.Text + '\n' return text diff --git a/openlp/plugins/presentations/lib/pptviewcontroller.py b/openlp/plugins/presentations/lib/pptviewcontroller.py index 7e03e322f..4aea501b4 100644 --- a/openlp/plugins/presentations/lib/pptviewcontroller.py +++ b/openlp/plugins/presentations/lib/pptviewcontroller.py @@ -29,6 +29,11 @@ import logging import os +import logging +import zipfile +import re +from xml.etree import ElementTree + from openlp.core.common import is_win @@ -127,14 +132,14 @@ class PptviewDocument(PresentationDocument): temp_folder = self.get_temp_folder() size = ScreenList().current['size'] rect = RECT(size.x(), size.y(), size.right(), size.bottom()) - file_path = os.path.normpath(self.file_path) + self.file_path = os.path.normpath(self.file_path) preview_path = os.path.join(temp_folder, 'slide') # Ensure that the paths are null terminated - file_path = file_path.encode('utf-16-le') + b'\0' + self.file_path = self.file_path.encode('utf-16-le') + b'\0' preview_path = preview_path.encode('utf-16-le') + b'\0' if not os.path.isdir(temp_folder): os.makedirs(temp_folder) - self.ppt_id = self.controller.process.OpenPPT(file_path, None, rect, preview_path) + self.ppt_id = self.controller.process.OpenPPT(self.file_path, None, rect, preview_path) if self.ppt_id >= 0: self.create_thumbnails() self.stop_presentation() @@ -154,6 +159,68 @@ class PptviewDocument(PresentationDocument): path = '%s\\slide%s.bmp' % (self.get_temp_folder(), str(idx + 1)) self.convert_thumbnail(path, idx + 1) + def create_titles_and_notes(self): + """ + Extracts the titles and notes from the zipped file + and writes the list of titles (one per slide) + to 'titles.txt' + and the notes to 'slideNotes[x].txt' + in the thumbnails directory + """ + titles = None + notes = None + filename = os.path.normpath(self.file_path) + # let's make sure we have a valid zipped presentation + if os.path.exists(filename) and zipfile.is_zipfile(filename): + namespaces = {"p": "http://schemas.openxmlformats.org/presentationml/2006/main", + "a": "http://schemas.openxmlformats.org/drawingml/2006/main"} + # open the file + with zipfile.ZipFile(filename) as zip_file: + # find the presentation.xml to get the slide count + with zip_file.open('ppt/presentation.xml') as pres: + tree = ElementTree.parse(pres) + nodes = tree.getroot().findall(".//p:sldIdLst/p:sldId", namespaces=namespaces) + # initialize the lists + titles = ['' for i in range(len(nodes))] + notes = ['' for i in range(len(nodes))] + # loop thru the file list to find slides and notes + for zip_info in zip_file.infolist(): + node_type = '' + index = -1 + list_to_add = None + # check if it is a slide + match = re.search("slides/slide(.+)\.xml", zip_info.filename) + if match: + index = int(match.group(1))-1 + node_type = 'ctrTitle' + list_to_add = titles + # or a note + match = re.search("notesSlides/notesSlide(.+)\.xml", zip_info.filename) + if match: + index = int(match.group(1))-1 + node_type = 'body' + list_to_add = notes + # if it is one of our files, index shouldn't be -1 + if index >= 0: + with zip_file.open(zip_info) as zipped_file: + tree = ElementTree.parse(zipped_file) + text = '' + nodes = tree.getroot().findall(".//p:ph[@type='" + node_type + "']../../..//p:txBody//a:t", + namespaces=namespaces) + # if we found any content + if nodes and len(nodes) > 0: + for node in nodes: + if len(text) > 0: + text += '\n' + text += node.text + # Let's remove the \n from the titles and + # just add one at the end + if node_type == 'ctrTitle': + text = text.replace('\n', ' ').replace('\x0b', ' ') + '\n' + list_to_add[index] = text + # now let's write the files + 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 diff --git a/openlp/plugins/presentations/lib/pptviewlib/test.pptx b/openlp/plugins/presentations/lib/pptviewlib/test.pptx new file mode 100644 index 000000000..c8beab172 Binary files /dev/null and b/openlp/plugins/presentations/lib/pptviewlib/test.pptx differ diff --git a/openlp/plugins/presentations/lib/presentationcontroller.py b/openlp/plugins/presentations/lib/presentationcontroller.py index 3060bcdb0..c64d70016 100644 --- a/openlp/plugins/presentations/lib/presentationcontroller.py +++ b/openlp/plugins/presentations/lib/presentationcontroller.py @@ -293,6 +293,49 @@ class PresentationDocument(object): """ return '' + def get_titles_and_notes(self): + """ + Reads the titles from the titles file and + the notes files and returns the content in two lists + """ + titles = [] + notes = [] + titles_file = os.path.join(self.get_thumbnail_folder(), 'titles.txt') + if os.path.exists(titles_file): + try: + with open(titles_file) as fi: + titles = fi.read().splitlines() + except: + log.exception('Failed to open/read existing titles file') + titles = [] + for slide_no, title in enumerate(titles, 1): + notes_file = os.path.join(self.get_thumbnail_folder(), 'slideNotes%d.txt' % slide_no) + note = '' + if os.path.exists(notes_file): + try: + with open(notes_file) as fn: + note = fn.read() + except: + log.exception('Failed to open/read notes file') + note = '' + notes.append(note) + return titles, notes + + def save_titles_and_notes(self, titles, notes): + """ + Performs the actual persisting of titles to the titles.txt + and notes to the slideNote%.txt + """ + if titles: + titles_file = os.path.join(self.get_thumbnail_folder(), 'titles.txt') + with open(titles_file, mode='w') as fo: + fo.writelines(titles) + if notes: + for slide_no, note in enumerate(notes, 1): + notes_file = os.path.join(self.get_thumbnail_folder(), 'slideNotes%d.txt' % slide_no) + with open(notes_file, mode='w') as fn: + fn.write(note) + class PresentationController(object): """ @@ -427,3 +470,12 @@ class PresentationController(object): def close_presentation(self): pass + + +class TextType(object): + """ + Type Enumeration for Types of Text to request + """ + Title = 0 + SlideText = 1 + Notes = 2 diff --git a/openlp/plugins/remotes/html/openlp.js b/openlp/plugins/remotes/html/openlp.js index ffc9430c2..9f18c1552 100644 --- a/openlp/plugins/remotes/html/openlp.js +++ b/openlp/plugins/remotes/html/openlp.js @@ -87,16 +87,30 @@ window.OpenLP = { var ul = $("#slide-controller > div[data-role=content] > ul[data-role=listview]"); ul.html(""); for (idx in data.results.slides) { - var text = data.results.slides[idx]["tag"]; - if (text != "") text = text + ": "; - text = text + data.results.slides[idx]["text"]; + var indexInt = parseInt(idx,10); + var slide = data.results.slides[idx]; + var text = slide["tag"]; + if (text != "") { + text = text + ": "; + } + if (slide["title"]) { + text += slide["title"] + } else { + text += slide["text"]; + } + if (slide["notes"]) { + text += ("
" + slide["notes"] + "
"); + } text = text.replace(/\n/g, '
'); - var li = $("
  • ").append( - $("").attr("value", parseInt(idx, 10)).html(text)); - if (data.results.slides[idx]["selected"]) { + if (slide["img"]) { + text += ""; + } + var li = $("
  • ").append($("").html(text)); + if (slide["selected"]) { li.attr("data-theme", "e"); } li.children("a").click(OpenLP.setSlide); + li.find("*").attr("value", indexInt ); ul.append(li); } OpenLP.currentItem = data.results.item; diff --git a/openlp/plugins/remotes/html/stage.js b/openlp/plugins/remotes/html/stage.js index ec88706c1..e63025b80 100644 --- a/openlp/plugins/remotes/html/stage.js +++ b/openlp/plugins/remotes/html/stage.js @@ -102,7 +102,21 @@ window.OpenLP = { $("#verseorder span").removeClass("currenttag"); $("#tag" + OpenLP.currentTags[OpenLP.currentSlide]).addClass("currenttag"); var slide = OpenLP.currentSlides[OpenLP.currentSlide]; - var text = slide["text"]; + var text = ""; + // use title if available + if (slide["title"]) { + text = slide["title"]; + } else { + text = slide["text"]; + } + // use thumbnail if available + if (slide["img"]) { + text += "

    "; + } + // use notes if available + if (slide["notes"]) { + text += '
    ' + slide["notes"]; + } text = text.replace(/\n/g, "
    "); $("#currentslide").html(text); text = ""; @@ -110,7 +124,11 @@ window.OpenLP = { for (var idx = OpenLP.currentSlide + 1; idx < OpenLP.currentSlides.length; idx++) { if (OpenLP.currentTags[idx] != OpenLP.currentTags[idx - 1]) text = text + "

    "; - text = text + OpenLP.currentSlides[idx]["text"]; + if (OpenLP.currentSlides[idx]["title"]) { + text = text + OpenLP.currentSlides[idx]["title"]; + } else { + text = text + OpenLP.currentSlides[idx]["text"]; + } if (OpenLP.currentTags[idx] != OpenLP.currentTags[idx - 1]) text = text + "

    "; else diff --git a/openlp/plugins/remotes/lib/httprouter.py b/openlp/plugins/remotes/lib/httprouter.py index 4241b34dc..ff28de399 100644 --- a/openlp/plugins/remotes/lib/httprouter.py +++ b/openlp/plugins/remotes/lib/httprouter.py @@ -125,7 +125,7 @@ from mako.template import Template from PyQt4 import QtCore from openlp.core.common import Registry, RegistryProperties, AppLocation, Settings, translate -from openlp.core.lib import PluginStatus, StringContent, image_to_byte +from openlp.core.lib import PluginStatus, StringContent, image_to_byte, ItemCapabilities, create_thumb log = logging.getLogger(__name__) FILE_TYPES = { @@ -159,6 +159,7 @@ class HttpRouter(RegistryProperties): ('^/(stage)$', {'function': self.serve_file, 'secure': False}), ('^/(main)$', {'function': self.serve_file, 'secure': False}), (r'^/files/(.*)$', {'function': self.serve_file, 'secure': False}), + (r'^/(\w+)/thumbnails([^/]+)?/(.*)$', {'function': self.serve_thumbnail, 'secure': False}), (r'^/api/poll$', {'function': self.poll, 'secure': False}), (r'^/main/poll$', {'function': self.main_poll, 'secure': False}), (r'^/main/image$', {'function': self.main_image, 'secure': False}), @@ -328,7 +329,8 @@ class HttpRouter(RegistryProperties): 'no_results': translate('RemotePlugin.Mobile', 'No Results'), 'options': translate('RemotePlugin.Mobile', 'Options'), 'service': translate('RemotePlugin.Mobile', 'Service'), - 'slides': translate('RemotePlugin.Mobile', 'Slides') + 'slides': translate('RemotePlugin.Mobile', 'Slides'), + 'settings': translate('RemotePlugin.Mobile', 'Settings'), } def serve_file(self, file_name=None): @@ -380,6 +382,42 @@ class HttpRouter(RegistryProperties): content_type = FILE_TYPES.get(ext, 'text/plain') return ext, content_type + def serve_thumbnail(self, controller_name=None, dimensions=None, file_name=None): + """ + Serve an image file. If not found return 404. + """ + log.debug('serve thumbnail %s/thumbnails%s/%s' % (controller_name, dimensions, file_name)) + supported_controllers = ['presentations', 'images'] + # -1 means use the default dimension in ImageManager + width = -1 + height = -1 + if dimensions: + match = re.search('(\d+)x(\d+)', dimensions) + if match: + # let's make sure that the dimensions are within reason + width = sorted([10, int(match.group(1)), 1000])[1] + height = sorted([10, int(match.group(2)), 1000])[1] + content = '' + content_type = None + if controller_name and file_name: + if controller_name in supported_controllers: + full_path = urllib.parse.unquote(file_name) + if '..' not in full_path: # no hacking please + full_path = os.path.normpath(os.path.join(AppLocation.get_section_data_path(controller_name), + 'thumbnails/' + full_path)) + if os.path.exists(full_path): + path, just_file_name = os.path.split(full_path) + self.image_manager.add_image(full_path, just_file_name, None, width, height) + ext, content_type = self.get_content_type(full_path) + image = self.image_manager.get_image(full_path, just_file_name, width, height) + content = image_to_byte(image, False) + if len(content) == 0: + return self.do_not_found() + self.send_response(200) + self.send_header('Content-type', content_type) + self.end_headers() + return content + def poll(self): """ Poll OpenLP to determine the current slide number and item name. @@ -458,6 +496,7 @@ class HttpRouter(RegistryProperties): if current_item: for index, frame in enumerate(current_item.get_frames()): item = {} + # Handle text (songs, custom, bibles) if current_item.is_text(): if frame['verseTag']: item['tag'] = str(frame['verseTag']) @@ -465,11 +504,37 @@ class HttpRouter(RegistryProperties): item['tag'] = str(index + 1) item['text'] = str(frame['text']) item['html'] = str(frame['html']) - else: + # Handle images, unless a custom thumbnail is given or if thumbnails is disabled + elif current_item.is_image() and not frame.get('image', '') and Settings().value('remotes/thumbnails'): item['tag'] = str(index + 1) + thumbnail_path = os.path.join('images', 'thumbnails', frame['title']) + full_thumbnail_path = os.path.join(AppLocation.get_data_path(), thumbnail_path) + # Create thumbnail if it doesn't exists + if not os.path.exists(full_thumbnail_path): + create_thumb(current_item.get_frame_path(index), full_thumbnail_path, False) + item['img'] = urllib.request.pathname2url(os.path.sep + thumbnail_path) + item['text'] = str(frame['title']) + item['html'] = str(frame['title']) + else: + # Handle presentation etc. + item['tag'] = str(index + 1) + if current_item.is_capable(ItemCapabilities.HasDisplayTitle): + item['title'] = str(frame['display_title']) + if current_item.is_capable(ItemCapabilities.HasNotes): + item['notes'] = str(frame['notes']) + if current_item.is_capable(ItemCapabilities.HasThumbnails) and \ + Settings().value('remotes/thumbnails'): + # If the file is under our app directory tree send the portion after the match + data_path = AppLocation.get_data_path() + print(frame) + if frame['image'][0:len(data_path)] == data_path: + item['img'] = urllib.request.pathname2url(frame['image'][len(data_path):]) item['text'] = str(frame['title']) item['html'] = str(frame['title']) item['selected'] = (self.live_controller.selected_row == index) + if current_item.notes: + item['notes'] = item.get('notes', '') + '\n' + current_item.notes + print(item) data.append(item) json_data = {'results': {'slides': data}} if current_item: diff --git a/openlp/plugins/remotes/lib/httpserver.py b/openlp/plugins/remotes/lib/httpserver.py index 9a904090d..826b0530d 100644 --- a/openlp/plugins/remotes/lib/httpserver.py +++ b/openlp/plugins/remotes/lib/httpserver.py @@ -144,6 +144,7 @@ class OpenLPServer(RegistryProperties): try: self.httpd = server_class((address, port), CustomHandler) log.debug("Server started for class %s %s %d" % (server_class, address, port)) + break except OSError: log.debug("failed to start http server thread state %d %s" % (loop, self.http_thread.isRunning())) @@ -151,6 +152,8 @@ class OpenLPServer(RegistryProperties): time.sleep(0.1) except: log.error('Failed to start server ') + loop += 1 + time.sleep(0.1) def stop_server(self): """ diff --git a/openlp/plugins/remotes/lib/remotetab.py b/openlp/plugins/remotes/lib/remotetab.py index 4db25cfc2..45f13faac 100644 --- a/openlp/plugins/remotes/lib/remotetab.py +++ b/openlp/plugins/remotes/lib/remotetab.py @@ -62,6 +62,9 @@ class RemoteTab(SettingsTab): self.twelve_hour_check_box = QtGui.QCheckBox(self.server_settings_group_box) self.twelve_hour_check_box.setObjectName('twelve_hour_check_box') self.server_settings_layout.addRow(self.twelve_hour_check_box) + self.thumbnails_check_box = QtGui.QCheckBox(self.server_settings_group_box) + self.thumbnails_check_box.setObjectName('thumbnails_check_box') + self.server_settings_layout.addRow(self.thumbnails_check_box) self.left_layout.addWidget(self.server_settings_group_box) self.http_settings_group_box = QtGui.QGroupBox(self.left_column) self.http_settings_group_box.setObjectName('http_settings_group_box') @@ -163,6 +166,7 @@ class RemoteTab(SettingsTab): self.left_layout.addStretch() self.right_layout.addStretch() self.twelve_hour_check_box.stateChanged.connect(self.on_twelve_hour_check_box_changed) + self.thumbnails_check_box.stateChanged.connect(self.on_thumbnails_check_box_changed) self.address_edit.textChanged.connect(self.set_urls) self.port_spin_box.valueChanged.connect(self.set_urls) self.https_port_spin_box.valueChanged.connect(self.set_urls) @@ -176,6 +180,8 @@ class RemoteTab(SettingsTab): self.stage_url_label.setText(translate('RemotePlugin.RemoteTab', 'Stage view URL:')) self.live_url_label.setText(translate('RemotePlugin.RemoteTab', 'Live view URL:')) self.twelve_hour_check_box.setText(translate('RemotePlugin.RemoteTab', 'Display stage time in 12h format')) + self.thumbnails_check_box.setText(translate('RemotePlugin.RemoteTab', + 'Show thumbnails of non-text slides in remote and stage view.')) self.android_app_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'Android App')) self.qr_description_label.setText( translate('RemotePlugin.RemoteTab', 'Scan the QR code or click
    export_load.png + openlp-osx-wizard.png wizard_exportsong.bmp wizard_importsong.bmp wizard_importbible.bmp @@ -151,10 +152,10 @@ messagebox_warning.png - network_server.png + network_server.png network_ssl.png - network_auth.png - + network_auth.png + song_usage_active.png song_usage_inactive.png diff --git a/resources/images/openlp-osx-wizard.png b/resources/images/openlp-osx-wizard.png new file mode 100644 index 000000000..79437b5ed Binary files /dev/null and b/resources/images/openlp-osx-wizard.png differ diff --git a/tests/functional/openlp_core_common/test_applocation.py b/tests/functional/openlp_core_common/test_applocation.py index 27a47de6e..cc2f36c87 100644 --- a/tests/functional/openlp_core_common/test_applocation.py +++ b/tests/functional/openlp_core_common/test_applocation.py @@ -162,9 +162,9 @@ class TestAppLocation(TestCase): patch('openlp.core.common.applocation.os.path.abspath') as mocked_abspath, \ patch('openlp.core.common.applocation.os.path.split') as mocked_split, \ patch('openlp.core.common.applocation.sys') as mocked_sys: - mocked_abspath.return_value = 'plugins/dir' + mocked_abspath.return_value = os.path.join('plugins', 'dir') mocked_split.return_value = ['openlp'] - mocked_get_frozen_path.return_value = 'plugins/dir' + mocked_get_frozen_path.return_value = os.path.join('plugins', 'dir') mocked_sys.frozen = 1 mocked_sys.argv = ['openlp'] @@ -172,7 +172,7 @@ class TestAppLocation(TestCase): directory = AppLocation.get_directory(AppLocation.PluginsDir) # THEN: The correct directory should be returned - self.assertEqual('plugins/dir', directory, 'Directory should be "plugins/dir"') + self.assertEqual(os.path.join('plugins', 'dir'), directory, 'Directory should be "plugins/dir"') def get_frozen_path_in_unfrozen_app_test(self): """ diff --git a/tests/functional/openlp_core_lib/test_image_manager.py b/tests/functional/openlp_core_lib/test_image_manager.py index 072978993..293e890d6 100644 --- a/tests/functional/openlp_core_lib/test_image_manager.py +++ b/tests/functional/openlp_core_lib/test_image_manager.py @@ -69,16 +69,17 @@ class TestImageManager(TestCase, TestMixin): Test the Image Manager setup basic functionality """ # GIVEN: the an image add to the image manager - self.image_manager.add_image(TEST_PATH, 'church.jpg', None) + full_path = os.path.normpath(os.path.join(TEST_PATH, 'church.jpg')) + self.image_manager.add_image(full_path, 'church.jpg', None) # WHEN the image is retrieved - image = self.image_manager.get_image(TEST_PATH, 'church.jpg') + image = self.image_manager.get_image(full_path, 'church.jpg') # THEN returned record is a type of image self.assertEqual(isinstance(image, QtGui.QImage), True, 'The returned object should be a QImage') # WHEN: The image bytes are requested. - byte_array = self.image_manager.get_image_bytes(TEST_PATH, 'church.jpg') + byte_array = self.image_manager.get_image_bytes(full_path, 'church.jpg') # THEN: Type should be a str. self.assertEqual(isinstance(byte_array, str), True, 'The returned object should be a str') @@ -89,6 +90,38 @@ class TestImageManager(TestCase, TestMixin): self.image_manager.get_image(TEST_PATH, 'church1.jpg') self.assertNotEquals(context.exception, '', 'KeyError exception should have been thrown for missing image') + def different_dimension_image_test(self): + """ + Test the Image Manager with dimensions + """ + # GIVEN: add an image with specific dimensions + full_path = os.path.normpath(os.path.join(TEST_PATH, 'church.jpg')) + self.image_manager.add_image(full_path, 'church.jpg', None, 80, 80) + + # WHEN: the image is retrieved + image = self.image_manager.get_image(full_path, 'church.jpg', 80, 80) + + # THEN: The return should be of type image + self.assertEqual(isinstance(image, QtGui.QImage), True, 'The returned object should be a QImage') + + # WHEN: adding the same image with different dimensions + self.image_manager.add_image(full_path, 'church.jpg', None, 100, 100) + + # THEN: the cache should contain two pictures + self.assertEqual(len(self.image_manager._cache), 2, + 'Image manager should consider two dimensions of the same picture as different') + + # WHEN: adding the same image with first dimensions + self.image_manager.add_image(full_path, 'church.jpg', None, 80, 80) + + # THEN: the cache should still contain only two pictures + self.assertEqual(len(self.image_manager._cache), 2, 'Same dimensions should not be added again') + + # WHEN: calling with correct image, but wrong dimensions + with self.assertRaises(KeyError) as context: + self.image_manager.get_image(full_path, 'church.jpg', 120, 120) + self.assertNotEquals(context.exception, '', 'KeyError exception should have been thrown for missing dimension') + def process_cache_test(self): """ Test the process_cache method @@ -151,7 +184,7 @@ class TestImageManager(TestCase, TestMixin): :param image: The name of the image. E. g. ``image1`` """ - return self.image_manager._cache[(TEST_PATH, image)].priority + return self.image_manager._cache[(TEST_PATH, image, -1, -1)].priority def mocked_resize_image(self, *args): """ diff --git a/tests/functional/openlp_core_lib/test_serviceitem.py b/tests/functional/openlp_core_lib/test_serviceitem.py index 4fd6f6b83..629b17114 100644 --- a/tests/functional/openlp_core_lib/test_serviceitem.py +++ b/tests/functional/openlp_core_lib/test_serviceitem.py @@ -32,13 +32,11 @@ Package to test the openlp.core.lib package. import os from unittest import TestCase - from tests.functional import MagicMock, patch from tests.utils import assert_length, convert_file_service_item from openlp.core.common import Registry -from openlp.core.lib import ItemCapabilities, ServiceItem - +from openlp.core.lib import ItemCapabilities, ServiceItem, ServiceItemType VERSE = 'The Lord said to {r}Noah{/r}: \n'\ 'There\'s gonna be a {su}floody{/su}, {sb}floody{/sb}\n'\ @@ -120,13 +118,17 @@ class TestServiceItem(TestCase): # WHEN: adding an image from a saved Service and mocked exists line = convert_file_service_item(TEST_PATH, 'serviceitem_image_1.osj') - with patch('openlp.core.ui.servicemanager.os.path.exists') as mocked_exists: + with patch('openlp.core.ui.servicemanager.os.path.exists') as mocked_exists,\ + patch('openlp.core.lib.serviceitem.create_thumb') as mocked_create_thumb,\ + patch('openlp.core.lib.serviceitem.AppLocation.get_section_data_path') as \ + mocked_get_section_data_path: mocked_exists.return_value = True + mocked_get_section_data_path.return_value = os.path.normpath('/path/') service_item.set_from_service(line, TEST_PATH) # THEN: We should get back a valid service item self.assertTrue(service_item.is_valid, 'The new service item should be valid') - self.assertEqual(test_file, service_item.get_rendered_frame(0), + self.assertEqual(os.path.normpath(test_file), os.path.normpath(service_item.get_rendered_frame(0)), 'The first frame should match the path to the image') self.assertEqual(frame_array, service_item.get_frames()[0], 'The return should match frame array1') @@ -153,8 +155,8 @@ class TestServiceItem(TestCase): # GIVEN: A new service item and a mocked add icon function image_name1 = 'image_1.jpg' image_name2 = 'image_2.jpg' - test_file1 = os.path.join('/home/openlp', image_name1) - test_file2 = os.path.join('/home/openlp', image_name2) + test_file1 = os.path.normpath(os.path.join('/home/openlp', image_name1)) + test_file2 = os.path.normpath(os.path.join('/home/openlp', image_name2)) frame_array1 = {'path': test_file1, 'title': image_name1} frame_array2 = {'path': test_file2, 'title': image_name2} @@ -168,8 +170,12 @@ class TestServiceItem(TestCase): line = convert_file_service_item(TEST_PATH, 'serviceitem_image_2.osj') line2 = convert_file_service_item(TEST_PATH, 'serviceitem_image_2.osj', 1) - with patch('openlp.core.ui.servicemanager.os.path.exists') as mocked_exists: + with patch('openlp.core.ui.servicemanager.os.path.exists') as mocked_exists, \ + patch('openlp.core.lib.serviceitem.create_thumb') as mocked_create_thumb, \ + patch('openlp.core.lib.serviceitem.AppLocation.get_section_data_path') as \ + mocked_get_section_data_path: mocked_exists.return_value = True + mocked_get_section_data_path.return_value = os.path.normpath('/path/') service_item2.set_from_service(line2) service_item.set_from_service(line) @@ -207,6 +213,44 @@ class TestServiceItem(TestCase): self.assertTrue(service_item.is_capable(ItemCapabilities.CanAppend), 'This service item should be able to have new items added to it') + def add_from_command_for_a_presentation_test(self): + """ + Test the Service Item - adding a presentation + """ + # GIVEN: A service item, a mocked icon and presentation data + service_item = ServiceItem(None) + presentation_name = 'test.pptx' + image = MagicMock() + display_title = 'DisplayTitle' + notes = 'Note1\nNote2\n' + frame = {'title': presentation_name, 'image': image, 'path': TEST_PATH, + 'display_title': display_title, 'notes': notes} + + # WHEN: adding presentation to service_item + service_item.add_from_command(TEST_PATH, presentation_name, image, display_title, notes) + + # THEN: verify that it is setup as a Command and that the frame data matches + self.assertEqual(service_item.service_item_type, ServiceItemType.Command, 'It should be a Command') + self.assertEqual(service_item.get_frames()[0], frame, 'Frames should match') + + def add_from_comamnd_without_display_title_and_notes_test(self): + """ + Test the Service Item - add from command, but not presentation + """ + # GIVEN: A new service item, a mocked icon and image data + service_item = ServiceItem(None) + image_name = 'test.img' + image = MagicMock() + frame = {'title': image_name, 'image': image, 'path': TEST_PATH, + 'display_title': None, 'notes': None} + + # WHEN: adding image to service_item + service_item.add_from_command(TEST_PATH, image_name, image) + + # THEN: verify that it is setup as a Command and that the frame data matches + self.assertEqual(service_item.service_item_type, ServiceItemType.Command, 'It should be a Command') + self.assertEqual(service_item.get_frames()[0], frame, 'Frames should match') + def service_item_load_optical_media_from_service_test(self): """ Test the Service Item - load an optical media item diff --git a/tests/functional/openlp_core_lib/test_theme.py b/tests/functional/openlp_core_lib/test_theme.py index bcdced35f..7b09135dc 100644 --- a/tests/functional/openlp_core_lib/test_theme.py +++ b/tests/functional/openlp_core_lib/test_theme.py @@ -51,7 +51,7 @@ class TestTheme(TestCase): """ pass - def test_new_theme(self): + def new_theme_test(self): """ Test the theme creation - basic test """ diff --git a/tests/functional/openlp_core_lib/test_ui.py b/tests/functional/openlp_core_lib/test_ui.py index 591762947..7030c27f3 100644 --- a/tests/functional/openlp_core_lib/test_ui.py +++ b/tests/functional/openlp_core_lib/test_ui.py @@ -29,10 +29,14 @@ """ Package to test the openlp.core.lib.ui package. """ -from PyQt4 import QtGui +from PyQt4 import QtCore, QtGui from unittest import TestCase -from openlp.core.lib.ui import * +from openlp.core.common import UiStrings, translate +from openlp.core.lib.ui import add_welcome_page, create_button_box, create_horizontal_adjusting_combo_box, \ + create_button, create_action, create_valign_selection_widgets, find_and_set_in_combo_box, create_widget_action, \ + set_case_insensitive_completer +from tests.functional import MagicMock, patch class TestUi(TestCase): @@ -40,7 +44,7 @@ class TestUi(TestCase): Test the functions in the ui module """ - def test_add_welcome_page(self): + def add_welcome_page_test(self): """ Test appending a welcome page to a wizard """ @@ -54,7 +58,7 @@ class TestUi(TestCase): self.assertEqual(1, len(wizard.pageIds()), 'The wizard should have one page.') self.assertIsInstance(wizard.page(0).pixmap(QtGui.QWizard.WatermarkPixmap), QtGui.QPixmap) - def test_create_button_box(self): + def create_button_box_test(self): """ Test creating a button box for a dialog """ @@ -82,7 +86,7 @@ class TestUi(TestCase): self.assertEqual(1, len(btnbox.buttons())) self.assertEqual(QtGui.QDialogButtonBox.HelpRole, btnbox.buttonRole(btnbox.buttons()[0])) - def test_create_horizontal_adjusting_combo_box(self): + def create_horizontal_adjusting_combo_box_test(self): """ Test creating a horizontal adjusting combo box """ @@ -97,7 +101,7 @@ class TestUi(TestCase): self.assertEqual('combo1', combo.objectName()) self.assertEqual(QtGui.QComboBox.AdjustToMinimumContentsLength, combo.sizeAdjustPolicy()) - def test_create_button(self): + def create_button_test(self): """ Test creating a button """ @@ -129,7 +133,7 @@ class TestUi(TestCase): self.assertEqual('my_btn', btn.objectName()) self.assertTrue(btn.isEnabled()) - def test_create_action(self): + def create_action_test(self): """ Test creating an action """ @@ -154,9 +158,46 @@ class TestUi(TestCase): self.assertEqual('my tooltip', action.toolTip()) self.assertEqual('my statustip', action.statusTip()) - def test_create_checked_enabled_visible_action(self): + def create_action_on_mac_osx_test(self): """ - Test creating an action with the 'checked', 'enabled' and 'visible' properties. + Test creating an action on OS X calls the correct method + """ + # GIVEN: A dialog and a mocked out is_macosx() method to always return True + with patch('openlp.core.lib.ui.is_macosx') as mocked_is_macosx, \ + patch('openlp.core.lib.ui.QtGui.QAction') as MockedQAction: + mocked_is_macosx.return_value = True + mocked_action = MagicMock() + MockedQAction.return_value = mocked_action + dialog = QtGui.QDialog() + + # WHEN: An action is created + create_action(dialog, 'my_action') + + # THEN: setIconVisibleInMenu should be called + mocked_action.setIconVisibleInMenu.assert_called_with(False) + + def create_action_not_on_mac_osx_test(self): + """ + Test creating an action on something other than OS X doesn't call the method + """ + # GIVEN: A dialog and a mocked out is_macosx() method to always return True + with patch('openlp.core.lib.ui.is_macosx') as mocked_is_macosx, \ + patch('openlp.core.lib.ui.QtGui.QAction') as MockedQAction: + mocked_is_macosx.return_value = False + mocked_action = MagicMock() + MockedQAction.return_value = mocked_action + dialog = QtGui.QDialog() + + # WHEN: An action is created + create_action(dialog, 'my_action') + + # THEN: setIconVisibleInMenu should not be called + self.assertEqual(0, mocked_action.setIconVisibleInMenu.call_count, + 'setIconVisibleInMenu should not have been called') + + def create_checked_disabled_invisible_action_test(self): + """ + Test that an invisible, disabled, checked action is created correctly """ # GIVEN: A dialog dialog = QtGui.QDialog() @@ -165,11 +206,24 @@ class TestUi(TestCase): action = create_action(dialog, 'my_action', checked=True, enabled=False, visible=False) # THEN: These properties should be set - self.assertEqual(True, action.isChecked()) - self.assertEqual(False, action.isEnabled()) - self.assertEqual(False, action.isVisible()) + self.assertTrue(action.isChecked(), 'The action should be checked') + self.assertFalse(action.isEnabled(), 'The action should be disabled') + self.assertFalse(action.isVisible(), 'The action should be invisble') - def test_create_valign_selection_widgets(self): + def create_action_separator_test(self): + """ + Test creating an action as separator + """ + # GIVEN: A dialog + dialog = QtGui.QDialog() + + # WHEN: We create an action as a separator + action = create_action(dialog, 'my_action', separator=True) + + # THEN: The action should be a separator + self.assertTrue(action.isSeparator(), 'The action should be a separator') + + def create_valign_selection_widgets_test(self): """ Test creating a combo box for valign selection """ @@ -186,7 +240,7 @@ class TestUi(TestCase): for text in [UiStrings().Top, UiStrings().Middle, UiStrings().Bottom]: self.assertTrue(combo.findText(text) >= 0) - def test_find_and_set_in_combo_box(self): + def find_and_set_in_combo_box_test(self): """ Test finding a string in a combo box and setting it as the selected item if present """ @@ -213,7 +267,7 @@ class TestUi(TestCase): # THEN: The index should have changed self.assertEqual(2, combo.currentIndex()) - def test_create_widget_action(self): + def create_widget_action_test(self): """ Test creating an action for a widget """ @@ -227,7 +281,7 @@ class TestUi(TestCase): self.assertIsInstance(action, QtGui.QAction) self.assertEqual(action.objectName(), 'some action') - def test_set_case_insensitive_completer(self): + def set_case_insensitive_completer_test(self): """ Test setting a case insensitive completer on a widget """ diff --git a/tests/functional/openlp_core_ui/test_firsttimeform.py b/tests/functional/openlp_core_ui/test_firsttimeform.py index 2e26c286a..35bd1675d 100644 --- a/tests/functional/openlp_core_ui/test_firsttimeform.py +++ b/tests/functional/openlp_core_ui/test_firsttimeform.py @@ -47,7 +47,7 @@ class TestFirstTimeForm(TestCase, TestMixin): Registry().register('application', self.app) self.first_time_form = FirstTimeForm(screens) - def test_access_to_config(self): + def access_to_config_test(self): """ Test if we can access the First Time Form's config file """ @@ -59,7 +59,7 @@ class TestFirstTimeForm(TestCase, TestMixin): self.assertTrue(self.first_time_form.web_access, 'First Time Wizard\'s web configuration file should be available') - def test_parsable_config(self): + def parsable_config_test(self): """ Test if the First Time Form's config file is parsable """ diff --git a/tests/functional/openlp_core_ui/test_formattingtagscontroller.py b/tests/functional/openlp_core_ui/test_formattingtagscontroller.py index 1d8512940..38cae0bf4 100644 --- a/tests/functional/openlp_core_ui/test_formattingtagscontroller.py +++ b/tests/functional/openlp_core_ui/test_formattingtagscontroller.py @@ -39,7 +39,7 @@ class TestFormattingTagController(TestCase): def setUp(self): self.services = FormattingTagController() - def test_strip(self): + def strip_test(self): """ Test that the _strip strips the correct chars """ @@ -52,7 +52,7 @@ class TestFormattingTagController(TestCase): # THEN: The tag should be returned with the wrappers removed. self.assertEqual(result, 'tag', 'FormattingTagForm._strip should return u\'tag\' when called with u\'{tag}\'') - def test_end_tag_changed_processes_correctly(self): + def end_tag_changed_processes_correctly_test(self): """ Test that the end html tags are generated correctly """ @@ -77,7 +77,7 @@ class TestFormattingTagController(TestCase): self.assertTrue(error == test['valid'], 'Function should not generate unexpected error messages : %s ' % error) - def test_start_tag_changed_processes_correctly(self): + def start_tag_changed_processes_correctly_test(self): """ Test that the end html tags are generated correctly """ @@ -100,7 +100,7 @@ class TestFormattingTagController(TestCase): self.assertTrue(error == test['valid'], 'Function should not generate unexpected error messages : %s ' % error) - def test_start_html_to_end_html(self): + def start_html_to_end_html_test(self): """ Test that the end html tags are generated correctly """ diff --git a/tests/functional/openlp_core_ui/test_formattingtagsform.py b/tests/functional/openlp_core_ui/test_formattingtagsform.py index e71a75651..736a306c3 100644 --- a/tests/functional/openlp_core_ui/test_formattingtagsform.py +++ b/tests/functional/openlp_core_ui/test_formattingtagsform.py @@ -29,17 +29,17 @@ """ Package to test the openlp.core.ui.formattingtagsform package. """ +from PyQt4 import QtGui from unittest import TestCase +from openlp.core.common import translate -from tests.functional import MagicMock, patch +from tests.functional import MagicMock, patch, call from openlp.core.ui.formattingtagform import FormattingTagForm # TODO: Tests Still TODO # __init__ # exec_ -# on_new_clicked -# on_delete_clicked # on_saved_clicked # _reloadTable @@ -47,30 +47,60 @@ from openlp.core.ui.formattingtagform import FormattingTagForm class TestFormattingTagForm(TestCase): def setUp(self): - self.init_patcher = patch('openlp.core.ui.formattingtagform.FormattingTagForm.__init__') - self.qdialog_patcher = patch('openlp.core.ui.formattingtagform.QtGui.QDialog') - self.ui_formatting_tag_dialog_patcher = patch('openlp.core.ui.formattingtagform.Ui_FormattingTagDialog') - self.mocked_init = self.init_patcher.start() - self.mocked_qdialog = self.qdialog_patcher.start() - self.mocked_ui_formatting_tag_dialog = self.ui_formatting_tag_dialog_patcher.start() - self.mocked_init.return_value = None + """ + Mock out stuff for all the tests + """ + self.setup_patcher = patch('openlp.core.ui.formattingtagform.FormattingTagForm._setup') + self.setup_patcher.start() def tearDown(self): - self.init_patcher.stop() - self.qdialog_patcher.stop() - self.ui_formatting_tag_dialog_patcher.stop() - - def test_on_text_edited(self): """ - Test that the appropriate actions are preformed when on_text_edited is called + Remove the mocks + """ + self.setup_patcher.stop() + + def on_row_selected_test(self): + """ + Test that the appropriate actions are preformed when on_row_selected is called + """ + # GIVEN: An instance of the Formatting Tag Form and a mocked delete_button + form = FormattingTagForm(None) + form.delete_button = MagicMock() + + # WHEN: on_row_selected is called + form.on_row_selected() + + # THEN: setEnabled and should have been called on delete_button + form.delete_button.setEnabled.assert_called_with(True) + + def on_new_clicked_test(self): + """ + Test that clicking the Add a new tag button does the right thing """ - # GIVEN: An instance of the Formatting Tag Form and a mocked save_push_button - form = FormattingTagForm() - form.save_button = MagicMock() + # GIVEN: A formatting tag form and a mocked out tag table widget + form = FormattingTagForm(None) + form.tag_table_widget = MagicMock() + row_count = 5 + form.tag_table_widget.rowCount.return_value = row_count - # WHEN: on_text_edited is called with an arbitrary value - # form.on_text_edited('text') + # WHEN: on_new_clicked is run (i.e. the Add new button was clicked) + with patch('openlp.core.ui.formattingtagform.QtGui.QTableWidgetItem') as MockedQTableWidgetItem: + mocked_table_widget = MagicMock() + MockedQTableWidgetItem.return_value = mocked_table_widget + form.on_new_clicked() - # THEN: setEnabled and setDefault should have been called on save_push_button - # form.save_button.setEnabled.assert_called_with(True) + # THEN: A new row should be added to the table + form.tag_table_widget.rowCount.assert_called_with() + form.tag_table_widget.insertRow.assert_called_with(row_count) + expected_set_item_calls = [ + call(row_count, 0, mocked_table_widget), + call(row_count, 1, mocked_table_widget), + call(row_count, 2, mocked_table_widget), + call(row_count, 3, mocked_table_widget) + ] + self.assertEqual(expected_set_item_calls, form.tag_table_widget.setItem.call_args_list, + 'setItem should have been called correctly') + form.tag_table_widget.resizeRowsToContents.assert_called_with() + form.tag_table_widget.scrollToBottom.assert_called_with() + form.tag_table_widget.selectRow.assert_called_with(row_count) diff --git a/tests/functional/openlp_core_ui/test_thememanager.py b/tests/functional/openlp_core_ui/test_thememanager.py index 0f3fa8ac4..d8f2116f9 100644 --- a/tests/functional/openlp_core_ui/test_thememanager.py +++ b/tests/functional/openlp_core_ui/test_thememanager.py @@ -57,17 +57,18 @@ class TestThemeManager(TestCase): # GIVEN: A new ThemeManager instance. theme_manager = ThemeManager() theme_manager.path = os.path.join(TEST_RESOURCES_PATH, 'themes') - zipfile.ZipFile.__init__ = MagicMock() - zipfile.ZipFile.__init__.return_value = None - zipfile.ZipFile.write = MagicMock() + with patch('zipfile.ZipFile.__init__') as mocked_zipfile_init, \ + patch('zipfile.ZipFile.write') as mocked_zipfile_write: + mocked_zipfile_init.return_value = None - # WHEN: The theme is exported - theme_manager._export_theme(os.path.join('some', 'path'), 'Default') + # WHEN: The theme is exported + theme_manager._export_theme(os.path.join('some', 'path'), 'Default') - # THEN: The zipfile should be created at the given path - zipfile.ZipFile.__init__.assert_called_with(os.path.join('some', 'path', 'Default.otz'), 'w') - zipfile.ZipFile.write.assert_called_with(os.path.join(TEST_RESOURCES_PATH, 'themes', 'Default', 'Default.xml'), - os.path.join('Default', 'Default.xml')) + # THEN: The zipfile should be created at the given path + mocked_zipfile_init.assert_called_with(os.path.join('some', 'path', 'Default.otz'), 'w') + mocked_zipfile_write.assert_called_with(os.path.join(TEST_RESOURCES_PATH, 'themes', + 'Default', 'Default.xml'), + os.path.join('Default', 'Default.xml')) def initial_theme_manager_test(self): """ diff --git a/tests/functional/openlp_plugins/presentations/test_impresscontroller.py b/tests/functional/openlp_plugins/presentations/test_impresscontroller.py new file mode 100644 index 000000000..bb3b43732 --- /dev/null +++ b/tests/functional/openlp_plugins/presentations/test_impresscontroller.py @@ -0,0 +1,229 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2014 Raoul Snyman # +# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan # +# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, # +# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. # +# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, # +# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, # +# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, # +# Frode Woldsund, Martin Zibricky, Patrick Zimmermann # +# --------------------------------------------------------------------------- # +# 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; version 2 of the License. # +# # +# 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, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" +Functional tests to test the Impress class and related methods. +""" +from unittest import TestCase +import os +import shutil +from tempfile import mkdtemp + +from tests.functional import patch, MagicMock +from tests.utils.constants import TEST_RESOURCES_PATH +from tests.helpers.testmixin import TestMixin + +from openlp.plugins.presentations.lib.impresscontroller import \ + ImpressController, ImpressDocument, TextType + + +class TestImpressController(TestCase, TestMixin): + """ + Test the ImpressController Class + """ + + def setUp(self): + """ + Set up the patches and mocks need for all tests. + """ + self.get_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) + + def constructor_test(self): + """ + Test the Constructor from the ImpressController + """ + # GIVEN: No presentation controller + controller = None + + # WHEN: The presentation controller object is created + controller = ImpressController(plugin=self.mock_plugin) + + # THEN: The name of the presentation controller should be correct + self.assertEqual('Impress', controller.name, + 'The name of the presentation controller should be correct') + + +class TestImpressDocument(TestCase): + """ + Test the ImpressDocument Class + """ + def setUp(self): + mocked_plugin = MagicMock() + mocked_plugin.settings_section = 'presentations' + self.file_name = os.path.join(TEST_RESOURCES_PATH, 'presentations', 'test.pptx') + self.ppc = ImpressController(mocked_plugin) + self.doc = ImpressDocument(self.ppc, self.file_name) + + def create_titles_and_notes_test(self): + """ + Test ImpressDocument.create_titles_and_notes + """ + # GIVEN: mocked PresentationController.save_titles_and_notes with + # 0 pages and the LibreOffice Document + self.doc.save_titles_and_notes = MagicMock() + self.doc.document = MagicMock() + self.doc.document.getDrawPages.return_value = MagicMock() + self.doc.document.getDrawPages().getCount.return_value = 0 + + # WHEN reading the titles and notes + self.doc.create_titles_and_notes() + + # THEN save_titles_and_notes should have been called with empty arrays + self.doc.save_titles_and_notes.assert_called_once_with([], []) + + # GIVEN: reset mock and set it to 2 pages + self.doc.save_titles_and_notes.reset_mock() + self.doc.document.getDrawPages().getCount.return_value = 2 + + # WHEN: a new call to create_titles_and_notes + self.doc.create_titles_and_notes() + + # THEN: save_titles_and_notes should have been called once with + # two arrays of two elements + self.doc.save_titles_and_notes.assert_called_once_with(['\n', '\n'], [' ', ' ']) + + def get_text_from_page_out_of_bound_test(self): + """ + Test ImpressDocument.__get_text_from_page with out-of-bounds index + """ + # GIVEN: mocked LibreOffice Document with one slide, + # two notes and three texts + self.doc.document = self._mock_a_LibreOffice_document(1, 2, 3) + + # WHEN: __get_text_from_page is called with an index of 0x00 + result = self.doc._ImpressDocument__get_text_from_page(0, TextType.Notes) + + # THEN: the result should be an empty string + self.assertEqual(result, '', 'Result should be an empty string') + + # WHEN: regardless of the type of text, index 0x00 is out of bounds + result = self.doc._ImpressDocument__get_text_from_page(0, TextType.Title) + + # THEN: result should be an empty string + self.assertEqual(result, '', 'Result should be an empty string') + + # WHEN: when called with 2, it should also be out of bounds + result = self.doc._ImpressDocument__get_text_from_page(2, TextType.SlideText) + + # THEN: result should be an empty string ... and, getByIndex should + # have never been called + self.assertEqual(result, '', 'Result should be an empty string') + self.assertEqual(self.doc.document.getDrawPages().getByIndex.call_count, 0, + 'There should be no call to getByIndex') + + def get_text_from_page_wrong_type_test(self): + """ + Test ImpressDocument.__get_text_from_page with wrong TextType + """ + # GIVEN: mocked LibreOffice Document with one slide, two notes and + # three texts + self.doc.document = self._mock_a_LibreOffice_document(1, 2, 3) + + # WHEN: called with TextType 3 + result = self.doc._ImpressDocument__get_text_from_page(1, 3) + + # THEN: result should be an empty string + self.assertEqual(result, '', 'Result should be and empty string') + self.assertEqual(self.doc.document.getDrawPages().getByIndex.call_count, 0, + 'There should be no call to getByIndex') + + def get_text_from_page_valid_params_test(self): + """ + Test ImpressDocument.__get_text_from_page with valid parameters + """ + # GIVEN: mocked LibreOffice Document with one slide, + # two notes and three texts + self.doc.document = self._mock_a_LibreOffice_document(1, 2, 3) + + # WHEN: __get_text_from_page is called to get the Notes + result = self.doc._ImpressDocument__get_text_from_page(1, TextType.Notes) + + # THEN: result should be 'Note\nNote\n' + self.assertEqual(result, 'Note\nNote\n', 'Result should be \'Note\\n\' times the count of notes in the page') + + # WHEN: get the Title + result = self.doc._ImpressDocument__get_text_from_page(1, TextType.Title) + + # THEN: result should be 'Title\n' + self.assertEqual(result, 'Title\n', 'Result should be exactly \'Title\\n\'') + + # WHEN: get all text + result = self.doc._ImpressDocument__get_text_from_page(1, TextType.SlideText) + + # THEN: result should be 'Title\nString\nString\n' + self.assertEqual(result, 'Title\nString\nString\n', 'Result should be exactly \'Title\\nString\\nString\\n\'') + + def _mock_a_LibreOffice_document(self, page_count, note_count, text_count): + """ + Helper function, creates a mock libreoffice document. + + :param page_count: Number of pages in the document + :param note_count: Number of note pages in the document + :param text_count: Number of text pages in the document + """ + pages = MagicMock() + page = MagicMock() + pages.getByIndex.return_value = page + notes_page = MagicMock() + notes_page.getCount.return_value = note_count + shape = MagicMock() + shape.supportsService.return_value = True + shape.getString.return_value = 'Note' + notes_page.getByIndex.return_value = shape + page.getNotesPage.return_value = notes_page + page.getCount.return_value = text_count + page.getByIndex.side_effect = self._get_page_shape_side_effect + pages.getCount.return_value = page_count + document = MagicMock() + document.getDrawPages.return_value = pages + document.getByIndex.return_value = page + return document + + def _get_page_shape_side_effect(*args): + """ + Helper function. + """ + page_shape = MagicMock() + page_shape.supportsService.return_value = True + if args[1] == 0: + page_shape.getShapeType.return_value = 'com.sun.star.presentation.TitleTextShape' + page_shape.getString.return_value = 'Title' + else: + page_shape.getString.return_value = 'String' + return page_shape diff --git a/tests/functional/openlp_plugins/presentations/test_powerpointcontroller.py b/tests/functional/openlp_plugins/presentations/test_powerpointcontroller.py index da58ef880..b74f7b6fc 100644 --- a/tests/functional/openlp_plugins/presentations/test_powerpointcontroller.py +++ b/tests/functional/openlp_plugins/presentations/test_powerpointcontroller.py @@ -30,16 +30,20 @@ Functional tests to test the PowerPointController class and related methods. """ import os -if os.name == 'nt': - import pywintypes import shutil from unittest import TestCase from tempfile import mkdtemp from tests.functional import patch, MagicMock from tests.helpers.testmixin import TestMixin +from tests.utils.constants import TEST_RESOURCES_PATH -from openlp.plugins.presentations.lib.powerpointcontroller import PowerpointController, PowerpointDocument +from openlp.plugins.presentations.lib.powerpointcontroller import PowerpointController, PowerpointDocument,\ + _get_text_from_shapes +from openlp.core.common import is_win + +if is_win(): + import pywintypes class TestPowerpointController(TestCase, TestMixin): @@ -79,7 +83,7 @@ class TestPowerpointController(TestCase, TestMixin): 'The name of the presentation controller should be correct') -class TestPowerpointDocument(TestCase): +class TestPowerpointDocument(TestCase, TestMixin): """ Test the PowerpointDocument Class """ @@ -88,6 +92,11 @@ class TestPowerpointDocument(TestCase): """ Set up the patches and mocks need for all tests. """ + self.get_application() + self.build_settings() + self.mock_plugin = MagicMock() + self.temp_folder = mkdtemp() + self.mock_plugin.settings_section = self.temp_folder self.powerpoint_document_stop_presentation_patcher = patch( 'openlp.plugins.presentations.lib.powerpointcontroller.PowerpointDocument.stop_presentation') self.presentation_document_get_temp_folder_patcher = patch( @@ -100,6 +109,8 @@ class TestPowerpointDocument(TestCase): self.mock_controller = MagicMock() self.mock_presentation = MagicMock() self.mock_presentation_document_get_temp_folder.return_value = 'temp folder' + self.file_name = os.path.join(TEST_RESOURCES_PATH, 'presentations', 'test.pptx') + self.real_controller = PowerpointController(self.mock_plugin) def tearDown(self): """ @@ -108,12 +119,14 @@ class TestPowerpointDocument(TestCase): self.powerpoint_document_stop_presentation_patcher.stop() self.presentation_document_get_temp_folder_patcher.stop() self.presentation_document_setup_patcher.stop() + self.destroy_settings() + shutil.rmtree(self.temp_folder) def show_error_msg_test(self): """ Test the PowerpointDocument.show_error_msg() method gets called on com exception """ - if os.name == 'nt': + if is_win(): # GIVEN: A PowerpointDocument with mocked controller and presentation with patch('openlp.plugins.presentations.lib.powerpointcontroller.critical_error_message_box') as \ mocked_critical_error_message_box: @@ -129,3 +142,95 @@ class TestPowerpointDocument(TestCase): 'integration and the presentation will be stopped.' ' Restart the presentation if you wish to ' 'present it.') + + # add _test to the following if necessary + def verify_loading_document(self): + """ + Test loading a document in PowerPoint + """ + if is_win() and self.real_controller.check_available(): + # GIVEN: A PowerpointDocument and a presentation + doc = PowerpointDocument(self.real_controller, self.file_name) + + # WHEN: loading the filename + doc.load_presentation() + result = doc.is_loaded() + + # THEN: result should be true + self.assertEqual(result, True, 'The result should be True') + else: + self.skipTest('Powerpoint not available, skipping test.') + + def create_titles_and_notes_test(self): + """ + Test creating the titles from PowerPoint + """ + if is_win() and self.real_controller.check_available(): + # GIVEN: mocked save_titles_and_notes, _get_text_from_shapes and two mocked slides + self.doc = PowerpointDocument(self.real_controller, self.file_name) + self.doc.save_titles_and_notes = MagicMock() + self.doc._PowerpointDocument__get_text_from_shapes = MagicMock() + slide = MagicMock() + slide.Shapes.Title.TextFrame.TextRange.Text = 'SlideText' + pres = MagicMock() + pres.Slides = [slide, slide] + self.doc.presentation = pres + + # WHEN reading the titles and notes + self.doc.create_titles_and_notes() + + # THEN the save should have been called exactly once with 2 titles and 2 notes + self.doc.save_titles_and_notes.assert_called_once_with(['SlideText\n', 'SlideText\n'], [' ', ' ']) + else: + self.skipTest('Powerpoint not available, skipping test.') + + def create_titles_and_notes_with_no_slides_test(self): + """ + Test creating the titles from PowerPoint when it returns no slides + """ + if is_win() and self.real_controller.check_available(): + # GIVEN: mocked save_titles_and_notes, _get_text_from_shapes and two mocked slides + doc = PowerpointDocument(self.real_controller, self.file_name) + doc.save_titles_and_notes = MagicMock() + doc._PowerpointDocument__get_text_from_shapes = MagicMock() + pres = MagicMock() + pres.Slides = [] + doc.presentation = pres + + # WHEN reading the titles and notes + doc.create_titles_and_notes() + + # THEN the save should have been called exactly once with empty titles and notes + doc.save_titles_and_notes.assert_called_once_with([], []) + else: + self.skipTest('Powerpoint not available, skipping test.') + + def get_text_from_shapes_test(self): + """ + Test getting text from powerpoint shapes + """ + # GIVEN: mocked shapes + shape = MagicMock() + shape.PlaceholderFormat.Type = 2 + shape.HasTextFrame = shape.TextFrame.HasText = True + shape.TextFrame.TextRange.Text = 'slideText' + shapes = [shape, shape] + + # WHEN: getting the text + result = _get_text_from_shapes(shapes) + + # THEN: it should return the text + self.assertEqual(result, 'slideText\nslideText\n', 'result should match \'slideText\nslideText\n\'') + + def get_text_from_shapes_with_no_shapes_test(self): + """ + Test getting text from powerpoint shapes with no shapes + """ + # GIVEN: empty shapes array + shapes = [] + + # WHEN: getting the text + result = _get_text_from_shapes(shapes) + + # THEN: it should not fail but return empty string + self.assertEqual(result, '', 'result should be empty') diff --git a/tests/functional/openlp_plugins/presentations/test_pptviewcontroller.py b/tests/functional/openlp_plugins/presentations/test_pptviewcontroller.py index c3d0912c0..0d4d2ab1e 100644 --- a/tests/functional/openlp_plugins/presentations/test_pptviewcontroller.py +++ b/tests/functional/openlp_plugins/presentations/test_pptviewcontroller.py @@ -4,8 +4,8 @@ ############################################################################### # OpenLP - Open Source Lyrics Projection # # --------------------------------------------------------------------------- # -# Copyright (c) 2008-2013 Raoul Snyman # -# Portions copyright (c) 2008-2013 Tim Bentley, Gerald Britton, Jonathan # +# Copyright (c) 2008-2014 Raoul Snyman # +# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan # # Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, # # Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. # # Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, # @@ -31,16 +31,19 @@ This module contains tests for the pptviewcontroller module of the Presentations """ import os import shutil -if os.name == 'nt': - from ctypes import cdll from tempfile import mkdtemp from unittest import TestCase from tests.functional import MagicMock, patch from tests.helpers.testmixin import TestMixin +from tests.utils.constants import TEST_RESOURCES_PATH from openlp.plugins.presentations.lib.pptviewcontroller import PptviewDocument, PptviewController +from openlp.core.common import is_win + +if is_win(): + from ctypes import cdll class TestPptviewController(TestCase, TestMixin): @@ -98,7 +101,7 @@ class TestPptviewController(TestCase, TestMixin): available = controller.check_available() # THEN: On windows it should return True, on other platforms False - if os.name == 'nt': + if is_win(): self.assertTrue(available, 'check_available should return True on windows.') else: self.assertFalse(available, 'check_available should return False when not on windows.') @@ -130,7 +133,7 @@ class TestPptviewDocument(TestCase): """ Set up the patches and mocks need for all tests. """ - self.os_patcher = patch('openlp.plugins.presentations.lib.pptviewcontroller.os') + self.os_isdir_patcher = patch('openlp.plugins.presentations.lib.pptviewcontroller.os.path.isdir') self.pptview_document_create_thumbnails_patcher = patch( 'openlp.plugins.presentations.lib.pptviewcontroller.PptviewDocument.create_thumbnails') self.pptview_document_stop_presentation_patcher = patch( @@ -141,46 +144,45 @@ class TestPptviewDocument(TestCase): 'openlp.plugins.presentations.lib.pptviewcontroller.PresentationDocument._setup') self.screen_list_patcher = patch('openlp.plugins.presentations.lib.pptviewcontroller.ScreenList') self.rect_patcher = MagicMock() - - self.mock_os = self.os_patcher.start() + self.mock_os_isdir = self.os_isdir_patcher.start() self.mock_pptview_document_create_thumbnails = self.pptview_document_create_thumbnails_patcher.start() self.mock_pptview_document_stop_presentation = self.pptview_document_stop_presentation_patcher.start() self.mock_presentation_document_get_temp_folder = self.presentation_document_get_temp_folder_patcher.start() self.mock_presentation_document_setup = self.presentation_document_setup_patcher.start() self.mock_rect = self.rect_patcher.start() self.mock_screen_list = self.screen_list_patcher.start() - self.mock_controller = MagicMock() self.mock_presentation = MagicMock() - - self.mock_presentation_document_get_temp_folder.return_value = 'temp folder' + self.temp_folder = mkdtemp() + self.mock_presentation_document_get_temp_folder.return_value = self.temp_folder def tearDown(self): """ Stop the patches """ - self.os_patcher.stop() + self.os_isdir_patcher.stop() self.pptview_document_create_thumbnails_patcher.stop() self.pptview_document_stop_presentation_patcher.stop() self.presentation_document_get_temp_folder_patcher.stop() self.presentation_document_setup_patcher.stop() self.rect_patcher.stop() self.screen_list_patcher.stop() + shutil.rmtree(self.temp_folder) def load_presentation_succesfull_test(self): """ Test the PptviewDocument.load_presentation() method when the PPT is successfully opened """ # GIVEN: A reset mocked_os - self.mock_os.reset() + self.mock_os_isdir.reset() # WHEN: The temporary directory exists and OpenPPT returns successfully (not -1) - self.mock_os.path.isdir.return_value = True + self.mock_os_isdir.return_value = True self.mock_controller.process.OpenPPT.return_value = 0 instance = PptviewDocument(self.mock_controller, self.mock_presentation) instance.file_path = 'test\path.ppt' - if os.name == 'nt': + if is_win(): result = instance.load_presentation() # THEN: PptviewDocument.load_presentation should return True @@ -191,17 +193,78 @@ class TestPptviewDocument(TestCase): Test the PptviewDocument.load_presentation() method when the temporary directory does not exist and the PPT is not successfully opened """ - # GIVEN: A reset mocked_os - self.mock_os.reset() + # GIVEN: A reset mock_os_isdir + self.mock_os_isdir.reset() # WHEN: The temporary directory does not exist and OpenPPT returns unsuccessfully (-1) - self.mock_os.path.isdir.return_value = False - self.mock_controller.process.OpenPPT.return_value = -1 - instance = PptviewDocument(self.mock_controller, self.mock_presentation) - instance.file_path = 'test\path.ppt' - if os.name == 'nt': - result = instance.load_presentation() + with patch('openlp.plugins.presentations.lib.pptviewcontroller.os.makedirs') as mock_makedirs: + self.mock_os_isdir.return_value = False + self.mock_controller.process.OpenPPT.return_value = -1 + instance = PptviewDocument(self.mock_controller, self.mock_presentation) + instance.file_path = 'test\path.ppt' + if is_win(): + result = instance.load_presentation() - # THEN: The temporary directory should be created and PptviewDocument.load_presentation should return False - self.mock_os.makedirs.assert_called_once_with('temp folder') - self.assertFalse(result) + # THEN: The temp folder should be created and PptviewDocument.load_presentation should return False + mock_makedirs.assert_called_once_with(self.temp_folder) + self.assertFalse(result) + + def create_titles_and_notes_test(self): + """ + Test PowerpointController.create_titles_and_notes + """ + # GIVEN: mocked PresentationController.save_titles_and_notes and a pptx file + doc = PptviewDocument(self.mock_controller, self.mock_presentation) + doc.file_path = os.path.join(TEST_RESOURCES_PATH, 'presentations', 'test.pptx') + doc.save_titles_and_notes = MagicMock() + + # WHEN reading the titles and notes + doc.create_titles_and_notes() + + # THEN save_titles_and_notes should have been called once with empty arrays + doc.save_titles_and_notes.assert_called_once_with(['Test 1\n', '\n', 'Test 2\n', 'Test 4\n', 'Test 3\n'], + ['Notes for slide 1', 'Inserted', 'Notes for slide 2', + 'Notes \nfor slide 4', 'Notes for slide 3']) + + def create_titles_and_notes_nonexistent_file_test(self): + """ + Test PowerpointController.create_titles_and_notes with nonexistent file + """ + # GIVEN: mocked PresentationController.save_titles_and_notes and an nonexistent file + with patch('builtins.open') as mocked_open, \ + patch('openlp.plugins.presentations.lib.pptviewcontroller.os.path.exists') as mocked_exists, \ + patch('openlp.plugins.presentations.lib.presentationcontroller.check_directory_exists') as \ + mocked_dir_exists: + mocked_exists.return_value = False + mocked_dir_exists.return_value = False + doc = PptviewDocument(self.mock_controller, self.mock_presentation) + doc.file_path = 'Idontexist.pptx' + doc.save_titles_and_notes = MagicMock() + + # WHEN: Reading the titles and notes + doc.create_titles_and_notes() + + # THEN: File existens should have been checked, and not have been opened. + doc.save_titles_and_notes.assert_called_once_with(None, None) + mocked_exists.assert_any_call('Idontexist.pptx') + self.assertEqual(mocked_open.call_count, 0, 'There should be no calls to open a file.') + + def create_titles_and_notes_invalid_file_test(self): + """ + Test PowerpointController.create_titles_and_notes with invalid file + """ + # GIVEN: mocked PresentationController.save_titles_and_notes and an invalid file + with patch('builtins.open') as mocked_open, \ + patch('openlp.plugins.presentations.lib.pptviewcontroller.zipfile.is_zipfile') as mocked_is_zf: + mocked_is_zf.return_value = False + mocked_open.filesize = 10 + doc = PptviewDocument(self.mock_controller, self.mock_presentation) + doc.file_path = os.path.join(TEST_RESOURCES_PATH, 'presentations', 'test.ppt') + doc.save_titles_and_notes = MagicMock() + + # WHEN: reading the titles and notes + doc.create_titles_and_notes() + + # THEN: + doc.save_titles_and_notes.assert_called_once_with(None, None) + self.assertEqual(mocked_is_zf.call_count, 1, 'is_zipfile should have been called once') diff --git a/tests/functional/openlp_plugins/presentations/test_presentationcontroller.py b/tests/functional/openlp_plugins/presentations/test_presentationcontroller.py index bd9ea4d19..0d20018c9 100644 --- a/tests/functional/openlp_plugins/presentations/test_presentationcontroller.py +++ b/tests/functional/openlp_plugins/presentations/test_presentationcontroller.py @@ -1,158 +1,166 @@ -# -*- coding: utf-8 -*- -# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 - -############################################################################### -# OpenLP - Open Source Lyrics Projection # -# --------------------------------------------------------------------------- # -# Copyright (c) 2008-2014 Raoul Snyman # -# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan # -# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, # -# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. # -# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, # -# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, # -# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, # -# Frode Woldsund, Martin Zibricky, Patrick Zimmermann # -# --------------------------------------------------------------------------- # -# 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; version 2 of the License. # -# # -# 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, write to the Free Software Foundation, Inc., 59 # -# Temple Place, Suite 330, Boston, MA 02111-1307 USA # -############################################################################### -""" -This module contains tests for the Presentation Controller. -""" -from unittest import TestCase - -from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument -from tests.functional import MagicMock, patch - - -class TestPresentationController(TestCase): - """ - Test the PresentationController. - """ - # TODO: Items left to test - # PresentationController - # __init__ - # enabled - # is_available - # check_available - # start_process - # kill - # add_document - # remove_doc - # close_presentation - # _get_plugin_manager - - def constructor_test(self): - """ - Test the Constructor - """ - # GIVEN: No presentation controller - controller = None - - # WHEN: The presentation controller object is created - mock_plugin = MagicMock() - mock_plugin.settings_section = '' - controller = PresentationController(plugin=mock_plugin) - - # THEN: The name of the presentation controller should be correct - self.assertEqual('PresentationController', controller.name, - 'The name of the presentation controller should be correct') - - -class TestPresentationDocument(TestCase): - """ - Test the PresentationDocument Class - """ - # TODO: Items left to test - # PresentationDocument - # __init__ - # load_presentation - # presentation_deleted - # get_file_name - # get_thumbnail_folder - # get_temp_folder - # check_thumbnails - # close_presentation - # is_active - # is_loaded - # blank_screen - # unblank_screen - # is_blank - # stop_presentation - # start_presentation - # get_slide_number - # get_slide_count - # goto_slide - # next_step - # previous_step - # convert_thumbnail - # get_thumbnail_path - # poll_slidenumber - # get_slide_text - # get_slide_notes - - def setUp(self): - """ - Set up the patches and mocks need for all tests. - """ - self.check_directory_exists_patcher = \ - patch('openlp.plugins.presentations.lib.presentationcontroller.check_directory_exists') - self.get_thumbnail_folder_patcher = \ - patch('openlp.plugins.presentations.lib.presentationcontroller.PresentationDocument.get_thumbnail_folder') - self._setup_patcher = \ - patch('openlp.plugins.presentations.lib.presentationcontroller.PresentationDocument._setup') - - self.mock_check_directory_exists = self.check_directory_exists_patcher.start() - self.mock_get_thumbnail_folder = self.get_thumbnail_folder_patcher.start() - self.mock_setup = self._setup_patcher.start() - - self.mock_controller = MagicMock() - - self.mock_get_thumbnail_folder.return_value = 'returned/path/' - - def tearDown(self): - """ - Stop the patches - """ - self.check_directory_exists_patcher.stop() - self.get_thumbnail_folder_patcher.stop() - self._setup_patcher.stop() - - def initialise_presentation_document_test(self): - """ - Test the PresentationDocument __init__ method when initialising the PresentationDocument Class - """ - # GIVEN: A reset mock_setup and mocked controller - self.mock_setup.reset() - - # WHEN: Creating an instance of PresentationDocument - PresentationDocument(self.mock_controller, 'Name') - - # THEN: PresentationDocument.__init__ should have been called with the correct arguments - self.mock_setup.assert_called_once_with('Name') - - def presentation_document_setup_test(self): - """ - Test the PresentationDocument _setup method when initialising the PresentationDocument Class - """ - self._setup_patcher.stop() - - # GIVEN: A mocked controller, patched check_directory_exists_patcher and patched get_thumbnail_folder method - - # WHEN: Creating an instance of PresentationDocument - PresentationDocument(self.mock_controller, 'Name') - - # THEN: check_directory_exists should have been called with the correct arguments - self.mock_check_directory_exists.assert_called_once_with('returned/path/') - - self._setup_patcher.start() +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2014 Raoul Snyman # +# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan # +# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, # +# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. # +# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, # +# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, # +# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, # +# Frode Woldsund, Martin Zibricky, Patrick Zimmermann # +# --------------------------------------------------------------------------- # +# 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; version 2 of the License. # +# # +# 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, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" +Functional tests to test the PresentationController and PresentationDocument +classes and related methods. +""" +from unittest import TestCase +import os +from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument +from tests.functional import MagicMock, patch, mock_open + +FOLDER_TO_PATCH = 'openlp.plugins.presentations.lib.presentationcontroller.PresentationDocument.get_thumbnail_folder' + + +class TestPresentationController(TestCase): + """ + Test the PresentationController. + """ + def setUp(self): + mocked_plugin = MagicMock() + mocked_plugin.settings_section = 'presentations' + self.presentation = PresentationController(mocked_plugin) + self.document = PresentationDocument(self.presentation, '') + + def constructor_test(self): + """ + Test the Constructor + """ + # GIVEN: A mocked plugin + + # WHEN: The PresentationController is created + + # THEN: The name of the presentation controller should be correct + self.assertEqual('PresentationController', self.presentation.name, + 'The name of the presentation controller should be correct') + + def save_titles_and_notes_test(self): + """ + Test PresentationDocument.save_titles_and_notes method with two valid lists + """ + # GIVEN: two lists of length==2 and a mocked open and get_thumbnail_folder + mocked_open = mock_open() + with patch('builtins.open', mocked_open), patch(FOLDER_TO_PATCH) as mocked_get_thumbnail_folder: + titles = ['uno', 'dos'] + notes = ['one', 'two'] + + # WHEN: calling save_titles_and_notes + mocked_get_thumbnail_folder.return_value = 'test' + self.document.save_titles_and_notes(titles, notes) + + # THEN: the last call to open should have been for slideNotes2.txt + mocked_open.assert_any_call(os.path.join('test', 'titles.txt'), mode='w') + mocked_open.assert_any_call(os.path.join('test', 'slideNotes1.txt'), mode='w') + mocked_open.assert_any_call(os.path.join('test', 'slideNotes2.txt'), mode='w') + self.assertEqual(mocked_open.call_count, 3, 'There should be exactly three files opened') + mocked_open().writelines.assert_called_once_with(['uno', 'dos']) + mocked_open().write.assert_called_any('one') + mocked_open().write.assert_called_any('two') + + def save_titles_and_notes_with_None_test(self): + """ + Test PresentationDocument.save_titles_and_notes method with no data + """ + # GIVEN: None and an empty list and a mocked open and get_thumbnail_folder + with patch('builtins.open') as mocked_open, patch(FOLDER_TO_PATCH) as mocked_get_thumbnail_folder: + titles = None + notes = None + + # WHEN: calling save_titles_and_notes + mocked_get_thumbnail_folder.return_value = 'test' + self.document.save_titles_and_notes(titles, notes) + + # THEN: No file should have been created + self.assertEqual(mocked_open.call_count, 0, 'No file should be created') + + def get_titles_and_notes_test(self): + """ + Test PresentationDocument.get_titles_and_notes method + """ + # GIVEN: A mocked open, get_thumbnail_folder and exists + + with patch('builtins.open', mock_open(read_data='uno\ndos\n')) as mocked_open, \ + patch(FOLDER_TO_PATCH) as mocked_get_thumbnail_folder, \ + patch('openlp.plugins.presentations.lib.presentationcontroller.os.path.exists') as mocked_exists: + mocked_get_thumbnail_folder.return_value = 'test' + mocked_exists.return_value = True + + # WHEN: calling get_titles_and_notes + result_titles, result_notes = self.document.get_titles_and_notes() + + # THEN: it should return two items for the titles and two empty strings for the notes + self.assertIs(type(result_titles), list, 'result_titles should be of type list') + self.assertEqual(len(result_titles), 2, 'There should be two items in the titles') + self.assertIs(type(result_notes), list, 'result_notes should be of type list') + self.assertEqual(len(result_notes), 2, 'There should be two items in the notes') + self.assertEqual(mocked_open.call_count, 3, 'Three files should be opened') + mocked_open.assert_any_call(os.path.join('test', 'titles.txt')) + mocked_open.assert_any_call(os.path.join('test', 'slideNotes1.txt')) + mocked_open.assert_any_call(os.path.join('test', 'slideNotes2.txt')) + self.assertEqual(mocked_exists.call_count, 3, 'Three files should have been checked') + + def get_titles_and_notes_with_file_not_found_test(self): + """ + Test PresentationDocument.get_titles_and_notes method with file not found + """ + # GIVEN: A mocked open, get_thumbnail_folder and exists + with patch('builtins.open') as mocked_open, \ + patch(FOLDER_TO_PATCH) as mocked_get_thumbnail_folder, \ + patch('openlp.plugins.presentations.lib.presentationcontroller.os.path.exists') as mocked_exists: + mocked_get_thumbnail_folder.return_value = 'test' + mocked_exists.return_value = False + + # WHEN: calling get_titles_and_notes + result_titles, result_notes = self.document.get_titles_and_notes() + + # THEN: it should return two empty lists + self.assertIs(type(result_titles), list, 'result_titles should be of type list') + self.assertEqual(len(result_titles), 0, 'there be no titles') + self.assertIs(type(result_notes), list, 'result_notes should be a list') + self.assertEqual(len(result_notes), 0, 'but the list should be empty') + self.assertEqual(mocked_open.call_count, 0, 'No calls to open files') + self.assertEqual(mocked_exists.call_count, 1, 'There should be one call to file exists') + + def get_titles_and_notes_with_file_error_test(self): + """ + Test PresentationDocument.get_titles_and_notes method with file errors + """ + # GIVEN: A mocked open, get_thumbnail_folder and exists + with patch('builtins.open') as mocked_open, \ + patch(FOLDER_TO_PATCH) as mocked_get_thumbnail_folder, \ + patch('openlp.plugins.presentations.lib.presentationcontroller.os.path.exists') as mocked_exists: + mocked_get_thumbnail_folder.return_value = 'test' + mocked_exists.return_value = True + mocked_open.side_effect = IOError() + + # WHEN: calling get_titles_and_notes + result_titles, result_notes = self.document.get_titles_and_notes() + + # THEN: it should return two empty lists + self.assertIs(type(result_titles), list, 'result_titles should be a list') diff --git a/tests/functional/openlp_plugins/remotes/test_remotetab.py b/tests/functional/openlp_plugins/remotes/test_remotetab.py index 7560c9ea7..61714e9c5 100644 --- a/tests/functional/openlp_plugins/remotes/test_remotetab.py +++ b/tests/functional/openlp_plugins/remotes/test_remotetab.py @@ -48,7 +48,8 @@ __default_settings__ = { 'remotes/user id': 'openlp', 'remotes/password': 'password', 'remotes/authentication enabled': False, - 'remotes/ip address': '0.0.0.0' + 'remotes/ip address': '0.0.0.0', + 'remotes/thumbnails': True } ZERO_URL = '0.0.0.0' TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..', 'resources')) diff --git a/tests/functional/openlp_plugins/remotes/test_router.py b/tests/functional/openlp_plugins/remotes/test_router.py index 037169b88..c0190ecda 100644 --- a/tests/functional/openlp_plugins/remotes/test_router.py +++ b/tests/functional/openlp_plugins/remotes/test_router.py @@ -30,10 +30,12 @@ This module contains tests for the lib submodule of the Remotes plugin. """ import os +import urllib.request from unittest import TestCase from openlp.core.common import Settings, Registry from openlp.plugins.remotes.lib.httpserver import HttpRouter +from urllib.parse import urlparse from tests.functional import MagicMock, patch, mock_open from tests.helpers.testmixin import TestMixin @@ -186,3 +188,86 @@ class TestRouter(TestCase, TestMixin): self.router.send_response.assert_called_once_with(200) self.router.send_header.assert_called_once_with('Content-type', 'text/html') self.assertEqual(self.router.end_headers.call_count, 1, 'end_headers called once') + + def serve_thumbnail_without_params_test(self): + """ + Test the serve_thumbnail routine without params + """ + self.router.send_response = MagicMock() + self.router.send_header = MagicMock() + self.router.end_headers = MagicMock() + self.router.wfile = MagicMock() + self.router.serve_thumbnail() + self.router.send_response.assert_called_once_with(404) + self.assertEqual(self.router.send_response.call_count, 1, 'Send response called once') + self.assertEqual(self.router.end_headers.call_count, 1, 'end_headers called once') + + def serve_thumbnail_with_invalid_params_test(self): + """ + Test the serve_thumbnail routine with invalid params + """ + # GIVEN: Mocked send_header, send_response, end_headers and wfile + self.router.send_response = MagicMock() + self.router.send_header = MagicMock() + self.router.end_headers = MagicMock() + self.router.wfile = MagicMock() + + # WHEN: pass a bad controller + self.router.serve_thumbnail('badcontroller', 'tecnologia 1.pptx/slide1.png') + + # THEN: a 404 should be returned + self.assertEqual(len(self.router.send_header.mock_calls), 1, 'One header') + self.assertEqual(len(self.router.send_response.mock_calls), 1, 'One response') + self.assertEqual(len(self.router.wfile.mock_calls), 1, 'Once call to write to the socket') + self.router.send_response.assert_called_once_with(404) + + # WHEN: pass a bad filename + self.router.send_response.reset_mock() + self.router.serve_thumbnail('presentations', 'tecnologia 1.pptx/badfilename.png') + + # THEN: return a 404 + self.router.send_response.assert_called_once_with(404) + + # WHEN: a dangerous URL is passed + self.router.send_response.reset_mock() + self.router.serve_thumbnail('presentations', '../tecnologia 1.pptx/slide1.png') + + # THEN: return a 404 + self.router.send_response.assert_called_once_with(404) + + def serve_thumbnail_with_valid_params_test(self): + """ + Test the serve_thumbnail routine with valid params + """ + # GIVEN: Mocked send_header, send_response, end_headers and wfile + self.router.send_response = MagicMock() + self.router.send_header = MagicMock() + self.router.end_headers = MagicMock() + self.router.wfile = MagicMock() + mocked_image_manager = MagicMock() + Registry.create() + Registry().register('image_manager', mocked_image_manager) + file_name = 'another%20test/slide1.png' + full_path = os.path.normpath(os.path.join('thumbnails', file_name)) + width = 120 + height = 90 + with patch('openlp.core.lib.os.path.exists') as mocked_exists, \ + patch('builtins.open', mock_open(read_data='123')), \ + patch('openlp.plugins.remotes.lib.httprouter.AppLocation') as mocked_location, \ + patch('openlp.plugins.remotes.lib.httprouter.image_to_byte') as mocked_image_to_byte: + mocked_exists.return_value = True + mocked_image_to_byte.return_value = '123' + mocked_location.get_section_data_path.return_value = '' + + # WHEN: pass good controller and filename + result = self.router.serve_thumbnail('presentations', '{0}x{1}'.format(width, height), file_name) + + # THEN: a file should be returned + self.assertEqual(self.router.send_header.call_count, 1, 'One header') + self.assertEqual(self.router.send_response.call_count, 1, 'Send response called once') + self.assertEqual(self.router.end_headers.call_count, 1, 'end_headers called once') + mocked_exists.assert_called_with(urllib.parse.unquote(full_path)) + self.assertEqual(mocked_image_to_byte.call_count, 1, 'Called once') + mocked_image_manager.assert_called_any(os.path.normpath('thumbnails\\another test'), + 'slide1.png', None, '120x90') + mocked_image_manager.assert_called_any(os.path.normpath('thumbnails\\another test'), 'slide1.png', '120x90') diff --git a/tests/interfaces/openlp_plugins/media/__init__.py b/tests/interfaces/openlp_plugins/media/__init__.py index e69de29bb..6b241e7fc 100644 --- a/tests/interfaces/openlp_plugins/media/__init__.py +++ b/tests/interfaces/openlp_plugins/media/__init__.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2014 Raoul Snyman # +# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan # +# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, # +# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. # +# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, # +# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, # +# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, # +# Frode Woldsund, Martin Zibricky, Patrick Zimmermann # +# --------------------------------------------------------------------------- # +# 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; version 2 of the License. # +# # +# 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, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### diff --git a/tests/interfaces/openlp_plugins/media/forms/__init__.py b/tests/interfaces/openlp_plugins/media/forms/__init__.py new file mode 100644 index 000000000..6b241e7fc --- /dev/null +++ b/tests/interfaces/openlp_plugins/media/forms/__init__.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2014 Raoul Snyman # +# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan # +# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, # +# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. # +# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, # +# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, # +# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, # +# Frode Woldsund, Martin Zibricky, Patrick Zimmermann # +# --------------------------------------------------------------------------- # +# 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; version 2 of the License. # +# # +# 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, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### diff --git a/tests/interfaces/openlp_plugins/media/forms/test_mediaclipselectorform.py b/tests/interfaces/openlp_plugins/media/forms/test_mediaclipselectorform.py index e97a06238..89810aa50 100644 --- a/tests/interfaces/openlp_plugins/media/forms/test_mediaclipselectorform.py +++ b/tests/interfaces/openlp_plugins/media/forms/test_mediaclipselectorform.py @@ -60,6 +60,7 @@ class TestMediaClipSelectorForm(TestCase, TestMixin): # Mock VLC so we don't actually use it self.vlc_patcher = patch('openlp.plugins.media.forms.mediaclipselectorform.vlc') self.vlc_patcher.start() + Registry().register('application', self.app) # Mock the media item self.mock_media_item = MagicMock() # create form to test @@ -67,6 +68,9 @@ class TestMediaClipSelectorForm(TestCase, TestMixin): mock_media_state_wait = MagicMock() mock_media_state_wait.return_value = True self.form.media_state_wait = mock_media_state_wait + self.form.application.set_busy_cursor = MagicMock() + self.form.application.set_normal_cursor = MagicMock() + self.form.find_optical_devices = MagicMock() def tearDown(self): """ @@ -155,3 +159,21 @@ class TestMediaClipSelectorForm(TestCase, TestMixin): self.form.audio_tracks_combobox.itemData.assert_any_call(0) self.form.audio_tracks_combobox.itemData.assert_any_call(1) self.form.subtitle_tracks_combobox.itemData.assert_any_call(0) + + def click_save_button_test(self): + """ + Test that the correct function is called when save is clicked, and that it behaves as expected. + """ + # GIVEN: Mocked methods. + with patch('openlp.plugins.media.forms.mediaclipselectorform.critical_error_message_box') as \ + mocked_critical_error_message_box,\ + patch('PyQt4.QtGui.QDialog.exec_') as mocked_exec: + self.form.exec_() + + # WHEN: The save button is clicked with a NoneType in start_time_ms or end_time_ms + self.form.accept() + + # THEN: we should get an error message + mocked_critical_error_message_box.assert_called_with('DVD not loaded correctly', + 'The DVD was not loaded correctly, ' + 'please re-load and try again.') diff --git a/tests/resources/presentations/test.ppt b/tests/resources/presentations/test.ppt new file mode 100644 index 000000000..102cff5d0 Binary files /dev/null and b/tests/resources/presentations/test.ppt differ diff --git a/tests/resources/presentations/test.pptx b/tests/resources/presentations/test.pptx new file mode 100644 index 000000000..c8beab172 Binary files /dev/null and b/tests/resources/presentations/test.pptx differ