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/.version b/openlp/.version index cd5ac039d..d41002840 100644 --- a/openlp/.version +++ b/openlp/.version @@ -1 +1 @@ -2.0 +2.1.0-bzr2141 diff --git a/openlp/core/lib/db.py b/openlp/core/lib/db.py index ff4f8234b..ce920d598 100644 --- a/openlp/core/lib/db.py +++ b/openlp/core/lib/db.py @@ -34,7 +34,6 @@ import logging import os from urllib import quote_plus as urlquote -from PyQt4 import QtCore from sqlalchemy import Table, MetaData, Column, types, create_engine from sqlalchemy.exc import SQLAlchemyError, InvalidRequestError, DBAPIError, OperationalError from sqlalchemy.orm import scoped_session, sessionmaker, mapper diff --git a/openlp/core/lib/formattingtags.py b/openlp/core/lib/formattingtags.py index 594a4322e..eb52dc26f 100644 --- a/openlp/core/lib/formattingtags.py +++ b/openlp/core/lib/formattingtags.py @@ -31,8 +31,6 @@ Provide HTML Tag management and Formatting Tag access class """ import cPickle -from PyQt4 import QtCore - from openlp.core.lib import translate, Settings class FormattingTags(object): diff --git a/openlp/core/lib/serviceitem.py b/openlp/core/lib/serviceitem.py index 18e9f54d8..ddfbe7f9b 100644 --- a/openlp/core/lib/serviceitem.py +++ b/openlp/core/lib/serviceitem.py @@ -37,7 +37,9 @@ import logging import os import uuid -from openlp.core.lib import build_icon, clean_tags, expand_tags, translate, ImageSource +from PyQt4 import QtGui + +from openlp.core.lib import build_icon, clean_tags, expand_tags, translate, ImageSource, Settings log = logging.getLogger(__name__) @@ -181,7 +183,11 @@ 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() def _new_item(self): @@ -190,6 +196,7 @@ class ServiceItem(object): service items to see if they are the same. """ self._uuid = unicode(uuid.uuid1()) + self.validate_item() def add_capability(self, capability): """ @@ -340,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, @@ -394,7 +404,11 @@ 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: self.background_audio = [] for filename in header[u'background_audio']: @@ -405,20 +419,23 @@ class ServiceItem(object): for slide in serviceitem[u'serviceitem'][u'data']: self._raw_frames.append(slide) elif self.service_item_type == ServiceItemType.Image: + settingsSection = serviceitem[u'serviceitem'][u'header'][u'name'] + background = QtGui.QColor(Settings().value(settingsSection + u'/background color', u'#000000')) if path: + self.has_original_files = False for text_image in serviceitem[u'serviceitem'][u'data']: filename = os.path.join(path, text_image) - self.add_from_image(filename, text_image) + self.add_from_image(filename, text_image, background) else: for text_image in serviceitem[u'serviceitem'][u'data']: - self.add_from_image(text_image[u'path'], text_image[u'title']) + self.add_from_image(text_image[u'path'], text_image[u'title'], background) elif self.service_item_type == ServiceItemType.Command: for text_image in serviceitem[u'serviceitem'][u'data']: if path: + self.has_original_files = False 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): @@ -605,8 +622,25 @@ class ServiceItem(object): if self.get_frame_path(frame=frame) in invalid_paths: self.remove_frame(frame) - def validate(self): + def missing_frames(self): """ - Validates this service item + Returns if there are any frames in the service item """ - return bool(self._raw_frames) + return not bool(self._raw_frames) + + def validate_item(self, suffix_list=None): + """ + Validates a service item to make sure it is valid + """ + self.is_valid = True + for frame in self._raw_frames: + if self.is_image() and not os.path.exists((frame[u'path'])): + self.is_valid = False + elif self.is_command(): + file = os.path.join(frame[u'path'],frame[u'title']) + if not os.path.exists(file): + self.is_valid = False + if suffix_list and not self.is_text(): + type = frame[u'title'].split(u'.')[-1] + if type.lower() not in suffix_list: + self.is_valid = False diff --git a/openlp/core/theme/__init__.py b/openlp/core/theme/__init__.py index f9961eedd..4d1997b82 100644 --- a/openlp/core/theme/__init__.py +++ b/openlp/core/theme/__init__.py @@ -26,5 +26,9 @@ # with this program; if not, write to the Free Software Foundation, Inc., 59 # # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### +""" +The :mod:`~openlp.core.theme` module contains all the themeing functions used by +OpenLP when displaying a song or a scripture. +""" from openlp.core.theme.theme import Theme diff --git a/openlp/core/ui/__init__.py b/openlp/core/ui/__init__.py index e117a0809..132465b2e 100644 --- a/openlp/core/ui/__init__.py +++ b/openlp/core/ui/__init__.py @@ -29,9 +29,7 @@ """ The :mod:`ui` module provides the core user interface for OpenLP """ -from PyQt4 import QtGui -from openlp.core.lib import translate class HideMode(object): """ diff --git a/openlp/core/ui/exceptiondialog.py b/openlp/core/ui/exceptiondialog.py index bb127be43..76a92938d 100644 --- a/openlp/core/ui/exceptiondialog.py +++ b/openlp/core/ui/exceptiondialog.py @@ -29,7 +29,7 @@ from PyQt4 import QtCore, QtGui -from openlp.core.lib import translate, build_icon +from openlp.core.lib import translate from openlp.core.lib.ui import create_button, create_button_box class Ui_ExceptionDialog(object): diff --git a/openlp/core/ui/media/mediacontroller.py b/openlp/core/ui/media/mediacontroller.py index 7be9f58cf..f371610f0 100644 --- a/openlp/core/ui/media/mediacontroller.py +++ b/openlp/core/ui/media/mediacontroller.py @@ -29,19 +29,49 @@ import logging import os -import sys +import datetime from PyQt4 import QtCore, QtGui from openlp.core.lib import OpenLPToolbar, Receiver, translate, Settings from openlp.core.lib.ui import UiStrings, critical_error_message_box -from openlp.core.ui.media import MediaState, MediaInfo, MediaType, \ - get_media_players, set_media_players +from openlp.core.ui.media import MediaState, MediaInfo, MediaType, get_media_players, set_media_players from openlp.core.ui.media.mediaplayer import MediaPlayer from openlp.core.utils import AppLocation from openlp.core.ui import DisplayControllerType log = logging.getLogger(__name__) +class MediaSlider(QtGui.QSlider): + """ + Allows the mouse events of a slider to be overridden and extra functionality added + """ + def __init__(self, direction, manager, controller, parent=None): + QtGui.QSlider.__init__(self, direction) + self.manager = manager + self.controller = controller + + def mouseMoveEvent(self, event): + """ + Override event to allow hover time to be displayed. + """ + timevalue = QtGui.QStyle.sliderValueFromPosition(self.minimum(), self.maximum(), event.x(), self.width()) + self.setToolTip(u'%s' % datetime.timedelta(seconds=int(timevalue/1000))) + QtGui.QSlider.mouseMoveEvent(self, event) + + def mousePressEvent(self, event): + """ + Mouse Press event no new functionality + """ + QtGui.QSlider.mousePressEvent(self, event) + + def mouseReleaseEvent(self, event): + """ + Set the slider position when the mouse is clicked and released on the slider. + """ + self.setValue(QtGui.QStyle.sliderValueFromPosition(self.minimum(), self.maximum(), event.x(), self.width())) + QtGui.QSlider.mouseReleaseEvent(self, event) + + class MediaController(object): """ The implementation of the Media Controller. The Media Controller adds an own @@ -69,8 +99,8 @@ class MediaController(object): QtCore.QObject.connect(Receiver.get_receiver(), QtCore.SIGNAL(u'playbackPlay'), self.media_play_msg) QtCore.QObject.connect(Receiver.get_receiver(), QtCore.SIGNAL(u'playbackPause'), self.media_pause_msg) QtCore.QObject.connect(Receiver.get_receiver(), QtCore.SIGNAL(u'playbackStop'), self.media_stop_msg) - QtCore.QObject.connect(Receiver.get_receiver(), QtCore.SIGNAL(u'seekSlider'), self.media_seek) - QtCore.QObject.connect(Receiver.get_receiver(), QtCore.SIGNAL(u'volumeSlider'), self.media_volume) + QtCore.QObject.connect(Receiver.get_receiver(), QtCore.SIGNAL(u'seekSlider'), self.media_seek_msg) + QtCore.QObject.connect(Receiver.get_receiver(), QtCore.SIGNAL(u'volumeSlider'), self.media_volume_msg) QtCore.QObject.connect(Receiver.get_receiver(), QtCore.SIGNAL(u'media_hide'), self.media_hide) QtCore.QObject.connect(Receiver.get_receiver(), QtCore.SIGNAL(u'media_blank'), self.media_blank) QtCore.QObject.connect(Receiver.get_receiver(), QtCore.SIGNAL(u'media_unblank'), self.media_unblank) @@ -241,9 +271,10 @@ class MediaController(object): icon=u':/slides/media_playback_stop.png', tooltip=translate('OpenLP.SlideController', 'Stop playing media.'), triggers=controller.sendToPlugins) # Build the seekSlider. - controller.seekSlider = QtGui.QSlider(QtCore.Qt.Horizontal) + controller.seekSlider = MediaSlider(QtCore.Qt.Horizontal, self, controller) controller.seekSlider.setMaximum(1000) - controller.seekSlider.setTracking(False) + controller.seekSlider.setTracking(True) + controller.seekSlider.setMouseTracking(True) controller.seekSlider.setToolTip(translate('OpenLP.SlideController', 'Video position.')) controller.seekSlider.setGeometry(QtCore.QRect(90, 260, 221, 24)) controller.seekSlider.setObjectName(u'seekSlider') @@ -344,12 +375,8 @@ class MediaController(object): # stop running videos self.media_reset(controller) controller.media_info = MediaInfo() - if videoBehindText: - controller.media_info.volume = 0 - controller.media_info.is_background = True - else: - controller.media_info.volume = controller.volumeSlider.value() - controller.media_info.is_background = False + controller.media_info.volume = controller.volumeSlider.value() + controller.media_info.is_background = videoBehindText controller.media_info.file_info = QtCore.QFileInfo(serviceItem.get_frame_path()) display = self._define_display(controller) if controller.isLive: @@ -361,7 +388,7 @@ class MediaController(object): controller.media_info.start_time = 0 controller.media_info.end_time = 0 else: - controller.media_info.start_time = display.serviceItem.start_time + controller.media_info.start_time = serviceItem.start_time controller.media_info.end_time = serviceItem.end_time elif controller.previewDisplay: isValid = self._check_file_type(controller, display, serviceItem) @@ -483,9 +510,17 @@ class MediaController(object): The controller to be played """ log.debug(u'media_play') + controller.seekSlider.blockSignals(True) + controller.volumeSlider.blockSignals(True) display = self._define_display(controller) if not self.currentMediaPlayer[controller.controllerType].play(display): + controller.seekSlider.blockSignals(False) + controller.volumeSlider.blockSignals(False) return False + if controller.media_info.is_background: + self.media_volume(controller, 0) + else: + self.media_volume(controller, controller.media_info.volume) if status: display.frame.evaluateJavaScript(u'show_blank("desktop");') self.currentMediaPlayer[controller.controllerType].set_visible(display, True) @@ -503,6 +538,8 @@ class MediaController(object): # Start Timer for ui updates if not self.timer.isActive(): self.timer.start() + controller.seekSlider.blockSignals(False) + controller.volumeSlider.blockSignals(False) return True def media_pause_msg(self, msg): @@ -557,7 +594,7 @@ class MediaController(object): controller.mediabar.actions[u'playbackStop'].setVisible(False) controller.mediabar.actions[u'playbackPause'].setVisible(False) - def media_volume(self, msg): + def media_volume_msg(self, msg): """ Changes the volume of a running video @@ -566,11 +603,21 @@ class MediaController(object): """ controller = msg[0] vol = msg[1][0] - log.debug(u'media_volume %d' % vol) - display = self._define_display(controller) - self.currentMediaPlayer[controller.controllerType].volume(display, vol) + self.media_volume(controller, vol) - def media_seek(self, msg): + def media_volume(self, controller, volume): + """ + Changes the volume of a running video + + ``msg`` + First element is the controller which should be used + """ + log.debug(u'media_volume %d' % volume) + display = self._define_display(controller) + self.currentMediaPlayer[controller.controllerType].volume(display, volume) + controller.volumeSlider.setValue(volume) + + def media_seek_msg(self, msg): """ Responds to the request to change the seek Slider of a loaded video @@ -581,6 +628,17 @@ class MediaController(object): log.debug(u'media_seek') controller = msg[0] seekVal = msg[1][0] + self.media_seek(controller, seekVal) + + def media_seek(self, controller, seekVal): + """ + Responds to the request to change the seek Slider of a loaded video + + ``msg`` + First element is the controller which should be used + Second element is a list with the seek Value as first element + """ + log.debug(u'media_seek') display = self._define_display(controller) self.currentMediaPlayer[controller.controllerType].seek(display, seekVal) @@ -610,7 +668,8 @@ class MediaController(object): return controller = self.mainWindow.liveController display = self._define_display(controller) - if self.currentMediaPlayer[controller.controllerType].state == MediaState.Playing: + if controller.controllerType in self.currentMediaPlayer and \ + self.currentMediaPlayer[controller.controllerType].state == MediaState.Playing: self.currentMediaPlayer[controller.controllerType].pause(display) self.currentMediaPlayer[controller.controllerType].set_visible(display, False) diff --git a/openlp/core/ui/media/phononplayer.py b/openlp/core/ui/media/phononplayer.py index 4d93c3e03..bcba26e2d 100644 --- a/openlp/core/ui/media/phononplayer.py +++ b/openlp/core/ui/media/phononplayer.py @@ -31,7 +31,7 @@ import logging import mimetypes from datetime import datetime -from PyQt4 import QtCore, QtGui +from PyQt4 import QtGui from PyQt4.phonon import Phonon from openlp.core.lib import Receiver, translate, Settings @@ -215,8 +215,9 @@ class PhononPlayer(MediaPlayer): self.stop(display) self.set_visible(display, False) if not controller.seekSlider.isSliderDown(): - controller.seekSlider.setSliderPosition( - display.mediaObject.currentTime()) + controller.seekSlider.blockSignals(True) + controller.seekSlider.setSliderPosition(display.mediaObject.currentTime()) + controller.seekSlider.blockSignals(False) def get_media_display_css(self): """ diff --git a/openlp/core/ui/media/vendor/__init__.py b/openlp/core/ui/media/vendor/__init__.py index 7dc74a8e4..317fb9f81 100644 --- a/openlp/core/ui/media/vendor/__init__.py +++ b/openlp/core/ui/media/vendor/__init__.py @@ -26,3 +26,7 @@ # with this program; if not, write to the Free Software Foundation, Inc., 59 # # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### +""" +The :mod:`~openlp.core.ui.media.vendor` module contains any scripts or libraries +from 3rd party vendors which are required to make certain media modules work. +""" diff --git a/openlp/core/ui/media/vlcplayer.py b/openlp/core/ui/media/vlcplayer.py index 3cb3448a9..20191d5b6 100644 --- a/openlp/core/ui/media/vlcplayer.py +++ b/openlp/core/ui/media/vlcplayer.py @@ -33,7 +33,7 @@ import logging import os import sys -from PyQt4 import QtCore, QtGui +from PyQt4 import QtGui from openlp.core.lib import Receiver, translate, Settings from openlp.core.ui.media import MediaState @@ -109,6 +109,7 @@ class VlcPlayer(MediaPlayer): def setup(self, display): display.vlcWidget = QtGui.QFrame(display) + display.vlcWidget.setFrameStyle(QtGui.QFrame.NoFrame) # creating a basic vlc instance command_line_options = u'--no-video-title-show' if not display.hasAudio: @@ -188,6 +189,7 @@ class VlcPlayer(MediaPlayer): display.vlcMediaPlayer.play() if not self.media_state_wait(display, vlc.State.Playing): return False + self.volume(display, controller.media_info.volume) if start_time > 0: self.seek(display, controller.media_info.start_time * 1000) controller.media_info.length = int(display.vlcMediaPlayer.get_media().get_duration() / 1000) @@ -234,7 +236,9 @@ class VlcPlayer(MediaPlayer): self.stop(display) self.set_visible(display, False) if not controller.seekSlider.isSliderDown(): + controller.seekSlider.blockSignals(True) controller.seekSlider.setSliderPosition(display.vlcMediaPlayer.get_time()) + controller.seekSlider.blockSignals(False) def get_info(self): return(translate('Media.player', 'VLC is an external player which ' diff --git a/openlp/core/ui/media/webkitplayer.py b/openlp/core/ui/media/webkitplayer.py index 72df26bc4..58e7a40f5 100644 --- a/openlp/core/ui/media/webkitplayer.py +++ b/openlp/core/ui/media/webkitplayer.py @@ -423,7 +423,9 @@ class WebkitPlayer(MediaPlayer): controller.media_info.length = length controller.seekSlider.setMaximum(length) if not controller.seekSlider.isSliderDown(): + controller.seekSlider.blockSignals(True) controller.seekSlider.setSliderPosition(currentTime) + controller.seekSlider.blockSignals(False) def get_info(self): return(translate('Media.player', 'Webkit is a media player which runs ' diff --git a/openlp/core/ui/screen.py b/openlp/core/ui/screen.py index 5dd41f609..cbd133e08 100644 --- a/openlp/core/ui/screen.py +++ b/openlp/core/ui/screen.py @@ -69,7 +69,7 @@ class ScreenList(object): screen_list.screen_list = [] screen_list.display_count = 0 screen_list.screen_count_changed() - screen_list._load_screen_settings() + screen_list.load_screen_settings() QtCore.QObject.connect(desktop, QtCore.SIGNAL(u'resized(int)'), screen_list.screen_resolution_changed) QtCore.QObject.connect(desktop, QtCore.SIGNAL(u'screenCountChanged(int)'), screen_list.screen_count_changed) return screen_list @@ -237,7 +237,7 @@ class ScreenList(object): y >= size.y() and y <= (size.y() + size.height()): return screen[u'number'] - def _load_screen_settings(self): + def load_screen_settings(self): """ Loads the screen size and the monitor number from the settings. """ diff --git a/openlp/core/ui/servicemanager.py b/openlp/core/ui/servicemanager.py index 7da61dc63..8a9b01d04 100644 --- a/openlp/core/ui/servicemanager.py +++ b/openlp/core/ui/servicemanager.py @@ -114,6 +114,7 @@ class ServiceManager(QtGui.QWidget): # is a new service and has not been saved self._modified = False self._fileName = u'' + self.service_has_all_original_files = True self.serviceNoteForm = ServiceNoteForm(self.mainwindow) self.serviceItemEditForm = ServiceItemEditForm(self.mainwindow) self.startTimeForm = StartTimeForm(self.mainwindow) @@ -256,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. @@ -458,7 +473,7 @@ class ServiceManager(QtGui.QWidget): for item in list(self.serviceItems): self.mainwindow.incrementProgressBar() item[u'service_item'].remove_invalid_frames(missing_list) - if not item[u'service_item'].validate(): + if item[u'service_item'].missing_frames(): self.serviceItems.remove(item) else: service_item = item[u'service_item'].get_service_repr(self._saveLite) @@ -620,7 +635,7 @@ class ServiceManager(QtGui.QWidget): path = os.path.join(directory, default_filename) # SaveAs from osz to oszl is not valid as the files will be deleted # on exit which is not sensible or usable in the long term. - if self._fileName.endswith(u'oszl') or not self._fileName: + if self._fileName.endswith(u'oszl') or self.service_has_all_original_files: fileName = QtGui.QFileDialog.getSaveFileName(self.mainwindow, UiStrings().SaveService, path, translate('OpenLP.ServiceManager', 'OpenLP Service Files (*.osz);; OpenLP Service Files - lite (*.oszl)')) @@ -693,7 +708,7 @@ class ServiceManager(QtGui.QWidget): serviceItem.set_from_service(item) else: serviceItem.set_from_service(item, self.servicePath) - self.validateItem(serviceItem) + serviceItem.validate_item(self.suffixes) self.load_item_uuid = 0 if serviceItem.is_capable(ItemCapabilities.OnLoadUpdate): Receiver.send_message(u'%s_service_load' % serviceItem.name.lower(), serviceItem) @@ -765,6 +780,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): @@ -810,6 +841,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', 5) + 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', 5) + 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', 5) + 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. @@ -1032,9 +1116,12 @@ class ServiceManager(QtGui.QWidget): """ # Correct order of items in array count = 1 + self.service_has_all_original_files = True for item in self.serviceItems: item[u'order'] = count count += 1 + if not item[u'service_item'].has_original_files: + self.service_has_all_original_files = False # Repaint the screen self.serviceManagerList.clear() for itemcount, item in enumerate(self.serviceItems): @@ -1093,18 +1180,6 @@ class ServiceManager(QtGui.QWidget): self.serviceManagerList.setCurrentItem(treewidgetitem) treewidgetitem.setExpanded(item[u'expanded']) - def validateItem(self, serviceItem): - """ - Validates the service item and if the suffix matches an accepted - one it allows the item to be displayed. - """ - #@todo check file items exist - if serviceItem.is_command(): - type = serviceItem._raw_frames[0][u'title'].split(u'.')[-1] - if type.lower() not in self.suffixes: - serviceItem.is_valid = False - #@todo check file items exist - def cleanUp(self): """ Empties the servicePath of temporary files on system exit. @@ -1144,7 +1219,7 @@ class ServiceManager(QtGui.QWidget): Receiver.send_message(u'cursor_busy') log.debug(u'regenerateServiceItems') # force reset of renderer as theme data has changed - self.mainwindow.renderer.themedata = None + self.service_has_all_original_files = True if self.serviceItems: for item in self.serviceItems: item[u'selected'] = False @@ -1299,6 +1374,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/settingsform.py b/openlp/core/ui/settingsform.py index 55e94e3a6..2807f215a 100644 --- a/openlp/core/ui/settingsform.py +++ b/openlp/core/ui/settingsform.py @@ -31,7 +31,7 @@ The :mod:`settingsform` provides a user interface for the OpenLP settings """ import logging -from PyQt4 import QtCore, QtGui +from PyQt4 import QtGui from openlp.core.lib import Receiver, build_icon, PluginStatus from openlp.core.ui import AdvancedTab, GeneralTab, ThemesTab @@ -141,4 +141,4 @@ class SettingsForm(QtGui.QDialog, Ui_SettingsDialog): """ if self.resetSuffixes: self.mainWindow.serviceManagerContents.resetSupportedSuffixes() - self.resetSuffixes = False \ No newline at end of file + self.resetSuffixes = False diff --git a/openlp/core/ui/slidecontroller.py b/openlp/core/ui/slidecontroller.py index bacb6ea60..7e6879bdf 100644 --- a/openlp/core/ui/slidecontroller.py +++ b/openlp/core/ui/slidecontroller.py @@ -692,6 +692,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): """ @@ -881,6 +889,7 @@ class SlideController(DisplayController): Settings().remove(self.parent().generalSettingsSection + u'/screen blank') self.blankPlugin() self.updatePreview() + self.onToggleLoop() def onThemeDisplay(self, checked=None): """ @@ -899,6 +908,7 @@ class SlideController(DisplayController): Settings().remove(self.parent().generalSettingsSection + u'/screen blank') self.blankPlugin() self.updatePreview() + self.onToggleLoop() def onHideDisplay(self, checked=None): """ @@ -917,6 +927,7 @@ class SlideController(DisplayController): Settings().remove(self.parent().generalSettingsSection + u'/screen blank') self.hidePlugin(checked) self.updatePreview() + self.onToggleLoop() def blankPlugin(self): """ @@ -933,8 +944,7 @@ class SlideController(DisplayController): else: if not self.serviceItem.is_command(): Receiver.send_message(u'live_display_show') - Receiver.send_message(u'%s_unblank' % self.serviceItem.name.lower(), - [self.serviceItem, self.isLive]) + Receiver.send_message(u'%s_unblank' % self.serviceItem.name.lower(), [self.serviceItem, self.isLive]) else: if hide_mode: Receiver.send_message(u'live_display_hide', hide_mode) @@ -949,13 +959,11 @@ class SlideController(DisplayController): if self.serviceItem is not None: if hide: Receiver.send_message(u'live_display_hide', HideMode.Screen) - Receiver.send_message(u'%s_hide' % self.serviceItem.name.lower(), - [self.serviceItem, self.isLive]) + Receiver.send_message(u'%s_hide' % self.serviceItem.name.lower(), [self.serviceItem, self.isLive]) else: if not self.serviceItem.is_command(): Receiver.send_message(u'live_display_show') - Receiver.send_message(u'%s_unblank' % self.serviceItem.name.lower(), - [self.serviceItem, self.isLive]) + Receiver.send_message(u'%s_unblank' % self.serviceItem.name.lower(), [self.serviceItem, self.isLive]) else: if hide: Receiver.send_message(u'live_display_hide', HideMode.Screen) @@ -995,6 +1003,7 @@ class SlideController(DisplayController): self.selectedRow = row self.__checkUpdateSelectedSlide(row) Receiver.send_message(u'slidecontroller_%s_changed' % self.typePrefix, row) + self.display.setFocus() def onSlideChange(self, row): """ @@ -1094,7 +1103,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() @@ -1128,11 +1138,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): @@ -1149,11 +1159,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/openlp/core/utils/__init__.py b/openlp/core/utils/__init__.py index 2a303e87d..21b1b4f30 100644 --- a/openlp/core/utils/__init__.py +++ b/openlp/core/utils/__init__.py @@ -29,7 +29,7 @@ """ The :mod:`openlp.core.utils` module provides the utility libraries for OpenLP. """ -from datetime import datetime +from datetime import datetime, timedelta from distutils.version import LooseVersion import logging import locale @@ -277,20 +277,31 @@ def check_latest_version(current_version): ``current_version`` The current version of OpenLP. + + **Rules around versions and version files:** + + * If a version number has a build (i.e. -bzr1234), then it is a nightly. + * If a version number's minor version is an odd number, it is a development release. + * If a version number's minor version is an even number, it is a stable release. """ version_string = current_version[u'full'] # set to prod in the distribution config file. settings = Settings() settings.beginGroup(u'general') - last_test = settings.value(u'last version test', datetime.now().date()) + # This defaults to yesterday in order to force the update check to run when you've never run it before. + last_test = settings.value(u'last version test', datetime.now().date() - timedelta(days=1)) this_test = datetime.now().date() settings.setValue(u'last version test', this_test) settings.endGroup() if last_test != this_test: if current_version[u'build']: - req = urllib2.Request(u'http://www.openlp.org/files/dev_version.txt') + req = urllib2.Request(u'http://www.openlp.org/files/nightly_version.txt') else: - req = urllib2.Request(u'http://www.openlp.org/files/version.txt') + version_parts = current_version[u'version'].split(u'.') + if int(version_parts[1]) % 2 != 0: + req = urllib2.Request(u'http://www.openlp.org/files/dev_version.txt') + else: + req = urllib2.Request(u'http://www.openlp.org/files/version.txt') req.add_header(u'User-Agent', u'OpenLP/%s' % current_version[u'full']) remote_version = None try: diff --git a/openlp/plugins/alerts/forms/__init__.py b/openlp/plugins/alerts/forms/__init__.py index 121e24f97..e97fdfed3 100644 --- a/openlp/plugins/alerts/forms/__init__.py +++ b/openlp/plugins/alerts/forms/__init__.py @@ -26,5 +26,31 @@ # with this program; if not, write to the Free Software Foundation, Inc., 59 # # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### +""" +Forms in OpenLP are made up of two classes. One class holds all the graphical +elements, like buttons and lists, and the other class holds all the functional +code, like slots and loading and saving. + +The first class, commonly known as the **Dialog** class, is typically named +``Ui_Dialog``. It is a slightly modified version of the class that the +``pyuic4`` command produces from Qt4's .ui file. Typical modifications will be +converting most strings from "" to u'' and using OpenLP's ``translate()`` +function for translating strings. + +The second class, commonly known as the **Form** class, is typically named +``Form``. This class is the one which is instantiated and used. It uses +dual inheritance to inherit from (usually) QtGui.QDialog and the Ui class +mentioned above, like so:: + + class AuthorsForm(QtGui.QDialog, Ui_AuthorsDialog): + + def __init__(self, parent=None): + QtGui.QDialog.__init__(self, parent) + self.setupUi(self) + +This allows OpenLP to use ``self.object`` for all the GUI elements while keeping +them separate from the functionality, so that it is easier to recreate the GUI +from the .ui files later if necessary. +""" from alertform import AlertForm diff --git a/openlp/plugins/alerts/lib/alertsmanager.py b/openlp/plugins/alerts/lib/alertsmanager.py index a3691c7dc..ab0f7c1d9 100644 --- a/openlp/plugins/alerts/lib/alertsmanager.py +++ b/openlp/plugins/alerts/lib/alertsmanager.py @@ -26,6 +26,10 @@ # with this program; if not, write to the Free Software Foundation, Inc., 59 # # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### +""" +The :mod:`~openlp.plugins.alerts.lib.alertsmanager` module contains the part of +the plugin which manages storing and displaying of alerts. +""" import logging diff --git a/openlp/plugins/bibles/bibleplugin.py b/openlp/plugins/bibles/bibleplugin.py index 8e8f8c458..f22fc8bf8 100644 --- a/openlp/plugins/bibles/bibleplugin.py +++ b/openlp/plugins/bibles/bibleplugin.py @@ -29,7 +29,7 @@ import logging -from PyQt4 import QtCore, QtGui +from PyQt4 import QtGui from openlp.core.lib import Plugin, StringContent, build_icon, translate, Settings from openlp.core.lib.ui import create_action, UiStrings diff --git a/openlp/plugins/bibles/lib/__init__.py b/openlp/plugins/bibles/lib/__init__.py index 7346fc697..01314ddc9 100644 --- a/openlp/plugins/bibles/lib/__init__.py +++ b/openlp/plugins/bibles/lib/__init__.py @@ -34,7 +34,7 @@ import logging import re from openlp.core.lib import translate, Settings -from openlp.plugins.bibles.lib.db import BiblesResourcesDB + log = logging.getLogger(__name__) diff --git a/openlp/plugins/bibles/lib/manager.py b/openlp/plugins/bibles/lib/manager.py index 15250223c..1fa07ba86 100644 --- a/openlp/plugins/bibles/lib/manager.py +++ b/openlp/plugins/bibles/lib/manager.py @@ -30,8 +30,6 @@ import logging import os -from PyQt4 import QtCore - from openlp.core.lib import Receiver, SettingsManager, translate, Settings from openlp.core.utils import AppLocation, delete_file from openlp.plugins.bibles.lib import parse_reference, get_reference_separator, LanguageSelection diff --git a/openlp/plugins/custom/customplugin.py b/openlp/plugins/custom/customplugin.py index 2f0d73bff..3d57d1cc2 100644 --- a/openlp/plugins/custom/customplugin.py +++ b/openlp/plugins/custom/customplugin.py @@ -26,6 +26,10 @@ # with this program; if not, write to the Free Software Foundation, Inc., 59 # # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### +""" +The :mod:`~openlp.plugins.custom.customplugin` module contains the Plugin class +for the Custom Slides plugin. +""" import logging diff --git a/openlp/plugins/custom/lib/customtab.py b/openlp/plugins/custom/lib/customtab.py index a9e55d016..531703297 100644 --- a/openlp/plugins/custom/lib/customtab.py +++ b/openlp/plugins/custom/lib/customtab.py @@ -26,6 +26,10 @@ # with this program; if not, write to the Free Software Foundation, Inc., 59 # # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### +""" +The :mod:`~openlp.plugins.custom.lib.customtab` module contains the settings tab +for the Custom Slides plugin, which is inserted into the configuration dialog. +""" from PyQt4 import QtCore, QtGui @@ -66,6 +70,9 @@ class CustomTab(SettingsTab): 'Import missing custom slides from service files')) def onDisplayFooterCheckBoxChanged(self, check_state): + """ + Toggle the setting for displaying the footer. + """ self.displayFooter = False # we have a set value convert to True/False if check_state == QtCore.Qt.Checked: diff --git a/openlp/plugins/custom/lib/customxmlhandler.py b/openlp/plugins/custom/lib/customxmlhandler.py index 4d5627899..a8d2e4ce6 100644 --- a/openlp/plugins/custom/lib/customxmlhandler.py +++ b/openlp/plugins/custom/lib/customxmlhandler.py @@ -50,6 +50,7 @@ from lxml import etree, objectify log = logging.getLogger(__name__) +#TODO: These classes need to be refactored into a single class. class CustomXMLBuilder(object): """ This class builds the XML used to describe songs. @@ -84,11 +85,11 @@ class CustomXMLBuilder(object): self.lyrics.setAttribute(u'language', u'en') self.song.appendChild(self.lyrics) - def add_verse_to_lyrics(self, type, number, content): + def add_verse_to_lyrics(self, verse_type, number, content): """ Add a verse to the ```` tag. - ``type`` + ``verse_type`` A string denoting the type of verse. Possible values are "Chorus", "Verse", "Bridge", and "Custom". @@ -99,7 +100,7 @@ class CustomXMLBuilder(object): The actual text of the verse to be stored. """ verse = self.custom_xml.createElement(u'verse') - verse.setAttribute(u'type', type) + verse.setAttribute(u'type', verse_type) verse.setAttribute(u'label', number) self.lyrics.appendChild(verse) # add data as a CDATA section to protect the XML from special chars diff --git a/openlp/plugins/media/lib/mediaitem.py b/openlp/plugins/media/lib/mediaitem.py index aeb873886..1c44a4cda 100644 --- a/openlp/plugins/media/lib/mediaitem.py +++ b/openlp/plugins/media/lib/mediaitem.py @@ -33,11 +33,11 @@ import os from PyQt4 import QtCore, QtGui from openlp.core.lib import MediaManagerItem, build_icon, ItemCapabilities, SettingsManager, translate, \ - check_item_selected, Receiver, MediaType, ServiceItem, build_html, ServiceItemContext, Settings + check_item_selected, Receiver, MediaType, ServiceItem, ServiceItemContext, Settings, check_directory_exists from openlp.core.lib.ui import UiStrings, critical_error_message_box, create_horizontal_adjusting_combo_box from openlp.core.ui import DisplayController, Display, DisplayControllerType from openlp.core.ui.media import get_media_players, set_media_players -from openlp.core.utils import locale_compare +from openlp.core.utils import AppLocation, locale_compare log = logging.getLogger(__name__) @@ -130,8 +130,7 @@ class MediaMediaItem(MediaManagerItem): """ Called to reset the Live background with the media selected, """ - self.plugin.liveController.mediaController.media_reset( - self.plugin.liveController) + self.plugin.liveController.mediaController.media_reset(self.plugin.liveController) self.resetAction.setVisible(False) def videobackgroundReplaced(self): @@ -145,8 +144,7 @@ class MediaMediaItem(MediaManagerItem): Called to replace Live background with the media selected. """ if check_item_selected(self.listView, - translate('MediaPlugin.MediaItem', - 'You must select a media file to replace the background with.')): + translate('MediaPlugin.MediaItem', 'You must select a media file to replace the background with.')): item = self.listView.currentItem() filename = item.data(QtCore.Qt.UserRole) if os.path.exists(filename): @@ -166,8 +164,8 @@ class MediaMediaItem(MediaManagerItem): translate('MediaPlugin.MediaItem', 'There was a problem replacing your background, the media file "%s" no longer exists.') % filename) - def generateSlideData(self, service_item, item=None, xmlVersion=False, - remote=False, context=ServiceItemContext.Live): + def generateSlideData(self, service_item, item=None, xmlVersion=False, remote=False, + context=ServiceItemContext.Live): if item is None: item = self.listView.currentItem() if item is None: @@ -201,6 +199,8 @@ class MediaMediaItem(MediaManagerItem): def initialise(self): self.listView.clear() self.listView.setIconSize(QtCore.QSize(88, 50)) + self.servicePath = os.path.join(AppLocation.get_section_data_path(self.settingsSection), u'thumbnails') + check_directory_exists(self.servicePath) self.loadList(SettingsManager.load_list(self.settingsSection, u'media')) self.populateDisplayTypes() @@ -247,14 +247,13 @@ class MediaMediaItem(MediaManagerItem): """ Remove a media item from the list. """ - if check_item_selected(self.listView, translate('MediaPlugin.MediaItem', - 'You must select a media file to delete.')): + if check_item_selected(self.listView, + translate('MediaPlugin.MediaItem', 'You must select a media file to delete.')): row_list = [item.row() for item in self.listView.selectedIndexes()] row_list.sort(reverse=True) for row in row_list: self.listView.takeItem(row) - SettingsManager.set_list(self.settingsSection, - u'media', self.getFileList()) + SettingsManager.set_list(self.settingsSection, u'media', self.getFileList()) def loadList(self, media): # Sort the media by its filename considering language specific diff --git a/openlp/plugins/presentations/lib/messagelistener.py b/openlp/plugins/presentations/lib/messagelistener.py index fb9a5b9ea..3063bfa7b 100644 --- a/openlp/plugins/presentations/lib/messagelistener.py +++ b/openlp/plugins/presentations/lib/messagelistener.py @@ -28,7 +28,6 @@ ############################################################################### import logging -import os from PyQt4 import QtCore diff --git a/openlp/plugins/presentations/presentationplugin.py b/openlp/plugins/presentations/presentationplugin.py index ccd3b2f9e..937b78641 100644 --- a/openlp/plugins/presentations/presentationplugin.py +++ b/openlp/plugins/presentations/presentationplugin.py @@ -77,7 +77,7 @@ class PresentationPlugin(Plugin): if self.controllers[controller].enabled(): try: self.controllers[controller].start_process() - except: + except Exception: log.warn(u'Failed to start controller process') self.controllers[controller].available = False self.mediaItem.buildFileMaskString() diff --git a/openlp/plugins/songs/forms/__init__.py b/openlp/plugins/songs/forms/__init__.py index 58dd0408e..a2e80dc54 100644 --- a/openlp/plugins/songs/forms/__init__.py +++ b/openlp/plugins/songs/forms/__init__.py @@ -26,7 +26,6 @@ # with this program; if not, write to the Free Software Foundation, Inc., 59 # # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### - """ Forms in OpenLP are made up of two classes. One class holds all the graphical elements, like buttons and lists, and the other class holds all the functional diff --git a/openlp/plugins/songs/forms/editsongform.py b/openlp/plugins/songs/forms/editsongform.py index b6e74691b..e22937cd1 100644 --- a/openlp/plugins/songs/forms/editsongform.py +++ b/openlp/plugins/songs/forms/editsongform.py @@ -26,6 +26,10 @@ # with this program; if not, write to the Free Software Foundation, Inc., 59 # # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### +""" +The :mod:`~openlp.plugins.songs.forms.editsongform` module contains the form +used to edit songs. +""" import logging import re @@ -42,7 +46,7 @@ from openlp.plugins.songs.forms import EditVerseForm, MediaFilesForm from openlp.plugins.songs.lib import SongXML, VerseType, clean_song from openlp.plugins.songs.lib.db import Book, Song, Author, Topic, MediaFile from openlp.plugins.songs.lib.ui import SongStrings -from editsongdialog import Ui_EditSongDialog +from openlp.plugins.songs.forms.editsongdialog import Ui_EditSongDialog log = logging.getLogger(__name__) @@ -56,7 +60,7 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog): """ Constructor """ - QtGui.QDialog.__init__(self, parent) + super(EditSongForm, self).__init__(parent) self.mediaitem = mediaitem self.song = None # can this be automated? @@ -113,12 +117,18 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog): self.whitespace = re.compile(r'\W+', re.UNICODE) def initialise(self): + """ + Set up the form for when it is displayed. + """ self.verseEditButton.setEnabled(False) self.verseDeleteButton.setEnabled(False) self.authorRemoveButton.setEnabled(False) self.topicRemoveButton.setEnabled(False) def loadAuthors(self): + """ + Load the authors from the database into the combobox. + """ authors = self.manager.get_all_objects(Author, order_by_ref=Author.display_name) self.authorsComboBox.clear() @@ -132,14 +142,23 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog): set_case_insensitive_completer(self.authors, self.authorsComboBox) def loadTopics(self): + """ + Load the topics into the combobox. + """ self.topics = [] self.__loadObjects(Topic, self.topicsComboBox, self.topics) def loadBooks(self): + """ + Load the song books into the combobox + """ self.books = [] self.__loadObjects(Book, self.songBookComboBox, self.books) def __loadObjects(self, cls, combo, cache): + """ + Generically load a set of objects into a cache and a combobox. + """ objects = self.manager.get_all_objects(cls, order_by_ref=cls.name) combo.clear() combo.addItem(u'') @@ -151,6 +170,9 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog): set_case_insensitive_completer(cache, combo) def loadThemes(self, theme_list): + """ + Load the themes into a combobox. + """ self.themeComboBox.clear() self.themeComboBox.addItem(u'') self.themes = theme_list @@ -158,6 +180,9 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog): set_case_insensitive_completer(self.themes, self.themeComboBox) def loadMediaFiles(self): + """ + Load the media files into a combobox. + """ self.audioAddFromMediaButton.setVisible(False) for plugin in self.parent().pluginManager.plugins: if plugin.name == u'media' and plugin.status == PluginStatus.Active: @@ -166,6 +191,9 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog): break def newSong(self): + """ + Blank the edit form out in preparation for a new song. + """ log.debug(u'New Song') self.song = None self.initialise() @@ -313,6 +341,9 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog): self.verseListWidget.repaint() def onAuthorAddButtonClicked(self): + """ + Add the author to the list of authors associated with this song when the button is clicked. + """ item = int(self.authorsComboBox.currentIndex()) text = self.authorsComboBox.currentText().strip(u' \r\n\t') # This if statement is for OS X, which doesn't seem to work well with @@ -361,10 +392,16 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog): self.authorsListView.addItem(author_item) def onAuthorsListViewClicked(self): + """ + Run a set of actions when an author in the list is selected (mainly enable the delete button). + """ if self.authorsListView.count() > 1: self.authorRemoveButton.setEnabled(True) def onAuthorRemoveButtonClicked(self): + """ + Remove the author from the list when the delete button is clicked. + """ self.authorRemoveButton.setEnabled(False) item = self.authorsListView.currentItem() row = self.authorsListView.row(item) diff --git a/openlp/plugins/songs/lib/__init__.py b/openlp/plugins/songs/lib/__init__.py index c9328b31c..39ecb0ae2 100644 --- a/openlp/plugins/songs/lib/__init__.py +++ b/openlp/plugins/songs/lib/__init__.py @@ -28,7 +28,7 @@ ############################################################################### import re -from PyQt4 import QtGui, QtCore +from PyQt4 import QtGui from openlp.core.lib import translate from openlp.core.utils import CONTROL_CHARS, locale_direct_compare diff --git a/openlp/plugins/songs/lib/db.py b/openlp/plugins/songs/lib/db.py index 9c425b9d7..db5f59357 100644 --- a/openlp/plugins/songs/lib/db.py +++ b/openlp/plugins/songs/lib/db.py @@ -36,7 +36,6 @@ import re from sqlalchemy import Column, ForeignKey, Table, types from sqlalchemy.orm import mapper, relation, reconstructor from sqlalchemy.sql.expression import func -from PyQt4 import QtCore from openlp.core.lib.db import BaseModel, init_db diff --git a/openlp/plugins/songs/lib/dreambeamimport.py b/openlp/plugins/songs/lib/dreambeamimport.py index 759f8b055..4f8d343c4 100644 --- a/openlp/plugins/songs/lib/dreambeamimport.py +++ b/openlp/plugins/songs/lib/dreambeamimport.py @@ -30,8 +30,6 @@ The :mod:`dreambeamimport` module provides the functionality for importing DreamBeam songs into the OpenLP database. """ -import os -import sys import logging from lxml import etree, objectify @@ -46,11 +44,11 @@ class DreamBeamImport(SongImport): """ The :class:`DreamBeamImport` class provides the ability to import song files from DreamBeam. - + An example of DreamBeam xml mark-up:: - + - false 0.80 @@ -84,7 +82,7 @@ class DreamBeamImport(SongImport): * \*.xml """ - + def doImport(self): """ Receive a single file or a list of files to import. diff --git a/openlp/plugins/songs/lib/mediashoutimport.py b/openlp/plugins/songs/lib/mediashoutimport.py index 5f6cf6276..e3358c044 100644 --- a/openlp/plugins/songs/lib/mediashoutimport.py +++ b/openlp/plugins/songs/lib/mediashoutimport.py @@ -30,8 +30,6 @@ The :mod:`mediashoutimport` module provides the functionality for importing a MediaShout database into the OpenLP database. """ -import re -import os import pyodbc from openlp.core.lib import translate diff --git a/openlp/plugins/songs/lib/songproimport.py b/openlp/plugins/songs/lib/songproimport.py index 52ac79431..7556454d8 100644 --- a/openlp/plugins/songs/lib/songproimport.py +++ b/openlp/plugins/songs/lib/songproimport.py @@ -31,9 +31,7 @@ The :mod:`songproimport` module provides the functionality for importing SongPro songs into the OpenLP database. """ import re -import os -from openlp.core.lib import translate from openlp.plugins.songs.lib import strip_rtf from openlp.plugins.songs.lib.songimport import SongImport diff --git a/openlp/plugins/songs/lib/wowimport.py b/openlp/plugins/songs/lib/wowimport.py index b9f854468..aa327c0a0 100644 --- a/openlp/plugins/songs/lib/wowimport.py +++ b/openlp/plugins/songs/lib/wowimport.py @@ -107,13 +107,13 @@ class WowImport(SongImport): """ if isinstance(self.importSource, list): self.importWizard.progressBar.setMaximum(len(self.importSource)) - for file in self.importSource: + for source in self.importSource: if self.stopImportFlag: return self.setDefaults() - song_data = open(file, 'rb') + song_data = open(source, 'rb') if song_data.read(19) != u'WoW File\nSong Words': - self.logError(file, unicode(translate('SongsPlugin.WordsofWorshipSongImport', + self.logError(source, unicode(translate('SongsPlugin.WordsofWorshipSongImport', ('Invalid Words of Worship song file. Missing "Wow File\\nSong Words" header.')))) continue # Seek to byte which stores number of blocks in the song @@ -121,7 +121,7 @@ class WowImport(SongImport): no_of_blocks = ord(song_data.read(1)) song_data.seek(66) if song_data.read(16) != u'CSongDoc::CBlock': - self.logError(file, unicode(translate('SongsPlugin.WordsofWorshipSongImport', + self.logError(source, unicode(translate('SongsPlugin.WordsofWorshipSongImport', ('Invalid Words of Worship song file. Missing "CSongDoc::CBlock" string.')))) continue # Seek to the beginning of the first block @@ -150,9 +150,9 @@ class WowImport(SongImport): copyright_length = ord(song_data.read(1)) if copyright_length: self.addCopyright(unicode(song_data.read(copyright_length), u'cp1252')) - file_name = os.path.split(file)[1] + file_name = os.path.split(source)[1] # Get the song title self.title = file_name.rpartition(u'.')[0] song_data.close() if not self.finish(): - self.logError(file) + self.logError(source) diff --git a/openlp/plugins/songs/songsplugin.py b/openlp/plugins/songs/songsplugin.py index 31c7f10bf..b29291369 100644 --- a/openlp/plugins/songs/songsplugin.py +++ b/openlp/plugins/songs/songsplugin.py @@ -26,6 +26,10 @@ # with this program; if not, write to the Free Software Foundation, Inc., 59 # # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### +""" +The :mod:`~openlp.plugins.songs.songsplugin` module contains the Plugin class +for the Songs plugin. +""" import logging import os diff --git a/scripts/check_dependencies.py b/scripts/check_dependencies.py index fef8fc2be..be15414b4 100755 --- a/scripts/check_dependencies.py +++ b/scripts/check_dependencies.py @@ -38,6 +38,7 @@ modules, simply run this script:: """ import os import sys +from distutils.version import LooseVersion is_win = sys.platform.startswith('win') @@ -89,15 +90,13 @@ OPTIONAL_MODULES = [ w = sys.stdout.write def check_vers(version, required, text): - if type(version) is str: - version = version.split('.') - version = map(int, version) - if type(required) is str: - required = required.split('.') - required = map(int, required) - w(' %s >= %s ... ' % (text, '.'.join(map(str, required)))) - if version >= required: - w('.'.join(map(str, version)) + os.linesep) + if type(version) is not str: + version = '.'.join(map(str, version)) + if type(required) is not str: + required = '.'.join(map(str, required)) + w(' %s >= %s ... ' % (text, required)) + if LooseVersion(version) >= LooseVersion(required): + w(version + os.linesep) return True else: w('FAIL' + os.linesep) diff --git a/tests/functional/openlp_core_lib/test_lib.py b/tests/functional/openlp_core_lib/test_lib.py index 90e429b9a..de2847ac5 100644 --- a/tests/functional/openlp_core_lib/test_lib.py +++ b/tests/functional/openlp_core_lib/test_lib.py @@ -5,7 +5,8 @@ from unittest import TestCase 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 class TestLib(TestCase): @@ -197,3 +198,104 @@ 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' + + diff --git a/tests/functional/openlp_core_lib/test_serviceitem.py b/tests/functional/openlp_core_lib/test_serviceitem.py new file mode 100644 index 000000000..c2b9aacb1 --- /dev/null +++ b/tests/functional/openlp_core_lib/test_serviceitem.py @@ -0,0 +1,163 @@ +""" + Package to test the openlp.core.lib package. +""" +import os + +from unittest import TestCase +from mock import MagicMock +from openlp.core.lib import ServiceItem + +VERSE = u'The Lord said to {r}Noah{/r}: \n'\ + 'There\'s gonna be a {su}floody{/su}, {sb}floody{/sb}\n'\ + 'The Lord said to {g}Noah{/g}:\n'\ + 'There\'s gonna be a {st}floody{/st}, {it}floody{/it}\n'\ + 'Get those children out of the muddy, muddy \n'\ + '{r}C{/r}{b}h{/b}{bl}i{/bl}{y}l{/y}{g}d{/g}{pk}'\ + 'r{/pk}{o}e{/o}{pp}n{/pp} of the Lord\n' +FOOTER = [u'Arky Arky (Unknown)', u'Public Domain', u'CCLI 123456'] + +TESTPATH = os.path.abspath(os.path.join(os.path.dirname(__file__), u'..', u'..', u'resources')) + +class TestServiceItem(TestCase): + + def serviceitem_basic_test(self): + """ + Test the Service Item basic test + """ + # GIVEN: A new service item + + # WHEN:A service item is created (without a plugin) + service_item = ServiceItem(None) + + # THEN: We should get back a valid service item + assert service_item.is_valid is True, u'The new service item should be valid' + assert service_item.missing_frames() is True, u'There should not be any frames in the service item' + + def serviceitem_add_text_test(self): + """ + Test the Service Item add text test + """ + # GIVEN: A new service item + service_item = ServiceItem(None) + + # WHEN: adding text to a service item + service_item.add_from_text(VERSE) + service_item.raw_footer = FOOTER + + # THEN: We should get back a valid service item + assert service_item.is_valid is True, u'The new service item should be valid' + assert service_item.missing_frames() is False, u'check frames loaded ' + + # GIVEN: A service item with text + mocked_renderer = MagicMock() + mocked_renderer.format_slide.return_value = [VERSE] + service_item.renderer = mocked_renderer + + # WHEN: Render called + assert len(service_item._display_frames) == 0, u'A blank Service Item with no display frames' + service_item.render(True) + + # THEN: We should have a page of output. + assert len(service_item._display_frames) == 1, u'A valid rendered Service Item has 1 display frame' + assert service_item.get_rendered_frame(0) == VERSE.split(u'\n')[0], u'A output has rendered correctly.' + + def serviceitem_add_image_test(self): + """ + Test the Service Item add image test + """ + # GIVEN: A new service item and a mocked renderer + service_item = ServiceItem(None) + service_item.name = u'test' + mocked_renderer = MagicMock() + service_item.renderer = mocked_renderer + + # WHEN: adding image to a service item + test_image = os.path.join(TESTPATH, u'church.jpg') + service_item.add_from_image(test_image, u'Image Title') + + # THEN: We should get back a valid service item + assert service_item.is_valid is True, u'The new service item should be valid' + assert len(service_item._display_frames) == 0, u'The service item has no display frames' + + # THEN: We should have a page of output. + assert len(service_item._raw_frames) == 1, u'A valid rendered Service Item has display frames' + assert service_item.get_rendered_frame(0) == test_image + + # WHEN: adding a second image to a service item + service_item.add_from_image(test_image, u'Image1 Title') + + # THEN: We should have an increased page of output. + assert len(service_item._raw_frames) == 2, u'A valid rendered Service Item has display frames' + assert service_item.get_rendered_frame(0) == test_image + assert service_item.get_rendered_frame(0) == service_item.get_rendered_frame(1) + + # WHEN requesting a saved service item + service = service_item.get_service_repr(True) + + # THEN: We should have two parts of the service. + assert len(service) == 2, u'A saved service has two parts' + assert service[u'header'][u'name'] == u'test' , u'A test plugin was returned' + assert service[u'data'][0][u'title'] == u'Image Title' , u'The first title name matches the request' + assert service[u'data'][0][u'path'] == test_image , u'The first image name matches' + assert service[u'data'][0][u'title'] != service[u'data'][1][u'title'], \ + u'The individual titles should not match' + assert service[u'data'][0][u'path'] == service[u'data'][1][u'path'], u'The file paths should match' + + # WHEN validating a service item + service_item.validate_item([u'jpg']) + + # THEN the service item should be valid + assert service_item.is_valid is True, u'The new service item should be valid' + + # WHEN: adding a second image to a service item + service_item.add_from_image(u'resources/church1.jpg', u'Image1 Title') + + # WHEN validating a service item + service_item.validate_item([u'jpg']) + + # THEN the service item should be valid + assert service_item.is_valid is False, u'The service item is not valid due to validation changes' + + def serviceitem_add_command_test(self): + """ + Test the Service Item add command test + """ + # GIVEN: A new service item and a mocked renderer + service_item = ServiceItem(None) + service_item.name = u'test' + mocked_renderer = MagicMock() + service_item.renderer = mocked_renderer + + # WHEN: adding image to a service item + test_file = os.path.join(TESTPATH, u'church.jpg') + service_item.add_from_command(TESTPATH, u'church.jpg', test_file) + + # THEN: We should get back a valid service item + assert service_item.is_valid is True, u'The new service item should be valid' + assert len(service_item._display_frames) == 0, u'The service item has no display frames ' + + # THEN: We should have a page of output. + assert len(service_item._raw_frames) == 1, u'A valid rendered Service Item has one raw frame' + assert service_item.get_rendered_frame(0) == test_file, u'The image matches the input' + + # WHEN requesting a saved service item + service = service_item.get_service_repr(True) + + # THEN: We should have two parts of the service. + assert len(service) == 2, u'A saved service has two parts' + assert service[u'header'][u'name'] == u'test' , u'A test plugin' + assert service[u'data'][0][u'title'] == u'church.jpg' , u'The first title name ' + assert service[u'data'][0][u'path'] == TESTPATH , u'The first image name' + assert service[u'data'][0][u'image'] == test_file , u'The first image name' + + # WHEN validating a service item + service_item.validate_item([u'jpg']) + + # THEN the service item should be valid + assert service_item.is_valid is True, u'The service item is valid' + + # WHEN validating a service item with a different suffix + 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' diff --git a/tests/functional/openlp_core_ui/test_starttimedialog.py b/tests/functional/openlp_core_ui/test_starttimedialog.py new file mode 100644 index 000000000..5bac62229 --- /dev/null +++ b/tests/functional/openlp_core_ui/test_starttimedialog.py @@ -0,0 +1,48 @@ +""" +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 + +class TestStartTimeDialog(TestCase): + + def setUp(self): + """ + Create the UI + """ + self.app = QtGui.QApplication(sys.argv) + self.window = QtGui.QMainWindow() + self.form = starttimeform.StartTimeForm(self.window) + + def ui_defaults_test(self): + """ + Test StartTimeDialog defaults + """ + self.assertEqual(self.form.hourSpinBox.minimum(), 0) + self.assertEqual(self.form.hourSpinBox.maximum(), 4) + self.assertEqual(self.form.minuteSpinBox.minimum(), 0) + self.assertEqual(self.form.minuteSpinBox.maximum(), 59) + self.assertEqual(self.form.secondSpinBox.minimum(), 0) + self.assertEqual(self.form.secondSpinBox.maximum(), 59) + self.assertEqual(self.form.hourFinishSpinBox.minimum(), 0) + self.assertEqual(self.form.hourFinishSpinBox.maximum(), 4) + self.assertEqual(self.form.minuteFinishSpinBox.minimum(), 0) + self.assertEqual(self.form.minuteFinishSpinBox.maximum(), 59) + self.assertEqual(self.form.secondFinishSpinBox.minimum(), 0) + self.assertEqual(self.form.secondFinishSpinBox.maximum(), 59) + + def time_display_test(self): + """ + Test StartTimeDialog display initialisation + """ + #GIVEN: A service item with with time + mocked_serviceitem = MagicMock() + mocked_serviceitem.start_time = 61 + mocked_serviceitem.end_time = 3701 + + self.form.item = mocked_serviceitem + #self.form.exec_() diff --git a/tests/resources/church.jpg b/tests/resources/church.jpg new file mode 100644 index 000000000..826180870 Binary files /dev/null and b/tests/resources/church.jpg differ diff --git a/tests/test_app.py b/tests/test_app.py index 53e2eab0a..c0b1e651a 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -34,4 +34,4 @@ from openlp.core.ui.mainwindow import MainWindow def test_start_app(openlpapp): assert type(openlpapp) == OpenLP assert type(openlpapp.mainWindow) == MainWindow - assert unicode(openlpapp.mainWindow.windowTitle()) == u'OpenLP 2.0' + assert unicode(openlpapp.mainWindow.windowTitle()) == u'OpenLP 2.1'