diff --git a/openlp/core/lib/__init__.py b/openlp/core/lib/__init__.py index 155c10141..d6c338271 100644 --- a/openlp/core/lib/__init__.py +++ b/openlp/core/lib/__init__.py @@ -30,6 +30,7 @@ The :mod:`lib` module contains most of the components and libraries that make OpenLP work. """ +from __future__ import division from distutils.version import LooseVersion import logging import os @@ -155,7 +156,7 @@ def build_icon(icon): ``icon`` The icon to build. This can be a QIcon, a resource string in the form ``:/resource/file.png``, or a file - location like ``/path/to/file.png``. + location like ``/path/to/file.png``. However, the **recommended** way is to specify a resource string. """ button_icon = QtGui.QIcon() if isinstance(icon, QtGui.QIcon): @@ -202,12 +203,13 @@ def create_thumb(image_path, thumb_path, return_icon=True, size=None): States if an icon should be build and returned from the thumb. Defaults to ``True``. ``size`` - Allows to state a own size to use. Defaults to ``None``, which means that a default height of 88 is used. + Allows to state a own size (QtCore.QSize) to use. Defaults to ``None``, which means that a default height of 88 + is used. """ ext = os.path.splitext(thumb_path)[1].lower() reader = QtGui.QImageReader(image_path) if size is None: - ratio = float(reader.size().width()) / float(reader.size().height()) + ratio = reader.size().width() / reader.size().height() reader.setScaledSize(QtCore.QSize(int(ratio * 88), 88)) else: reader.setScaledSize(size) @@ -260,8 +262,8 @@ def resize_image(image_path, width, height, background=u'#000000'): log.debug(u'resize_image - start') reader = QtGui.QImageReader(image_path) # The image's ratio. - image_ratio = float(reader.size().width()) / float(reader.size().height()) - resize_ratio = float(width) / float(height) + image_ratio = reader.size().width() / reader.size().height() + resize_ratio = width / height # Figure out the size we want to resize the image to (keep aspect ratio). if image_ratio == resize_ratio: size = QtCore.QSize(width, height) @@ -282,7 +284,7 @@ def resize_image(image_path, width, height, background=u'#000000'): new_image = QtGui.QImage(width, height, QtGui.QImage.Format_ARGB32_Premultiplied) painter = QtGui.QPainter(new_image) painter.fillRect(new_image.rect(), QtGui.QColor(background)) - painter.drawImage((width - real_width) / 2, (height - real_height) / 2, preview) + painter.drawImage((width - real_width) // 2, (height - real_height) // 2, preview) return new_image diff --git a/openlp/core/lib/dockwidget.py b/openlp/core/lib/dockwidget.py index 30182c901..15c116e0f 100644 --- a/openlp/core/lib/dockwidget.py +++ b/openlp/core/lib/dockwidget.py @@ -30,6 +30,7 @@ """ Provide additional functionality required by OpenLP from the inherited QDockWidget. """ +from __future__ import division import logging from PyQt4 import QtGui @@ -55,7 +56,7 @@ class OpenLPDockWidget(QtGui.QDockWidget): self.setWindowIcon(build_icon(icon)) # Sort out the minimum width. screens = ScreenList() - main_window_docbars = screens.current[u'size'].width() / 5 + main_window_docbars = screens.current[u'size'].width() // 5 if main_window_docbars > 300: self.setMinimumWidth(300) else: diff --git a/openlp/core/lib/htmlbuilder.py b/openlp/core/lib/htmlbuilder.py index 9c8b04076..d4e22b0dd 100644 --- a/openlp/core/lib/htmlbuilder.py +++ b/openlp/core/lib/htmlbuilder.py @@ -26,7 +26,7 @@ # with this program; if not, write to the Free Software Foundation, Inc., 59 # # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### - +from __future__ import division import logging from PyQt4 import QtWebKit @@ -276,7 +276,7 @@ def build_background_css(item, width): ``item`` Service Item containing theme and location information """ - width = int(width) / 2 + width = int(width) // 2 theme = item.themedata background = u'background-color: black' if theme: diff --git a/openlp/core/lib/mediamanageritem.py b/openlp/core/lib/mediamanageritem.py index cd2dcc67e..c3e1fa366 100644 --- a/openlp/core/lib/mediamanageritem.py +++ b/openlp/core/lib/mediamanageritem.py @@ -102,7 +102,6 @@ class MediaManagerItem(QtGui.QWidget): self.setupUi() self.retranslateUi() self.auto_select_id = -1 - Registry().register_function(u'%s_service_load' % self.plugin.name, self.service_load) # Need to use event as called across threads and UI is updated QtCore.QObject.connect(self, QtCore.SIGNAL(u'%s_go_live' % self.plugin.name), self.go_live_remote) QtCore.QObject.connect(self, QtCore.SIGNAL(u'%s_add_to_service' % self.plugin.name), self.add_to_service_remote) @@ -585,12 +584,15 @@ class MediaManagerItem(QtGui.QWidget): else: return None - def service_load(self, message): + def service_load(self, item): """ Method to add processing when a service has been loaded and individual service items need to be processed by the plugins. + + ``item`` + The item to be processed and returned. """ - pass + return item def check_search_result(self): """ diff --git a/openlp/core/lib/renderer.py b/openlp/core/lib/renderer.py index c426e8871..5161246c0 100644 --- a/openlp/core/lib/renderer.py +++ b/openlp/core/lib/renderer.py @@ -26,7 +26,7 @@ # with this program; if not, write to the Free Software Foundation, Inc., 59 # # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### - +from __future__ import division import logging from PyQt4 import QtGui, QtCore, QtWebKit @@ -327,7 +327,7 @@ class Renderer(object): screen_size = self.screens.current[u'size'] self.width = screen_size.width() self.height = screen_size.height() - self.screen_ratio = float(self.height) / float(self.width) + self.screen_ratio = self.height / self.width log.debug(u'_calculate default %s, %f' % (screen_size, self.screen_ratio)) # 90% is start of footer self.footer_start = int(self.height * 0.90) @@ -546,15 +546,15 @@ class Renderer(object): """ smallest_index = 0 highest_index = len(html_list) - 1 - index = int(highest_index / 2) + index = highest_index // 2 while True: if not self._text_fits_on_slide(previous_html + separator.join(html_list[:index + 1]).strip()): # We know that it does not fit, so change/calculate the new index and highest_index accordingly. highest_index = index - index = int(index - (index - smallest_index) / 2) + index = index - (index - smallest_index) // 2 else: smallest_index = index - index = int(index + (highest_index - index) / 2) + index = index + (highest_index - index) // 2 # We found the number of words which will fit. if smallest_index == index or highest_index == index: index = smallest_index @@ -582,7 +582,7 @@ class Renderer(object): html_list[0] = html_tags + html_list[0] smallest_index = 0 highest_index = len(html_list) - 1 - index = int(highest_index / 2) + index = highest_index // 2 return previous_html, previous_raw def _text_fits_on_slide(self, text): diff --git a/openlp/core/lib/screen.py b/openlp/core/lib/screen.py index 84e7e4258..146b492db 100644 --- a/openlp/core/lib/screen.py +++ b/openlp/core/lib/screen.py @@ -30,6 +30,7 @@ The :mod:`screen` module provides management functionality for a machines' displays. """ +from __future__ import division import logging import copy @@ -232,8 +233,8 @@ class ScreenList(object): ``window`` A QWidget we are finding the location of. """ - x = window.x() + (window.width() / 2) - y = window.y() + (window.height() / 2) + x = window.x() + (window.width() // 2) + y = window.y() + (window.height() // 2) for screen in self.screen_list: size = screen[u'size'] if x >= size.x() and x <= (size.x() + size.width()) and y >= size.y() and y <= (size.y() + size.height()): diff --git a/openlp/core/lib/searchedit.py b/openlp/core/lib/searchedit.py index 9623b0f53..a0c51cb74 100644 --- a/openlp/core/lib/searchedit.py +++ b/openlp/core/lib/searchedit.py @@ -26,7 +26,7 @@ # with this program; if not, write to the Free Software Foundation, Inc., 59 # # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### - +from __future__ import division import logging from PyQt4 import QtCore, QtGui @@ -85,10 +85,10 @@ class SearchEdit(QtGui.QLineEdit): size = self.clear_button.size() frame_width = self.style().pixelMetric(QtGui.QStyle.PM_DefaultFrameWidth) self.clear_button.move(self.rect().right() - frame_width - size.width(), - (self.rect().bottom() + 1 - size.height()) / 2) + (self.rect().bottom() + 1 - size.height()) // 2) if hasattr(self, u'menu_button'): size = self.menu_button.size() - self.menu_button.move(self.rect().left() + frame_width + 2, (self.rect().bottom() + 1 - size.height()) / 2) + self.menu_button.move(self.rect().left() + frame_width + 2, (self.rect().bottom() + 1 - size.height()) // 2) def current_search_type(self): """ diff --git a/openlp/core/lib/serviceitem.py b/openlp/core/lib/serviceitem.py index c4ac846c9..b32e1aaf0 100644 --- a/openlp/core/lib/serviceitem.py +++ b/openlp/core/lib/serviceitem.py @@ -58,8 +58,7 @@ class ItemCapabilities(object): Provides an enumeration of a service item's capabilities ``CanPreview`` - The capability to allow the ServiceManager to add to the preview - tab when making the previous item live. + The capability to allow the ServiceManager to add to the preview tab when making the previous item live. ``CanEdit`` The capability to allow the ServiceManager to allow the item to be edited @@ -71,8 +70,7 @@ class ItemCapabilities(object): Determines is the service_item needs a Media Player ``CanLoop`` - The capability to allow the SlideController to allow the loop - processing. + The capability to allow the SlideController to allow the loop processing. ``CanAppend`` The capability to allow the ServiceManager to add leaves to the @@ -82,22 +80,19 @@ class ItemCapabilities(object): The capability to remove lines breaks in the renderer ``OnLoadUpdate`` - The capability to update MediaManager when a service Item is - loaded. + The capability to update MediaManager when a service Item is loaded. ``AddIfNewItem`` Not Used ``ProvidesOwnDisplay`` - The capability to tell the SlideController the service Item has a - different display. + The capability to tell the SlideController the service Item has a different display. ``HasDetailedTitleDisplay`` - ServiceItem provides a title + Being Removed and decommissioned. ``HasVariableStartTime`` - The capability to tell the ServiceManager that a change to start - time is possible. + The capability to tell the ServiceManager that a change to start time is possible. ``CanSoftBreak`` The capability to tell the renderer that Soft Break is allowed @@ -149,7 +144,7 @@ class ServiceItem(object): if plugin: self.name = plugin.name self.title = u'' - self.shortname = u'' + self.processor = None self.audit = u'' self.items = [] self.iconic_representation = None @@ -353,7 +348,8 @@ class ServiceItem(object): u'media_length': self.media_length, u'background_audio': self.background_audio, u'theme_overwritten': self.theme_overwritten, - u'will_auto_start': self.will_auto_start + u'will_auto_start': self.will_auto_start, + u'processor': self.processor } service_data = [] if self.service_item_type == ServiceItemType.Text: @@ -387,7 +383,6 @@ class ServiceItem(object): self.title = header[u'title'] self.name = header[u'name'] self.service_item_type = header[u'type'] - self.shortname = header[u'plugin'] self.theme = header[u'theme'] self.add_icon(header[u'icon']) self.raw_footer = header[u'footer'] @@ -406,7 +401,13 @@ class ServiceItem(object): 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.processor = header.get(u'processor', None) self.has_original_files = True + #TODO Remove me in 2,3 build phase + if self.is_capable(ItemCapabilities.HasDetailedTitleDisplay): + self.capabilities.remove(ItemCapabilities.HasDetailedTitleDisplay) + self.processor = self.title + self.title = None if u'background_audio' in header: self.background_audio = [] for filename in header[u'background_audio']: @@ -429,6 +430,8 @@ class ServiceItem(object): 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 not self.title: + self.title = text_image[u'title'] if path: self.has_original_files = False self.add_from_command(path, text_image[u'title'], text_image[u'image']) @@ -443,9 +446,7 @@ class ServiceItem(object): if self.is_text(): return self.title else: - if ItemCapabilities.HasDetailedTitleDisplay in self.capabilities: - return self._raw_frames[0][u'title'] - elif len(self._raw_frames) > 1: + if len(self._raw_frames) > 1: return self.title else: return self._raw_frames[0][u'title'] diff --git a/openlp/core/lib/settingstab.py b/openlp/core/lib/settingstab.py index d63f9c678..ab775599a 100644 --- a/openlp/core/lib/settingstab.py +++ b/openlp/core/lib/settingstab.py @@ -30,6 +30,7 @@ The :mod:`~openlp.core.lib.settingstab` module contains the base SettingsTab class which plugins use for adding their own tab to the settings dialog. """ +from __future__ import division from PyQt4 import QtGui @@ -90,7 +91,7 @@ class SettingsTab(QtGui.QWidget): QtGui.QWidget.resizeEvent(self, event) width = self.width() - self.tab_layout.spacing() - \ self.tab_layout.contentsMargins().left() - self.tab_layout.contentsMargins().right() - left_width = min(width - self.right_column.minimumSizeHint().width(), width / 2) + left_width = min(width - self.right_column.minimumSizeHint().width(), width // 2) left_width = max(left_width, self.left_column.minimumSizeHint().width()) self.left_column.setFixedWidth(left_width) diff --git a/openlp/core/ui/maindisplay.py b/openlp/core/ui/maindisplay.py index 2504520c0..02ae469d6 100644 --- a/openlp/core/ui/maindisplay.py +++ b/openlp/core/ui/maindisplay.py @@ -35,6 +35,7 @@ Some of the code for this form is based on the examples at: * `http://html5demos.com/two-videos`_ """ +from __future__ import division import cgi import logging import sys @@ -207,8 +208,8 @@ class MainDisplay(Display): painter_image.begin(self.initial_fame) painter_image.fillRect(self.initial_fame.rect(), background_color) painter_image.drawImage( - (self.screen[u'size'].width() - splash_image.width()) / 2, - (self.screen[u'size'].height() - splash_image.height()) / 2, + (self.screen[u'size'].width() - splash_image.width()) // 2, + (self.screen[u'size'].height() - splash_image.height()) // 2, splash_image) service_item = ServiceItem() service_item.bg_image_bytes = image_to_byte(self.initial_fame) @@ -268,7 +269,7 @@ class MainDisplay(Display): self.resize(self.width(), alert_height) self.setVisible(True) if location == AlertLocation.Middle: - self.move(self.screen[u'size'].left(), (self.screen[u'size'].height() - alert_height) / 2) + self.move(self.screen[u'size'].left(), (self.screen[u'size'].height() - alert_height) // 2) elif location == AlertLocation.Bottom: self.move(self.screen[u'size'].left(), self.screen[u'size'].height() - alert_height) else: diff --git a/openlp/core/ui/mainwindow.py b/openlp/core/ui/mainwindow.py index 2afbb4eb0..6b86bfe7a 100644 --- a/openlp/core/ui/mainwindow.py +++ b/openlp/core/ui/mainwindow.py @@ -779,8 +779,8 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow): """ We need to make sure, that the SlidePreview's size is correct. """ - self.preview_controller.previewSizeChanged() - self.live_controller.previewSizeChanged() + self.preview_controller.preview_size_changed() + self.live_controller.preview_size_changed() def on_settings_shortcuts_item_clicked(self): """ @@ -989,8 +989,8 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow): self.application.set_busy_cursor() self.image_manager.update_display() self.renderer.update_display() - self.preview_controller.screenSizeChanged() - self.live_controller.screenSizeChanged() + self.preview_controller.screen_size_changed() + self.live_controller.screen_size_changed() self.setFocus() self.activateWindow() self.application.set_normal_cursor() diff --git a/openlp/core/ui/media/mediacontroller.py b/openlp/core/ui/media/mediacontroller.py index 1e011a84d..71f2b4b10 100644 --- a/openlp/core/ui/media/mediacontroller.py +++ b/openlp/core/ui/media/mediacontroller.py @@ -466,8 +466,8 @@ class MediaController(object): The ServiceItem containing the details to be played. """ used_players = get_media_players()[0] - if service_item.title != UiStrings().Automatic: - used_players = [service_item.title.lower()] + if service_item.processor != UiStrings().Automatic: + used_players = [service_item.processor.lower()] if controller.media_info.file_info.isFile(): suffix = u'*.%s' % controller.media_info.file_info.suffix().lower() for title in used_players: diff --git a/openlp/core/ui/servicemanager.py b/openlp/core/ui/servicemanager.py index 43280aef8..8ddd4135e 100644 --- a/openlp/core/ui/servicemanager.py +++ b/openlp/core/ui/servicemanager.py @@ -295,8 +295,8 @@ class ServiceManager(QtGui.QWidget, ServiceManagerDialog): Sets up the service manager, toolbars, list view, et al. """ QtGui.QWidget.__init__(self, parent) - self.active = build_icon(QtGui.QImage(u':/media/auto-start_active.png')) - self.inactive = build_icon(QtGui.QImage(u':/media/auto-start_inactive.png')) + self.active = build_icon(u':/media/auto-start_active.png') + self.inactive = build_icon(u':/media/auto-start_inactive.png') Registry().register(u'service_manager', self) self.service_items = [] self.suffixes = [] @@ -715,13 +715,10 @@ class ServiceManager(QtGui.QWidget, ServiceManagerDialog): else: service_item.set_from_service(item, self.servicePath) service_item.validate_item(self.suffixes) - self.load_item_unique_identifier = 0 if service_item.is_capable(ItemCapabilities.OnLoadUpdate): - Registry().execute(u'%s_service_load' % service_item.name.lower(), service_item) - # if the item has been processed - if service_item.unique_identifier == self.load_item_unique_identifier: - service_item.edit_id = int(self.load_item_edit_id) - service_item.temporary_edit = self.load_item_temporary + new_item = Registry().get(service_item.name).service_load(service_item) + if new_item: + service_item = new_item self.add_service_item(service_item, repaint=False) delete_file(p_file) self.main_window.add_recent_file(file_name) @@ -1260,14 +1257,6 @@ class ServiceManager(QtGui.QWidget, ServiceManagerDialog): self.repaint_service_list(-1, -1) self.application.set_normal_cursor() - def service_item_update(self, edit_id, unique_identifier, temporary=False): - """ - Triggered from plugins to update service items. Save the values as they will be used as part of the service load - """ - self.load_item_unique_identifier = unique_identifier - self.load_item_edit_id = int(edit_id) - self.load_item_temporary = str_to_bool(temporary) - def replace_service_item(self, newItem): """ Using the service item passed replace the one with the same edit id if found. diff --git a/openlp/core/ui/slidecontroller.py b/openlp/core/ui/slidecontroller.py index 86b114e1e..febef0850 100644 --- a/openlp/core/ui/slidecontroller.py +++ b/openlp/core/ui/slidecontroller.py @@ -89,7 +89,7 @@ class SlideController(DisplayController): Set up the Slide Controller. """ DisplayController.__init__(self, parent, is_live) - Registry().register_function(u'bootstrap_post_set_up', self.screenSizeChanged) + Registry().register_function(u'bootstrap_post_set_up', self.screen_size_changed) self.screens = ScreenList() try: self.ratio = float(self.screens.current[u'size'].width()) / float(self.screens.current[u'size'].height()) @@ -121,6 +121,8 @@ class SlideController(DisplayController): self.update_slide_limits() self.panel = QtGui.QWidget(parent.controlSplitter) self.slideList = {} + self.slide_count = 0 + self.slide_image = None # Layout for holding panel self.panel_layout = QtGui.QVBoxLayout(self.panel) self.panel_layout.setSpacing(0) @@ -321,18 +323,18 @@ class SlideController(DisplayController): self.slide_layout.insertWidget(0, self.preview_display) self.preview_display.hide() # Actual preview screen - self.slidePreview = QtGui.QLabel(self) - sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.slidePreview.sizePolicy().hasHeightForWidth()) - self.slidePreview.setSizePolicy(sizePolicy) - self.slidePreview.setFrameShape(QtGui.QFrame.Box) - self.slidePreview.setFrameShadow(QtGui.QFrame.Plain) - self.slidePreview.setLineWidth(1) - self.slidePreview.setScaledContents(True) - self.slidePreview.setObjectName(u'slidePreview') - self.slide_layout.insertWidget(0, self.slidePreview) + self.slide_preview = QtGui.QLabel(self) + size_policy = QtGui.QSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed) + size_policy.setHorizontalStretch(0) + size_policy.setVerticalStretch(0) + size_policy.setHeightForWidth(self.slide_preview.sizePolicy().hasHeightForWidth()) + self.slide_preview.setSizePolicy(size_policy) + self.slide_preview.setFrameShape(QtGui.QFrame.Box) + self.slide_preview.setFrameShadow(QtGui.QFrame.Plain) + self.slide_preview.setLineWidth(1) + self.slide_preview.setScaledContents(True) + self.slide_preview.setObjectName(u'slide_preview') + self.slide_layout.insertWidget(0, self.slide_preview) self.grid.addLayout(self.slide_layout, 0, 0, 1, 1) if self.is_live: self.current_shortcut = u'' @@ -517,10 +519,9 @@ class SlideController(DisplayController): self.service_manager.next_item() self.keypress_loop = False - def screenSizeChanged(self): + def screen_size_changed(self): """ - Settings dialog has changed the screen size of adjust output and - screen previews. + Settings dialog has changed the screen size of adjust output and screen previews. """ # rebuild display as screen size changed if self.display: @@ -536,14 +537,14 @@ class SlideController(DisplayController): except ZeroDivisionError: self.ratio = 1 self.media_controller.setup_display(self.display, False) - self.previewSizeChanged() + self.preview_size_changed() self.preview_display.setup() service_item = ServiceItem() self.preview_display.web_view.setHtml(build_html(service_item, self.preview_display.screen, None, self.is_live, plugins=self.plugin_manager.plugins)) self.media_controller.setup_display(self.preview_display, True) if self.service_item: - self.refreshServiceItem() + self.refresh_service_item() def __addActionsToWidget(self, widget): """ @@ -554,7 +555,7 @@ class SlideController(DisplayController): self.previousService, self.nextService, self.escapeItem]) - def previewSizeChanged(self): + def preview_size_changed(self): """ Takes care of the SlidePreview's size. Is called when one of the the splitters is moved or when the screen size is changed. Note, that this @@ -563,14 +564,14 @@ class SlideController(DisplayController): if self.ratio < float(self.preview_frame.width()) / float(self.preview_frame.height()): # We have to take the height as limit. max_height = self.preview_frame.height() - self.grid.margin() * 2 - self.slidePreview.setFixedSize(QtCore.QSize(max_height * self.ratio, max_height)) + self.slide_preview.setFixedSize(QtCore.QSize(max_height * self.ratio, max_height)) self.preview_display.setFixedSize(QtCore.QSize(max_height * self.ratio, max_height)) self.preview_display.screen = { u'size': self.preview_display.geometry()} else: # We have to take the width as limit. max_width = self.preview_frame.width() - self.grid.margin() * 2 - self.slidePreview.setFixedSize(QtCore.QSize(max_width, max_width / self.ratio)) + self.slide_preview.setFixedSize(QtCore.QSize(max_width, max_width / self.ratio)) self.preview_display.setFixedSize(QtCore.QSize(max_width, max_width / self.ratio)) self.preview_display.screen = { u'size': self.preview_display.geometry()} @@ -624,17 +625,16 @@ class SlideController(DisplayController): """ self.slide_limits = Settings().value(self.main_window.advanced_settings_section + u'/slide limits') - def enableToolBar(self, item): + def enable_tool_bar(self, item): """ - Allows the toolbars to be reconfigured based on Controller Type - and ServiceItem Type + Allows the toolbars to be reconfigured based on Controller Type and ServiceItem Type """ if self.is_live: - self.enableLiveToolBar(item) + self.enable_live_tool_bar(item) else: - self.enablePreviewToolBar(item) + self.enable_preview_tool_bar(item) - def enableLiveToolBar(self, item): + def enable_live_tool_bar(self, item): """ Allows the live toolbar to be customised """ @@ -663,7 +663,7 @@ class SlideController(DisplayController): # See bug #791050 self.toolbar.show() - def enablePreviewToolBar(self, item): + def enable_preview_tool_bar(self, item): """ Allows the Preview toolbar to be customised """ @@ -682,15 +682,15 @@ class SlideController(DisplayController): # See bug #791050 self.toolbar.show() - def refreshServiceItem(self): + def refresh_service_item(self): """ Method to update the service item if the screen has changed """ - log.debug(u'refreshServiceItem live = %s' % self.is_live) + log.debug(u'refresh_service_item live = %s' % self.is_live) if self.service_item.is_text() or self.service_item.is_image(): item = self.service_item item.render() - self._processItem(item, self.selected_row) + self._process_item(item, self.selected_row) def add_service_item(self, item): """ @@ -703,14 +703,14 @@ class SlideController(DisplayController): if self.song_edit: slideno = self.selected_row self.song_edit = False - self._processItem(item, slideno) + self._process_item(item, slideno) def replaceServiceManagerItem(self, item): """ Replacement item following a remote edit """ if item == self.service_item: - self._processItem(item, self.preview_list_widget.currentRow()) + self._process_item(item, self.preview_list_widget.currentRow()) def addServiceManagerItem(self, item, slideno): """ @@ -729,7 +729,7 @@ class SlideController(DisplayController): self.__checkUpdateSelectedSlide(slidenum) self.slideSelected() else: - self._processItem(item, slidenum) + self._process_item(item, slidenum) if self.is_live and item.auto_play_slides_loop and item.timed_slide_interval > 0: self.play_slides_loop.setChecked(item.auto_play_slides_loop) self.delay_spin_box.setValue(int(item.timed_slide_interval)) @@ -739,7 +739,7 @@ class SlideController(DisplayController): self.delay_spin_box.setValue(int(item.timed_slide_interval)) self.onPlaySlidesOnce() - def _processItem(self, service_item, slideno): + def _process_item(self, service_item, slideno): """ Loads a ServiceItem into the system from ServiceManager Display the slide number passed @@ -827,10 +827,9 @@ class SlideController(DisplayController): self.preview_list_widget.setVerticalHeaderLabels(text) if self.service_item.is_text(): self.preview_list_widget.resizeRowsToContents() - self.preview_list_widget.setColumnWidth(0, - self.preview_list_widget.viewport().size().width()) + self.preview_list_widget.setColumnWidth(0, self.preview_list_widget.viewport().size().width()) self.__updatePreviewSelection(slideno) - self.enableToolBar(service_item) + self.enable_tool_bar(service_item) # Pass to display for viewing. # Postpone image build, we need to do this later to avoid the theme # flashing on the screen @@ -1050,27 +1049,28 @@ class SlideController(DisplayController): def updatePreview(self): """ - This updates the preview frame, for example after changing a slide or - using *Blank to Theme*. + This updates the preview frame, for example after changing a slide or using *Blank to Theme*. """ log.debug(u'updatePreview %s ' % self.screens.current[u'primary']) if not self.screens.current[u'primary'] and self.service_item and \ self.service_item.is_capable(ItemCapabilities.ProvidesOwnDisplay): - # Grab now, but try again in a couple of seconds if slide change - # is slow - QtCore.QTimer.singleShot(0.5, self.grabMainDisplay) - QtCore.QTimer.singleShot(2.5, self.grabMainDisplay) + # Grab now, but try again in a couple of seconds if slide change is slow + QtCore.QTimer.singleShot(0.5, self.grab_maindisplay) + QtCore.QTimer.singleShot(2.5, self.grab_maindisplay) else: - self.slidePreview.setPixmap(self.display.preview()) + self.slide_image = self.display.preview() + self.slide_preview.setPixmap(self.slide_image) + self.slide_count += 1 - def grabMainDisplay(self): + def grab_maindisplay(self): """ Creates an image of the current screen and updates the preview frame. """ - winid = QtGui.QApplication.desktop().winId() + win_id = QtGui.QApplication.desktop().winId() rect = self.screens.current[u'size'] - winimg = QtGui.QPixmap.grabWindow(winid, rect.x(), rect.y(), rect.width(), rect.height()) - self.slidePreview.setPixmap(winimg) + win_image = QtGui.QPixmap.grabWindow(win_id, rect.x(), rect.y(), rect.width(), rect.height()) + self.slide_preview.setPixmap(win_image) + self.slide_image = win_image def on_slide_selected_next_action(self, checked): """ @@ -1276,7 +1276,7 @@ class SlideController(DisplayController): self.media_controller.video(self.controller_type, item, self.hide_mode()) if not self.is_live: self.preview_display.show() - self.slidePreview.hide() + self.slide_preview.hide() def onMediaClose(self): """ @@ -1285,7 +1285,7 @@ class SlideController(DisplayController): log.debug(u'SlideController onMediaClose') self.media_controller.media_reset(self) self.preview_display.hide() - self.slidePreview.show() + self.slide_preview.show() def _resetBlank(self): """ diff --git a/openlp/core/ui/themestab.py b/openlp/core/ui/themestab.py index f0f821494..d6dba2880 100644 --- a/openlp/core/ui/themestab.py +++ b/openlp/core/ui/themestab.py @@ -29,6 +29,8 @@ """ The Themes configuration tab """ +from __future__ import division + from PyQt4 import QtCore, QtGui from openlp.core.lib import Registry, Settings, SettingsTab, UiStrings, translate @@ -90,7 +92,7 @@ class ThemesTab(SettingsTab): self.global_level_label.setObjectName(u'global_level_label') self.level_layout.addRow(self.global_level_radio_button, self.global_level_label) label_top_margin = (self.song_level_radio_button.sizeHint().height() - - self.song_level_label.sizeHint().height()) / 2 + self.song_level_label.sizeHint().height()) // 2 for label in [self.song_level_label, self.service_level_label, self.global_level_label]: rect = label.rect() rect.setTop(rect.top() + label_top_margin) diff --git a/openlp/plugins/alerts/forms/alertdialog.py b/openlp/plugins/alerts/forms/alertdialog.py index e8a50d183..db2579298 100644 --- a/openlp/plugins/alerts/forms/alertdialog.py +++ b/openlp/plugins/alerts/forms/alertdialog.py @@ -75,9 +75,9 @@ class Ui_AlertDialog(object): self.manage_button_layout.addWidget(self.delete_button) self.manage_button_layout.addStretch() self.alert_dialog_layout.addLayout(self.manage_button_layout, 1, 1) - displayIcon = build_icon(u':/general/general_live.png') - self.display_button = create_button(alert_dialog, u'display_button', icon=displayIcon, enabled=False) - self.display_close_button = create_button(alert_dialog, u'display_close_button', icon=displayIcon, + display_icon = build_icon(u':/general/general_live.png') + self.display_button = create_button(alert_dialog, u'display_button', icon=display_icon, enabled=False) + self.display_close_button = create_button(alert_dialog, u'display_close_button', icon=display_icon, enabled=False) self.button_box = create_button_box(alert_dialog, u'button_box', [u'close'], [self.display_button, self.display_close_button]) diff --git a/openlp/plugins/bibles/forms/bibleimportform.py b/openlp/plugins/bibles/forms/bibleimportform.py index 6920be61c..0662f78db 100644 --- a/openlp/plugins/bibles/forms/bibleimportform.py +++ b/openlp/plugins/bibles/forms/bibleimportform.py @@ -511,7 +511,7 @@ class BibleImportForm(OpenLPWizard): name = bible[u'abbreviation'] self.web_bible_list[download_type][version] = name.strip() - def preWizard(self): + def pre_wizard(self): """ Prepare the UI for the import. """ diff --git a/openlp/plugins/bibles/forms/bibleupgradeform.py b/openlp/plugins/bibles/forms/bibleupgradeform.py index d8f329ee7..caba4b30f 100644 --- a/openlp/plugins/bibles/forms/bibleupgradeform.py +++ b/openlp/plugins/bibles/forms/bibleupgradeform.py @@ -105,7 +105,7 @@ class BibleUpgradeForm(OpenLPWizard): Perform necessary functions depending on which wizard page is active. """ if self.page(pageId) == self.progress_page: - self.preWizard() + self.pre_wizard() self.performWizard() self.post_wizard() elif self.page(pageId) == self.selectPage and not self.files: @@ -329,7 +329,7 @@ class BibleUpgradeForm(OpenLPWizard): self.cancel_button.setVisible(True) settings.endGroup() - def preWizard(self): + def pre_wizard(self): """ Prepare the UI for the upgrade. """ diff --git a/openlp/plugins/custom/lib/mediaitem.py b/openlp/plugins/custom/lib/mediaitem.py index f51db8e6b..c2c67c304 100644 --- a/openlp/plugins/custom/lib/mediaitem.py +++ b/openlp/plugins/custom/lib/mediaitem.py @@ -40,6 +40,7 @@ from openlp.plugins.custom.lib.db import CustomSlide log = logging.getLogger(__name__) + class CustomSearch(object): """ An enumeration for custom search methods. @@ -214,7 +215,6 @@ class CustomMediaItem(MediaManagerItem): Settings().setValue(u'%s/last search type' % self.settings_section, self.search_text_edit.current_search_type()) # Reload the list considering the new search type. search_keywords = self.search_text_edit.displayText() - search_results = [] search_type = self.search_text_edit.current_search_type() if search_type == CustomSearch.Titles: log.debug(u'Titles Search') @@ -252,7 +252,8 @@ class CustomMediaItem(MediaManagerItem): and_(CustomSlide.title == item.title, CustomSlide.theme_name == item.theme, CustomSlide.credits == item.raw_footer[0][len(item.title) + 1:])) if custom: - self.service_manager.service_item_update(custom.id, item.unique_identifier) + item.edit_id = custom.id + return item else: if self.add_custom_from_service: self.create_from_service_item(item) @@ -281,8 +282,6 @@ class CustomMediaItem(MediaManagerItem): custom.text = unicode(custom_xml.extract_xml(), u'utf-8') self.plugin.manager.save_object(custom) self.on_search_text_button_clicked() - if item.name.lower() == u'custom': - Registry().execute(u'service_item_update', u'%s:%s:%s' % (custom.id, item.unique_identifier, False)) def on_clear_text_button_click(self): """ diff --git a/openlp/plugins/images/forms/choosegroupform.py b/openlp/plugins/images/forms/choosegroupform.py index f11c8324c..02f79e63b 100644 --- a/openlp/plugins/images/forms/choosegroupform.py +++ b/openlp/plugins/images/forms/choosegroupform.py @@ -50,6 +50,7 @@ class ChooseGroupForm(QtGui.QDialog, Ui_ChooseGroupDialog): ``selected_group`` The ID of the group that should be selected by default when showing the dialog. """ + self.new_group_edit.clear() if selected_group is not None: for index in range(self.group_combobox.count()): if self.group_combobox.itemData(index) == selected_group: diff --git a/openlp/plugins/images/lib/imagetab.py b/openlp/plugins/images/lib/imagetab.py index 83d015cb8..b43b19097 100644 --- a/openlp/plugins/images/lib/imagetab.py +++ b/openlp/plugins/images/lib/imagetab.py @@ -32,7 +32,6 @@ from PyQt4 import QtGui from openlp.core.lib import Registry, SettingsTab, Settings, UiStrings, translate - class ImageTab(SettingsTab): """ ImageTab is the images settings tab in the settings dialog. diff --git a/openlp/plugins/images/lib/mediaitem.py b/openlp/plugins/images/lib/mediaitem.py index 95f0971fd..deed14594 100644 --- a/openlp/plugins/images/lib/mediaitem.py +++ b/openlp/plugins/images/lib/mediaitem.py @@ -391,6 +391,7 @@ class ImageMediaItem(MediaManagerItem): ``initial_load`` When set to False, the busy cursor and progressbar will be shown while loading images """ + parent_group = None if target_group is None: # Find out if a group must be pre-selected preselect_group = None @@ -436,6 +437,8 @@ class ImageMediaItem(MediaManagerItem): parent_group.parent_id = 0 parent_group.group_name = self.choose_group_form.new_group_edit.text() self.manager.save_object(parent_group) + self.fill_groups_combobox(self.choose_group_form.group_combobox) + self.fill_groups_combobox(self.add_group_form.parent_group_combobox) else: parent_group = target_group.data(0, QtCore.Qt.UserRole) if isinstance(parent_group, ImageFilenames): @@ -550,28 +553,25 @@ class ImageMediaItem(MediaManagerItem): service_item.add_capability(ItemCapabilities.CanAppend) # force a nonexistent theme service_item.theme = -1 - missing_items = [] missing_items_filenames = [] + images_filenames = [] # Expand groups to images for bitem in items: if isinstance(bitem.data(0, QtCore.Qt.UserRole), ImageGroups) or bitem.data(0, QtCore.Qt.UserRole) is None: for index in range(0, bitem.childCount()): if isinstance(bitem.child(index).data(0, QtCore.Qt.UserRole), ImageFilenames): - items.append(bitem.child(index)) - items.remove(bitem) + images_filenames.append(bitem.child(index).data(0, QtCore.Qt.UserRole).filename) + elif isinstance(bitem.data(0, QtCore.Qt.UserRole), ImageFilenames): + images_filenames.append(bitem.data(0, QtCore.Qt.UserRole).filename) # Don't try to display empty groups - if not items: + if not images_filenames: return False # Find missing files - for bitem in items: - filename = bitem.data(0, QtCore.Qt.UserRole).filename + for filename in images_filenames: if not os.path.exists(filename): - missing_items.append(bitem) missing_items_filenames.append(filename) - for item in missing_items: - items.remove(item) # We cannot continue, as all images do not exist. - if not items: + if not images_filenames: if not remote: critical_error_message_box( translate('ImagePlugin.MediaItem', 'Missing Image(s)'), @@ -579,15 +579,14 @@ class ImageMediaItem(MediaManagerItem): u'\n'.join(missing_items_filenames)) return False # We have missing as well as existing images. We ask what to do. - elif missing_items and QtGui.QMessageBox.question(self, + elif missing_items_filenames and QtGui.QMessageBox.question(self, translate('ImagePlugin.MediaItem', 'Missing Image(s)'), translate('ImagePlugin.MediaItem', 'The following image(s) no longer exist: %s\n' 'Do you want to add the other images anyway?') % u'\n'.join(missing_items_filenames), QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.No | QtGui.QMessageBox.Yes)) == QtGui.QMessageBox.No: return False # Continue with the existing images. - for bitem in items: - filename = bitem.data(0, QtCore.Qt.UserRole).filename + for filename in images_filenames: name = os.path.split(filename)[1] service_item.add_from_image(filename, name, background) return True diff --git a/openlp/plugins/media/lib/mediaitem.py b/openlp/plugins/media/lib/mediaitem.py index 2037346ad..30748a225 100644 --- a/openlp/plugins/media/lib/mediaitem.py +++ b/openlp/plugins/media/lib/mediaitem.py @@ -44,10 +44,10 @@ log = logging.getLogger(__name__) CLAPPERBOARD = u':/media/slidecontroller_multimedia.png' -VIDEO_ICON = build_icon(QtGui.QImage(u':/media/media_video.png')) -AUDIO_ICON = build_icon(QtGui.QImage(u':/media/media_audio.png')) -DVD_ICON = build_icon(QtGui.QImage(u':/media/media_video.png')) -ERROR_ICON = build_icon(QtGui.QImage(u':/general/general_delete.png')) +VIDEO_ICON = build_icon(u':/media/media_video.png') +AUDIO_ICON = build_icon(u':/media/media_audio.png') +DVD_ICON = build_icon(u':/media/media_video.png') +ERROR_ICON = build_icon(u':/general/general_delete.png') class MediaMediaItem(MediaManagerItem): @@ -155,7 +155,7 @@ class MediaMediaItem(MediaManagerItem): if os.path.exists(filename): service_item = ServiceItem() service_item.title = u'webkit' - service_item.shortname = service_item.title + service_item.processor = u'webkit' (path, name) = os.path.split(filename) service_item.add_from_command(path, name,CLAPPERBOARD) if self.media_controller.video(DisplayControllerType.Live, service_item, video_behind_text=True): @@ -185,9 +185,9 @@ class MediaMediaItem(MediaManagerItem): translate('MediaPlugin.MediaItem', 'Missing Media File'), translate('MediaPlugin.MediaItem', 'The file %s no longer exists.') % filename) return False - service_item.title = self.display_type_combo_box.currentText() - service_item.shortname = service_item.title (path, name) = os.path.split(filename) + service_item.title = name + service_item.processor = self.display_type_combo_box.currentText() service_item.add_from_command(path, name, CLAPPERBOARD) # Only get start and end times if going to a service if context == ServiceItemContext.Service: @@ -196,7 +196,6 @@ class MediaMediaItem(MediaManagerItem): return False service_item.add_capability(ItemCapabilities.CanAutoStartForLive) service_item.add_capability(ItemCapabilities.RequiresMedia) - service_item.add_capability(ItemCapabilities.HasDetailedTitleDisplay) if Settings().value(self.settings_section + u'/media auto start') == QtCore.Qt.Checked: service_item.will_auto_start = True # force a non-existent theme @@ -260,8 +259,7 @@ class MediaMediaItem(MediaManagerItem): Settings().setValue(self.settings_section + u'/media files', self.get_file_list()) def load_list(self, media, target_group=None): - # Sort the media by its filename considering language specific - # characters. + # Sort the media by its filename considering language specific characters. media.sort(key=lambda filename: get_locale_key(os.path.split(unicode(filename))[1])) for track in media: track_info = QtCore.QFileInfo(track) diff --git a/openlp/plugins/presentations/lib/mediaitem.py b/openlp/plugins/presentations/lib/mediaitem.py index 2f48b99c1..fcfc495ed 100644 --- a/openlp/plugins/presentations/lib/mediaitem.py +++ b/openlp/plugins/presentations/lib/mediaitem.py @@ -244,22 +244,20 @@ class PresentationMediaItem(MediaManagerItem): items = self.list_view.selectedItems() if len(items) > 1: return False - service_item.title = self.display_type_combo_box.currentText() - service_item.shortname = self.display_type_combo_box.currentText() + service_item.processor = self.display_type_combo_box.currentText() service_item.add_capability(ItemCapabilities.ProvidesOwnDisplay) - service_item.add_capability(ItemCapabilities.HasDetailedTitleDisplay) - shortname = service_item.shortname - if not shortname: + if not self.display_type_combo_box.currentText(): return False for bitem in items: filename = bitem.data(QtCore.Qt.UserRole) + (path, name) = os.path.split(filename) + service_item.title = name if os.path.exists(filename): - if shortname == self.Automatic: - service_item.shortname = self.findControllerByType(filename) - if not service_item.shortname: + if service_item.processor == self.Automatic: + service_item.processor = self.findControllerByType(filename) + if not service_item.processor: return False - controller = self.controllers[service_item.shortname] - (path, name) = os.path.split(filename) + controller = self.controllers[service_item.processor] doc = controller.add_document(filename) if doc.get_thumbnail_path(1, True) is None: doc.load_presentation() diff --git a/openlp/plugins/presentations/lib/messagelistener.py b/openlp/plugins/presentations/lib/messagelistener.py index 330c36f5c..cd7c654a2 100644 --- a/openlp/plugins/presentations/lib/messagelistener.py +++ b/openlp/plugins/presentations/lib/messagelistener.py @@ -36,6 +36,7 @@ from openlp.core.ui import HideMode log = logging.getLogger(__name__) + class Controller(object): """ This is the Presentation listener who acts on events from the slide controller and passes the messages on the the @@ -314,7 +315,7 @@ class MessageListener(object): item = message[0] hide_mode = message[2] file = item.get_frame_path() - self.handler = item.title + self.handler = item.processor if self.handler == self.media_item.Automatic: self.handler = self.media_item.findControllerByType(file) if not self.handler: diff --git a/openlp/plugins/presentations/lib/presentationcontroller.py b/openlp/plugins/presentations/lib/presentationcontroller.py index 7501fd6df..85ebc42ad 100644 --- a/openlp/plugins/presentations/lib/presentationcontroller.py +++ b/openlp/plugins/presentations/lib/presentationcontroller.py @@ -38,6 +38,7 @@ from openlp.core.utils import AppLocation log = logging.getLogger(__name__) + class PresentationDocument(object): """ Base class for presentation documents to inherit from. Loads and closes the presentation as well as triggering the @@ -322,7 +323,7 @@ class PresentationController(object): ``supports`` The primary native file types this application supports. - ``alsosupports`` + ``also_supports`` Other file types the application can import, although not necessarily the first choice due to potential incompatibilities. @@ -358,7 +359,7 @@ class PresentationController(object): Name of the application, to appear in the application """ self.supports = [] - self.alsosupports = [] + self.also_supports = [] self.docs = [] self.plugin = plugin self.name = name diff --git a/openlp/plugins/presentations/presentationplugin.py b/openlp/plugins/presentations/presentationplugin.py index 1cb966aa5..cc1516b69 100644 --- a/openlp/plugins/presentations/presentationplugin.py +++ b/openlp/plugins/presentations/presentationplugin.py @@ -49,7 +49,7 @@ __default_settings__ = { u'presentations/Powerpoint': QtCore.Qt.Checked, u'presentations/Powerpoint Viewer': QtCore.Qt.Checked, u'presentations/presentations files': [] - } +} class PresentationPlugin(Plugin): diff --git a/openlp/plugins/remotes/html/live.css b/openlp/plugins/remotes/html/live.css new file mode 100644 index 000000000..7181129d9 --- /dev/null +++ b/openlp/plugins/remotes/html/live.css @@ -0,0 +1,39 @@ +/****************************************************************************** +* OpenLP - Open Source Lyrics Projection * +* --------------------------------------------------------------------------- * +* Copyright (c) 2008-2013 Raoul Snyman * +* Portions copyright (c) 2008-2013 Tim Bentley, Gerald Britton, Jonathan * +* Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, * +* Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. * +* Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, * +* Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, * +* Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, * +* Frode Woldsund, Martin Zibricky * +* --------------------------------------------------------------------------- * +* This program is free software; you can redistribute it and/or modify it * +* under the terms of the GNU General Public License as published by the Free * +* Software Foundation; version 2 of the License. * +* * +* This program is distributed in the hope that it will be useful, but WITHOUT * +* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * +* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for * +* more details. * +* * +* You should have received a copy of the GNU General Public License along * +* with this program; if not, write to the Free Software Foundation, Inc., 59 * +* Temple Place, Suite 330, Boston, MA 02111-1307 USA * +******************************************************************************/ +body { + background-color: black; + font-family: sans-serif; + overflow: hidden; +} + +.size { + position: absolute; + top: 0px; + vertical-align: middle; + height: 100%; + background-size: cover; + background-repeat: no-repeat; +} \ No newline at end of file diff --git a/openlp/plugins/remotes/html/live.html b/openlp/plugins/remotes/html/live.html new file mode 100644 index 000000000..f9a2c874c --- /dev/null +++ b/openlp/plugins/remotes/html/live.html @@ -0,0 +1,41 @@ + + + + + + ${live_title} + + + + + + + + + \ No newline at end of file diff --git a/openlp/plugins/remotes/html/live.js b/openlp/plugins/remotes/html/live.js new file mode 100644 index 000000000..d55072c16 --- /dev/null +++ b/openlp/plugins/remotes/html/live.js @@ -0,0 +1,52 @@ +/****************************************************************************** + * OpenLP - Open Source Lyrics Projection * + * --------------------------------------------------------------------------- * + * Copyright (c) 2008-2013 Raoul Snyman * + * Portions copyright (c) 2008-2013 Tim Bentley, Gerald Britton, Jonathan * + * Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, * + * Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. * + * Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, * + * Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, * + * Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, * + * Frode Woldsund, Martin Zibricky * + * --------------------------------------------------------------------------- * + * This program is free software; you can redistribute it and/or modify it * + * under the terms of the GNU General Public License as published by the Free * + * Software Foundation; version 2 of the License. * + * * + * This program is distributed in the hope that it will be useful, but WITHOUT * + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for * + * more details. * + * * + * You should have received a copy of the GNU General Public License along * + * with this program; if not, write to the Free Software Foundation, Inc., 59 * + * Temple Place, Suite 330, Boston, MA 02111-1307 USA * + ******************************************************************************/ +window.OpenLP = { + loadSlide: function (event) { + $.getJSON( + "/live/image", + function (data, status) { + var img = document.getElementById('image'); + img.src = data.results.slide_image; + img.style.display = 'block'; + } + ); + }, + pollServer: function () { + $.getJSON( + "/live/poll", + function (data, status) { + if (OpenLP.slideCount != data.results.slide_count) { + OpenLP.slideCount = data.results.slide_count; + OpenLP.loadSlide(); + } + } + ); + } +} +$.ajaxSetup({ cache: false }); +setInterval("OpenLP.pollServer();", 500); +OpenLP.pollServer(); + diff --git a/openlp/plugins/remotes/html/openlp.js b/openlp/plugins/remotes/html/openlp.js index 3cbe65366..10bc9e328 100644 --- a/openlp/plugins/remotes/html/openlp.js +++ b/openlp/plugins/remotes/html/openlp.js @@ -147,7 +147,7 @@ window.OpenLP = { }, pollServer: function () { $.getJSON( - "/stage/api/poll", + "/stage/poll", function (data, status) { var prevItem = OpenLP.currentItem; OpenLP.currentSlide = data.results.slide; diff --git a/openlp/plugins/remotes/html/stage.js b/openlp/plugins/remotes/html/stage.js index dff51537c..42b7712f9 100644 --- a/openlp/plugins/remotes/html/stage.js +++ b/openlp/plugins/remotes/html/stage.js @@ -26,7 +26,7 @@ window.OpenLP = { loadService: function (event) { $.getJSON( - "/stage/api/service/list", + "/stage/service/list", function (data, status) { OpenLP.nextSong = ""; $("#notes").html(""); @@ -46,7 +46,7 @@ window.OpenLP = { }, loadSlides: function (event) { $.getJSON( - "/stage/api/controller/live/text", + "/stage/controller/live/text", function (data, status) { OpenLP.currentSlides = data.results.slides; OpenLP.currentSlide = 0; @@ -137,7 +137,7 @@ window.OpenLP = { }, pollServer: function () { $.getJSON( - "/stage/api/poll", + "/stage/poll", function (data, status) { OpenLP.updateClock(data); if (OpenLP.currentItem != data.results.item || diff --git a/openlp/plugins/remotes/lib/httpserver.py b/openlp/plugins/remotes/lib/httpserver.py index 878b197b3..a2abbb41e 100644 --- a/openlp/plugins/remotes/lib/httpserver.py +++ b/openlp/plugins/remotes/lib/httpserver.py @@ -124,7 +124,7 @@ import cherrypy from mako.template import Template from PyQt4 import QtCore -from openlp.core.lib import Registry, Settings, PluginStatus, StringContent +from openlp.core.lib import Registry, Settings, PluginStatus, StringContent, image_to_byte from openlp.core.utils import AppLocation, translate from cherrypy._cpcompat import sha, ntob @@ -136,6 +136,7 @@ def make_sha_hash(password): """ Create an encrypted password for the given password. """ + log.debug("make_sha_hash") return sha(ntob(password)).hexdigest() @@ -143,6 +144,7 @@ def fetch_password(username): """ Fetch the password for a provided user. """ + log.debug("Fetch Password") if username != Settings().value(u'remotes/user id'): return None return make_sha_hash(Settings().value(u'remotes/password')) @@ -175,9 +177,11 @@ class HttpServer(object): self.root = self.Public() self.root.files = self.Files() self.root.stage = self.Stage() + self.root.live = self.Live() self.root.router = self.router self.root.files.router = self.router self.root.stage.router = self.router + self.root.live.router = self.router cherrypy.tree.mount(self.root, '/', config=self.define_config()) # Turn off the flood of access messages cause by poll cherrypy.log.access_log.propagate = False @@ -212,6 +216,9 @@ class HttpServer(object): u'tools.staticdir.dir': self.router.html_dir, u'tools.basic_auth.on': False}, u'/stage': {u'tools.staticdir.on': True, + u'tools.staticdir.dir': self.router.html_dir, + u'tools.basic_auth.on': False}, + u'/live': {u'tools.staticdir.on': True, u'tools.staticdir.dir': self.router.html_dir, u'tools.basic_auth.on': False}} return directory_config @@ -239,7 +246,16 @@ class HttpServer(object): class Stage(object): """ - Stageview is read only so security is not relevant and would reduce it's usability + Stage view is read only so security is not relevant and would reduce it's usability + """ + @cherrypy.expose + def default(self, *args, **kwargs): + url = urlparse.urlparse(cherrypy.url()) + return self.router.process_http_request(url.path, *args) + + class Live(object): + """ + Live view is read only so security is not relevant and would reduce it's usability """ @cherrypy.expose def default(self, *args, **kwargs): @@ -265,13 +281,16 @@ class HttpRouter(object): self.routes = [ (u'^/$', self.serve_file), (u'^/(stage)$', self.serve_file), + (u'^/(live)$', self.serve_file), (r'^/files/(.*)$', self.serve_file), (r'^/api/poll$', self.poll), - (r'^/stage/api/poll$', self.poll), + (r'^/stage/poll$', self.poll), + (r'^/live/poll$', self.live_poll), + (r'^/live/image$', self.live_image), (r'^/api/controller/(live|preview)/(.*)$', self.controller), - (r'^/stage/api/controller/(live|preview)/(.*)$', self.controller), + (r'^/stage/controller/(live|preview)/(.*)$', self.controller), (r'^/api/service/(.*)$', self.service), - (r'^/stage/api/service/(.*)$', self.service), + (r'^/stage/service/(.*)$', self.service), (r'^/api/display/(hide|show|blank|theme|desktop)$', self.display), (r'^/api/alert$', self.alert), (r'^/api/plugin/(search)$', self.plugin_info), @@ -305,6 +324,7 @@ class HttpRouter(object): if response: return response else: + log.debug('Path not found %s', url_path) return self._http_not_found() def _get_service_items(self): @@ -334,6 +354,7 @@ class HttpRouter(object): self.template_vars = { 'app_title': translate('RemotePlugin.Mobile', 'OpenLP 2.1 Remote'), 'stage_title': translate('RemotePlugin.Mobile', 'OpenLP 2.1 Stage View'), + 'live_title': translate('RemotePlugin.Mobile', 'OpenLP 2.1 Live View'), 'service_manager': translate('RemotePlugin.Mobile', 'Service Manager'), 'slide_controller': translate('RemotePlugin.Mobile', 'Slide Controller'), 'alerts': translate('RemotePlugin.Mobile', 'Alerts'), @@ -359,18 +380,19 @@ class HttpRouter(object): def serve_file(self, filename=None): """ - Send a file to the socket. For now, just a subset of file types - and must be top level inside the html folder. + Send a file to the socket. For now, just a subset of file types and must be top level inside the html folder. If subfolders requested return 404, easier for security for the present. - Ultimately for i18n, this could first look for xx/file.html before - falling back to file.html... where xx is the language, e.g. 'en' + Ultimately for i18n, this could first look for xx/file.html before falling back to file.html. + where xx is the language, e.g. 'en' """ log.debug(u'serve file request %s' % filename) if not filename: filename = u'index.html' elif filename == u'stage': filename = u'stage.html' + elif filename == u'live': + filename = u'live.html' path = os.path.normpath(os.path.join(self.html_dir, filename)) if not path.startswith(self.html_dir): return self._http_not_found() @@ -425,6 +447,26 @@ class HttpRouter(object): cherrypy.response.headers['Content-Type'] = u'application/json' return json.dumps({u'results': result}) + def live_poll(self): + """ + Poll OpenLP to determine the current slide count. + """ + result = { + u'slide_count': self.live_controller.slide_count + } + cherrypy.response.headers['Content-Type'] = u'application/json' + return json.dumps({u'results': result}) + + def live_image(self): + """ + Return the latest display image as a byte stream. + """ + result = { + u'slide_image': u'data:image/png;base64,' + str(image_to_byte(self.live_controller.slide_image)) + } + cherrypy.response.headers['Content-Type'] = u'application/json' + return json.dumps({u'results': result}) + def display(self, action): """ Hide or show the display screen. diff --git a/openlp/plugins/remotes/lib/remotetab.py b/openlp/plugins/remotes/lib/remotetab.py index 09934b58c..c8ed9303e 100644 --- a/openlp/plugins/remotes/lib/remotetab.py +++ b/openlp/plugins/remotes/lib/remotetab.py @@ -86,6 +86,12 @@ class RemoteTab(SettingsTab): self.stage_url.setObjectName(u'stage_url') self.stage_url.setOpenExternalLinks(True) self.http_setting_layout.addRow(self.stage_url_label, self.stage_url) + self.live_url_label = QtGui.QLabel(self.http_settings_group_box) + self.live_url_label.setObjectName(u'live_url_label') + self.live_url = QtGui.QLabel(self.http_settings_group_box) + self.live_url.setObjectName(u'live_url') + self.live_url.setOpenExternalLinks(True) + self.http_setting_layout.addRow(self.live_url_label, self.live_url) self.left_layout.addWidget(self.http_settings_group_box) self.https_settings_group_box = QtGui.QGroupBox(self.left_column) self.https_settings_group_box.setCheckable(True) @@ -116,6 +122,12 @@ class RemoteTab(SettingsTab): self.stage_https_url.setObjectName(u'stage_https_url') self.stage_https_url.setOpenExternalLinks(True) self.https_settings_layout.addRow(self.stage_https_url_label, self.stage_https_url) + self.live_https_url_label = QtGui.QLabel(self.https_settings_group_box) + self.live_https_url_label.setObjectName(u'live_url_label') + self.live_https_url = QtGui.QLabel(self.https_settings_group_box) + self.live_https_url.setObjectName(u'live_https_url') + self.live_https_url.setOpenExternalLinks(True) + self.https_settings_layout.addRow(self.live_https_url_label, self.live_https_url) self.left_layout.addWidget(self.https_settings_group_box) self.user_login_group_box = QtGui.QGroupBox(self.left_column) self.user_login_group_box.setCheckable(True) @@ -163,6 +175,7 @@ class RemoteTab(SettingsTab): self.port_label.setText(translate('RemotePlugin.RemoteTab', 'Port number:')) self.remote_url_label.setText(translate('RemotePlugin.RemoteTab', 'Remote URL:')) self.stage_url_label.setText(translate('RemotePlugin.RemoteTab', 'Stage view URL:')) + self.live_url_label.setText(translate('RemotePlugin.RemoteTab', 'Live view URL:')) self.twelve_hour_check_box.setText(translate('RemotePlugin.RemoteTab', 'Display stage time in 12h format')) self.android_app_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'Android App')) self.qr_description_label.setText(translate('RemotePlugin.RemoteTab', @@ -176,6 +189,7 @@ class RemoteTab(SettingsTab): self.https_port_label.setText(self.port_label.text()) self.remote_https_url_label.setText(self.remote_url_label.text()) self.stage_https_url_label.setText(self.stage_url_label.text()) + self.live_https_url_label.setText(self.live_url_label.text()) self.user_login_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'User Authentication')) self.user_id_label.setText(translate('RemotePlugin.RemoteTab', 'User id:')) self.password_label.setText(translate('RemotePlugin.RemoteTab', 'Password:')) @@ -203,10 +217,14 @@ class RemoteTab(SettingsTab): https_url = u'https://%s:%s/' % (ip_address, self.https_port_spin_box.value()) self.remote_url.setText(u'%s' % (http_url, http_url)) self.remote_https_url.setText(u'%s' % (https_url, https_url)) - http_url += u'stage' - https_url += u'stage' - self.stage_url.setText(u'%s' % (http_url, http_url)) - self.stage_https_url.setText(u'%s' % (https_url, https_url)) + http_url_temp = http_url + u'stage' + https_url_temp = https_url + u'stage' + self.stage_url.setText(u'%s' % (http_url_temp, http_url_temp)) + self.stage_https_url.setText(u'%s' % (https_url_temp, https_url_temp)) + http_url_temp = http_url + u'live' + https_url_temp = https_url + u'live' + self.live_url.setText(u'%s' % (http_url_temp, http_url_temp)) + self.live_https_url.setText(u'%s' % (https_url_temp, https_url_temp)) def load(self): """ diff --git a/openlp/plugins/songs/forms/__init__.py b/openlp/plugins/songs/forms/__init__.py index a1c177a9d..588f359f6 100644 --- a/openlp/plugins/songs/forms/__init__.py +++ b/openlp/plugins/songs/forms/__init__.py @@ -52,3 +52,4 @@ 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 editsongform import EditSongForm diff --git a/openlp/plugins/songs/forms/songexportform.py b/openlp/plugins/songs/forms/songexportform.py index f0554f588..36ed50cee 100644 --- a/openlp/plugins/songs/forms/songexportform.py +++ b/openlp/plugins/songs/forms/songexportform.py @@ -235,7 +235,7 @@ class SongExportForm(OpenLPWizard): self.availableListWidget.addItem(item) self.application.set_normal_cursor() - def preWizard(self): + def pre_wizard(self): """ Perform pre export tasks. """ diff --git a/openlp/plugins/songs/forms/songimportform.py b/openlp/plugins/songs/forms/songimportform.py index 4c749f4d0..add306505 100644 --- a/openlp/plugins/songs/forms/songimportform.py +++ b/openlp/plugins/songs/forms/songimportform.py @@ -325,7 +325,7 @@ class SongImportForm(OpenLPWizard): self.error_copy_to_button.setHidden(True) self.error_save_to_button.setHidden(True) - def preWizard(self): + def pre_wizard(self): """ Perform pre import tasks """ diff --git a/openlp/plugins/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py index e8e6901ea..b60c0b162 100644 --- a/openlp/plugins/songs/lib/mediaitem.py +++ b/openlp/plugins/songs/lib/mediaitem.py @@ -72,10 +72,7 @@ class SongMediaItem(MediaManagerItem): def __init__(self, parent, plugin): self.icon_path = u'songs/song' MediaManagerItem.__init__(self, parent, plugin) - self.edit_song_form = EditSongForm(self, self.main_window, self.plugin.manager) - self.openLyrics = OpenLyrics(self.plugin.manager) self.single_service_item = False - self.song_maintenance_form = SongMaintenanceForm(self.plugin.manager, self) # Holds information about whether the edit is remotely triggered and which Song is required. self.remote_song = -1 self.edit_item = None @@ -132,6 +129,12 @@ class SongMediaItem(MediaManagerItem): 'Maintain the lists of authors, topics and books.')) def initialise(self): + """ + Initialise variables when they cannot be initialised in the constructor. + """ + self.song_maintenance_form = SongMaintenanceForm(self.plugin.manager, self) + self.edit_song_form = EditSongForm(self, self.main_window, self.plugin.manager) + self.openLyrics = OpenLyrics(self.plugin.manager) self.search_text_edit.set_search_types([ (SongSearch.Entire, u':/songs/song_search_all.png', translate('SongsPlugin.MediaItem', 'Entire Song'), @@ -157,7 +160,6 @@ class SongMediaItem(MediaManagerItem): Settings().setValue(u'%s/last search type' % self.settings_section, self.search_text_edit.current_search_type()) # Reload the list considering the new search type. search_keywords = unicode(self.search_text_edit.displayText()) - search_results = [] search_type = self.search_text_edit.current_search_type() if search_type == SongSearch.Entire: log.debug(u'Entire Song Search') @@ -457,14 +459,7 @@ class SongMediaItem(MediaManagerItem): for slide in verses: service_item.add_from_text(unicode(slide)) service_item.title = song.title - author_list = [unicode(author.display_name) for author in song.authors] - service_item.raw_footer.append(song.title) - service_item.raw_footer.append(create_separated_list(author_list)) - service_item.raw_footer.append(song.copyright) - if Settings().value(u'core/ccli number'): - service_item.raw_footer.append(translate('SongsPlugin.MediaItem', 'CCLI License: ') + - Settings().value(u'core/ccli number')) - service_item.audit = [song.title, author_list, song.copyright, unicode(song.ccli_number)] + author_list = self.generate_footer(service_item, song) service_item.data_string = {u'title': song.search_title, u'authors': u', '.join(author_list)} service_item.xml_version = self.openLyrics.song_to_xml(song) # Add the audio file to the service item. @@ -473,6 +468,30 @@ class SongMediaItem(MediaManagerItem): service_item.background_audio = [m.file_name for m in song.media_files] return True + def generate_footer(self, item, song): + """ + Generates the song footer based on a song and adds details to a service item. + author_list is only required for initial song generation. + + ``item`` + The service item to be amended + + ``song`` + The song to be used to generate the footer + """ + author_list = [unicode(author.display_name) for author in song.authors] + item.audit = [ + song.title, author_list, song.copyright, unicode(song.ccli_number) + ] + item.raw_footer = [] + item.raw_footer.append(song.title) + item.raw_footer.append(create_separated_list(author_list)) + item.raw_footer.append(song.copyright) + if Settings().value(u'core/ccli number'): + item.raw_footer.append(translate('SongsPlugin.MediaItem', 'CCLI License: ') + + Settings().value(u'core/ccli number')) + return author_list + def service_load(self, item): """ Triggered by a song being loaded by the service manager. @@ -490,9 +509,8 @@ class SongMediaItem(MediaManagerItem): else: search_results = self.plugin.manager.get_all_objects(Song, Song.search_title == item.data_string[u'title'], Song.search_title.asc()) - editId = 0 + edit_id = 0 add_song = True - temporary = False if search_results: for song in search_results: author_list = item.data_string[u'authors'] @@ -505,7 +523,7 @@ class SongMediaItem(MediaManagerItem): break if same_authors and author_list.strip(u', ') == u'': add_song = False - editId = song.id + edit_id = song.id break # If there's any backing tracks, copy them over. if item.background_audio: @@ -523,11 +541,11 @@ class SongMediaItem(MediaManagerItem): # If there's any backing tracks, copy them over. if item.background_audio: self._update_background_audio(song, item) - editId = song.id - temporary = True - # Update service with correct song id. - if editId: - self.service_manager.service_item_update(editId, item.unique_identifier, temporary) + edit_id = song.id + # Update service with correct song id and return it to caller. + item.edit_id = edit_id + self.generate_footer(item, song) + return item def search(self, string, showError): """ diff --git a/openlp/plugins/songs/songsplugin.py b/openlp/plugins/songs/songsplugin.py index 7d1210d3a..1c895d327 100644 --- a/openlp/plugins/songs/songsplugin.py +++ b/openlp/plugins/songs/songsplugin.py @@ -251,8 +251,7 @@ class SongsPlugin(Plugin): u'delete': translate('SongsPlugin', 'Delete the selected song.'), u'preview': translate('SongsPlugin', 'Preview the selected song.'), u'live': translate('SongsPlugin', 'Send the selected song live.'), - u'service': translate('SongsPlugin', - 'Add the selected song to the service.') + u'service': translate('SongsPlugin', 'Add the selected song to the service.') } self.set_plugin_ui_text_strings(tooltips) diff --git a/tests/functional/openlp_core_lib/test_lib.py b/tests/functional/openlp_core_lib/test_lib.py index c03a11265..b0d1a4bed 100644 --- a/tests/functional/openlp_core_lib/test_lib.py +++ b/tests/functional/openlp_core_lib/test_lib.py @@ -1,13 +1,20 @@ """ Package to test the openlp.core.lib package. """ +import os + from unittest import TestCase from datetime import datetime, timedelta from mock import MagicMock, patch +from PyQt4 import QtCore, QtGui + +from openlp.core.lib import str_to_bool, create_thumb, translate, check_directory_exists, get_text_file_string, \ + build_icon, image_to_byte, check_item_selected, validate_thumb, create_separated_list, clean_tags, expand_tags + + +TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), u'..', u'..', u'resources')) -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, create_separated_list, clean_tags, expand_tags class TestLib(TestCase): @@ -125,7 +132,7 @@ class TestLib(TestCase): Test the check_directory_exists() function """ with patch(u'openlp.core.lib.os.path.exists') as mocked_exists, \ - patch(u'openlp.core.lib.os.makedirs') as mocked_makedirs: + patch(u'openlp.core.lib.os.makedirs') as mocked_makedirs: # GIVEN: A directory to check and a mocked out os.makedirs and os.path.exists directory_to_check = u'existing/directory' @@ -219,7 +226,7 @@ class TestLib(TestCase): 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: + patch(u'openlp.core.lib.QtGui.QPixmap') as MockedQPixmap: # GIVEN: A mocked QIcon and a mocked QPixmap MockedQtGui.QIcon = MagicMock MockedQtGui.QIcon.Normal = 1 @@ -261,9 +268,43 @@ class TestLib(TestCase): 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 create_thumb_with_size_test(self): + """ + Test the create_thumb() function + """ + # GIVEN: An image to create a thumb of. + image_path = os.path.join(TEST_PATH, u'church.jpg') + thumb_path = os.path.join(TEST_PATH, u'church_thumb.jpg') + thumb_size = QtCore.QSize(10, 20) + + # Remove the thumb so that the test actually tests if the thumb will be created. Maybe it was not deleted in the + # last test. + try: + os.remove(thumb_path) + except: + pass + + # Only continue when the thumb does not exist. + assert not os.path.exists(thumb_path), u'Test was not ran, because the thumb already exists.' + + # WHEN: Create the thumb. + icon = create_thumb(image_path, thumb_path, size=thumb_size) + + # THEN: Check if the thumb was created. + assert os.path.exists(thumb_path), u'Test was not ran, because the thumb already exists.' + assert isinstance(icon, QtGui.QIcon), u'The icon should be a QIcon.' + assert not icon.isNull(), u'The icon should not be null.' + assert QtGui.QImageReader(thumb_path).size() == thumb_size, u'The thumb should have the given size.' + + # Remove the thumb so that the test actually tests if the thumb will be created. + try: + os.remove(thumb_path) + except: + pass + def check_item_selected_true_test(self): """ - Test that the check_item_selected() function returns True when there are selected indexes. + 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') @@ -423,7 +464,7 @@ class TestLib(TestCase): def create_separated_list_qlocate_test(self): """ - Test the create_separated_list function using the Qt provided method. + Test the create_separated_list function using the Qt provided method """ with patch(u'openlp.core.lib.Qt') as mocked_qt, \ patch(u'openlp.core.lib.QtCore.QLocale.createSeparatedList') as mocked_createSeparatedList: @@ -442,7 +483,7 @@ class TestLib(TestCase): def create_separated_list_empty_list_test(self): """ - Test the create_separated_list function with an empty list. + Test the create_separated_list function with an empty list """ with patch(u'openlp.core.lib.Qt') as mocked_qt: # GIVEN: An empty list and the mocked Qt module. @@ -458,7 +499,7 @@ class TestLib(TestCase): def create_separated_list_with_one_item_test(self): """ - Test the create_separated_list function with a list consisting of only one entry. + Test the create_separated_list function with a list consisting of only one entry """ with patch(u'openlp.core.lib.Qt') as mocked_qt: # GIVEN: A list with a string and the mocked Qt module. @@ -474,7 +515,7 @@ class TestLib(TestCase): def create_separated_list_with_two_items_test(self): """ - Test the create_separated_list function with a list of two entries. + Test the create_separated_list function with a list of two entries """ with patch(u'openlp.core.lib.Qt') as mocked_qt, patch(u'openlp.core.lib.translate') as mocked_translate: # GIVEN: A list of strings and the mocked Qt module. @@ -491,7 +532,7 @@ class TestLib(TestCase): def create_separated_list_with_three_items_test(self): """ - Test the create_separated_list function with a list of three items. + Test the create_separated_list function with a list of three items """ with patch(u'openlp.core.lib.Qt') as mocked_qt, patch(u'openlp.core.lib.translate') as mocked_translate: # GIVEN: A list with a string and the mocked Qt module. diff --git a/tests/functional/openlp_core_lib/test_screen.py b/tests/functional/openlp_core_lib/test_screen.py index 007889a12..7c1bcaf24 100644 --- a/tests/functional/openlp_core_lib/test_screen.py +++ b/tests/functional/openlp_core_lib/test_screen.py @@ -1,7 +1,7 @@ """ Package to test the openlp.core.lib.screenlist package. """ -import copy + from unittest import TestCase from mock import MagicMock diff --git a/tests/functional/openlp_core_lib/test_serviceitem.py b/tests/functional/openlp_core_lib/test_serviceitem.py index 26e9e7d44..e051f8c76 100644 --- a/tests/functional/openlp_core_lib/test_serviceitem.py +++ b/tests/functional/openlp_core_lib/test_serviceitem.py @@ -210,7 +210,6 @@ class TestServiceItem(TestCase): # THEN: We should get back a valid service item assert service_item.is_valid is True, u'The new service item should be valid' - print service_item.get_rendered_frame(0) assert service_item.get_rendered_frame(0) == test_file, u'The first frame should match the path to the image' assert service_item.get_frames()[0] == frame_array, u'The return should match frame array1' assert service_item.get_frame_path(0) == test_file, u'The frame path should match the full path to the image' @@ -268,6 +267,26 @@ class TestServiceItem(TestCase): assert service_item.is_capable(ItemCapabilities.CanAppend) is True, \ u'This service item should be able to have new items added to it' + def serviceitem_migrate_test_20_22(self): + """ + Test the Service Item - migrating a media only service item from 2.0 to 2.2 format + """ + # GIVEN: A new service item and a mocked add icon function + service_item = ServiceItem(None) + service_item.add_icon = MagicMock() + + # WHEN: adding an media from a saved Service and mocked exists + line = self.convert_file_service_item(u'migrate_video_20_22.osd') + with patch('os.path.exists'): + service_item.set_from_service(line, TEST_PATH) + + # THEN: We should get back a converted service item + assert service_item.is_valid is True, u'The new service item should be valid' + assert service_item.processor is None, u'The Processor should have been set' + assert service_item.title is None, u'The title should be set to a value' + assert service_item.is_capable(ItemCapabilities.HasDetailedTitleDisplay) is False, \ + u'The Capability should have been removed' + def convert_file_service_item(self, name): service_file = os.path.join(TEST_PATH, name) try: diff --git a/tests/functional/openlp_plugins/images/test_lib.py b/tests/functional/openlp_plugins/images/test_lib.py index 5033f0645..0aecc0a7f 100644 --- a/tests/functional/openlp_plugins/images/test_lib.py +++ b/tests/functional/openlp_plugins/images/test_lib.py @@ -9,7 +9,7 @@ from unittest import TestCase from mock import MagicMock, patch from openlp.core.lib import Registry -from openlp.plugins.images.lib.db import ImageFilenames +from openlp.plugins.images.lib.db import ImageFilenames, ImageGroups from openlp.plugins.images.lib.mediaitem import ImageMediaItem @@ -23,6 +23,7 @@ class TestImageMediaItem(TestCase): Registry.create() Registry().register(u'service_list', MagicMock()) Registry().register(u'main_window', self.mocked_main_window) + Registry().register(u'live_controller', MagicMock()) mocked_parent = MagicMock() mocked_plugin = MagicMock() with patch(u'openlp.plugins.images.lib.mediaitem.ImageMediaItem.__init__') as mocked_init: @@ -35,7 +36,7 @@ class TestImageMediaItem(TestCase): """ # GIVEN: An empty image_list image_list = [] - with patch(u'openlp.plugins.images.lib.mediaitem.ImageMediaItem.load_full_list') as mocked_loadFullList: + with patch(u'openlp.plugins.images.lib.mediaitem.ImageMediaItem.load_full_list') as mocked_load_full_list: self.media_item.manager = MagicMock() # WHEN: We run save_new_images_list with the empty list @@ -51,7 +52,7 @@ class TestImageMediaItem(TestCase): """ # GIVEN: A list with 1 image image_list = [ u'test_image.jpg' ] - with patch(u'openlp.plugins.images.lib.mediaitem.ImageMediaItem.load_full_list') as mocked_loadFullList: + with patch(u'openlp.plugins.images.lib.mediaitem.ImageMediaItem.load_full_list') as mocked_load_full_list: ImageFilenames.filename = '' self.media_item.manager = MagicMock() @@ -59,7 +60,7 @@ class TestImageMediaItem(TestCase): self.media_item.save_new_images_list(image_list, reload_list=True) # THEN: load_full_list() should have been called - assert mocked_loadFullList.call_count == 1, u'load_full_list() should have been called' + assert mocked_load_full_list.call_count == 1, u'load_full_list() should have been called' # CLEANUP: Remove added attribute from ImageFilenames delattr(ImageFilenames, 'filename') @@ -70,14 +71,14 @@ class TestImageMediaItem(TestCase): """ # GIVEN: A list with 1 image image_list = [ u'test_image.jpg' ] - with patch(u'openlp.plugins.images.lib.mediaitem.ImageMediaItem.load_full_list') as mocked_loadFullList: + with patch(u'openlp.plugins.images.lib.mediaitem.ImageMediaItem.load_full_list') as mocked_load_full_list: self.media_item.manager = MagicMock() # WHEN: We run save_new_images_list with reload_list=False self.media_item.save_new_images_list(image_list, reload_list=False) # THEN: load_full_list() should not have been called - assert mocked_loadFullList.call_count == 0, u'load_full_list() should not have been called' + assert mocked_load_full_list.call_count == 0, u'load_full_list() should not have been called' def save_new_images_list_multiple_images_test(self): """ @@ -85,7 +86,7 @@ class TestImageMediaItem(TestCase): """ # GIVEN: A list with 3 images image_list = [ u'test_image_1.jpg', u'test_image_2.jpg', u'test_image_3.jpg' ] - with patch(u'openlp.plugins.images.lib.mediaitem.ImageMediaItem.load_full_list') as mocked_loadFullList: + with patch(u'openlp.plugins.images.lib.mediaitem.ImageMediaItem.load_full_list') as mocked_load_full_list: self.media_item.manager = MagicMock() # WHEN: We run save_new_images_list with the list of 3 images @@ -101,7 +102,7 @@ class TestImageMediaItem(TestCase): """ # GIVEN: A list with images and objects image_list = [ u'test_image_1.jpg', None, True, ImageFilenames(), 'test_image_2.jpg' ] - with patch(u'openlp.plugins.images.lib.mediaitem.ImageMediaItem.load_full_list') as mocked_loadFullList: + with patch(u'openlp.plugins.images.lib.mediaitem.ImageMediaItem.load_full_list') as mocked_load_full_list: self.media_item.manager = MagicMock() # WHEN: We run save_new_images_list with the list of images and objects @@ -110,3 +111,68 @@ class TestImageMediaItem(TestCase): # THEN: load_full_list() should not have been called assert self.media_item.manager.save_object.call_count == 2, \ u'load_full_list() should have been called only once' + + def on_reset_click_test(self): + """ + Test that on_reset_click() actually resets the background + """ + # GIVEN: A mocked version of reset_action + self.media_item.reset_action = MagicMock() + + # WHEN: on_reset_click is called + self.media_item.on_reset_click() + + # THEN: the reset_action should be set visible, and the image should be reset + self.media_item.reset_action.setVisible.assert_called_with(False) + self.media_item.live_controller.display.reset_image.assert_called_with() + + def _recursively_delete_group_side_effect(*args, **kwargs): + """ + Side effect method that creates custom retun values for the recursively_delete_group method + """ + if args[1] == ImageFilenames and args[2]: + # Create some fake objects that should be removed + returned_object1 = ImageFilenames() + returned_object1.id = 1 + returned_object1.filename = u'/tmp/test_file_1.jpg' + returned_object2 = ImageFilenames() + returned_object2.id = 2 + returned_object2.filename = u'/tmp/test_file_2.jpg' + returned_object3 = ImageFilenames() + returned_object3.id = 3 + returned_object3.filename = u'/tmp/test_file_3.jpg' + return [returned_object1, returned_object2, returned_object3] + if args[1] == ImageGroups and args[2]: + # Change the parent_id that is matched so we don't get into an endless loop + ImageGroups.parent_id = 0 + # Create a fake group that will be used in the next run + returned_object1 = ImageGroups() + returned_object1.id = 1 + return [returned_object1] + return [] + + def recursively_delete_group_test(self): + """ + Test that recursively_delete_group() works + """ + # GIVEN: An ImageGroups object and mocked functions + with patch(u'openlp.core.utils.delete_file') as mocked_delete_file: + ImageFilenames.group_id = 1 + ImageGroups.parent_id = 1 + self.media_item.manager = MagicMock() + self.media_item.manager.get_all_objects.side_effect = self._recursively_delete_group_side_effect + self.media_item.servicePath = "" + test_group = ImageGroups() + test_group.id = 1 + + # WHEN: recursively_delete_group() is called + self.media_item.recursively_delete_group(test_group) + + # THEN: + assert mocked_delete_file.call_count == 0, u'delete_file() should not be called' + assert self.media_item.manager.delete_object.call_count == 7, \ + u'manager.delete_object() should be called exactly 7 times' + + # CLEANUP: Remove added attribute from ImageFilenames and ImageGroups + delattr(ImageFilenames, 'group_id') + delattr(ImageGroups, 'parent_id') diff --git a/tests/functional/openlp_plugins/songs/test_mediaitem.py b/tests/functional/openlp_plugins/songs/test_mediaitem.py new file mode 100644 index 000000000..37f1c76e4 --- /dev/null +++ b/tests/functional/openlp_plugins/songs/test_mediaitem.py @@ -0,0 +1,125 @@ +""" +This module contains tests for the lib submodule of the Songs plugin. +""" +import os +from tempfile import mkstemp +from unittest import TestCase + +from mock import patch, MagicMock + +from PyQt4 import QtGui + +from openlp.core.lib import Registry, ServiceItem, Settings + +from openlp.plugins.songs.lib.mediaitem import SongMediaItem + + +class TestMediaItem(TestCase): + """ + Test the functions in the :mod:`lib` module. + """ + def setUp(self): + """ + Set up the components need for all tests. + """ + Registry.create() + Registry().register(u'service_list', MagicMock()) + Registry().register(u'main_window', MagicMock()) + with patch('openlp.core.lib.mediamanageritem.MediaManagerItem.__init__'), \ + patch('openlp.plugins.songs.forms.editsongform.EditSongForm.__init__'): + self.media_item = SongMediaItem(MagicMock(), MagicMock()) + + fd, self.ini_file = mkstemp(u'.ini') + Settings().set_filename(self.ini_file) + self.application = QtGui.QApplication.instance() + + def tearDown(self): + """ + Delete all the C++ objects at the end so that we don't have a segfault + """ + del self.application + # Not all tests use settings! + try: + os.unlink(self.ini_file) + os.unlink(Settings().fileName()) + except Exception: + pass + + def build_song_footer_one_author_test(self): + """ + Test build songs footer with basic song and one author + """ + # GIVEN: A Song and a Service Item + mock_song = MagicMock() + mock_song.title = u'My Song' + mock_author = MagicMock() + mock_author.display_name = u'my author' + mock_song.authors = [] + mock_song.authors.append(mock_author) + mock_song.copyright = u'My copyright' + service_item = ServiceItem(None) + + # WHEN: I generate the Footer with default settings + author_list = self.media_item.generate_footer(service_item, mock_song) + + # THEN: I get the following Array returned + self.assertEqual(service_item.raw_footer, [u'My Song', u'my author', u'My copyright'], + u'The array should be returned correctly with a song, one author and copyright') + self.assertEqual(author_list, [u'my author'], + u'The author list should be returned correctly with one author') + + def build_song_footer_two_authors_test(self): + """ + Test build songs footer with basic song and two authors + """ + # GIVEN: A Song and a Service Item + mock_song = MagicMock() + mock_song.title = u'My Song' + mock_author = MagicMock() + mock_author.display_name = u'my author' + mock_song.authors = [] + mock_song.authors.append(mock_author) + mock_author = MagicMock() + mock_author.display_name = u'another author' + mock_song.authors.append(mock_author) + mock_song.copyright = u'My copyright' + service_item = ServiceItem(None) + + # WHEN: I generate the Footer with default settings + author_list = self.media_item.generate_footer(service_item, mock_song) + + # THEN: I get the following Array returned + self.assertEqual(service_item.raw_footer, [u'My Song', u'my author and another author', u'My copyright'], + u'The array should be returned correctly with a song, two authors and copyright') + self.assertEqual(author_list, [u'my author', u'another author'], + u'The author list should be returned correctly with two authors') + + def build_song_footer_base_ccli_test(self): + """ + Test build songs footer with basic song and two authors + """ + # GIVEN: A Song and a Service Item and a configured CCLI license + mock_song = MagicMock() + mock_song.title = u'My Song' + mock_author = MagicMock() + mock_author.display_name = u'my author' + mock_song.authors = [] + mock_song.authors.append(mock_author) + mock_song.copyright = u'My copyright' + service_item = ServiceItem(None) + Settings().setValue(u'core/ccli number', u'1234') + + # WHEN: I generate the Footer with default settings + self.media_item.generate_footer(service_item, mock_song) + + # THEN: I get the following Array returned + self.assertEqual(service_item.raw_footer, [u'My Song', u'my author', u'My copyright', u'CCLI License: 1234'], + u'The array should be returned correctly with a song, an author, copyright and ccli') + + # WHEN: I amend the CCLI value + Settings().setValue(u'core/ccli number', u'4321') + self.media_item.generate_footer(service_item, mock_song) + + # THEN: I would get an amended footer string + self.assertEqual(service_item.raw_footer, [u'My Song', u'my author', u'My copyright', u'CCLI License: 4321'], + u'The array should be returned correctly with a song, an author, copyright and amended ccli') diff --git a/tests/resources/migrate_video_20_22.osd b/tests/resources/migrate_video_20_22.osd new file mode 100644 index 000000000..ee4b2c0c4 --- /dev/null +++ b/tests/resources/migrate_video_20_22.osd @@ -0,0 +1,98 @@ +(lp1 +(dp2 +Vserviceitem +p3 +(dp4 +Vheader +p5 +(dp6 +Vxml_version +p7 +NsVauto_play_slides_loop +p8 +I00 +sVauto_play_slides_once +p9 +I00 +sVwill_auto_start +p10 +I01 +sVtitle +p11 +VVLC +p12 +sVcapabilities +p13 +(lp14 +I12 +aI16 +aI4 +aI11 +asVtheme +p15 +I-1 +sVbackground_audio +p16 +(lp17 +sVicon +p18 +V:/plugins/plugin_media.png +p19 +sVtype +p20 +I3 +sVstart_time +p21 +I0 +sVfrom_plugin +p22 +I00 +sVmedia_length +p23 +I144 +sVdata +p24 +V +sVtimed_slide_interval +p25 +I0 +sVaudit +p26 +V +sVsearch +p27 +V +sVname +p28 +Vmedia +p29 +sVfooter +p30 +(lp31 +sVnotes +p32 +V +sVplugin +p33 +g29 +sVtheme_overwritten +p34 +I00 +sVend_time +p35 +I0 +ssg24 +(lp36 +(dp37 +Vpath +p38 +V/home/tim/Videos/puppets +p39 +sVimage +p40 +V:/media/slidecontroller_multimedia.png +p41 +sg11 +VMVI_3405.MOV +p42 +sassa. \ No newline at end of file