diff --git a/openlp.py b/openlp.py index 5d507606d..e383cd86a 100755 --- a/openlp.py +++ b/openlp.py @@ -28,7 +28,9 @@ ############################################################################### import sys +import multiprocessing +from openlp.core.common import is_win, is_macosx from openlp.core import main @@ -36,9 +38,13 @@ if __name__ == '__main__': """ Instantiate and run the application. """ + # Add support for using multiprocessing from frozen Windows executable (built using PyInstaller), + # see https://docs.python.org/3/library/multiprocessing.html#multiprocessing.freeze_support + if is_win(): + multiprocessing.freeze_support() # Mac OS X passes arguments like '-psn_XXXX' to the application. This argument is actually a process serial number. # However, this causes a conflict with other OpenLP arguments. Since we do not use this argument we can delete it # to avoid any potential conflicts. - if sys.platform.startswith('darwin'): + if is_macosx(): sys.argv = [x for x in sys.argv if not x.startswith('-psn')] main() diff --git a/openlp/core/lib/__init__.py b/openlp/core/lib/__init__.py index dd9eb8978..85a43b9ed 100644 --- a/openlp/core/lib/__init__.py +++ b/openlp/core/lib/__init__.py @@ -318,6 +318,7 @@ def create_separated_list(string_list): return translate('OpenLP.core.lib', '%s, %s', 'Locale list separator: start') % (string_list[0], merged) +from .colorbutton import ColorButton from .filedialog import FileDialog from .screen import ScreenList from .listwidgetwithdnd import ListWidgetWithDnD diff --git a/openlp/core/lib/colorbutton.py b/openlp/core/lib/colorbutton.py new file mode 100644 index 000000000..6ebf9ee99 --- /dev/null +++ b/openlp/core/lib/colorbutton.py @@ -0,0 +1,89 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2014 Raoul Snyman # +# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan # +# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, # +# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. # +# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, # +# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, # +# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, # +# Frode Woldsund, Martin Zibricky, Patrick Zimmermann # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### + +""" +Provide a custom widget based on QPushButton for the selection of colors +""" +from PyQt4 import QtCore, QtGui + +from openlp.core.common import translate + + +class ColorButton(QtGui.QPushButton): + """ + Subclasses QPushbutton to create a "Color Chooser" button + """ + + colorChanged = QtCore.pyqtSignal(str) + + def __init__(self, parent=None): + """ + Initialise the ColorButton + """ + super(ColorButton, self).__init__() + self.parent = parent + self.change_color('#ffffff') + self.setToolTip(translate('OpenLP.ColorButton', 'Click to select a color.')) + self.clicked.connect(self.on_clicked) + + def change_color(self, color): + """ + Sets the _color variable and the background color. + + :param color: String representation of a hexidecimal color + """ + self._color = color + self.setStyleSheet('background-color: %s' % color) + + @property + def color(self): + """ + Property method to return the color variable + + :return: String representation of a hexidecimal color + """ + return self._color + + @color.setter + def color(self, color): + """ + Property setter to change the imstamce color + + :param color: String representation of a hexidecimal color + """ + self.change_color(color) + + def on_clicked(self): + """ + Handle the PushButton clicked signal, showing the ColorDialog and validating the input + """ + new_color = QtGui.QColorDialog.getColor(QtGui.QColor(self._color), self.parent) + if new_color.isValid() and self._color != new_color.name(): + self.change_color(new_color.name()) + self.colorChanged.emit(new_color.name()) diff --git a/openlp/core/lib/renderer.py b/openlp/core/lib/renderer.py index ab4a5a4df..3f7ffc80f 100644 --- a/openlp/core/lib/renderer.py +++ b/openlp/core/lib/renderer.py @@ -250,7 +250,13 @@ class Renderer(OpenLPMixin, RegistryMixin, RegistryProperties): # Remove two or more option slide breaks next to each other (causing infinite loop). while '\n[---]\n[---]\n' in text: text = text.replace('\n[---]\n[---]\n', '\n[---]\n') - while True: + while ' [---]' in text: + text = text.replace(' [---]', '[---]') + while '[---] ' in text: + text = text.replace('[---] ', '[---]') + count = 0 + # only loop 5 times as there will never be more than 5 incorrect logical splits on a single slide. + while True and count < 5: slides = text.split('\n[---]\n', 2) # If there are (at least) two occurrences of [---] we use the first two slides (and neglect the last # for now). @@ -296,6 +302,7 @@ class Renderer(OpenLPMixin, RegistryMixin, RegistryProperties): lines = text.strip('\n').split('\n') pages.extend(self._paginate_slide(lines, line_end)) break + count =+ 1 else: # Clean up line endings. pages = self._paginate_slide(text.split('\n'), line_end) diff --git a/openlp/core/ui/advancedtab.py b/openlp/core/ui/advancedtab.py index a5ce09bdf..c4d966562 100644 --- a/openlp/core/ui/advancedtab.py +++ b/openlp/core/ui/advancedtab.py @@ -37,7 +37,7 @@ import sys from PyQt4 import QtCore, QtGui from openlp.core.common import AppLocation, Settings, SlideLimits, UiStrings, translate -from openlp.core.lib import SettingsTab, build_icon +from openlp.core.lib import ColorButton, SettingsTab, build_icon from openlp.core.utils import format_time, get_images_filter log = logging.getLogger(__name__) @@ -181,7 +181,7 @@ class AdvancedTab(SettingsTab): self.default_image_layout.setObjectName('default_image_layout') self.default_color_label = QtGui.QLabel(self.default_image_group_box) self.default_color_label.setObjectName('default_color_label') - self.default_color_button = QtGui.QPushButton(self.default_image_group_box) + self.default_color_button = ColorButton(self.default_image_group_box) self.default_color_button.setObjectName('default_color_button') self.default_image_layout.addRow(self.default_color_label, self.default_color_button) self.default_file_label = QtGui.QLabel(self.default_image_group_box) @@ -247,7 +247,7 @@ class AdvancedTab(SettingsTab): self.service_name_time.timeChanged.connect(self.update_service_name_example) self.service_name_edit.textChanged.connect(self.update_service_name_example) self.service_name_revert_button.clicked.connect(self.on_service_name_revert_button_clicked) - self.default_color_button.clicked.connect(self.on_default_color_button_clicked) + self.default_color_button.colorChanged.connect(self.on_background_color_changed) self.default_browse_button.clicked.connect(self.on_default_browse_button_clicked) self.default_revert_button.clicked.connect(self.on_default_revert_button_clicked) self.alternate_rows_check_box.toggled.connect(self.on_alternate_rows_check_box_toggled) @@ -299,7 +299,6 @@ class AdvancedTab(SettingsTab): self.hide_mouse_check_box.setText(translate('OpenLP.AdvancedTab', 'Hide mouse cursor when over display window')) self.default_image_group_box.setTitle(translate('OpenLP.AdvancedTab', 'Default Image')) self.default_color_label.setText(translate('OpenLP.AdvancedTab', 'Background color:')) - self.default_color_button.setToolTip(translate('OpenLP.AdvancedTab', 'Click to select a color.')) self.default_file_label.setText(translate('OpenLP.AdvancedTab', 'Image file:')) self.default_browse_button.setToolTip(translate('OpenLP.AdvancedTab', 'Browse for an image file to display.')) self.default_revert_button.setToolTip(translate('OpenLP.AdvancedTab', 'Revert to the default OpenLP logo.')) @@ -395,7 +394,7 @@ class AdvancedTab(SettingsTab): self.current_data_path = AppLocation.get_data_path() log.warning('User requested data path set to default %s' % self.current_data_path) self.data_directory_label.setText(os.path.abspath(self.current_data_path)) - self.default_color_button.setStyleSheet('background-color: %s' % self.default_color) + self.default_color_button.color = self.default_color # Don't allow data directory move if running portable. if settings.value('advanced/is portable'): self.data_directory_group_box.hide() @@ -498,14 +497,11 @@ class AdvancedTab(SettingsTab): self.service_name_edit.setText(UiStrings().DefaultServiceName) self.service_name_edit.setFocus() - def on_default_color_button_clicked(self): + def on_background_color_changed(self, color): """ Select the background colour of the default display screen. """ - new_color = QtGui.QColorDialog.getColor(QtGui.QColor(self.default_color), self) - if new_color.isValid(): - self.default_color = new_color.name() - self.default_color_button.setStyleSheet('background-color: %s' % self.default_color) + self.default_color = color def on_default_browse_button_clicked(self): """ diff --git a/openlp/core/ui/firsttimeform.py b/openlp/core/ui/firsttimeform.py index 148cb0f60..558db3087 100644 --- a/openlp/core/ui/firsttimeform.py +++ b/openlp/core/ui/firsttimeform.py @@ -112,6 +112,17 @@ class FirstTimeForm(QtGui.QWizard, UiFirstTimeWizard, RegistryProperties): """ self.application.process_events() if self.currentId() == FirstTimePage.Download: + if self.has_run_wizard: + self.songs_check_box.setChecked(self.plugin_manager.get_plugin_by_name('songs').is_active()) + self.bible_check_box.setChecked(self.plugin_manager.get_plugin_by_name('bibles').is_active()) + self.presentation_check_box.setChecked(self.plugin_manager.get_plugin_by_name( + 'presentations').is_active()) + self.image_check_box.setChecked(self.plugin_manager.get_plugin_by_name('images').is_active()) + self.media_check_box.setChecked(self.plugin_manager.get_plugin_by_name('media').is_active()) + self.remote_check_box.setChecked(self.plugin_manager.get_plugin_by_name('remotes').is_active()) + self.custom_check_box.setChecked(self.plugin_manager.get_plugin_by_name('custom').is_active()) + self.song_usage_check_box.setChecked(self.plugin_manager.get_plugin_by_name('songusage').is_active()) + self.alert_check_box.setChecked(self.plugin_manager.get_plugin_by_name('alerts').is_active()) if not self.web_access: return FirstTimePage.NoInternet else: diff --git a/openlp/core/ui/mainwindow.py b/openlp/core/ui/mainwindow.py index 48fc1fcbf..ed0e11ddd 100644 --- a/openlp/core/ui/mainwindow.py +++ b/openlp/core/ui/mainwindow.py @@ -431,7 +431,7 @@ class Ui_MainWindow(object): translate('OpenLP.MainWindow', 'Import OpenLP settings from a specified *.config file previously ' 'exported on this or another machine')) self.settings_import_item.setText(translate('OpenLP.MainWindow', 'Settings')) - self.view_projector_manager_item.setText(translate('OPenLP.MainWindow', '&ProjectorManager')) + self.view_projector_manager_item.setText(translate('OPenLP.MainWindow', '&Projector Manager')) self.view_projector_manager_item.setToolTip(translate('OpenLP.MainWindow', 'Toggle Projector Manager')) self.view_projector_manager_item.setStatusTip(translate('OpenLP.MainWindow', 'Toggle the visibility of the Projector Manager')) @@ -641,10 +641,10 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow, RegistryProperties): if view_mode == 'default': self.mode_default_item.setChecked(True) elif view_mode == 'setup': - self.set_view_mode(True, True, False, True, False) + self.set_view_mode(True, True, False, True, False, True) self.mode_setup_item.setChecked(True) elif view_mode == 'live': - self.set_view_mode(False, True, False, False, True) + self.set_view_mode(False, True, False, False, True, True) self.mode_live_item.setChecked(True) def app_startup(self): @@ -978,7 +978,14 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow, RegistryProperties): # FIXME: We are conflicting with the standard "General" section. if 'eneral' in section_key: section_key = section_key.lower() - key_value = settings.value(section_key) + try: + key_value = settings.value(section_key) + except KeyError: + QtGui.QMessageBox.critical(self, translate('OpenLP.MainWindow', 'Export setting error'), + translate('OpenLP.MainWindow', 'The key "%s" does not have a default value ' + 'so it will be skipped in this export.') % section_key, + QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.Ok)) + key_value = None if key_value is not None: export_settings.setValue(section_key, key_value) export_settings.sync() @@ -1000,21 +1007,21 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow, RegistryProperties): """ Put OpenLP into "Default" view mode. """ - self.set_view_mode(True, True, True, True, True, 'default') + self.set_view_mode(True, True, True, True, True, True, 'default') def on_mode_setup_item_clicked(self): """ Put OpenLP into "Setup" view mode. """ - self.set_view_mode(True, True, False, True, False, 'setup') + self.set_view_mode(True, True, False, True, False, True, 'setup') def on_mode_live_item_clicked(self): """ Put OpenLP into "Live" view mode. """ - self.set_view_mode(False, True, False, False, True, 'live') + self.set_view_mode(False, True, False, False, True, True, 'live') - def set_view_mode(self, media=True, service=True, theme=True, preview=True, live=True, mode=''): + def set_view_mode(self, media=True, service=True, theme=True, preview=True, live=True, projector=True, mode=''): """ Set OpenLP to a different view mode. """ @@ -1024,6 +1031,7 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow, RegistryProperties): self.media_manager_dock.setVisible(media) self.service_manager_dock.setVisible(service) self.theme_manager_dock.setVisible(theme) + self.projector_manager_dock.setVisible(projector) self.set_preview_panel_visibility(preview) self.set_live_panel_visibility(live) @@ -1186,18 +1194,22 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow, RegistryProperties): self.theme_manager_dock.setFeatures(QtGui.QDockWidget.NoDockWidgetFeatures) self.service_manager_dock.setFeatures(QtGui.QDockWidget.NoDockWidgetFeatures) self.media_manager_dock.setFeatures(QtGui.QDockWidget.NoDockWidgetFeatures) + self.projector_manager_dock.setFeatures(QtGui.QDockWidget.NoDockWidgetFeatures) self.view_media_manager_item.setEnabled(False) self.view_service_manager_item.setEnabled(False) self.view_theme_manager_item.setEnabled(False) + self.view_projector_manager_item.setEnabled(False) self.view_preview_panel.setEnabled(False) self.view_live_panel.setEnabled(False) else: self.theme_manager_dock.setFeatures(QtGui.QDockWidget.AllDockWidgetFeatures) self.service_manager_dock.setFeatures(QtGui.QDockWidget.AllDockWidgetFeatures) self.media_manager_dock.setFeatures(QtGui.QDockWidget.AllDockWidgetFeatures) + self.projector_manager_dock.setFeatures(QtGui.QDockWidget.AllDockWidgetFeatures) self.view_media_manager_item.setEnabled(True) self.view_service_manager_item.setEnabled(True) self.view_theme_manager_item.setEnabled(True) + self.view_projector_manager_item.setEnabled(True) self.view_preview_panel.setEnabled(True) self.view_live_panel.setEnabled(True) Settings().setValue('user interface/lock panel', lock) diff --git a/openlp/core/ui/media/playertab.py b/openlp/core/ui/media/playertab.py index 09bac5125..f9667b3de 100644 --- a/openlp/core/ui/media/playertab.py +++ b/openlp/core/ui/media/playertab.py @@ -32,7 +32,7 @@ The :mod:`~openlp.core.ui.media.playertab` module holds the configuration tab fo from PyQt4 import QtCore, QtGui from openlp.core.common import Registry, Settings, UiStrings, translate -from openlp.core.lib import SettingsTab +from openlp.core.lib import ColorButton, SettingsTab from openlp.core.lib.ui import create_button from openlp.core.ui.media import get_media_players, set_media_players @@ -76,7 +76,7 @@ class PlayerTab(SettingsTab): self.background_color_label = QtGui.QLabel(self.background_color_group_box) self.background_color_label.setObjectName('background_color_label') self.color_layout.addWidget(self.background_color_label) - self.background_color_button = QtGui.QPushButton(self.background_color_group_box) + self.background_color_button = ColorButton(self.background_color_group_box) self.background_color_button.setObjectName('background_color_button') self.color_layout.addWidget(self.background_color_button) self.form_layout.addRow(self.color_layout) @@ -124,7 +124,7 @@ class PlayerTab(SettingsTab): self.left_layout.addStretch() self.right_layout.addStretch() # Signals and slots - self.background_color_button.clicked.connect(self.on_background_color_button_clicked) + self.background_color_button.colorChanged.connect(self.on_background_color_changed) def retranslateUi(self): """ @@ -138,14 +138,11 @@ class PlayerTab(SettingsTab): 'Visible background for videos with aspect ratio different to screen.')) self.retranslate_players() - def on_background_color_button_clicked(self): + def on_background_color_changed(self, color): """ Set the background color """ - new_color = QtGui.QColorDialog.getColor(QtGui.QColor(self.background_color), self) - if new_color.isValid(): - self.background_color = new_color.name() - self.background_color_button.setStyleSheet('background-color: %s' % self.background_color) + self.background_color = color def on_player_check_box_changed(self, check_state): """ @@ -212,7 +209,7 @@ class PlayerTab(SettingsTab): self.background_color = settings.value('background color') self.initial_color = self.background_color settings.endGroup() - self.background_color_button.setStyleSheet('background-color: %s' % self.background_color) + self.background_color_button.color = self.background_color def save(self): """ diff --git a/openlp/core/ui/projector/__init__.py b/openlp/core/ui/projector/__init__.py index 9b51f110b..9a09145ac 100644 --- a/openlp/core/ui/projector/__init__.py +++ b/openlp/core/ui/projector/__init__.py @@ -28,4 +28,4 @@ ############################################################################### """ The Projector driver module. -""" \ No newline at end of file +""" diff --git a/openlp/core/ui/servicemanager.py b/openlp/core/ui/servicemanager.py index 48d4de34a..e8b201cf6 100644 --- a/openlp/core/ui/servicemanager.py +++ b/openlp/core/ui/servicemanager.py @@ -494,7 +494,7 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ServiceManage service.append({'openlp_core': core}) return service - def save_file(self): + def save_file(self, field=None): """ Save the current service file. @@ -1427,9 +1427,10 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ServiceManage self.drop_position = -1 self.set_modified() - def make_preview(self): + def make_preview(self, field=None): """ Send the current item to the Preview slide controller + :param field: """ self.application.set_busy_cursor() item, child = self.find_service_item() diff --git a/openlp/core/ui/settingsdialog.py b/openlp/core/ui/settingsdialog.py index ab5931584..61ee10e14 100644 --- a/openlp/core/ui/settingsdialog.py +++ b/openlp/core/ui/settingsdialog.py @@ -46,7 +46,7 @@ class Ui_SettingsDialog(object): """ settings_dialog.setObjectName('settings_dialog') settings_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg')) - settings_dialog.resize(800, 500) + settings_dialog.resize(800, 700) self.dialog_layout = QtGui.QGridLayout(settings_dialog) self.dialog_layout.setObjectName('dialog_layout') self.dialog_layout.setMargin(8) diff --git a/openlp/core/ui/slidecontroller.py b/openlp/core/ui/slidecontroller.py index d12506452..85fe240d4 100644 --- a/openlp/core/ui/slidecontroller.py +++ b/openlp/core/ui/slidecontroller.py @@ -683,7 +683,8 @@ class SlideController(DisplayController, RegistryProperties): self.play_slides_loop.setChecked(False) self.play_slides_loop.setIcon(build_icon(':/media/media_time.png')) if item.is_text(): - if Settings().value(self.main_window.songs_settings_section + '/display songbar') and self.slide_list: + if (Settings().value(self.main_window.songs_settings_section + '/display songbar') + and not self.song_menu.menu().isEmpty()): self.toolbar.set_widget_visible(['song_menu'], True) if item.is_capable(ItemCapabilities.CanLoop) and len(item.get_frames()) > 1: self.toolbar.set_widget_visible(LOOP_LIST) @@ -691,7 +692,8 @@ class SlideController(DisplayController, RegistryProperties): self.mediabar.show() self.previous_item.setVisible(not item.is_media()) self.next_item.setVisible(not item.is_media()) - self.set_blank_menu() + # The layout of the toolbar is size dependent, so make sure it fits + self.on_controller_size_changed(self.controller.width()) # Work-around for OS X, hide and then show the toolbar # See bug #791050 self.toolbar.show() diff --git a/openlp/core/ui/themeform.py b/openlp/core/ui/themeform.py index 46fd227dd..611ac2d4d 100644 --- a/openlp/core/ui/themeform.py +++ b/openlp/core/ui/themeform.py @@ -65,18 +65,18 @@ class ThemeForm(QtGui.QWizard, Ui_ThemeWizard, RegistryProperties): self.theme_layout_form = ThemeLayoutForm(self) self.background_combo_box.currentIndexChanged.connect(self.on_background_combo_box_current_index_changed) self.gradient_combo_box.currentIndexChanged.connect(self.on_gradient_combo_box_current_index_changed) - self.color_button.clicked.connect(self.on_color_button_clicked) - self.image_color_button.clicked.connect(self.on_image_color_button_clicked) - self.gradient_start_button.clicked.connect(self.on_gradient_start_button_clicked) - self.gradient_end_button.clicked.connect(self.on_gradient_end_button_clicked) + self.color_button.colorChanged.connect(self.on_color_changed) + self.image_color_button.colorChanged.connect(self.on_image_color_changed) + self.gradient_start_button.colorChanged.connect(self.on_gradient_start_button_changed) + self.gradient_end_button.colorChanged.connect(self.on_gradient_end_button_changed) self.image_browse_button.clicked.connect(self.on_image_browse_button_clicked) self.image_file_edit.editingFinished.connect(self.on_image_file_edit_editing_finished) - self.main_color_button.clicked.connect(self.on_main_color_button_clicked) - self.outline_color_button.clicked.connect(self.on_outline_color_button_clicked) - self.shadow_color_button.clicked.connect(self.on_shadow_color_button_clicked) + self.main_color_button.colorChanged.connect(self.on_main_color_changed) + self.outline_color_button.colorChanged.connect(self.on_outline_color_changed) + self.shadow_color_button.colorChanged.connect(self.on_shadow_color_changed) self.outline_check_box.stateChanged.connect(self.on_outline_check_check_box_state_changed) self.shadow_check_box.stateChanged.connect(self.on_shadow_check_check_box_state_changed) - self.footer_color_button.clicked.connect(self.on_footer_color_button_clicked) + self.footer_color_button.colorChanged.connect(self.on_footer_color_changed) self.customButtonClicked.connect(self.on_custom_1_button_clicked) self.main_position_check_box.stateChanged.connect(self.on_main_position_check_box_state_changed) self.footer_position_check_box.stateChanged.connect(self.on_footer_position_check_box_state_changed) @@ -295,14 +295,14 @@ class ThemeForm(QtGui.QWizard, Ui_ThemeWizard, RegistryProperties): Handle the display and state of the Background page. """ if self.theme.background_type == BackgroundType.to_string(BackgroundType.Solid): - self.color_button.setStyleSheet('background-color: %s' % self.theme.background_color) + self.color_button.color = self.theme.background_color self.setField('background_type', 0) elif self.theme.background_type == BackgroundType.to_string(BackgroundType.Gradient): - self.gradient_start_button.setStyleSheet('background-color: %s' % self.theme.background_start_color) - self.gradient_end_button.setStyleSheet('background-color: %s' % self.theme.background_end_color) + self.gradient_start_button.color = self.theme.background_start_color + self.gradient_end_button.color = self.theme.background_end_color self.setField('background_type', 1) elif self.theme.background_type == BackgroundType.to_string(BackgroundType.Image): - self.image_color_button.setStyleSheet('background-color: %s' % self.theme.background_border_color) + self.image_color_button.color = self.theme.background_border_color self.image_file_edit.setText(self.theme.background_filename) self.setField('background_type', 2) elif self.theme.background_type == BackgroundType.to_string(BackgroundType.Transparent): @@ -323,14 +323,14 @@ class ThemeForm(QtGui.QWizard, Ui_ThemeWizard, RegistryProperties): Handle the display and state of the Main Area page. """ self.main_font_combo_box.setCurrentFont(QtGui.QFont(self.theme.font_main_name)) - self.main_color_button.setStyleSheet('background-color: %s' % self.theme.font_main_color) + self.main_color_button.color = self.theme.font_main_color self.setField('main_size_spin_box', self.theme.font_main_size) self.setField('line_spacing_spin_box', self.theme.font_main_line_adjustment) self.setField('outline_check_box', self.theme.font_main_outline) - self.outline_color_button.setStyleSheet('background-color: %s' % self.theme.font_main_outline_color) + self.outline_color_button.color = self.theme.font_main_outline_color self.setField('outline_size_spin_box', self.theme.font_main_outline_size) self.setField('shadow_check_box', self.theme.font_main_shadow) - self.shadow_color_button.setStyleSheet('background-color: %s' % self.theme.font_main_shadow_color) + self.shadow_color_button.color = self.theme.font_main_shadow_color self.setField('shadow_size_spin_box', self.theme.font_main_shadow_size) self.setField('main_bold_check_box', self.theme.font_main_bold) self.setField('main_italics_check_box', self.theme.font_main_italics) @@ -340,7 +340,7 @@ class ThemeForm(QtGui.QWizard, Ui_ThemeWizard, RegistryProperties): Handle the display and state of the Footer Area page. """ self.footer_font_combo_box.setCurrentFont(QtGui.QFont(self.theme.font_footer_name)) - self.footer_color_button.setStyleSheet('background-color: %s' % self.theme.font_footer_color) + self.footer_color_button.color = self.theme.font_footer_color self.setField('footer_size_spin_box', self.theme.font_footer_size) def set_position_page_values(self): @@ -399,33 +399,29 @@ class ThemeForm(QtGui.QWizard, Ui_ThemeWizard, RegistryProperties): self.theme.background_direction = BackgroundGradientType.to_string(index) self.set_background_page_values() - def on_color_button_clicked(self): + def on_color_changed(self, color): """ Background / Gradient 1 _color button pushed. """ - self.theme.background_color = self._color_button(self.theme.background_color) - self.set_background_page_values() + self.theme.background_color = color - def on_image_color_button_clicked(self): + def on_image_color_changed(self, color): """ Background / Gradient 1 _color button pushed. """ - self.theme.background_border_color = self._color_button(self.theme.background_border_color) - self.set_background_page_values() + self.theme.background_border_color = color - def on_gradient_start_button_clicked(self): + def on_gradient_start_button_changed(self, color): """ Gradient 2 _color button pushed. """ - self.theme.background_start_color = self._color_button(self.theme.background_start_color) - self.set_background_page_values() + self.theme.background_start_color = color - def on_gradient_end_button_clicked(self): + def on_gradient_end_button_changed(self, color): """ Gradient 2 _color button pushed. """ - self.theme.background_end_color = self._color_button(self.theme.background_end_color) - self.set_background_page_values() + self.theme.background_end_color = color def on_image_browse_button_clicked(self): """ @@ -445,33 +441,29 @@ class ThemeForm(QtGui.QWizard, Ui_ThemeWizard, RegistryProperties): """ self.theme.background_filename = str(self.image_file_edit.text()) - def on_main_color_button_clicked(self): + def on_main_color_changed(self, color): """ Set the main colour value """ - self.theme.font_main_color = self._color_button(self.theme.font_main_color) - self.set_main_area_page_values() + self.theme.font_main_color = color - def on_outline_color_button_clicked(self): + def on_outline_color_changed(self, color): """ Set the outline colour value """ - self.theme.font_main_outline_color = self._color_button(self.theme.font_main_outline_color) - self.set_main_area_page_values() + self.theme.font_main_outline_color = color - def on_shadow_color_button_clicked(self): + def on_shadow_color_changed(self, color): """ Set the shadow colour value """ - self.theme.font_main_shadow_color = self._color_button(self.theme.font_main_shadow_color) - self.set_main_area_page_values() + self.theme.font_main_shadow_color = color - def on_footer_color_button_clicked(self): + def on_footer_color_changed(self, color): """ Set the footer colour value """ - self.theme.font_footer_color = self._color_button(self.theme.font_footer_color) - self.set_footer_area_page_values() + self.theme.font_footer_color = color def update_theme(self): """ @@ -532,12 +524,3 @@ class ThemeForm(QtGui.QWizard, Ui_ThemeWizard, RegistryProperties): return self.theme_manager.save_theme(self.theme, save_from, save_to) return QtGui.QDialog.accept(self) - - def _color_button(self, field): - """ - Handle _color buttons - """ - new_color = QtGui.QColorDialog.getColor(QtGui.QColor(field), self) - if new_color.isValid(): - field = new_color.name() - return field diff --git a/openlp/core/ui/thememanager.py b/openlp/core/ui/thememanager.py index 03890a028..89e269f24 100644 --- a/openlp/core/ui/thememanager.py +++ b/openlp/core/ui/thememanager.py @@ -37,7 +37,7 @@ from xml.etree.ElementTree import ElementTree, XML from PyQt4 import QtCore, QtGui from openlp.core.common import Registry, RegistryProperties, AppLocation, Settings, OpenLPMixin, RegistryMixin, \ - check_directory_exists, UiStrings, translate + check_directory_exists, UiStrings, translate, is_win from openlp.core.lib import FileDialog, ImageSource, OpenLPToolbar, get_text_file_string, build_icon, \ check_item_selected, create_thumb, validate_thumb from openlp.core.lib.theme import ThemeXML, BackgroundType @@ -662,8 +662,12 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ThemeManager, R out_file.close() if image_from and os.path.abspath(image_from) != os.path.abspath(image_to): try: - encoding = get_filesystem_encoding() - shutil.copyfile(str(image_from).encode(encoding), str(image_to).encode(encoding)) + # Windows is always unicode, so no need to encode filenames + if is_win(): + shutil.copyfile(image_from, image_to) + else: + encoding = get_filesystem_encoding() + shutil.copyfile(image_from.encode(encoding), image_to.encode(encoding)) except IOError as xxx_todo_changeme: shutil.Error = xxx_todo_changeme self.log_exception('Failed to save theme image') diff --git a/openlp/core/ui/themewizard.py b/openlp/core/ui/themewizard.py index be3fa4034..6d954133f 100644 --- a/openlp/core/ui/themewizard.py +++ b/openlp/core/ui/themewizard.py @@ -32,7 +32,7 @@ The Create/Edit theme wizard from PyQt4 import QtCore, QtGui from openlp.core.common import UiStrings, translate, is_macosx -from openlp.core.lib import build_icon +from openlp.core.lib import build_icon, ColorButton from openlp.core.lib.theme import HorizontalType, BackgroundType, BackgroundGradientType from openlp.core.lib.ui import add_welcome_page, create_valign_selection_widgets @@ -82,7 +82,7 @@ class Ui_ThemeWizard(object): self.color_layout.setObjectName('color_layout') self.color_label = QtGui.QLabel(self.color_widget) self.color_label.setObjectName('color_label') - self.color_button = QtGui.QPushButton(self.color_widget) + self.color_button = ColorButton(self.color_widget) self.color_button.setObjectName('color_button') self.color_layout.addRow(self.color_label, self.color_button) self.color_layout.setItem(1, QtGui.QFormLayout.LabelRole, self.spacer) @@ -94,12 +94,12 @@ class Ui_ThemeWizard(object): self.gradient_layout.setObjectName('gradient_layout') self.gradient_start_label = QtGui.QLabel(self.gradient_widget) self.gradient_start_label.setObjectName('gradient_start_label') - self.gradient_start_button = QtGui.QPushButton(self.gradient_widget) + self.gradient_start_button = ColorButton(self.gradient_widget) self.gradient_start_button.setObjectName('gradient_start_button') self.gradient_layout.addRow(self.gradient_start_label, self.gradient_start_button) self.gradient_end_label = QtGui.QLabel(self.gradient_widget) self.gradient_end_label.setObjectName('gradient_end_label') - self.gradient_end_button = QtGui.QPushButton(self.gradient_widget) + self.gradient_end_button = ColorButton(self.gradient_widget) self.gradient_end_button.setObjectName('gradient_end_button') self.gradient_layout.addRow(self.gradient_end_label, self.gradient_end_button) self.gradient_type_label = QtGui.QLabel(self.gradient_widget) @@ -117,7 +117,7 @@ class Ui_ThemeWizard(object): self.image_layout.setObjectName('image_layout') self.image_color_label = QtGui.QLabel(self.color_widget) self.image_color_label.setObjectName('image_color_label') - self.image_color_button = QtGui.QPushButton(self.color_widget) + self.image_color_button = ColorButton(self.color_widget) self.image_color_button.setObjectName('image_color_button') self.image_layout.addRow(self.image_color_label, self.image_color_button) self.image_label = QtGui.QLabel(self.image_widget) @@ -156,7 +156,7 @@ class Ui_ThemeWizard(object): self.main_color_label.setObjectName('main_color_label') self.main_properties_layout = QtGui.QHBoxLayout() self.main_properties_layout.setObjectName('main_properties_layout') - self.main_color_button = QtGui.QPushButton(self.main_area_page) + self.main_color_button = ColorButton(self.main_area_page) self.main_color_button.setObjectName('main_color_button') self.main_properties_layout.addWidget(self.main_color_button) self.main_properties_layout.addSpacing(20) @@ -192,7 +192,7 @@ class Ui_ThemeWizard(object): self.outline_check_box.setObjectName('outline_check_box') self.outline_layout = QtGui.QHBoxLayout() self.outline_layout.setObjectName('outline_layout') - self.outline_color_button = QtGui.QPushButton(self.main_area_page) + self.outline_color_button = ColorButton(self.main_area_page) self.outline_color_button.setEnabled(False) self.outline_color_button.setObjectName('Outline_color_button') self.outline_layout.addWidget(self.outline_color_button) @@ -209,7 +209,7 @@ class Ui_ThemeWizard(object): self.shadow_check_box.setObjectName('shadow_check_box') self.shadow_layout = QtGui.QHBoxLayout() self.shadow_layout.setObjectName('shadow_layout') - self.shadow_color_button = QtGui.QPushButton(self.main_area_page) + self.shadow_color_button = ColorButton(self.main_area_page) self.shadow_color_button.setEnabled(False) self.shadow_color_button.setObjectName('shadow_color_button') self.shadow_layout.addWidget(self.shadow_color_button) @@ -235,7 +235,7 @@ class Ui_ThemeWizard(object): self.footer_area_layout.addRow(self.footer_font_label, self.footer_font_combo_box) self.footer_color_label = QtGui.QLabel(self.footer_area_page) self.footer_color_label.setObjectName('footer_color_label') - self.footer_color_button = QtGui.QPushButton(self.footer_area_page) + self.footer_color_button = ColorButton(self.footer_area_page) self.footer_color_button.setObjectName('footer_color_button') self.footer_area_layout.addRow(self.footer_color_label, self.footer_color_button) self.footer_size_label = QtGui.QLabel(self.footer_area_page) diff --git a/openlp/plugins/alerts/lib/alertstab.py b/openlp/plugins/alerts/lib/alertstab.py index 7ac5db672..b92062f82 100644 --- a/openlp/plugins/alerts/lib/alertstab.py +++ b/openlp/plugins/alerts/lib/alertstab.py @@ -30,7 +30,7 @@ from PyQt4 import QtGui from openlp.core.common import Settings, UiStrings, translate -from openlp.core.lib import SettingsTab +from openlp.core.lib import ColorButton, SettingsTab from openlp.core.lib.ui import create_valign_selection_widgets @@ -57,14 +57,14 @@ class AlertsTab(SettingsTab): self.font_color_label.setObjectName('font_color_label') self.color_layout = QtGui.QHBoxLayout() self.color_layout.setObjectName('color_layout') - self.font_color_button = QtGui.QPushButton(self.font_group_box) + self.font_color_button = ColorButton(self.font_group_box) self.font_color_button.setObjectName('font_color_button') self.color_layout.addWidget(self.font_color_button) self.color_layout.addSpacing(20) self.background_color_label = QtGui.QLabel(self.font_group_box) self.background_color_label.setObjectName('background_color_label') self.color_layout.addWidget(self.background_color_label) - self.background_color_button = QtGui.QPushButton(self.font_group_box) + self.background_color_button = ColorButton(self.font_group_box) self.background_color_button.setObjectName('background_color_button') self.color_layout.addWidget(self.background_color_button) self.font_layout.addRow(self.font_color_label, self.color_layout) @@ -95,8 +95,8 @@ class AlertsTab(SettingsTab): self.right_layout.addWidget(self.preview_group_box) self.right_layout.addStretch() # Signals and slots - self.background_color_button.clicked.connect(self.on_background_color_button_clicked) - self.font_color_button.clicked.connect(self.on_font_color_button_clicked) + self.background_color_button.colorChanged.connect(self.on_background_color_changed) + self.font_color_button.colorChanged.connect(self.on_font_color_changed) self.font_combo_box.activated.connect(self.on_font_combo_box_clicked) self.timeout_spin_box.valueChanged.connect(self.on_timeout_spin_box_changed) self.font_size_spin_box.valueChanged.connect(self.on_font_size_spin_box_changed) @@ -113,15 +113,12 @@ class AlertsTab(SettingsTab): self.preview_group_box.setTitle(UiStrings().Preview) self.font_preview.setText(UiStrings().OLPV2x) - def on_background_color_button_clicked(self): + def on_background_color_changed(self, color): """ The background color has been changed. """ - new_color = QtGui.QColorDialog.getColor(QtGui.QColor(self.background_color), self) - if new_color.isValid(): - self.background_color = new_color.name() - self.background_color_button.setStyleSheet('background-color: %s' % self.background_color) - self.update_display() + self.background_color = color + self.update_display() def on_font_combo_box_clicked(self): """ @@ -129,15 +126,12 @@ class AlertsTab(SettingsTab): """ self.update_display() - def on_font_color_button_clicked(self): + def on_font_color_changed(self, color): """ The Font Color button has clicked. """ - new_color = QtGui.QColorDialog.getColor(QtGui.QColor(self.font_color), self) - if new_color.isValid(): - self.font_color = new_color.name() - self.font_color_button.setStyleSheet('background-color: %s' % self.font_color) - self.update_display() + self.font_color = color + self.update_display() def on_timeout_spin_box_changed(self): """ @@ -169,8 +163,8 @@ class AlertsTab(SettingsTab): settings.endGroup() self.font_size_spin_box.setValue(self.font_size) self.timeout_spin_box.setValue(self.timeout) - self.font_color_button.setStyleSheet('background-color: %s' % self.font_color) - self.background_color_button.setStyleSheet('background-color: %s' % self.background_color) + self.font_color_button.color = self.font_color + self.background_color_button.color = self.background_color self.vertical_combo_box.setCurrentIndex(self.location) font = QtGui.QFont() font.setFamily(self.font_face) diff --git a/openlp/plugins/bibles/lib/__init__.py b/openlp/plugins/bibles/lib/__init__.py index d67319797..fb10e6740 100644 --- a/openlp/plugins/bibles/lib/__init__.py +++ b/openlp/plugins/bibles/lib/__init__.py @@ -330,7 +330,10 @@ def parse_reference(reference, bible, language_selection, book_ref_id=False): if not book_ref_id: book_ref_id = bible.get_book_ref_id_by_localised_name(book, language_selection) elif not bible.get_book_by_book_ref_id(book_ref_id): - book_ref_id = False + return False + # We have not found the book so do not continue + if not book_ref_id: + return False ranges = match.group('ranges') range_list = get_reference_match('range_separator').split(ranges) ref_list = [] diff --git a/openlp/plugins/images/lib/imagetab.py b/openlp/plugins/images/lib/imagetab.py index 975d06fdb..27a48eddc 100644 --- a/openlp/plugins/images/lib/imagetab.py +++ b/openlp/plugins/images/lib/imagetab.py @@ -30,7 +30,7 @@ from PyQt4 import QtGui from openlp.core.common import Settings, UiStrings, translate -from openlp.core.lib import SettingsTab +from openlp.core.lib import ColorButton, SettingsTab class ImageTab(SettingsTab): @@ -51,7 +51,7 @@ class ImageTab(SettingsTab): self.background_color_label = QtGui.QLabel(self.background_color_group_box) self.background_color_label.setObjectName('background_color_label') self.color_layout.addWidget(self.background_color_label) - self.background_color_button = QtGui.QPushButton(self.background_color_group_box) + self.background_color_button = ColorButton(self.background_color_group_box) self.background_color_button.setObjectName('background_color_button') self.color_layout.addWidget(self.background_color_button) self.form_layout.addRow(self.color_layout) @@ -64,7 +64,7 @@ class ImageTab(SettingsTab): self.right_column.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Preferred) self.right_layout.addStretch() # Signals and slots - self.background_color_button.clicked.connect(self.on_background_color_button_clicked) + self.background_color_button.colorChanged.connect(self.on_background_color_changed) def retranslateUi(self): self.background_color_group_box.setTitle(UiStrings().BackgroundColor) @@ -72,11 +72,8 @@ class ImageTab(SettingsTab): self.information_label.setText( translate('ImagesPlugin.ImageTab', 'Visible background for images with aspect ratio different to screen.')) - def on_background_color_button_clicked(self): - new_color = QtGui.QColorDialog.getColor(QtGui.QColor(self.background_color), self) - if new_color.isValid(): - self.background_color = new_color.name() - self.background_color_button.setStyleSheet('background-color: %s' % self.background_color) + def on_background_color_changed(self, color): + self.background_color = color def load(self): settings = Settings() @@ -84,7 +81,7 @@ class ImageTab(SettingsTab): self.background_color = settings.value('background color') self.initial_color = self.background_color settings.endGroup() - self.background_color_button.setStyleSheet('background-color: %s' % self.background_color) + self.background_color_button.color = self.background_color def save(self): settings = Settings() diff --git a/openlp/plugins/media/forms/mediaclipselectorform.py b/openlp/plugins/media/forms/mediaclipselectorform.py index 86727d336..2cdbbc6ef 100644 --- a/openlp/plugins/media/forms/mediaclipselectorform.py +++ b/openlp/plugins/media/forms/mediaclipselectorform.py @@ -446,6 +446,13 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector, RegistryPropert # Set media length info self.playback_length = self.vlc_media_player.get_length() log.debug('playback_length: %d ms' % self.playback_length) + # if length is 0, wait a bit, maybe vlc will change its mind... + loop_count = 0 + while self.playback_length == 0 and loop_count < 20: + sleep(0.1) + self.playback_length = self.vlc_media_player.get_length() + loop_count += 1 + log.debug('in loop, playback_length: %d ms' % self.playback_length) self.position_slider.setMaximum(self.playback_length) # setup start and end time rounded_vlc_ms_length = int(round(self.playback_length / 100.0) * 100.0) diff --git a/openlp/plugins/songs/forms/duplicatesongremovalform.py b/openlp/plugins/songs/forms/duplicatesongremovalform.py index c411c8c1c..4bcce1c44 100644 --- a/openlp/plugins/songs/forms/duplicatesongremovalform.py +++ b/openlp/plugins/songs/forms/duplicatesongremovalform.py @@ -48,14 +48,15 @@ log = logging.getLogger(__name__) def song_generator(songs): """ - This is a generator function to return tuples of two songs. When completed then all songs have once been returned - combined with any other songs. + This is a generator function to return tuples of tuple with two songs and their position in the song array. + When completed then all songs have once been returned combined with any other songs. :param songs: All songs in the database. """ for outer_song_counter in range(len(songs) - 1): for inner_song_counter in range(outer_song_counter + 1, len(songs)): - yield (songs[outer_song_counter], songs[inner_song_counter]) + yield ((outer_song_counter, songs[outer_song_counter].search_lyrics), + (inner_song_counter, songs[inner_song_counter].search_lyrics)) class DuplicateSongRemovalForm(OpenLPWizard, RegistryProperties): @@ -187,16 +188,17 @@ class DuplicateSongRemovalForm(OpenLPWizard, RegistryProperties): # Do not accept any further tasks. Also this closes the processes if all tasks are done. pool.close() # While the processes are still working, start to look at the results. - for song_tuple in result: + for pos_tuple in result: self.duplicate_search_progress_bar.setValue(self.duplicate_search_progress_bar.value() + 1) # The call to process_events() will keep the GUI responsive. self.application.process_events() if self.break_search: pool.terminate() return - if song_tuple is None: + if pos_tuple is None: continue - song1, song2 = song_tuple + song1 = songs[pos_tuple[0]] + song2 = songs[pos_tuple[1]] duplicate_added = self.add_duplicates_to_song_list(song1, song2) if duplicate_added: self.found_duplicates_edit.appendPlainText(song1.title + " = " + song2.title) diff --git a/openlp/plugins/songs/forms/songselectform.py b/openlp/plugins/songs/forms/songselectform.py index aecdf9682..0a8280f52 100755 --- a/openlp/plugins/songs/forms/songselectform.py +++ b/openlp/plugins/songs/forms/songselectform.py @@ -89,13 +89,19 @@ class SongSelectForm(QtGui.QDialog, Ui_SongSelectDialog): def __init__(self, parent=None, plugin=None, db_manager=None): QtGui.QDialog.__init__(self, parent) + self.plugin = plugin + self.db_manager = db_manager self.setup_ui(self) + + def initialise(self): + """ + Initialise the SongSelectForm + """ self.thread = None self.worker = None self.song_count = 0 self.song = None - self.plugin = plugin - self.song_select_importer = SongSelectImport(db_manager) + self.song_select_importer = SongSelectImport(self.db_manager) self.save_password_checkbox.toggled.connect(self.on_save_password_checkbox_toggled) self.login_button.clicked.connect(self.on_login_button_clicked) self.search_button.clicked.connect(self.on_search_button_clicked) @@ -264,6 +270,9 @@ class SongSelectForm(QtGui.QDialog, Ui_SongSelectDialog): self.login_progress_bar.setValue(0) self.login_spacer.setVisible(True) self.login_button.setEnabled(True) + self.username_edit.setEnabled(True) + self.password_edit.setEnabled(True) + self.save_password_checkbox.setEnabled(True) self.search_combobox.setFocus() self.application.process_events() diff --git a/openlp/plugins/songs/lib/importers/wordsofworship.py b/openlp/plugins/songs/lib/importers/wordsofworship.py index 1b398c604..29b384085 100644 --- a/openlp/plugins/songs/lib/importers/wordsofworship.py +++ b/openlp/plugins/songs/lib/importers/wordsofworship.py @@ -99,7 +99,7 @@ class WordsOfWorshipImport(SongImport): """ Initialise the Words of Worship importer. """ - SongImport.__init__(self, manager, **kwargs) + super(WordsOfWorshipImport, self).__init__(manager, **kwargs) def do_import(self): """ @@ -112,17 +112,17 @@ class WordsOfWorshipImport(SongImport): return self.set_defaults() song_data = open(source, 'rb') - if song_data.read(19) != 'WoW File\nSong Words': + if song_data.read(19).decode() != 'WoW File\nSong Words': self.log_error(source, str(translate('SongsPlugin.WordsofWorshipSongImport', - 'Invalid Words of Worship song file. Missing "Wow File\\nSong ' + 'Invalid Words of Worship song file. Missing "WoW File\\nSong ' 'Words" header.'))) continue # Seek to byte which stores number of blocks in the song song_data.seek(56) no_of_blocks = ord(song_data.read(1)) song_data.seek(66) - if song_data.read(16) != 'CSongDoc::CBlock': + if song_data.read(16).decode() != 'CSongDoc::CBlock': self.log_error(source, str(translate('SongsPlugin.WordsofWorshipSongImport', 'Invalid Words of Worship song file. Missing "CSongDoc::CBlock" ' @@ -131,11 +131,17 @@ class WordsOfWorshipImport(SongImport): # Seek to the beginning of the first block song_data.seek(82) for block in range(no_of_blocks): + skip_char_at_end = True self.lines_to_read = ord(song_data.read(4)[:1]) block_text = '' while self.lines_to_read: self.line_text = str(song_data.read(ord(song_data.read(1))), 'cp1252') - song_data.seek(1, os.SEEK_CUR) + if skip_char_at_end: + skip_char = ord(song_data.read(1)) + # Check if we really should skip a char. In some wsg files we shouldn't + if skip_char != 0: + song_data.seek(-1, os.SEEK_CUR) + skip_char_at_end = False if block_text: block_text += '\n' block_text += self.line_text diff --git a/openlp/plugins/songs/lib/importers/worshipassistant.py b/openlp/plugins/songs/lib/importers/worshipassistant.py index 6ddc71159..965e7d857 100644 --- a/openlp/plugins/songs/lib/importers/worshipassistant.py +++ b/openlp/plugins/songs/lib/importers/worshipassistant.py @@ -93,7 +93,7 @@ class WorshipAssistantImport(SongImport): details = chardet.detect(detect_content) detect_file.close() songs_file = open(self.import_source, 'r', encoding=details['encoding']) - songs_reader = csv.DictReader(songs_file) + songs_reader = csv.DictReader(songs_file, escapechar='\\') try: records = list(songs_reader) except csv.Error as e: @@ -104,6 +104,8 @@ class WorshipAssistantImport(SongImport): num_records = len(records) log.info('%s records found in CSV file' % num_records) self.import_wizard.progress_bar.setMaximum(num_records) + # Create regex to strip html tags + re_html_strip = re.compile(r'<[^>]+>') for index, record in enumerate(records, 1): if self.stop_import_flag: return @@ -119,13 +121,12 @@ class WorshipAssistantImport(SongImport): self.title = record['TITLE'] if record['AUTHOR'] != EMPTY_STR: self.parse_author(record['AUTHOR']) - print(record['AUTHOR']) if record['COPYRIGHT'] != EMPTY_STR: self.add_copyright(record['COPYRIGHT']) if record['CCLINR'] != EMPTY_STR: self.ccli_number = record['CCLINR'] if record['ROADMAP'] != EMPTY_STR: - verse_order_list = record['ROADMAP'].split(',') + verse_order_list = [x.strip() for x in record['ROADMAP'].split(',')] lyrics = record['LYRICS2'] except UnicodeDecodeError as e: self.log_error(translate('SongsPlugin.WorshipAssistantImport', 'Record %d' % index), @@ -136,8 +137,15 @@ class WorshipAssistantImport(SongImport): 'File not valid WorshipAssistant CSV format.'), 'TypeError: %s' % e) return verse = '' + used_verses = [] for line in lyrics.splitlines(): if line.startswith('['): # verse marker + # Add previous verse + if verse: + # remove trailing linebreak, part of the WA syntax + self.add_verse(verse[:-1], verse_id) + used_verses.append(verse_id) + verse = '' # drop the square brackets right_bracket = line.find(']') content = line[1:right_bracket].lower() @@ -152,19 +160,31 @@ class WorshipAssistantImport(SongImport): verse_index = VerseType.from_loose_input(verse_tag) if verse_tag else 0 verse_tag = VerseType.tags[verse_index] # Update verse order when the verse name has changed - if content != verse_tag + verse_num: + verse_id = verse_tag + verse_num + # Make sure we've not choosen an id already used + while verse_id in verse_order_list and content in verse_order_list: + verse_num = str(int(verse_num) + 1) + verse_id = verse_tag + verse_num + if content != verse_id: for i in range(len(verse_order_list)): if verse_order_list[i].lower() == content.lower(): - verse_order_list[i] = verse_tag + verse_num - elif line and not line.isspace(): - verse += line + '\n' - elif verse: - self.add_verse(verse, verse_tag+verse_num) - verse = '' + verse_order_list[i] = verse_id + else: + # add line text to verse. Strip out html + verse += re_html_strip.sub('', line) + '\n' if verse: - self.add_verse(verse, verse_tag+verse_num) + # remove trailing linebreak, part of the WA syntax + if verse.endswith('\n\n'): + verse = verse[:-1] + self.add_verse(verse, verse_id) + used_verses.append(verse_id) if verse_order_list: - self.verse_order_list = verse_order_list + # Use the verse order in the import, but remove entries that doesn't have a text + cleaned_verse_order_list = [] + for verse in verse_order_list: + if verse in used_verses: + cleaned_verse_order_list.append(verse) + self.verse_order_list = cleaned_verse_order_list if not self.finish(): self.log_error(translate('SongsPlugin.WorshipAssistantImport', 'Record %d') % index + (': "' + self.title + '"' if self.title else '')) diff --git a/openlp/plugins/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py index 33c4f2e53..18d964115 100644 --- a/openlp/plugins/songs/lib/mediaitem.py +++ b/openlp/plugins/songs/lib/mediaitem.py @@ -339,6 +339,9 @@ class SongMediaItem(MediaManagerItem): self.remote_song = -1 self.remote_triggered = None if item: + if preview: + # A song can only be edited if it comes from plugin, so we set it again for the new item. + item.from_plugin = True return item return None diff --git a/openlp/plugins/songs/lib/openlyricsexport.py b/openlp/plugins/songs/lib/openlyricsexport.py index 0458b893b..84647dbee 100644 --- a/openlp/plugins/songs/lib/openlyricsexport.py +++ b/openlp/plugins/songs/lib/openlyricsexport.py @@ -75,9 +75,14 @@ class OpenLyricsExport(RegistryProperties): filename = '%s (%s)' % (song.title, ', '.join([author.display_name for author in song.authors])) filename = clean_filename(filename) # Ensure the filename isn't too long for some filesystems - filename = '%s.xml' % filename[0:250 - len(self.save_path)] + filename_with_ext = '%s.xml' % filename[0:250 - len(self.save_path)] + # Make sure we're not overwriting an existing file + conflicts = 0 + while os.path.exists(os.path.join(self.save_path, filename_with_ext)): + conflicts += 1 + filename_with_ext = '%s-%d.xml' % (filename[0:247 - len(self.save_path)], conflicts) # Pass a file object, because lxml does not cope with some special # characters in the path (see lp:757673 and lp:744337). - tree.write(open(os.path.join(self.save_path, filename), 'wb'), encoding='utf-8', xml_declaration=True, - pretty_print=True) + tree.write(open(os.path.join(self.save_path, filename_with_ext), 'wb'), encoding='utf-8', + xml_declaration=True, pretty_print=True) return True diff --git a/openlp/plugins/songs/lib/songcompare.py b/openlp/plugins/songs/lib/songcompare.py index 8a6cc7130..ddd5e4552 100644 --- a/openlp/plugins/songs/lib/songcompare.py +++ b/openlp/plugins/songs/lib/songcompare.py @@ -59,12 +59,14 @@ def songs_probably_equal(song_tupel): :param song_tupel: A tuple of two songs to compare. """ song1, song2 = song_tupel - if len(song1.search_lyrics) < len(song2.search_lyrics): - small = song1.search_lyrics - large = song2.search_lyrics + pos1, lyrics1 = song1 + pos2, lyrics2 = song2 + if len(lyrics1) < len(lyrics2): + small = lyrics1 + large = lyrics2 else: - small = song2.search_lyrics - large = song1.search_lyrics + small = lyrics2 + large = lyrics1 differ = difflib.SequenceMatcher(a=large, b=small) diff_tuples = differ.get_opcodes() diff_no_typos = _remove_typos(diff_tuples) @@ -77,7 +79,7 @@ def songs_probably_equal(song_tupel): length_of_equal_blocks += _op_length(element) if length_of_equal_blocks >= MIN_BLOCK_SIZE: - return song1, song2 + return pos1, pos2 # Check 2: Similarity based on the relative length of the longest equal block. # Calculate the length of the largest equal block of the diff set. length_of_longest_equal_block = 0 @@ -85,7 +87,7 @@ def songs_probably_equal(song_tupel): if element[0] == "equal" and _op_length(element) > length_of_longest_equal_block: length_of_longest_equal_block = _op_length(element) if length_of_longest_equal_block > len(small) * 2 // 3: - return song1, song2 + return pos1, pos2 # Both checks failed. We assume the songs are not equal. return None diff --git a/openlp/plugins/songs/songsplugin.py b/openlp/plugins/songs/songsplugin.py index 56834a6eb..b7daebaab 100644 --- a/openlp/plugins/songs/songsplugin.py +++ b/openlp/plugins/songs/songsplugin.py @@ -104,6 +104,7 @@ class SongsPlugin(Plugin): log.info('Songs Initialising') super(SongsPlugin, self).initialise() self.songselect_form = SongSelectForm(Registry().get('main_window'), self, self.manager) + self.songselect_form.initialise() self.song_import_item.setVisible(True) self.song_export_item.setVisible(True) self.tools_reindex_item.setVisible(True) diff --git a/openlp/plugins/songusage/forms/songusagedetailform.py b/openlp/plugins/songusage/forms/songusagedetailform.py index e8111c058..5d8470a99 100644 --- a/openlp/plugins/songusage/forms/songusagedetailform.py +++ b/openlp/plugins/songusage/forms/songusagedetailform.py @@ -99,7 +99,7 @@ class SongUsageDetailForm(QtGui.QDialog, Ui_SongUsageDetailDialog, RegistryPrope report_file_name = os.path.join(path, file_name) file_handle = None try: - file_handle = open(report_file_name, 'w') + file_handle = open(report_file_name, 'wb') for instance in usage: record = '\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",' \ '\"%s\",\"%s\"\n' % \ diff --git a/tests/functional/openlp_core_common/test_settings.py b/tests/functional/openlp_core_common/test_settings.py index ea4bcf849..e3ce6990d 100644 --- a/tests/functional/openlp_core_common/test_settings.py +++ b/tests/functional/openlp_core_common/test_settings.py @@ -115,3 +115,15 @@ class TestSettings(TestCase, TestMixin): # THEN the new value is returned when re-read self.assertEqual('very short', Settings().value('test/extend'), 'The saved value should be returned') + + def settings_nonexisting_test(self): + """ + Test the Settings on query for non-existing value + """ + # GIVEN: A new Settings setup + with self.assertRaises(KeyError) as cm: + # WHEN reading a setting that doesn't exists + does_not_exist_value = Settings().value('core/does not exists') + + # THEN: An exception with the non-existing key should be thrown + self.assertEqual(str(cm.exception), "'core/does not exists'", 'We should get an exception') diff --git a/tests/functional/openlp_core_lib/test_color_button.py b/tests/functional/openlp_core_lib/test_color_button.py new file mode 100644 index 000000000..a7b743918 --- /dev/null +++ b/tests/functional/openlp_core_lib/test_color_button.py @@ -0,0 +1,206 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2014 Raoul Snyman # +# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan # +# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, # +# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. # +# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, # +# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, # +# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, # +# Frode Woldsund, Martin Zibricky, Patrick Zimmermann # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" +This module contains tests for the openlp.core.lib.filedialog module +""" +from unittest import TestCase + +from openlp.core.lib.colorbutton import ColorButton +from tests.functional import MagicMock, call, patch + + +class TestColorDialog(TestCase): + """ + Test the :class:`~openlp.core.lib.colorbutton.ColorButton` class + """ + def setUp(self): + self.change_color_patcher = patch('openlp.core.lib.colorbutton.ColorButton.change_color') + self.clicked_patcher = patch('openlp.core.lib.colorbutton.ColorButton.clicked') + self.color_changed_patcher = patch('openlp.core.lib.colorbutton.ColorButton.colorChanged') + self.qt_gui_patcher = patch('openlp.core.lib.colorbutton.QtGui') + self.translate_patcher = patch('openlp.core.lib.colorbutton.translate', **{'return_value': 'Tool Tip Text'}) + self.addCleanup(self.change_color_patcher.stop) + self.addCleanup(self.clicked_patcher.stop) + self.addCleanup(self.color_changed_patcher.stop) + self.addCleanup(self.qt_gui_patcher.stop) + self.addCleanup(self.translate_patcher.stop) + self.mocked_change_color = self.change_color_patcher.start() + self.mocked_clicked = self.clicked_patcher.start() + self.mocked_color_changed = self.color_changed_patcher.start() + self.mocked_qt_gui = self.qt_gui_patcher.start() + self.mocked_translate = self.translate_patcher.start() + + def constructor_test(self): + """ + Test that constructing a ColorButton object works correctly + """ + + # GIVEN: The ColorButton class, a mocked change_color, setToolTip methods and clicked signal + with patch('openlp.core.lib.colorbutton.ColorButton.setToolTip') as mocked_set_tool_tip: + + # WHEN: The ColorButton object is instantiated + widget = ColorButton() + + # THEN: The widget __init__ method should have the correct properties and methods called + self.assertEqual(widget.parent, None, + 'The parent should be the same as the one that the class was instianted with') + self.mocked_change_color.assert_called_once_with('#ffffff') + mocked_set_tool_tip.assert_called_once_with('Tool Tip Text') + self.mocked_clicked.connect.assert_called_once_with(widget.on_clicked) + + def change_color_test(self): + """ + Test that change_color sets the new color and the stylesheet + """ + self.change_color_patcher.stop() + + # GIVEN: An instance of the ColorButton object, and a mocked out setStyleSheet + with patch('openlp.core.lib.colorbutton.ColorButton.setStyleSheet') as mocked_set_style_sheet: + widget = ColorButton() + + # WHEN: Changing the color + widget.change_color('#000000') + + # THEN: The _color attribute should be set to #000000 and setStyleSheet should have been called twice + self.assertEqual(widget._color, '#000000', '_color should have been set to #000000') + mocked_set_style_sheet.assert_has_calls( + [call('background-color: #ffffff'), call('background-color: #000000')]) + + self.mocked_change_color = self.change_color_patcher.start() + + def color_test(self): + """ + Test that the color property method returns the set color + """ + + # GIVEN: An instance of ColorButton, with a set _color attribute + widget = ColorButton() + widget._color = '#000000' + + # WHEN: Accesing the color property + value = widget.color + + # THEN: The value set in _color should be returned + self.assertEqual(value, '#000000', 'The value returned should be equal to the one we set') + + def color_test(self): + """ + Test that the color property method returns the set color + """ + + # GIVEN: An instance of ColorButton, with a set _color attribute + widget = ColorButton() + widget._color = '#000000' + + # WHEN: Accesing the color property + value = widget.color + + # THEN: The value set in _color should be returned + self.assertEqual(value, '#000000', 'The value returned should be equal to the one we set') + + def color_setter_test(self): + """ + Test that the color property setter method sets the color + """ + + # GIVEN: An instance of ColorButton, with a mocked __init__ + with patch('openlp.core.lib.colorbutton.ColorButton.__init__', **{'return_value': None}): + widget = ColorButton() + + # WHEN: Setting the color property + widget.color = '#000000' + + # THEN: Then change_color should have been called with the value we set + self.mocked_change_color.assert_called_once_with('#000000') + + def on_clicked_invalid_color_test(self): + """ + Test the on_click method when an invalid color has been supplied + """ + + # GIVEN: An instance of ColorButton, and a set _color attribute + widget = ColorButton() + self.mocked_change_color.reset_mock() + self.mocked_color_changed.reset_mock() + widget._color = '#000000' + + # WHEN: The on_clicked method is called, and the color is invalid + self.mocked_qt_gui.QColorDialog.getColor.return_value = MagicMock(**{'isValid.return_value': False}) + widget.on_clicked() + + # THEN: change_color should not have been called and the colorChanged signal should not have been emitted + self.assertEqual( + self.mocked_change_color.call_count, 0, 'change_color should not have been called with an invalid color') + self.assertEqual( + self.mocked_color_changed.emit.call_count, 0, + 'colorChange signal should not have been emitted with an invalid color') + + def on_clicked_same_color_test(self): + """ + Test the on_click method when a new color has not been chosen + """ + + # GIVEN: An instance of ColorButton, and a set _color attribute + widget = ColorButton() + self.mocked_change_color.reset_mock() + self.mocked_color_changed.reset_mock() + widget._color = '#000000' + + # WHEN: The on_clicked method is called, and the color is valid, but the same as the existing color + self.mocked_qt_gui.QColorDialog.getColor.return_value = MagicMock( + **{'isValid.return_value': True, 'name.return_value': '#000000'}) + widget.on_clicked() + + # THEN: change_color should not have been called and the colorChanged signal should not have been emitted + self.assertEqual( + self.mocked_change_color.call_count, 0, + 'change_color should not have been called when the color has not changed') + self.assertEqual( + self.mocked_color_changed.emit.call_count, 0, + 'colorChange signal should not have been emitted when the color has not changed') + + def on_clicked_new_color_test(self): + """ + Test the on_click method when a new color has been chosen and is valid + """ + + # GIVEN: An instance of ColorButton, and a set _color attribute + widget = ColorButton() + self.mocked_change_color.reset_mock() + self.mocked_color_changed.reset_mock() + widget._color = '#000000' + + # WHEN: The on_clicked method is called, and the color is valid, and different to the existing color + self.mocked_qt_gui.QColorDialog.getColor.return_value = MagicMock( + **{'isValid.return_value': True, 'name.return_value': '#ffffff'}) + widget.on_clicked() + + # THEN: change_color should have been called and the colorChanged signal should have been emitted + self.mocked_change_color.assert_call_once_with('#ffffff') + self.mocked_color_changed.emit.assert_called_once_with('#ffffff') diff --git a/tests/functional/openlp_core_lib/test_file_dialog.py b/tests/functional/openlp_core_lib/test_file_dialog.py index 1190810da..6cfb356b6 100644 --- a/tests/functional/openlp_core_lib/test_file_dialog.py +++ b/tests/functional/openlp_core_lib/test_file_dialog.py @@ -31,7 +31,7 @@ class TestFileDialog(TestCase): Test that FileDialog.getOpenFileNames() returns and empty QStringList when QFileDialog is canceled (returns an empty QStringList) """ - self.mocked_os.reset() + self.mocked_os.reset_mock() # GIVEN: An empty QStringList as a return value from QFileDialog.getOpenFileNames self.mocked_qt_gui.QFileDialog.getOpenFileNames.return_value = [] @@ -50,8 +50,8 @@ class TestFileDialog(TestCase): Test that FileDialog.getOpenFileNames handles a list of files properly when QFileList.getOpenFileNames returns a good file name, a url encoded file name and a non-existing file """ - self.mocked_os.rest() - self.mocked_qt_gui.reset() + self.mocked_os.rest_mock() + self.mocked_qt_gui.reset_mock() # GIVEN: A List of known values as a return value from QFileDialog.getOpenFileNames and a list of valid file # names. @@ -59,6 +59,8 @@ class TestFileDialog(TestCase): '/Valid File', '/url%20encoded%20file%20%231', '/non-existing'] self.mocked_os.path.exists.side_effect = lambda file_name: file_name in [ '/Valid File', '/url encoded file #1'] + self.mocked_ui_strings().FileNotFound = 'File Not Found' + self.mocked_ui_strings().FileNotFoundMessage = 'File %s not found.\nPlease try selecting it individually.' # WHEN: FileDialog.getOpenFileNames is called result = FileDialog.getOpenFileNames(self.mocked_parent) @@ -68,6 +70,7 @@ class TestFileDialog(TestCase): call_list = [call('/Valid File'), call('/url%20encoded%20file%20%231'), call('/url encoded file #1'), call('/non-existing'), call('/non-existing')] self.mocked_os.path.exists.assert_has_calls(call_list) - self.mocked_qt_gui.QmessageBox.information.called_with(self.mocked_parent, UiStrings().FileNotFound, - UiStrings().FileNotFoundMessage % '/non-existing') + self.mocked_qt_gui.QMessageBox.information.assert_called_with( + self.mocked_parent, 'File Not Found', + 'File /non-existing not found.\nPlease try selecting it individually.') self.assertEqual(result, ['/Valid File', '/url encoded file #1'], 'The returned file list is incorrect') diff --git a/tests/functional/openlp_core_lib/test_renderer.py b/tests/functional/openlp_core_lib/test_renderer.py index 8df4816bb..5e3d087b9 100644 --- a/tests/functional/openlp_core_lib/test_renderer.py +++ b/tests/functional/openlp_core_lib/test_renderer.py @@ -34,7 +34,7 @@ from unittest import TestCase from PyQt4 import QtCore from openlp.core.common import Registry -from openlp.core.lib import Renderer, ScreenList +from openlp.core.lib import Renderer, ScreenList, ServiceItem from tests.interfaces import MagicMock @@ -88,7 +88,7 @@ class TestRenderer(TestCase): expected_tuple = ('{st}{r}Text text text{/r}{/st}', '{st}{r}', '') - # WHEN: + # WHEN: The renderer converts the start tags result = renderer._get_start_tags(given_raw_text) # THEN: Check if the correct tuple is returned. @@ -104,8 +104,59 @@ class TestRenderer(TestCase): given_line = 'beginning asdf \n end asdf' expected_words = ['beginning', 'asdf', 'end', 'asdf'] - # WHEN: Split the line + # WHEN: Split the line based on word split rules result_words = renderer._words_split(given_line) # THEN: The word lists should be the same. self.assertListEqual(result_words, expected_words) + + def format_slide_logical_split_test(self): + """ + Test that a line with text and a logic break does not break the renderer just returns the input + """ + # GIVEN: A line of with a space text and the logical split + renderer = Renderer() + renderer.empty_height = 25 + given_line = 'a\n[---]\nb' + expected_words = ['a
[---]
b'] + service_item = ServiceItem(None) + + # WHEN: Split the line based on word split rules + result_words = renderer.format_slide(given_line, service_item) + + # THEN: The word lists should be the same. + self.assertListEqual(result_words, expected_words) + + def format_slide_blank_before_split_test(self): + """ + Test that a line with blanks before the logical split at handled + """ + # GIVEN: A line of with a space before the logical split + renderer = Renderer() + renderer.empty_height = 25 + given_line = '\n [---]\n' + expected_words = ['
[---]'] + service_item = ServiceItem(None) + + # WHEN: Split the line based on word split rules + result_words = renderer.format_slide(given_line, service_item) + + # THEN: The blanks have been removed. + self.assertListEqual(result_words, expected_words) + + def format_slide_blank_after_split_test(self): + """ + Test that a line with blanks before the logical split at handled + """ + # GIVEN: A line of with a space after the logical split + renderer = Renderer() + renderer.empty_height = 25 + given_line = '\n[---] \n' + expected_words = ['
[---] '] + service_item = ServiceItem(None) + + # WHEN: Split the line based on word split rules + result_words = renderer.format_slide(given_line, service_item) + + # THEN: The blanks have been removed. + self.assertListEqual(result_words, expected_words) diff --git a/tests/functional/openlp_core_ui/test_settingsform.py b/tests/functional/openlp_core_ui/test_settingsform.py index 17a8b165f..7ea2339a7 100644 --- a/tests/functional/openlp_core_ui/test_settingsform.py +++ b/tests/functional/openlp_core_ui/test_settingsform.py @@ -157,4 +157,4 @@ class TestSettingsForm(TestCase): # THEN: The general tab's cancel() method should have been called, but not the themes tab mocked_general_cancel.assert_called_with() - self.assertEqual(0, mocked_theme_cancel.call_count, 'The Themes tab\'s cancel() should not have been called') \ No newline at end of file + self.assertEqual(0, mocked_theme_cancel.call_count, 'The Themes tab\'s cancel() should not have been called') diff --git a/tests/functional/openlp_plugins/images/test_imagetab.py b/tests/functional/openlp_plugins/images/test_imagetab.py index fade284a4..a4614e816 100644 --- a/tests/functional/openlp_plugins/images/test_imagetab.py +++ b/tests/functional/openlp_plugins/images/test_imagetab.py @@ -95,4 +95,4 @@ class TestImageMediaItem(TestCase, TestMixin): self.form.save() # THEN: the post process should be requested self.assertEqual(1, self.form.settings_form.register_post_process.call_count, - 'Image Post processing should have been requested') \ No newline at end of file + 'Image Post processing should have been requested') diff --git a/tests/functional/openlp_plugins/songs/test_lib.py b/tests/functional/openlp_plugins/songs/test_lib.py index 140126f26..3b6f9bf87 100644 --- a/tests/functional/openlp_plugins/songs/test_lib.py +++ b/tests/functional/openlp_plugins/songs/test_lib.py @@ -58,8 +58,6 @@ class TestLib(TestCase): i love that old cross where the dearest and best for a world of lost sinners was slain so ill cherish the old rugged cross till my trophies at last i lay down i will cling to the old rugged cross and exchange it some day for a crown''' - self.song1 = MagicMock() - self.song2 = MagicMock() def clean_string_test(self): """ @@ -92,53 +90,53 @@ class TestLib(TestCase): Test the songs_probably_equal function with twice the same song. """ # GIVEN: Two equal songs. - self.song1.search_lyrics = self.full_lyrics - self.song2.search_lyrics = self.full_lyrics + song_tuple1 = (2, self.full_lyrics) + song_tuple2 = (4, self.full_lyrics) # WHEN: We compare those songs for equality. - result = songs_probably_equal((self.song1, self.song2)) + result = songs_probably_equal((song_tuple1, song_tuple2)) # THEN: The result should be a tuple.. - assert result == (self.song1, self.song2), 'The result should be the tuble of songs' + assert result == (2, 4), 'The result should be the tuble of song positions' def songs_probably_equal_short_song_test(self): """ Test the songs_probably_equal function with a song and a shorter version of the same song. """ # GIVEN: A song and a short version of the same song. - self.song1.search_lyrics = self.full_lyrics - self.song2.search_lyrics = self.short_lyrics + song_tuple1 = (1, self.full_lyrics) + song_tuple2 = (3, self.short_lyrics) # WHEN: We compare those songs for equality. - result = songs_probably_equal((self.song1, self.song2)) + result = songs_probably_equal((song_tuple1, song_tuple2)) # THEN: The result should be a tuple.. - assert result == (self.song1, self.song2), 'The result should be the tuble of songs' + assert result == (1, 3), 'The result should be the tuble of song positions' def songs_probably_equal_error_song_test(self): """ Test the songs_probably_equal function with a song and a very erroneous version of the same song. """ # GIVEN: A song and the same song with lots of errors. - self.song1.search_lyrics = self.full_lyrics - self.song2.search_lyrics = self.error_lyrics + song_tuple1 = (4, self.full_lyrics) + song_tuple2 = (7, self.error_lyrics) # WHEN: We compare those songs for equality. - result = songs_probably_equal((self.song1, self.song2)) + result = songs_probably_equal((song_tuple1, song_tuple2)) - # THEN: The result should be a tuple of songs.. - assert result == (self.song1, self.song2), 'The result should be the tuble of songs' + # THEN: The result should be a tuple of song positions. + assert result == (4, 7), 'The result should be the tuble of song positions' def songs_probably_equal_different_song_test(self): """ Test the songs_probably_equal function with two different songs. """ # GIVEN: Two different songs. - self.song1.search_lyrics = self.full_lyrics - self.song2.search_lyrics = self.different_lyrics + song_tuple1 = (5, self.full_lyrics) + song_tuple2 = (8, self.different_lyrics) # WHEN: We compare those songs for equality. - result = songs_probably_equal((self.song1, self.song2)) + result = songs_probably_equal((song_tuple1, song_tuple2)) # THEN: The result should be None. assert result is None, 'The result should be None' diff --git a/tests/functional/openlp_plugins/songs/test_openlyricsexport.py b/tests/functional/openlp_plugins/songs/test_openlyricsexport.py new file mode 100644 index 000000000..df28db035 --- /dev/null +++ b/tests/functional/openlp_plugins/songs/test_openlyricsexport.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2014 Raoul Snyman # +# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan # +# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, # +# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. # +# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, # +# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, # +# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, # +# Frode Woldsund, Martin Zibricky, Patrick Zimmermann # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" +This module contains tests for the OpenLyrics song importer. +""" + +import os +import shutil +from unittest import TestCase +from tempfile import mkdtemp + +from tests.functional import MagicMock, patch +from tests.helpers.testmixin import TestMixin +from openlp.plugins.songs.lib.openlyricsexport import OpenLyricsExport +from openlp.core.common import Registry + + +class TestOpenLyricsExport(TestCase, TestMixin): + """ + Test the functions in the :mod:`openlyricsexport` module. + """ + def setUp(self): + """ + Create the registry + """ + Registry.create() + self.temp_folder = mkdtemp() + + def tearDown(self): + """ + Cleanup + """ + shutil.rmtree(self.temp_folder) + + def export_same_filename_test(self): + """ + Test that files is not overwritten if songs has same title and author + """ + # GIVEN: A mocked song_to_xml, 2 mocked songs, a mocked application and an OpenLyricsExport instance + with patch('openlp.plugins.songs.lib.openlyricsexport.OpenLyrics.song_to_xml') as mocked_song_to_xml: + mocked_song_to_xml.return_value = '\n' + author = MagicMock() + author.display_name = 'Test Author' + song = MagicMock() + song.authors = [author] + song.title = 'Test Title' + parent = MagicMock() + parent.stop_export_flag = False + mocked_application_object = MagicMock() + Registry().register('application', mocked_application_object) + ol_export = OpenLyricsExport(parent, [song, song], self.temp_folder) + + # WHEN: Doing the export + ol_export.do_export() + + # THEN: The exporter should have created 2 files + self.assertTrue(os.path.exists(os.path.join(self.temp_folder, + '%s (%s).xml' % (song.title, author.display_name)))) + self.assertTrue(os.path.exists(os.path.join(self.temp_folder, + '%s (%s)-1.xml' % (song.title, author.display_name)))) diff --git a/tests/functional/openlp_plugins/songs/test_songselect.py b/tests/functional/openlp_plugins/songs/test_songselect.py index 0f9001342..dd3a07b0c 100644 --- a/tests/functional/openlp_plugins/songs/test_songselect.py +++ b/tests/functional/openlp_plugins/songs/test_songselect.py @@ -31,14 +31,17 @@ This module contains tests for the CCLI SongSelect importer. """ from unittest import TestCase from urllib.error import URLError +from openlp.core import Registry +from openlp.plugins.songs.forms.songselectform import SongSelectForm from openlp.plugins.songs.lib import Author, Song from openlp.plugins.songs.lib.songselect import SongSelectImport, LOGOUT_URL, BASE_URL from tests.functional import MagicMock, patch, call +from tests.helpers.testmixin import TestMixin -class TestSongSelect(TestCase): +class TestSongSelectImport(TestCase, TestMixin): """ Test the :class:`~openlp.plugins.songs.lib.songselect.SongSelectImport` class """ @@ -380,3 +383,87 @@ class TestSongSelect(TestCase): mocked_db_manager.get_object_filtered.assert_called_with(MockedAuthor, False) self.assertEqual(0, MockedAuthor.populate.call_count, 'A new author should not have been instantiated') self.assertEqual(1, len(result.authors_songs), 'There should only be one author') + + +class TestSongSelectForm(TestCase, TestMixin): + """ + Test the :class:`~openlp.plugins.songs.forms.songselectform.SongSelectForm` class + """ + def setUp(self): + """ + Some set up for this test suite + """ + self.setup_application() + self.app.setApplicationVersion('0.0') + self.app.process_events = lambda: None + Registry.create() + Registry().register('application', self.app) + + def create_form_test(self): + """ + Test that we can create the SongSelect form + """ + # GIVEN: The SongSelectForm class and a mocked db manager + mocked_plugin = MagicMock() + mocked_db_manager = MagicMock() + + # WHEN: We create an instance + ssform = SongSelectForm(None, mocked_plugin, mocked_db_manager) + + # THEN: The correct properties should have been assigned + self.assertEqual(mocked_plugin, ssform.plugin, 'The correct plugin should have been assigned') + self.assertEqual(mocked_db_manager, ssform.db_manager, 'The correct db_manager should have been assigned') + + def login_fails_test(self): + """ + Test that when the login fails, the form returns to the correct state + """ + # GIVEN: A valid SongSelectForm with a mocked out SongSelectImport, and a bunch of mocked out controls + with patch('openlp.plugins.songs.forms.songselectform.SongSelectImport') as MockedSongSelectImport, \ + patch('openlp.plugins.songs.forms.songselectform.QtGui.QMessageBox.critical') as mocked_critical, \ + patch('openlp.plugins.songs.forms.songselectform.translate') as mocked_translate: + mocked_song_select_import = MagicMock() + mocked_song_select_import.login.return_value = False + MockedSongSelectImport.return_value = mocked_song_select_import + mocked_translate.side_effect = lambda *args: args[1] + ssform = SongSelectForm(None, MagicMock(), MagicMock()) + ssform.initialise() + with patch.object(ssform, 'username_edit') as mocked_username_edit, \ + patch.object(ssform, 'password_edit') as mocked_password_edit, \ + patch.object(ssform, 'save_password_checkbox') as mocked_save_password_checkbox, \ + patch.object(ssform, 'login_button') as mocked_login_button, \ + patch.object(ssform, 'login_spacer') as mocked_login_spacer, \ + patch.object(ssform, 'login_progress_bar') as mocked_login_progress_bar, \ + patch.object(ssform.application, 'process_events') as mocked_process_events: + + # WHEN: The login button is clicked, and the login is rigged to fail + ssform.on_login_button_clicked() + + # THEN: The right things should have happened in the right order + expected_username_calls = [call(False), call(True)] + expected_password_calls = [call(False), call(True)] + expected_save_password_calls = [call(False), call(True)] + expected_login_btn_calls = [call(False), call(True)] + expected_login_spacer_calls = [call(False), call(True)] + expected_login_progress_visible_calls = [call(True), call(False)] + expected_login_progress_value_calls = [call(0), call(0)] + self.assertEqual(expected_username_calls, mocked_username_edit.setEnabled.call_args_list, + 'The username edit should be disabled then enabled') + self.assertEqual(expected_password_calls, mocked_password_edit.setEnabled.call_args_list, + 'The password edit should be disabled then enabled') + self.assertEqual(expected_save_password_calls, mocked_save_password_checkbox.setEnabled.call_args_list, + 'The save password checkbox should be disabled then enabled') + self.assertEqual(expected_login_btn_calls, mocked_login_button.setEnabled.call_args_list, + 'The login button should be disabled then enabled') + self.assertEqual(expected_login_spacer_calls, mocked_login_spacer.setVisible.call_args_list, + 'Thee login spacer should be make invisible, then visible') + self.assertEqual(expected_login_progress_visible_calls, + mocked_login_progress_bar.setVisible.call_args_list, + 'Thee login progress bar should be make visible, then invisible') + self.assertEqual(expected_login_progress_value_calls, mocked_login_progress_bar.setValue.call_args_list, + 'Thee login progress bar should have the right values set') + self.assertEqual(2, mocked_process_events.call_count, + 'The process_events() method should be called twice') + mocked_critical.assert_called_with(ssform, 'Error Logging In', 'There was a problem logging in, ' + 'perhaps your username or password is ' + 'incorrect?') diff --git a/tests/functional/openlp_plugins/songs/test_wordsofworshipimport.py b/tests/functional/openlp_plugins/songs/test_wordsofworshipimport.py new file mode 100644 index 000000000..853868e91 --- /dev/null +++ b/tests/functional/openlp_plugins/songs/test_wordsofworshipimport.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2014 Raoul Snyman # +# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan # +# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, # +# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. # +# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, # +# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, # +# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, # +# Frode Woldsund, Martin Zibricky, Patrick Zimmermann # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" +This module contains tests for the Words of Worship song importer. +""" + +import os + +from tests.helpers.songfileimport import SongImportTestHelper +from openlp.plugins.songs.lib.importers.wordsofworship import WordsOfWorshipImport + +TEST_PATH = os.path.abspath( + os.path.join(os.path.dirname(__file__), '..', '..', '..', 'resources', 'wordsofworshipsongs')) + + +class TestWordsOfWorshipFileImport(SongImportTestHelper): + + def __init__(self, *args, **kwargs): + self.importer_class_name = 'WordsOfWorshipImport' + self.importer_module_name = 'wordsofworship' + super(TestWordsOfWorshipFileImport, self).__init__(*args, **kwargs) + + def test_song_import(self): + """ + Test that loading a Words of Worship file works correctly + """ + self.file_import([os.path.join(TEST_PATH, 'Amazing Grace (6 Verses).wow-song')], + self.load_external_result_data(os.path.join(TEST_PATH, 'Amazing Grace (6 Verses).json'))) + self.file_import([os.path.join(TEST_PATH, 'When morning gilds the skies.wsg')], + self.load_external_result_data(os.path.join(TEST_PATH, 'When morning gilds the skies.json'))) diff --git a/tests/functional/openlp_plugins/songs/test_worshipassistantimport.py b/tests/functional/openlp_plugins/songs/test_worshipassistantimport.py index 63ead5b30..346f9c83d 100644 --- a/tests/functional/openlp_plugins/songs/test_worshipassistantimport.py +++ b/tests/functional/openlp_plugins/songs/test_worshipassistantimport.py @@ -54,3 +54,5 @@ class TestWorshipAssistantFileImport(SongImportTestHelper): self.load_external_result_data(os.path.join(TEST_PATH, 'du_herr.json'))) self.file_import(os.path.join(TEST_PATH, 'would_you_be_free.csv'), self.load_external_result_data(os.path.join(TEST_PATH, 'would_you_be_free.json'))) + self.file_import(os.path.join(TEST_PATH, 'would_you_be_free2.csv'), + self.load_external_result_data(os.path.join(TEST_PATH, 'would_you_be_free.json'))) diff --git a/tests/interfaces/openlp_core_ui/test_mainwindow.py b/tests/interfaces/openlp_core_ui/test_mainwindow.py index c775be747..c5d170fff 100644 --- a/tests/interfaces/openlp_core_ui/test_mainwindow.py +++ b/tests/interfaces/openlp_core_ui/test_mainwindow.py @@ -62,6 +62,7 @@ class TestMainWindow(TestCase, TestMixin): patch('openlp.core.ui.mainwindow.QtGui.QMainWindow.addDockWidget') as mocked_add_dock_method, \ patch('openlp.core.ui.mainwindow.ServiceManager') as mocked_service_manager, \ patch('openlp.core.ui.mainwindow.ThemeManager') as mocked_theme_manager, \ + patch('openlp.core.ui.mainwindow.ProjectorManager') as mocked_projector_manager, \ patch('openlp.core.ui.mainwindow.Renderer') as mocked_renderer: self.main_window = MainWindow() @@ -85,3 +86,29 @@ class TestMainWindow(TestCase, TestMixin): # THEN: The current widget should have been set. self.main_window.media_tool_box.setCurrentIndex.assert_called_with(2) + + def projector_manager_dock_locked_test(self): + """ + Projector Manager enable UI options - bug #1390702 + """ + # GIVEN: A mocked projector manager dock item: + projector_dock = self.main_window.projector_manager_dock + + # WHEN: main_window.lock_panel action is triggered + self.main_window.lock_panel.triggered.emit(True) + + # THEN: Projector manager dock should have been called with disable UI features + projector_dock.setFeatures.assert_called_with(0) + + def projector_manager_dock_unlocked_test(self): + """ + Projector Manager disable UI options - bug #1390702 + """ + # GIVEN: A mocked projector manager dock item: + projector_dock = self.main_window.projector_manager_dock + + # WHEN: main_window.lock_panel action is triggered + self.main_window.lock_panel.triggered.emit(False) + + # THEN: Projector manager dock should have been called with enable UI features + projector_dock.setFeatures.assert_called_with(7) diff --git a/tests/interfaces/openlp_plugins/bibles/test_lib_parse_reference.py b/tests/interfaces/openlp_plugins/bibles/test_lib_parse_reference.py index e20105ea1..989d62583 100644 --- a/tests/interfaces/openlp_plugins/bibles/test_lib_parse_reference.py +++ b/tests/interfaces/openlp_plugins/bibles/test_lib_parse_reference.py @@ -105,3 +105,13 @@ class TestBibleManager(TestCase, TestMixin): # THEN a verse array should be returned self.assertEqual([(54, 1, 1, -1), (54, 2, 1, 1)], results, "The bible verses should match the expected results") + + def parse_reference_four_test(self): + """ + Test the parse_reference method with non existence book + """ + # GIVEN given a bible in the bible manager + # WHEN asking to parse the bible reference + results = parse_reference('Raoul 1', self.manager.db_cache['tests'], MagicMock()) + # THEN a verse array should be returned + self.assertEqual(False, results, "The bible Search should return False") diff --git a/tests/resources/wordsofworshipsongs/Amazing Grace (6 Verses).json b/tests/resources/wordsofworshipsongs/Amazing Grace (6 Verses).json new file mode 100644 index 000000000..563872ae7 --- /dev/null +++ b/tests/resources/wordsofworshipsongs/Amazing Grace (6 Verses).json @@ -0,0 +1,33 @@ +{ + "authors": [ + "John Newton (1725-1807)" + ], + "title": "Amazing Grace (6 Verses)", + "verse_order_list": [], + "verses": [ + [ + "Amazing grace! how sweet the sound\nThat saved a wretch like me;\nI once was lost, but now am found,\nWas blind, but now I see.", + "V" + ], + [ + "'Twas grace that taught my heart to fear,\nAnd grace my fears relieved;\nHow precious did that grace appear,\nThe hour I first believed!", + "V" + ], + [ + "Through many dangers, toils and snares\nI have already come;\n'Tis grace that brought me safe thus far,\nAnd grace will lead me home.", + "V" + ], + [ + "The Lord has promised good to me,\nHis word my hope secures;\nHe will my shield and portion be\nAs long as life endures.", + "V" + ], + [ + "Yes, when this heart and flesh shall fail,\nAnd mortal life shall cease,\nI shall possess within the veil\nA life of joy and peace.", + "V" + ], + [ + "When we've been there ten thousand years,\nBright shining as the sun,\nWe've no less days to sing God's praise\nThan when we first begun.", + "V" + ] + ] +} diff --git a/tests/resources/wordsofworshipsongs/Amazing Grace (6 Verses).wow-song b/tests/resources/wordsofworshipsongs/Amazing Grace (6 Verses).wow-song new file mode 100644 index 000000000..e45d22e2f Binary files /dev/null and b/tests/resources/wordsofworshipsongs/Amazing Grace (6 Verses).wow-song differ diff --git a/tests/resources/wordsofworshipsongs/When morning gilds the skies.json b/tests/resources/wordsofworshipsongs/When morning gilds the skies.json new file mode 100644 index 000000000..c7a4426dd --- /dev/null +++ b/tests/resources/wordsofworshipsongs/When morning gilds the skies.json @@ -0,0 +1,29 @@ +{ + "authors": [ + "Author Unknown. Tr. Edward Caswall" + ], + "title": "When morning gilds the skies", + "verse_order_list": [], + "verses": [ + [ + "When morning gilds the skies\nMy heart awaking cries:\n'May Jesus Christ be prais'd!'\nAlike at work and prayer to Jesus I repair:\n'May Jesus Christ be prais'd!'", + "V" + ], + [ + "Does sadness fill my mind?\nA solace here I find:\n'May Jesus Christ be praised!'\nWhen evil thoughts molest,\nWith this I shield my breast:\n'May Jesus Christ be prais'd!'", + "V" + ], + [ + "To God, the Word, on high\nThe hosts of angels cry:\n'May Jesus Christ be prais'd!'\nLet mortals, too, upraise\nTheir voice in hymns of praise:\n'May Jesus Christ be prais'd!'", + "V" + ], + [ + "Let earth's wide circle round\nIn joyful notes resound:\n'May Jesus Christ be prais'd!'\nLet air, and sea, and sky,\nFrom depth to height, reply:\n'May Jesus Christ be prais'd!'", + "V" + ], + [ + "Be this while life is mine\nMy canticle divine\n'May Jesus Christ be prais'd!'\nBe this the eternal song,\nThrough all the ages long:\n'May Jesus Christ be prais'd!'", + "V" + ] + ] +} diff --git a/tests/resources/wordsofworshipsongs/When morning gilds the skies.wsg b/tests/resources/wordsofworshipsongs/When morning gilds the skies.wsg new file mode 100644 index 000000000..901a2dc4f Binary files /dev/null and b/tests/resources/wordsofworshipsongs/When morning gilds the skies.wsg differ diff --git a/tests/resources/worshipassistantsongs/would_you_be_free.json b/tests/resources/worshipassistantsongs/would_you_be_free.json index 96bc06a59..2c0a39973 100644 --- a/tests/resources/worshipassistantsongs/would_you_be_free.json +++ b/tests/resources/worshipassistantsongs/would_you_be_free.json @@ -8,7 +8,7 @@ "copyright": "Public Domain", "verses": [ [ - "Would you be free from your burden of sin? \nThere's power in the blood, power in the blood \nWould you o'er evil a victory win? \nThere's wonderful power in the blood \n", + "Would you be free from your burden of sin? \nThere's power in the blood, power in the blood \nWould you o'er evil a victory win? \nThere's wonderful power in the blood \n ", "v1" ], [ diff --git a/tests/resources/worshipassistantsongs/would_you_be_free2.csv b/tests/resources/worshipassistantsongs/would_you_be_free2.csv new file mode 100644 index 000000000..ca4da9998 --- /dev/null +++ b/tests/resources/worshipassistantsongs/would_you_be_free2.csv @@ -0,0 +1,31 @@ +SONGNR,TITLE,AUTHOR,COPYRIGHT,FIRSTLINE,PRIKEY,ALTKEY,TEMPO,FOCUS,THEME,SCRIPTURE,ACTIVE,SONGBOOK,TIMESIG,INTRODUCED,LASTUSED,TIMESUSED,CCLINR,USER1,USER2,USER3,USER4,USER5,ROADMAP,FILELINK1,OVERMAP,FILELINK2,LYRICS,INFO,LYRICS2,Background +"7","Would You Be Free","Jones, Lewis E.","Public Domain","Would you be free from your burden of sin?","G","","Moderate","Only To Others","","","N","Y","","1899-12-30","1899-12-30","","","","","","","","1, C, 1","","","",".G C G + Would you be free from your burden of sin? +. D D7 G + There's power in the blood, power in the blood +. C G + Would you o'er evil a victory win? +. D D7 G + There's wonderful power in the blood + +.G C G + There is power, power, wonder working power +.D G + In the blood of the Lamb +. C G + There is power, power, wonder working power +. D G + In the precious blood of the Lamb +","","[1] +Would you be free from your burden of sin? +There's power in the blood, power in the blood +Would you o'er evil a victory win? +There's wonderful power in the blood + +[C] +There is power, power, wonder working power +In the blood of the Lamb +There is power, power, wonder working power +In the precious blood of the Lamb + +[x]",""