diff --git a/.bzrignore b/.bzrignore index 76a07245d..ab4adde18 100644 --- a/.bzrignore +++ b/.bzrignore @@ -23,3 +23,5 @@ resources/windows/warnOpenLP.txt openlp.cfg .idea openlp.pro +.kdev4 +tests.kdev4 diff --git a/openlp/core/lib/serviceitem.py b/openlp/core/lib/serviceitem.py index 08beacf38..002068109 100644 --- a/openlp/core/lib/serviceitem.py +++ b/openlp/core/lib/serviceitem.py @@ -183,6 +183,9 @@ class ServiceItem(object): self.background_audio = [] self.theme_overwritten = False self.temporary_edit = False + self.auto_play_slides_once = False + self.auto_play_slides_loop = False + self.timed_slide_interval = 0 self.will_auto_start = False self.has_original_files = True self._new_item() @@ -344,6 +347,9 @@ class ServiceItem(object): u'search': self.search_string, u'data': self.data_string, u'xml_version': self.xml_version, + u'auto_play_slides_once': self.auto_play_slides_once, + u'auto_play_slides_loop': self.auto_play_slides_loop, + u'timed_slide_interval': self.timed_slide_interval, u'start_time': self.start_time, u'end_time': self.end_time, u'media_length': self.media_length, @@ -398,6 +404,9 @@ class ServiceItem(object): self.start_time = header.get(u'start_time', 0) self.end_time = header.get(u'end_time', 0) self.media_length = header.get(u'media_length', 0) + self.auto_play_slides_once = header.get(u'auto_play_slides_once', False) + self.auto_play_slides_loop = header.get(u'auto_play_slides_loop', False) + self.timed_slide_interval = header.get(u'timed_slide_interval', 0) self.will_auto_start = header.get(u'will_auto_start', False) self.has_original_files = True if u'background_audio' in header: @@ -427,7 +436,6 @@ class ServiceItem(object): self.add_from_command(path, text_image[u'title'], text_image[u'image']) else: self.add_from_command(text_image[u'path'], text_image[u'title'], text_image[u'image']) - self._new_item() def get_display_title(self): diff --git a/openlp/core/ui/servicemanager.py b/openlp/core/ui/servicemanager.py index cff5be214..872deff0a 100644 --- a/openlp/core/ui/servicemanager.py +++ b/openlp/core/ui/servicemanager.py @@ -257,6 +257,20 @@ class ServiceManager(QtGui.QWidget): text=translate('OpenLP.ServiceManager', 'Create New &Custom Slide'), icon=u':/general/general_edit.png', triggers=self.create_custom) self.menu.addSeparator() + # Add AutoPlay menu actions + self.autoPlaySlidesGroup = QtGui.QMenu(translate('OpenLP.ServiceManager', '&Auto play slides')) + self.menu.addMenu(self.autoPlaySlidesGroup) + self.autoPlaySlidesLoop = create_widget_action(self.autoPlaySlidesGroup, + text=translate('OpenLP.ServiceManager', 'Auto play slides &Loop'), + checked=False, triggers=self.toggleAutoPlaySlidesLoop) + self.autoPlaySlidesOnce = create_widget_action(self.autoPlaySlidesGroup, + text=translate('OpenLP.ServiceManager', 'Auto play slides &Once'), + checked=False, triggers=self.toggleAutoPlaySlidesOnce) + self.autoPlaySlidesGroup.addSeparator() + self.timedSlideInterval = create_widget_action(self.autoPlaySlidesGroup, + text=translate('OpenLP.ServiceManager', '&Delay between slides'), + checked=False, triggers=self.onTimedSlideInterval) + self.menu.addSeparator() self.previewAction = create_widget_action(self.menu, text=translate('OpenLP.ServiceManager', 'Show &Preview'), icon=u':/general/general_preview.png', triggers=self.makePreview) # Add already existing make live action to the menu. @@ -762,6 +776,22 @@ class ServiceManager(QtGui.QWidget): self.maintainAction.setVisible(True) if item.parent() is None: self.notesAction.setVisible(True) + if serviceItem[u'service_item'].is_capable(ItemCapabilities.CanLoop) and \ + len(serviceItem[u'service_item'].get_frames()) > 1: + self.autoPlaySlidesGroup.menuAction().setVisible(True) + self.autoPlaySlidesOnce.setChecked(serviceItem[u'service_item'].auto_play_slides_once) + self.autoPlaySlidesLoop.setChecked(serviceItem[u'service_item'].auto_play_slides_loop) + self.timedSlideInterval.setChecked(serviceItem[u'service_item'].timed_slide_interval > 0) + if serviceItem[u'service_item'].timed_slide_interval > 0: + delay_suffix = u' ' + delay_suffix += unicode(serviceItem[u'service_item'].timed_slide_interval) + delay_suffix += u' s' + else: + delay_suffix = u' ...' + self.timedSlideInterval.setText(translate('OpenLP.ServiceManager', '&Delay between slides') + delay_suffix) + # TODO for future: make group explains itself more visually + else: + self.autoPlaySlidesGroup.menuAction().setVisible(False) if serviceItem[u'service_item'].is_capable(ItemCapabilities.HasVariableStartTime): self.timeAction.setVisible(True) if serviceItem[u'service_item'].is_capable(ItemCapabilities.CanAutoStartForLive): @@ -807,6 +837,59 @@ class ServiceManager(QtGui.QWidget): if self.startTimeForm.exec_(): self.repaintServiceList(item, -1) + def toggleAutoPlaySlidesOnce(self): + """ + Toggle Auto play slide once. + Inverts auto play once option for the item + """ + item = self.findServiceItem()[0] + service_item = self.serviceItems[item][u'service_item'] + service_item.auto_play_slides_once = not service_item.auto_play_slides_once + if service_item.auto_play_slides_once: + service_item.auto_play_slides_loop = False + self.autoPlaySlidesLoop.setChecked(False) + if service_item.auto_play_slides_once and service_item.timed_slide_interval == 0: + service_item.timed_slide_interval = Settings().value(u'loop delay') + self.setModified() + + def toggleAutoPlaySlidesLoop(self): + """ + Toggle Auto play slide loop. + """ + item = self.findServiceItem()[0] + service_item = self.serviceItems[item][u'service_item'] + service_item.auto_play_slides_loop = not service_item.auto_play_slides_loop + if service_item.auto_play_slides_loop: + service_item.auto_play_slides_once = False + self.autoPlaySlidesOnce.setChecked(False) + if service_item.auto_play_slides_loop and service_item.timed_slide_interval == 0: + service_item.timed_slide_interval = Settings().value(u'loop delay') + self.setModified() + + def onTimedSlideInterval(self): + """ + on set times slide interval. + Shows input dialog for enter interval in seconds for delay + """ + item = self.findServiceItem()[0] + service_item = self.serviceItems[item][u'service_item'] + if service_item.timed_slide_interval == 0: + timed_slide_interval = Settings().value(u'loop delay') + else: + timed_slide_interval = service_item.timed_slide_interval + timed_slide_interval, ok = QtGui.QInputDialog.getInteger(self, translate('OpenLP.ServiceManager', + 'Input delay'), translate('OpenLP.ServiceManager', 'Delay between slides in seconds.'), + timed_slide_interval, 0, 180, 1) + if ok: + service_item.timed_slide_interval = timed_slide_interval + if service_item.timed_slide_interval <> 0 and not service_item.auto_play_slides_loop\ + and not service_item.auto_play_slides_once: + service_item.auto_play_slides_loop = True + elif service_item.timed_slide_interval == 0: + service_item.auto_play_slides_loop = False + service_item.auto_play_slides_once = False + self.setModified() + def onAutoStart(self): """ Toggles to Auto Start Setting. @@ -1287,6 +1370,8 @@ class ServiceManager(QtGui.QWidget): if self.serviceItems and item < len(self.serviceItems) and \ self.serviceItems[item][u'service_item'].is_capable(ItemCapabilities.CanPreview): self.mainwindow.previewController.addServiceManagerItem(self.serviceItems[item][u'service_item'], 0) + next_item = self.serviceManagerList.topLevelItem(item) + self.serviceManagerList.setCurrentItem(next_item) self.mainwindow.liveController.previewListWidget.setFocus() else: critical_error_message_box(translate('OpenLP.ServiceManager', 'Missing Display Handler'), diff --git a/openlp/core/ui/slidecontroller.py b/openlp/core/ui/slidecontroller.py index 23690d0d6..4c20970cd 100644 --- a/openlp/core/ui/slidecontroller.py +++ b/openlp/core/ui/slidecontroller.py @@ -689,6 +689,14 @@ class SlideController(DisplayController): self.slideSelected() else: self._processItem(item, slidenum) + if self.isLive and item.auto_play_slides_loop and item.timed_slide_interval > 0: + self.playSlidesLoop.setChecked(item.auto_play_slides_loop) + self.delaySpinBox.setValue(int(item.timed_slide_interval)) + self.onPlaySlidesLoop() + elif self.isLive and item.auto_play_slides_once and item.timed_slide_interval > 0: + self.playSlidesOnce.setChecked(item.auto_play_slides_once) + self.delaySpinBox.setValue(int(item.timed_slide_interval)) + self.onPlaySlidesOnce() def _processItem(self, serviceItem, slideno): """ @@ -877,6 +885,7 @@ class SlideController(DisplayController): Settings().remove(self.parent().generalSettingsSection + u'/screen blank') self.blankPlugin() self.updatePreview() + self.onToggleLoop() def onThemeDisplay(self, checked=None): """ @@ -895,6 +904,7 @@ class SlideController(DisplayController): Settings().remove(self.parent().generalSettingsSection + u'/screen blank') self.blankPlugin() self.updatePreview() + self.onToggleLoop() def onHideDisplay(self, checked=None): """ @@ -913,6 +923,7 @@ class SlideController(DisplayController): Settings().remove(self.parent().generalSettingsSection + u'/screen blank') self.hidePlugin(checked) self.updatePreview() + self.onToggleLoop() def blankPlugin(self): """ @@ -1088,7 +1099,8 @@ class SlideController(DisplayController): """ Toggles the loop state. """ - if self.playSlidesLoop.isChecked() or self.playSlidesOnce.isChecked(): + hide_mode = self.hideMode() + if hide_mode is None and (self.playSlidesLoop.isChecked() or self.playSlidesOnce.isChecked()): self.onStartLoop() else: self.onStopLoop() @@ -1122,11 +1134,11 @@ class SlideController(DisplayController): self.playSlidesLoop.setText(UiStrings().StopPlaySlidesInLoop) self.playSlidesOnce.setIcon(build_icon(u':/media/media_time.png')) self.playSlidesOnce.setText(UiStrings().PlaySlidesToEnd) + self.playSlidesMenu.setDefaultAction(self.playSlidesLoop) + self.playSlidesOnce.setChecked(False) else: self.playSlidesLoop.setIcon(build_icon(u':/media/media_time.png')) self.playSlidesLoop.setText(UiStrings().PlaySlidesInLoop) - self.playSlidesMenu.setDefaultAction(self.playSlidesLoop) - self.playSlidesOnce.setChecked(False) self.onToggleLoop() def onPlaySlidesOnce(self, checked=None): @@ -1143,11 +1155,11 @@ class SlideController(DisplayController): self.playSlidesOnce.setText(UiStrings().StopPlaySlidesToEnd) self.playSlidesLoop.setIcon(build_icon(u':/media/media_time.png')) self.playSlidesLoop.setText(UiStrings().PlaySlidesInLoop) + self.playSlidesMenu.setDefaultAction(self.playSlidesOnce) + self.playSlidesLoop.setChecked(False) else: self.playSlidesOnce.setIcon(build_icon(u':/media/media_time')) self.playSlidesOnce.setText(UiStrings().PlaySlidesToEnd) - self.playSlidesMenu.setDefaultAction(self.playSlidesOnce) - self.playSlidesLoop.setChecked(False) self.onToggleLoop() def setAudioItemsVisibility(self, visible): diff --git a/tests/functional/openlp_core_lib/test_lib.py b/tests/functional/openlp_core_lib/test_lib.py index 90e429b9a..0484931cd 100644 --- a/tests/functional/openlp_core_lib/test_lib.py +++ b/tests/functional/openlp_core_lib/test_lib.py @@ -2,10 +2,12 @@ Package to test the openlp.core.lib package. """ from unittest import TestCase +from datetime import datetime, timedelta from mock import MagicMock, patch -from openlp.core.lib import str_to_bool, translate, check_directory_exists, get_text_file_string +from openlp.core.lib import str_to_bool, translate, check_directory_exists, get_text_file_string, build_icon, \ + image_to_byte, check_item_selected, validate_thumb class TestLib(TestCase): @@ -197,3 +199,163 @@ class TestLib(TestCase): """ assert True, u'Impossible to test due to conflicts when mocking out the "open" function' + def build_icon_with_qicon_test(self): + """ + Test the build_icon() function with a QIcon instance + """ + with patch(u'openlp.core.lib.QtGui') as MockedQtGui: + # GIVEN: A mocked QIcon + MockedQtGui.QIcon = MagicMock + mocked_icon = MockedQtGui.QIcon() + + # WHEN: We pass a QIcon instance in + result = build_icon(mocked_icon) + + # THEN: The result should be our mocked QIcon + assert result is mocked_icon, u'The result should be the mocked QIcon' + + def build_icon_with_resource_test(self): + """ + Test the build_icon() function with a resource URI + """ + with patch(u'openlp.core.lib.QtGui') as MockedQtGui, \ + patch(u'openlp.core.lib.QtGui.QPixmap') as MockedQPixmap: + # GIVEN: A mocked QIcon and a mocked QPixmap + MockedQtGui.QIcon = MagicMock + MockedQtGui.QIcon.Normal = 1 + MockedQtGui.QIcon.Off = 2 + MockedQPixmap.return_value = u'mocked_pixmap' + resource_uri = u':/resource/uri' + + # WHEN: We pass a QIcon instance in + result = build_icon(resource_uri) + + # THEN: The result should be our mocked QIcon + MockedQPixmap.assert_called_with(resource_uri) + # There really should be more assert statements here but due to type checking and things they all break. The + # best we can do is to assert that we get back a MagicMock object. + assert isinstance(result, MagicMock), u'The result should be a MagicMock, because we mocked it out' + + def image_to_byte_test(self): + """ + Test the image_to_byte() function + """ + with patch(u'openlp.core.lib.QtCore') as MockedQtCore: + # GIVEN: A set of mocked-out Qt classes + mocked_byte_array = MagicMock() + MockedQtCore.QByteArray.return_value = mocked_byte_array + mocked_byte_array.toBase64.return_value = u'base64mock' + mocked_buffer = MagicMock() + MockedQtCore.QBuffer.return_value = mocked_buffer + MockedQtCore.QIODevice.WriteOnly = u'writeonly' + mocked_image = MagicMock() + + # WHEN: We convert an image to a byte array + result = image_to_byte(mocked_image) + + # THEN: We should receive a value of u'base64mock' + MockedQtCore.QByteArray.assert_called_with() + MockedQtCore.QBuffer.assert_called_with(mocked_byte_array) + mocked_buffer.open.assert_called_with(u'writeonly') + mocked_image.save.assert_called_with(mocked_buffer, "PNG") + mocked_byte_array.toBase64.assert_called_with() + assert result == u'base64mock', u'The result should be the return value of the mocked out base64 method' + + def check_item_selected_true_test(self): + """ + Test that the check_item_selected() function returns True when there are selected indexes. + """ + # GIVEN: A mocked out QtGui module and a list widget with selected indexes + MockedQtGui = patch(u'openlp.core.lib.QtGui') + mocked_list_widget = MagicMock() + mocked_list_widget.selectedIndexes.return_value = True + message = u'message' + + # WHEN: We check if there are selected items + result = check_item_selected(mocked_list_widget, message) + + # THEN: The selectedIndexes function should have been called and the result should be true + mocked_list_widget.selectedIndexes.assert_called_with() + assert result, u'The result should be True' + + def check_item_selected_false_test(self): + """ + Test that the check_item_selected() function returns False when there are no selected indexes. + """ + # GIVEN: A mocked out QtGui module and a list widget with selected indexes + with patch(u'openlp.core.lib.QtGui') as MockedQtGui, \ + patch(u'openlp.core.lib.translate') as mocked_translate: + mocked_translate.return_value = u'mocked translate' + mocked_list_widget = MagicMock() + mocked_list_widget.selectedIndexes.return_value = False + mocked_list_widget.parent.return_value = u'parent' + message = u'message' + + # WHEN: We check if there are selected items + result = check_item_selected(mocked_list_widget, message) + + # THEN: The selectedIndexes function should have been called and the result should be true + mocked_list_widget.selectedIndexes.assert_called_with() + MockedQtGui.QMessageBox.information.assert_called_with(u'parent', u'mocked translate', 'message') + assert not result, u'The result should be False' + + def validate_thumb_file_does_not_exist_test(self): + """ + Test the validate_thumb() function when the thumbnail does not exist + """ + # GIVEN: A mocked out os module, with path.exists returning False, and fake paths to a file and a thumb + with patch(u'openlp.core.lib.os') as mocked_os: + file_path = u'path/to/file' + thumb_path = u'path/to/thumb' + mocked_os.path.exists.return_value = False + + # WHEN: we run the validate_thumb() function + result = validate_thumb(file_path, thumb_path) + + # THEN: we should have called a few functions, and the result should be False + mocked_os.path.exists.assert_called_with(thumb_path) + assert result is False, u'The result should be False' + + def validate_thumb_file_exists_and_newer_test(self): + """ + Test the validate_thumb() function when the thumbnail exists and has a newer timestamp than the file + """ + # GIVEN: A mocked out os module, functions rigged to work for us, and fake paths to a file and a thumb + with patch(u'openlp.core.lib.os') as mocked_os: + file_path = u'path/to/file' + thumb_path = u'path/to/thumb' + file_mocked_stat = MagicMock() + file_mocked_stat.st_mtime = datetime.now() + thumb_mocked_stat = MagicMock() + thumb_mocked_stat.st_mtime = datetime.now() + timedelta(seconds=10) + mocked_os.path.exists.return_value = True + mocked_os.stat.side_effect = [file_mocked_stat, thumb_mocked_stat] + + # WHEN: we run the validate_thumb() function + + # THEN: we should have called a few functions, and the result should be True + #mocked_os.path.exists.assert_called_with(thumb_path) + + def validate_thumb_file_exists_and_older_test(self): + """ + Test the validate_thumb() function when the thumbnail exists but is older than the file + """ + # GIVEN: A mocked out os module, functions rigged to work for us, and fake paths to a file and a thumb + with patch(u'openlp.core.lib.os') as mocked_os: + file_path = u'path/to/file' + thumb_path = u'path/to/thumb' + file_mocked_stat = MagicMock() + file_mocked_stat.st_mtime = datetime.now() + thumb_mocked_stat = MagicMock() + thumb_mocked_stat.st_mtime = datetime.now() - timedelta(seconds=10) + mocked_os.path.exists.return_value = True + mocked_os.stat.side_effect = [file_mocked_stat, thumb_mocked_stat] + + # WHEN: we run the validate_thumb() function + result = validate_thumb(file_path, thumb_path) + + # THEN: we should have called a few functions, and the result should be False + mocked_os.path.exists.assert_called_with(thumb_path) + mocked_os.stat.assert_any_call(file_path) + mocked_os.stat.assert_any_call(thumb_path) + assert result is False, u'The result should be False' diff --git a/tests/functional/openlp_core_lib/test_serviceitem.py b/tests/functional/openlp_core_lib/test_serviceitem.py index b9b66b5bd..c2b9aacb1 100644 --- a/tests/functional/openlp_core_lib/test_serviceitem.py +++ b/tests/functional/openlp_core_lib/test_serviceitem.py @@ -16,7 +16,7 @@ VERSE = u'The Lord said to {r}Noah{/r}: \n'\ 'r{/pk}{o}e{/o}{pp}n{/pp} of the Lord\n' FOOTER = [u'Arky Arky (Unknown)', u'Public Domain', u'CCLI 123456'] -TESTPATH = os.path.abspath(os.path.join(os.path.dirname(__file__), u'resources')) +TESTPATH = os.path.abspath(os.path.join(os.path.dirname(__file__), u'..', u'..', u'resources')) class TestServiceItem(TestCase): @@ -160,4 +160,4 @@ class TestServiceItem(TestCase): service_item.validate_item([u'png']) # THEN the service item should not be valid - assert service_item.is_valid is False, u'The service item is not valid' \ No newline at end of file + assert service_item.is_valid is False, u'The service item is not valid' diff --git a/tests/functional/openlp_core_ui/__init__.py b/tests/functional/openlp_core_ui/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/functional/openlp_core_ui/starttimedialog.py b/tests/functional/openlp_core_ui/test_starttimedialog.py similarity index 81% rename from tests/functional/openlp_core_ui/starttimedialog.py rename to tests/functional/openlp_core_ui/test_starttimedialog.py index 8b9d3193c..0aed81592 100644 --- a/tests/functional/openlp_core_ui/starttimedialog.py +++ b/tests/functional/openlp_core_ui/test_starttimedialog.py @@ -1,10 +1,11 @@ """ - Package to test the openlp.core.ui package. +Package to test the openlp.core.ui package. """ import sys - from unittest import TestCase + from mock import MagicMock + from openlp.core.ui import starttimeform from PyQt4 import QtCore, QtGui, QtTest @@ -14,10 +15,18 @@ class TestStartTimeDialog(TestCase): """ Create the UI """ - self.app = QtGui.QApplication(sys.argv) + self.app = QtGui.QApplication([]) self.window = QtGui.QMainWindow() self.form = starttimeform.StartTimeForm(self.window) + def tearDown(self): + """ + Delete all the C++ objects at the end so that we don't have a segfault + """ + del self.form + del self.window + del self.app + def ui_defaults_test(self): """ Test StartTimeDialog defaults @@ -40,9 +49,9 @@ class TestStartTimeDialog(TestCase): Test StartTimeDialog display initialisation """ #GIVEN: A service item with with time - mocked_serviceitem = MagicMock() + mocked_serviceitem = MagicMock() mocked_serviceitem.start_time = 61 mocked_serviceitem.end_time = 3701 self.form.item = mocked_serviceitem - #self.form.exec_() \ No newline at end of file + #self.form.exec_() diff --git a/tests/functional/resources/church.jpg b/tests/functional/resources/church.jpg deleted file mode 100644 index 826180870..000000000 Binary files a/tests/functional/resources/church.jpg and /dev/null differ diff --git a/tests/functional/openlp_core_lib/resources/church.jpg b/tests/resources/church.jpg similarity index 100% rename from tests/functional/openlp_core_lib/resources/church.jpg rename to tests/resources/church.jpg