diff --git a/openlp.pyw b/openlp.pyw index cc58c6a21..8d5c90957 100755 --- a/openlp.pyw +++ b/openlp.pyw @@ -26,12 +26,6 @@ # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### -# Import uuid now, to avoid the rare bug described in the support system: -# http://support.openlp.org/issues/102 -# If https://bugs.gentoo.org/show_bug.cgi?id=317557 is fixed, the import can be -# removed. -import uuid - from openlp.core import main diff --git a/openlp/core/__init__.py b/openlp/core/__init__.py index 71c27a1d0..d40d7c758 100644 --- a/openlp/core/__init__.py +++ b/openlp/core/__init__.py @@ -109,7 +109,7 @@ class OpenLP(QtGui.QApplication): QtCore.QObject.connect(Receiver.get_receiver(), QtCore.SIGNAL(u'cursor_normal'), self.setNormalCursor) # Decide how many screens we have and their size - screens = ScreenList(self.desktop()) + screens = ScreenList.create(self.desktop()) # First time checks in settings has_run_wizard = QtCore.QSettings().value( u'general/has run wizard', QtCore.QVariant(False)).toBool() diff --git a/openlp/core/lib/dockwidget.py b/openlp/core/lib/dockwidget.py index 4f74784ad..23ce9efcb 100644 --- a/openlp/core/lib/dockwidget.py +++ b/openlp/core/lib/dockwidget.py @@ -52,9 +52,8 @@ class OpenLPDockWidget(QtGui.QDockWidget): if icon: self.setWindowIcon(build_icon(icon)) # Sort out the minimum width. - screens = ScreenList.get_instance() - screen_width = screens.current[u'size'].width() - mainwindow_docbars = screen_width / 5 + screens = ScreenList() + mainwindow_docbars = screens.current[u'size'].width() / 5 if mainwindow_docbars > 300: self.setMinimumWidth(300) else: diff --git a/openlp/core/lib/formattingtags.py b/openlp/core/lib/formattingtags.py index bec2db63a..11fd898c3 100644 --- a/openlp/core/lib/formattingtags.py +++ b/openlp/core/lib/formattingtags.py @@ -46,13 +46,36 @@ class FormattingTags(object): """ Provide access to the html_expands list. """ - # Load user defined tags otherwise user defined tags are not present. return FormattingTags.html_expands @staticmethod - def reset_html_tags(): + def save_html_tags(): """ - Resets the html_expands list. + Saves all formatting tags except protected ones. + """ + tags = [] + for tag in FormattingTags.html_expands: + if not tag[u'protected'] and not tag.get(u'temporary'): + # Using dict ensures that copy is made and encoding of values + # a little later does not affect tags in the original list + tags.append(dict(tag)) + tag = tags[-1] + # Remove key 'temporary' from tags. + # It is not needed to be saved. + if u'temporary' in tag: + del tag[u'temporary'] + for element in tag: + if isinstance(tag[element], unicode): + tag[element] = tag[element].encode('utf8') + # Formatting Tags were also known as display tags. + QtCore.QSettings().setValue(u'displayTags/html_tags', + QtCore.QVariant(cPickle.dumps(tags) if tags else u'')) + + @staticmethod + def load_tags(): + """ + Load the Tags from store so can be used in the system or used to + update the display. """ temporary_tags = [tag for tag in FormattingTags.html_expands if tag.get(u'temporary')] @@ -140,38 +163,6 @@ class FormattingTags(object): FormattingTags.add_html_tags(base_tags) FormattingTags.add_html_tags(temporary_tags) - @staticmethod - def save_html_tags(): - """ - Saves all formatting tags except protected ones. - """ - tags = [] - for tag in FormattingTags.html_expands: - if not tag[u'protected'] and not tag.get(u'temporary'): - # Using dict ensures that copy is made and encoding of values - # a little later does not affect tags in the original list - tags.append(dict(tag)) - tag = tags[-1] - # Remove key 'temporary' from tags. - # It is not needed to be saved. - if u'temporary' in tag: - del tag[u'temporary'] - for element in tag: - if isinstance(tag[element], unicode): - tag[element] = tag[element].encode('utf8') - # Formatting Tags were also known as display tags. - QtCore.QSettings().setValue(u'displayTags/html_tags', - QtCore.QVariant(cPickle.dumps(tags) if tags else u'')) - - @staticmethod - def load_tags(): - """ - Load the Tags from store so can be used in the system or used to - update the display. If Cancel was selected this is needed to reset the - dsiplay to the correct version. - """ - # Initial Load of the Tags - FormattingTags.reset_html_tags() # Formatting Tags were also known as display tags. user_expands = QtCore.QSettings().value(u'displayTags/html_tags', QtCore.QVariant(u'')).toString() @@ -187,17 +178,13 @@ class FormattingTags(object): FormattingTags.add_html_tags(user_tags) @staticmethod - def add_html_tags(tags, save=False): + def add_html_tags(tags): """ Add a list of tags to the list. ``tags`` The list with tags to add. - ``save`` - Defaults to ``False``. If set to ``True`` the given ``tags`` are - saved to the config. - Each **tag** has to be a ``dict`` and should have the following keys: * desc @@ -225,8 +212,6 @@ class FormattingTags(object): displaying text containing the tag. It has to be a ``boolean``. """ FormattingTags.html_expands.extend(tags) - if save: - FormattingTags.save_html_tags() @staticmethod def remove_html_tag(tag_id): diff --git a/openlp/core/lib/imagemanager.py b/openlp/core/lib/imagemanager.py index b32e36194..47a7ed3f6 100644 --- a/openlp/core/lib/imagemanager.py +++ b/openlp/core/lib/imagemanager.py @@ -163,7 +163,7 @@ class ImageManager(QtCore.QObject): def __init__(self): QtCore.QObject.__init__(self) - current_screen = ScreenList.get_instance().current + current_screen = ScreenList().current self.width = current_screen[u'size'].width() self.height = current_screen[u'size'].height() self._cache = {} @@ -177,7 +177,7 @@ class ImageManager(QtCore.QObject): Screen has changed size so rebuild the cache to new size. """ log.debug(u'update_display') - current_screen = ScreenList.get_instance().current + current_screen = ScreenList().current self.width = current_screen[u'size'].width() self.height = current_screen[u'size'].height() # Mark the images as dirty for a rebuild by setting the image and byte diff --git a/openlp/core/lib/renderer.py b/openlp/core/lib/renderer.py index 2c9d2a5d1..aa39e779b 100644 --- a/openlp/core/lib/renderer.py +++ b/openlp/core/lib/renderer.py @@ -69,7 +69,7 @@ class Renderer(object): log.debug(u'Initialisation started') self.themeManager = themeManager self.imageManager = imageManager - self.screens = ScreenList.get_instance() + self.screens = ScreenList() self.service_theme = u'' self.theme_level = u'' self.override_background = None @@ -125,7 +125,7 @@ class Renderer(object): Set the appropriate theme depending on the theme level. Called by the service item when building a display frame - ``theme`` + ``override_theme`` The name of the song-level theme. None means the service item wants to use the given value. @@ -235,8 +235,8 @@ class Renderer(object): # the first two slides (and neglect the last for now). if len(slides) == 3: html_text = expand_tags(u'\n'.join(slides[:2])) - # We check both slides to determine if the optional break is - # needed (there is only one optional break). + # We check both slides to determine if the optional split is + # needed (there is only one optional split). else: html_text = expand_tags(u'\n'.join(slides)) html_text = html_text.replace(u'\n', u'
') @@ -247,14 +247,18 @@ class Renderer(object): else: # The first optional slide fits, which means we have to # render the first optional slide. - text_contains_break = u'[---]' in text - if text_contains_break: + text_contains_split = u'[---]' in text + if text_contains_split: try: text_to_render, text = \ text.split(u'\n[---]\n', 1) except: text_to_render = text.split(u'\n[---]\n')[0] text = u'' + text_to_render, raw_tags, html_tags = \ + self._get_start_tags(text_to_render) + if text: + text = raw_tags + text else: text_to_render = text text = u'' @@ -263,7 +267,7 @@ class Renderer(object): if len(slides) > 1 and text: # Add all slides apart from the last one the list. pages.extend(slides[:-1]) - if text_contains_break: + if text_contains_split: text = slides[-1] + u'\n[---]\n' + text else: text = slides[-1] + u'\n'+ text @@ -492,7 +496,7 @@ class Renderer(object): (raw_text.find(tag[u'start tag']), tag[u'start tag'], tag[u'end tag'])) html_tags.append( - (raw_text.find(tag[u'start tag']), tag[u'start html'])) + (raw_text.find(tag[u'start tag']), tag[u'start html'])) # Sort the lists, so that the tags which were opened first on the first # slide (the text we are checking) will be opened first on the next # slide as well. diff --git a/openlp/core/lib/serviceitem.py b/openlp/core/lib/serviceitem.py index c8fbe6d8a..36314ac7f 100644 --- a/openlp/core/lib/serviceitem.py +++ b/openlp/core/lib/serviceitem.py @@ -313,17 +313,12 @@ class ServiceItem(object): self.from_plugin = header[u'from_plugin'] self.capabilities = header[u'capabilities'] # Added later so may not be present in older services. - if u'search' in header: - self.search_string = header[u'search'] - self.data_string = header[u'data'] - if u'xml_version' in header: - self.xml_version = header[u'xml_version'] - if u'start_time' in header: - self.start_time = header[u'start_time'] - if u'end_time' in header: - self.end_time = header[u'end_time'] - if u'media_length' in header: - self.media_length = header[u'media_length'] + self.search_string = header.get(u'search', u'') + self.data_string = header.get(u'data', u'') + self.xml_version = header.get(u'xml_version') + self.start_time = header.get(u'start_time', 0) + self.end_time = header.get(u'end_time', 0) + self.media_length = header.get(u'media_length', 0) if u'background_audio' in header: self.background_audio = [] for filename in header[u'background_audio']: diff --git a/openlp/core/lib/ui.py b/openlp/core/lib/ui.py index c4b1181b1..c0472cce8 100644 --- a/openlp/core/lib/ui.py +++ b/openlp/core/lib/ui.py @@ -94,6 +94,7 @@ class UiStrings(object): self.NewService = translate('OpenLP.Ui', 'New Service') self.NewTheme = translate('OpenLP.Ui', 'New Theme') self.NextTrack = translate('OpenLP.Ui', 'Next Track') + self.NFdSs = translate('OpenLP.Ui', 'No Folder Selected', 'Singular') self.NFSs = translate('OpenLP.Ui', 'No File Selected', 'Singular') self.NFSp = translate('OpenLP.Ui', 'No Files Selected', 'Plural') self.NISs = translate('OpenLP.Ui', 'No Item Selected', 'Singular') diff --git a/openlp/core/ui/formattingtagform.py b/openlp/core/ui/formattingtagform.py index d6f880e3f..1084d6a3d 100644 --- a/openlp/core/ui/formattingtagform.py +++ b/openlp/core/ui/formattingtagform.py @@ -57,6 +57,14 @@ class FormattingTagForm(QtGui.QDialog, Ui_FormattingTagDialog): QtCore.SIGNAL(u'clicked()'), self.onDeleteClicked) QtCore.QObject.connect(self.buttonBox, QtCore.SIGNAL(u'rejected()'), self.close) + QtCore.QObject.connect(self.descriptionLineEdit, + QtCore.SIGNAL(u'textEdited(QString)'), self.onTextEdited) + QtCore.QObject.connect(self.tagLineEdit, + QtCore.SIGNAL(u'textEdited(QString)'), self.onTextEdited) + QtCore.QObject.connect(self.startTagLineEdit, + QtCore.SIGNAL(u'textEdited(QString)'), self.onTextEdited) + QtCore.QObject.connect(self.endTagLineEdit, + QtCore.SIGNAL(u'textEdited(QString)'), self.onTextEdited) # Forces reloading of tags from openlp configuration. FormattingTags.load_tags() @@ -65,7 +73,7 @@ class FormattingTagForm(QtGui.QDialog, Ui_FormattingTagDialog): Load Display and set field state. """ # Create initial copy from master - self._resetTable() + self._reloadTable() self.selected = -1 return QtGui.QDialog.exec_(self) @@ -73,9 +81,9 @@ class FormattingTagForm(QtGui.QDialog, Ui_FormattingTagDialog): """ Table Row selected so display items and set field state. """ - row = self.tagTableWidget.currentRow() - html = FormattingTags.html_expands[row] - self.selected = row + self.savePushButton.setEnabled(False) + self.selected = self.tagTableWidget.currentRow() + html = FormattingTags.get_html_tags()[self.selected] self.descriptionLineEdit.setText(html[u'desc']) self.tagLineEdit.setText(self._strip(html[u'start tag'])) self.startTagLineEdit.setText(html[u'start html']) @@ -85,21 +93,26 @@ class FormattingTagForm(QtGui.QDialog, Ui_FormattingTagDialog): self.tagLineEdit.setEnabled(False) self.startTagLineEdit.setEnabled(False) self.endTagLineEdit.setEnabled(False) - self.savePushButton.setEnabled(False) self.deletePushButton.setEnabled(False) else: self.descriptionLineEdit.setEnabled(True) self.tagLineEdit.setEnabled(True) self.startTagLineEdit.setEnabled(True) self.endTagLineEdit.setEnabled(True) - self.savePushButton.setEnabled(True) self.deletePushButton.setEnabled(True) + def onTextEdited(self, text): + """ + Enable the ``savePushButton`` when any of the selected tag's properties + has been changed. + """ + self.savePushButton.setEnabled(True) + def onNewClicked(self): """ Add a new tag to list only if it is not a duplicate. """ - for html in FormattingTags.html_expands: + for html in FormattingTags.get_html_tags(): if self._strip(html[u'start tag']) == u'n': critical_error_message_box( translate('OpenLP.FormattingTagForm', 'Update Error'), @@ -117,11 +130,13 @@ class FormattingTagForm(QtGui.QDialog, Ui_FormattingTagDialog): u'temporary': False } FormattingTags.add_html_tags([tag]) - self._resetTable() + FormattingTags.save_html_tags() + self._reloadTable() # Highlight new row self.tagTableWidget.selectRow(self.tagTableWidget.rowCount() - 1) self.onRowSelected() self.tagTableWidget.scrollToBottom() + #self.savePushButton.setEnabled(False) def onDeleteClicked(self): """ @@ -130,14 +145,14 @@ class FormattingTagForm(QtGui.QDialog, Ui_FormattingTagDialog): if self.selected != -1: FormattingTags.remove_html_tag(self.selected) self.selected = -1 - self._resetTable() - FormattingTags.save_html_tags() + FormattingTags.save_html_tags() + self._reloadTable() def onSavedClicked(self): """ Update Custom Tag details if not duplicate and save the data. """ - html_expands = FormattingTags.html_expands + html_expands = FormattingTags.get_html_tags() if self.selected != -1: html = html_expands[self.selected] tag = unicode(self.tagLineEdit.text()) @@ -157,14 +172,13 @@ class FormattingTagForm(QtGui.QDialog, Ui_FormattingTagDialog): # Keep temporary tags when the user changes one. html[u'temporary'] = False self.selected = -1 - self._resetTable() FormattingTags.save_html_tags() + self._reloadTable() - def _resetTable(self): + def _reloadTable(self): """ Reset List for loading. """ - FormattingTags.load_tags() self.tagTableWidget.clearContents() self.tagTableWidget.setRowCount(0) self.newPushButton.setEnabled(True) diff --git a/openlp/core/ui/generaltab.py b/openlp/core/ui/generaltab.py index baf28f40f..d0647d829 100644 --- a/openlp/core/ui/generaltab.py +++ b/openlp/core/ui/generaltab.py @@ -42,7 +42,7 @@ class GeneralTab(SettingsTab): """ Initialise the general settings tab """ - self.screens = ScreenList.get_instance() + self.screens = ScreenList() self.iconPath = u':/icon/openlp-logo-16x16.png' generalTranslated = translate('OpenLP.GeneralTab', 'General') SettingsTab.__init__(self, parent, u'General', generalTranslated) diff --git a/openlp/core/ui/maindisplay.py b/openlp/core/ui/maindisplay.py index 15fb9eefe..a04203387 100644 --- a/openlp/core/ui/maindisplay.py +++ b/openlp/core/ui/maindisplay.py @@ -119,7 +119,7 @@ class MainDisplay(Display): def __init__(self, parent, imageManager, live, controller): Display.__init__(self, parent, live, controller) self.imageManager = imageManager - self.screens = ScreenList.get_instance() + self.screens = ScreenList() self.plugins = PluginManager.get_instance().plugins self.rebuildCSS = False self.hideMode = None diff --git a/openlp/core/ui/mainwindow.py b/openlp/core/ui/mainwindow.py index e4a4e1616..bff9203db 100644 --- a/openlp/core/ui/mainwindow.py +++ b/openlp/core/ui/mainwindow.py @@ -795,7 +795,7 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow): if answer == QtGui.QMessageBox.No: return Receiver.send_message(u'cursor_busy') - screens = ScreenList.get_instance() + screens = ScreenList() FirstTimeForm(screens, self).exec_() self.firstTime() for plugin in self.pluginManager.plugins: diff --git a/openlp/core/ui/screen.py b/openlp/core/ui/screen.py index 21fbd6144..72c88908e 100644 --- a/openlp/core/ui/screen.py +++ b/openlp/core/ui/screen.py @@ -41,36 +41,40 @@ class ScreenList(object): """ Wrapper to handle the parameters of the display screen. - To get access to the screen list call ``ScreenList.get_instance()``. + To get access to the screen list call ``ScreenList()``. """ log.info(u'Screen loaded') - instance = None + __instance__ = None - @staticmethod - def get_instance(): - return ScreenList.instance + def __new__(cls): + if not cls.__instance__: + cls.__instance__ = object.__new__(cls) + return cls.__instance__ - def __init__(self, desktop): + @classmethod + def create(cls, desktop): """ Initialise the screen list. ``desktop`` A ``QDesktopWidget`` object. """ - ScreenList.instance = self - self.desktop = desktop - self.preview = None - self.current = None - self.override = None - self.screen_list = [] - self.display_count = 0 - self.screen_count_changed() - self._load_screen_settings() + screen_list = cls() + screen_list.desktop = desktop + screen_list.preview = None + screen_list.current = None + screen_list.override = None + screen_list.screen_list = [] + screen_list.display_count = 0 + screen_list.screen_count_changed() + screen_list._load_screen_settings() QtCore.QObject.connect(desktop, - QtCore.SIGNAL(u'resized(int)'), self.screen_resolution_changed) + QtCore.SIGNAL(u'resized(int)'), + screen_list.screen_resolution_changed) QtCore.QObject.connect(desktop, QtCore.SIGNAL(u'screenCountChanged(int)'), - self.screen_count_changed) + screen_list.screen_count_changed) + return screen_list def screen_resolution_changed(self, number): """ @@ -233,8 +237,8 @@ class ScreenList(object): y = window.y() + (window.height() / 2) for screen in self.screen_list: size = screen[u'size'] - if x >= size.x() and x <= (size.x() + size.width()) \ - and y >= size.y() and y <= (size.y() + size.height()): + if x >= size.x() and x <= (size.x() + size.width()) and \ + y >= size.y() and y <= (size.y() + size.height()): return screen[u'number'] def _load_screen_settings(self): diff --git a/openlp/core/ui/servicemanager.py b/openlp/core/ui/servicemanager.py index c9dfeae50..30567f2d2 100644 --- a/openlp/core/ui/servicemanager.py +++ b/openlp/core/ui/servicemanager.py @@ -51,7 +51,7 @@ class ServiceManagerList(QtGui.QTreeWidget): """ Set up key bindings and mouse behaviour for the service list """ - def __init__(self, serviceManager, parent=None, name=None): + def __init__(self, serviceManager, parent=None): QtGui.QTreeWidget.__init__(self, parent) self.serviceManager = serviceManager @@ -64,6 +64,9 @@ class ServiceManagerList(QtGui.QTreeWidget): elif event.key() == QtCore.Qt.Key_Down: self.serviceManager.onMoveSelectionDown() event.accept() + elif event.key() == QtCore.Qt.Key_Delete: + self.serviceManager.onDeleteFromService() + event.accept() event.ignore() else: event.ignore() @@ -101,7 +104,6 @@ class ServiceManager(QtGui.QWidget): QtGui.QWidget.__init__(self, parent) self.mainwindow = mainwindow self.serviceItems = [] - self.serviceName = u'' self.suffixes = [] self.dropPosition = 0 self.expandTabs = False @@ -219,6 +221,7 @@ class ServiceManager(QtGui.QWidget): icon=u':/general/general_delete.png', tooltip=translate('OpenLP.ServiceManager', 'Delete the selected item from the service.'), + shortcuts=[QtCore.Qt.Key_Delete], triggers=self.onDeleteFromService) self.orderToolbar.addSeparator() self.serviceManagerList.expand = self.orderToolbar.addToolbarAction( @@ -299,17 +302,14 @@ class ServiceManager(QtGui.QWidget): self.timeAction = create_widget_action(self.menu, text=translate('OpenLP.ServiceManager', '&Start Time'), icon=u':/media/media_time.png', triggers=self.onStartTimeForm) - self.deleteAction = create_widget_action(self.menu, - text=translate('OpenLP.ServiceManager', '&Delete From Service'), - icon=u':/general/general_delete.png', - triggers=self.onDeleteFromService) + # Add already existing delete action to the menu. + self.menu.addAction(self.serviceManagerList.delete) self.menu.addSeparator() self.previewAction = create_widget_action(self.menu, text=translate('OpenLP.ServiceManager', 'Show &Preview'), icon=u':/general/general_preview.png', triggers=self.makePreview) - self.liveAction = create_widget_action(self.menu, - text=translate('OpenLP.ServiceManager', 'Show &Live'), - icon=u':/general/general_live.png', triggers=self.makeLive) + # Add already existing make live action to the menu. + self.menu.addAction(self.serviceManagerList.makeLive) self.menu.addSeparator() self.themeMenu = QtGui.QMenu( translate('OpenLP.ServiceManager', '&Change Item Theme')) @@ -561,14 +561,12 @@ class ServiceManager(QtGui.QWidget): zip.write(audio_from, audio_to.encode(u'utf-8')) except IOError: log.exception(u'Failed to save service to disk: %s', temp_file_name) - # Add this line in after the release to notify the user that saving - # their file failed. Commented out due to string freeze. - #Receiver.send_message(u'openlp_error_message', { - # u'title': translate(u'OpenLP.ServiceManager', - # u'Error Saving File'), - # u'message': translate(u'OpenLP.ServiceManager', - # u'There was an error saving your file.') - #}) + Receiver.send_message(u'openlp_error_message', { + u'title': translate(u'OpenLP.ServiceManager', + u'Error Saving File'), + u'message': translate(u'OpenLP.ServiceManager', + u'There was an error saving your file.') + }) success = False finally: if zip: @@ -1318,15 +1316,15 @@ class ServiceManager(QtGui.QWidget): def findServiceItem(self): """ - Finds the selected ServiceItem in the list and returns the position of - the serviceitem and its selected child item. For example, if the third - child item (in the Slidecontroller known as slide) in the second service - item is selected this will return:: + Finds the first selected ServiceItem in the list and returns the + position of the serviceitem and its selected child item. For example, + if the third child item (in the Slidecontroller known as slide) in the + second service item is selected this will return:: (1, 2) """ items = self.serviceManagerList.selectedItems() - serviceItem = 0 + serviceItem = -1 serviceItemChild = -1 for item in items: parentitem = item.parent() @@ -1335,8 +1333,10 @@ class ServiceManager(QtGui.QWidget): else: serviceItem = parentitem.data(0, QtCore.Qt.UserRole).toInt()[0] serviceItemChild = item.data(0, QtCore.Qt.UserRole).toInt()[0] - # Adjust for zero based arrays. - serviceItem -= 1 + # Adjust for zero based arrays. + serviceItem -= 1 + # Only process the first item on the list for this method. + break return serviceItem, serviceItemChild def dragEnterEvent(self, event): diff --git a/openlp/core/ui/slidecontroller.py b/openlp/core/ui/slidecontroller.py index 1dc005aa6..99f2c8ad6 100644 --- a/openlp/core/ui/slidecontroller.py +++ b/openlp/core/ui/slidecontroller.py @@ -83,7 +83,7 @@ class SlideController(Controller): Set up the Slide Controller. """ Controller.__init__(self, parent, isLive) - self.screens = ScreenList.get_instance() + self.screens = ScreenList() try: self.ratio = float(self.screens.current[u'size'].width()) / \ float(self.screens.current[u'size'].height()) diff --git a/openlp/core/ui/themeform.py b/openlp/core/ui/themeform.py index f8c061851..60e073b13 100644 --- a/openlp/core/ui/themeform.py +++ b/openlp/core/ui/themeform.py @@ -136,16 +136,14 @@ class ThemeForm(QtGui.QWizard, Ui_ThemeWizard): """ self.backgroundPage.registerField( u'background_type', self.backgroundComboBox) - self.backgroundPage.registerField( - u'color', self.colorButton) + self.backgroundPage.registerField(u'color', self.colorButton) self.backgroundPage.registerField( u'grandient_start', self.gradientStartButton) self.backgroundPage.registerField( u'grandient_end', self.gradientEndButton) self.backgroundPage.registerField( u'background_image', self.imageFileEdit) - self.backgroundPage.registerField( - u'gradient', self.gradientComboBox) + self.backgroundPage.registerField(u'gradient', self.gradientComboBox) self.mainAreaPage.registerField( u'mainColorButton', self.mainColorButton) self.mainAreaPage.registerField( @@ -158,8 +156,7 @@ class ThemeForm(QtGui.QWizard, Ui_ThemeWizard): u'outlineColorButton', self.outlineColorButton) self.mainAreaPage.registerField( u'outlineSizeSpinBox', self.outlineSizeSpinBox) - self.mainAreaPage.registerField( - u'shadowCheckBox', self.shadowCheckBox) + self.mainAreaPage.registerField(u'shadowCheckBox', self.shadowCheckBox) self.mainAreaPage.registerField( u'mainBoldCheckBox', self.mainBoldCheckBox) self.mainAreaPage.registerField( @@ -170,10 +167,8 @@ class ThemeForm(QtGui.QWizard, Ui_ThemeWizard): u'shadowSizeSpinBox', self.shadowSizeSpinBox) self.mainAreaPage.registerField( u'footerSizeSpinBox', self.footerSizeSpinBox) - self.areaPositionPage.registerField( - u'mainPositionX', self.mainXSpinBox) - self.areaPositionPage.registerField( - u'mainPositionY', self.mainYSpinBox) + self.areaPositionPage.registerField(u'mainPositionX', self.mainXSpinBox) + self.areaPositionPage.registerField(u'mainPositionY', self.mainYSpinBox) self.areaPositionPage.registerField( u'mainPositionWidth', self.mainWidthSpinBox) self.areaPositionPage.registerField( @@ -188,12 +183,10 @@ class ThemeForm(QtGui.QWizard, Ui_ThemeWizard): u'footerPositionHeight', self.footerHeightSpinBox) self.backgroundPage.registerField( u'horizontal', self.horizontalComboBox) - self.backgroundPage.registerField( - u'vertical', self.verticalComboBox) + self.backgroundPage.registerField(u'vertical', self.verticalComboBox) self.backgroundPage.registerField( u'slideTransition', self.transitionsCheckBox) - self.backgroundPage.registerField( - u'name', self.themeNameEdit) + self.backgroundPage.registerField(u'name', self.themeNameEdit) def calculateLines(self): """ @@ -269,10 +262,7 @@ class ThemeForm(QtGui.QWizard, Ui_ThemeWizard): Change state as Outline check box changed """ if self.updateThemeAllowed: - if state == QtCore.Qt.Checked: - self.theme.font_main_outline = True - else: - self.theme.font_main_outline = False + self.theme.font_main_outline = state == QtCore.Qt.Checked self.outlineColorButton.setEnabled(self.theme.font_main_outline) self.outlineSizeSpinBox.setEnabled(self.theme.font_main_outline) self.calculateLines() @@ -350,19 +340,19 @@ class ThemeForm(QtGui.QWizard, Ui_ThemeWizard): if self.theme.background_type == \ BackgroundType.to_string(BackgroundType.Solid): self.colorButton.setStyleSheet(u'background-color: %s' % - self.theme.background_color) + self.theme.background_color) self.setField(u'background_type', QtCore.QVariant(0)) elif self.theme.background_type == \ BackgroundType.to_string(BackgroundType.Gradient): self.gradientStartButton.setStyleSheet(u'background-color: %s' % - self.theme.background_start_color) + self.theme.background_start_color) self.gradientEndButton.setStyleSheet(u'background-color: %s' % - self.theme.background_end_color) + self.theme.background_end_color) self.setField(u'background_type', QtCore.QVariant(1)) elif self.theme.background_type == \ BackgroundType.to_string(BackgroundType.Image): self.imageColorButton.setStyleSheet(u'background-color: %s' % - self.theme.background_border_color) + self.theme.background_border_color) self.imageFileEdit.setText(self.theme.background_filename) self.setField(u'background_type', QtCore.QVariant(2)) elif self.theme.background_type == \ @@ -642,8 +632,7 @@ class ThemeForm(QtGui.QWizard, Ui_ThemeWizard): """ Handle Color buttons """ - new_color = QtGui.QColorDialog.getColor( - QtGui.QColor(field), self) + 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/wizard.py b/openlp/core/ui/wizard.py index 5369c9799..0b4e8ec37 100644 --- a/openlp/core/ui/wizard.py +++ b/openlp/core/ui/wizard.py @@ -53,6 +53,7 @@ class WizardStrings(object): OL = u'OpenLyrics' OS = u'OpenSong' OSIS = u'OSIS' + PS = u'PowerSong 1.0' SB = u'SongBeamer' SoF = u'Songs of Fellowship' SSP = u'SongShow Plus' @@ -71,11 +72,14 @@ class WizardStrings(object): 'importer, you will need to install the "python-sqlite" ' 'module.') OpenTypeFile = unicode(translate('OpenLP.Ui', 'Open %s File')) + OpenTypeFolder = unicode(translate('OpenLP.Ui', 'Open %s Folder')) PercentSymbolFormat = unicode(translate('OpenLP.Ui', '%p%')) Ready = translate('OpenLP.Ui', 'Ready.') StartingImport = translate('OpenLP.Ui', 'Starting import...') YouSpecifyFile = unicode(translate('OpenLP.Ui', 'You need to specify at ' 'least one %s file to import from.', 'A file type e.g. OpenSong')) + YouSpecifyFolder = unicode(translate('OpenLP.Ui', 'You need to specify a ' + '%s folder to import from.', 'A file type e.g. OpenSong')) class OpenLPWizard(QtGui.QWizard): @@ -253,7 +257,7 @@ class OpenLPWizard(QtGui.QWizard): The title of the dialog (unicode). ``editbox`` - A editbox (QLineEdit). + An editbox (QLineEdit). ``filters`` The file extension filters. It should contain the file description @@ -264,11 +268,28 @@ class OpenLPWizard(QtGui.QWizard): if filters: filters += u';;' filters += u'%s (*)' % UiStrings().AllFiles - filename = QtGui.QFileDialog.getOpenFileName(self, title, + filename = unicode(QtGui.QFileDialog.getOpenFileName(self, title, os.path.dirname(SettingsManager.get_last_dir( - self.plugin.settingsSection, 1)), filters) + self.plugin.settingsSection, 1)), filters)) if filename: editbox.setText(filename) SettingsManager.set_last_dir(self.plugin.settingsSection, filename, 1) + def getFolder(self, title, editbox): + """ + Opens a QFileDialog and saves the selected folder to the given editbox. + + ``title`` + The title of the dialog (unicode). + + ``editbox`` + An editbox (QLineEdit). + """ + folder = unicode(QtGui.QFileDialog.getExistingDirectory(self, title, + os.path.dirname(SettingsManager.get_last_dir( + self.plugin.settingsSection, 1)), QtGui.QFileDialog.ShowDirsOnly)) + if folder: + editbox.setText(folder) + SettingsManager.set_last_dir(self.plugin.settingsSection, + folder, 1) diff --git a/openlp/plugins/bibles/lib/manager.py b/openlp/plugins/bibles/lib/manager.py index 9a82cc388..bfba082af 100644 --- a/openlp/plugins/bibles/lib/manager.py +++ b/openlp/plugins/bibles/lib/manager.py @@ -330,13 +330,7 @@ class BibleManager(object): 'Import Wizard to install one or more Bibles.') }) return None - language_selection = self.get_meta_data(bible, u'book_name_language') - if language_selection: - language_selection = int(language_selection.value) - if language_selection is None or language_selection == -1: - language_selection = QtCore.QSettings().value( - self.settingsSection + u'/bookname language', - QtCore.QVariant(0)).toInt()[0] + language_selection = self.get_language_selection(bible) reflist = parse_reference(versetext, self.db_cache[bible], language_selection, book_ref_id) if reflist: @@ -378,12 +372,16 @@ class BibleManager(object): """ log.debug(u'BibleManager.get_language_selection("%s")', bible) language_selection = self.get_meta_data(bible, u'book_name_language') - if language_selection and language_selection.value != u'None': - return int(language_selection.value) - if language_selection is None or language_selection.value == u'None': - return QtCore.QSettings().value( + if language_selection: + try: + language_selection = int(language_selection.value) + except (ValueError, TypeError): + language_selection = LanguageSelection.Application + if language_selection is None or language_selection == -1: + language_selection = QtCore.QSettings().value( self.settingsSection + u'/bookname language', QtCore.QVariant(0)).toInt()[0] + return language_selection def verse_search(self, bible, second_bible, text): """ diff --git a/openlp/plugins/bibles/lib/mediaitem.py b/openlp/plugins/bibles/lib/mediaitem.py index 3c723d760..288e0e2de 100644 --- a/openlp/plugins/bibles/lib/mediaitem.py +++ b/openlp/plugins/bibles/lib/mediaitem.py @@ -843,10 +843,11 @@ class BibleMediaItem(MediaManagerItem): items = [] language_selection = self.plugin.manager.get_language_selection(bible) for count, verse in enumerate(search_results): + book = None if language_selection == LanguageSelection.Bible: book = verse.book.name elif language_selection == LanguageSelection.Application: - book_names = BibleStrings().Booknames + book_names = BibleStrings().BookNames data = BiblesResourcesDB.get_book_by_id( verse.book.book_reference_id) book = unicode(book_names[data[u'abbreviation']]) diff --git a/openlp/plugins/songs/forms/songexportform.py b/openlp/plugins/songs/forms/songexportform.py index 397b52cfc..dcca098dc 100644 --- a/openlp/plugins/songs/forms/songexportform.py +++ b/openlp/plugins/songs/forms/songexportform.py @@ -363,11 +363,5 @@ class SongExportForm(OpenLPWizard): Called when the *directoryButton* was clicked. Opens a dialog and writes the path to *directoryLineEdit*. """ - path = unicode(QtGui.QFileDialog.getExistingDirectory(self, - translate('SongsPlugin.ExportWizardForm', - 'Select Destination Folder'), - SettingsManager.get_last_dir(self.plugin.settingsSection, 1), - options=QtGui.QFileDialog.ShowDirsOnly)) - SettingsManager.set_last_dir(self.plugin.settingsSection, path, 1) - self.directoryLineEdit.setText(path) - + self.getFolder(translate('SongsPlugin.ExportWizardForm', + 'Select Destination Folder'), self.directoryLineEdit) diff --git a/openlp/plugins/songs/forms/songimportform.py b/openlp/plugins/songs/forms/songimportform.py index 4a44c30ef..4bdabd1a2 100644 --- a/openlp/plugins/songs/forms/songimportform.py +++ b/openlp/plugins/songs/forms/songimportform.py @@ -105,6 +105,9 @@ class SongImportForm(OpenLPWizard): QtCore.QObject.connect(self.openLP1BrowseButton, QtCore.SIGNAL(u'clicked()'), self.onOpenLP1BrowseButtonClicked) + QtCore.QObject.connect(self.powerSongBrowseButton, + QtCore.SIGNAL(u'clicked()'), + self.onPowerSongBrowseButtonClicked) QtCore.QObject.connect(self.openLyricsAddButton, QtCore.SIGNAL(u'clicked()'), self.onOpenLyricsAddButtonClicked) @@ -217,6 +220,8 @@ class SongImportForm(OpenLPWizard): self.addFileSelectItem(u'foilPresenter') # Open Song self.addFileSelectItem(u'openSong', u'OpenSong') + # PowerSong + self.addFileSelectItem(u'powerSong', single_select=True) # SongBeamer self.addFileSelectItem(u'songBeamer') # Song Show Plus @@ -264,6 +269,8 @@ class SongImportForm(OpenLPWizard): self.formatComboBox.setItemText( SongFormat.FoilPresenter, WizardStrings.FP) self.formatComboBox.setItemText(SongFormat.OpenSong, WizardStrings.OS) + self.formatComboBox.setItemText( + SongFormat.PowerSong, WizardStrings.PS) self.formatComboBox.setItemText( SongFormat.SongBeamer, WizardStrings.SB) self.formatComboBox.setItemText( @@ -280,6 +287,9 @@ class SongImportForm(OpenLPWizard): translate('SongsPlugin.ImportWizardForm', 'Filename:')) self.openLP1BrowseButton.setText(UiStrings().Browse) self.openLP1DisabledLabel.setText(WizardStrings.NoSqlite) + self.powerSongFilenameLabel.setText( + translate('SongsPlugin.ImportWizardForm', 'Folder:')) + self.powerSongBrowseButton.setText(UiStrings().Browse) self.openLyricsAddButton.setText( translate('SongsPlugin.ImportWizardForm', 'Add Files...')) self.openLyricsRemoveButton.setText( @@ -375,6 +385,7 @@ class SongImportForm(OpenLPWizard): source_format = self.formatComboBox.currentIndex() QtCore.QSettings().setValue(u'songs/last import type', source_format) + import_class = SongFormat.get_class(source_format) if source_format == SongFormat.OpenLP2: if self.openLP2FilenameEdit.text().isEmpty(): critical_error_message_box(UiStrings().NFSs, @@ -387,6 +398,14 @@ class SongImportForm(OpenLPWizard): WizardStrings.YouSpecifyFile % UiStrings().OLPV1) self.openLP1BrowseButton.setFocus() return False + elif source_format == SongFormat.PowerSong: + if self.powerSongFilenameEdit.text().isEmpty() or \ + not import_class.isValidSource( + folder=self.powerSongFilenameEdit.text()): + critical_error_message_box(UiStrings().NFdSs, + WizardStrings.YouSpecifyFolder % WizardStrings.PS) + self.powerSongBrowseButton.setFocus() + return False elif source_format == SongFormat.OpenLyrics: if self.openLyricsFileListWidget.count() == 0: critical_error_message_box(UiStrings().NFSp, @@ -526,6 +545,13 @@ class SongImportForm(OpenLPWizard): 'openlp.org v1.x Databases') ) + def onPowerSongBrowseButtonClicked(self): + """ + Get PowerSong song database folder + """ + self.getFolder(WizardStrings.OpenTypeFolder % WizardStrings.PS, + self.powerSongFilenameEdit) + def onOpenLyricsAddButtonClicked(self): """ Get OpenLyrics song database files @@ -712,6 +738,7 @@ class SongImportForm(OpenLPWizard): self.formatComboBox.setCurrentIndex(last_import_type) self.openLP2FilenameEdit.setText(u'') self.openLP1FilenameEdit.setText(u'') + self.powerSongFilenameEdit.setText(u'') self.openLyricsFileListWidget.clear() self.openSongFileListWidget.clear() self.wordsOfWorshipFileListWidget.clear() @@ -757,6 +784,11 @@ class SongImportForm(OpenLPWizard): filename=unicode(self.openLP1FilenameEdit.text()), plugin=self.plugin ) + elif source_format == SongFormat.PowerSong: + # Import PowerSong folder + importer = self.plugin.importSongs(SongFormat.PowerSong, + folder=unicode(self.powerSongFilenameEdit.text()) + ) elif source_format == SongFormat.OpenLyrics: # Import OpenLyrics songs importer = self.plugin.importSongs(SongFormat.OpenLyrics, @@ -821,11 +853,7 @@ class SongImportForm(OpenLPWizard): filenames=self.getListOfFiles(self.foilPresenterFileListWidget) ) importer.doImport() - if importer.errorLog: - self.progressLabel.setText(translate( - 'SongsPlugin.SongImportForm', 'Your song import failed.')) - else: - self.progressLabel.setText(WizardStrings.FinishedImport) + self.progressLabel.setText(WizardStrings.FinishedImport) def onErrorCopyToButtonClicked(self): """ diff --git a/openlp/plugins/songs/lib/importer.py b/openlp/plugins/songs/lib/importer.py index 28a57339e..16d943a73 100644 --- a/openlp/plugins/songs/lib/importer.py +++ b/openlp/plugins/songs/lib/importer.py @@ -36,6 +36,7 @@ from openlyricsimport import OpenLyricsImport from wowimport import WowImport from cclifileimport import CCLIFileImport from dreambeamimport import DreamBeamImport +from powersongimport import PowerSongImport from ewimport import EasyWorshipSongImport from songbeamerimport import SongBeamerImport from songshowplusimport import SongShowPlusImport @@ -79,16 +80,17 @@ class SongFormat(object): EasyWorship = 7 FoilPresenter = 8 OpenSong = 9 - SongBeamer = 10 - SongShowPlus = 11 - SongsOfFellowship = 12 - WordsOfWorship = 13 - #CSV = 14 + PowerSong = 10 + SongBeamer = 11 + SongShowPlus = 12 + SongsOfFellowship = 13 + WordsOfWorship = 14 + #CSV = 15 @staticmethod def get_class(format): """ - Return the appropriate imeplementation class. + Return the appropriate implementation class. ``format`` The song format. @@ -111,6 +113,8 @@ class SongFormat(object): return CCLIFileImport elif format == SongFormat.DreamBeam: return DreamBeamImport + elif format == SongFormat.PowerSong: + return PowerSongImport elif format == SongFormat.EasySlides: return EasySlidesImport elif format == SongFormat.EasyWorship: @@ -139,6 +143,7 @@ class SongFormat(object): SongFormat.EasyWorship, SongFormat.FoilPresenter, SongFormat.OpenSong, + SongFormat.PowerSong, SongFormat.SongBeamer, SongFormat.SongShowPlus, SongFormat.SongsOfFellowship, diff --git a/openlp/plugins/songs/lib/powersongimport.py b/openlp/plugins/songs/lib/powersongimport.py new file mode 100644 index 000000000..9946d273d --- /dev/null +++ b/openlp/plugins/songs/lib/powersongimport.py @@ -0,0 +1,225 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=80 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2012 Raoul Snyman # +# Portions copyright (c) 2008-2012 Tim Bentley, Gerald Britton, Jonathan # +# Corwin, Michael Gorven, Scott Guerrieri, Matthias Hub, Meinert Jordan, # +# Armin Köhler, Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias # +# Põldaru, Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, # +# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Frode Woldsund # +# --------------------------------------------------------------------------- # +# 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 # +############################################################################### +""" +The :mod:`powersongimport` module provides the functionality for importing +PowerSong songs into the OpenLP database. +""" +import logging +import fnmatch +import os + +from openlp.core.lib import translate +from openlp.core.ui.wizard import WizardStrings +from openlp.plugins.songs.lib.songimport import SongImport + +log = logging.getLogger(__name__) + +class PowerSongImport(SongImport): + """ + The :class:`PowerSongImport` class provides the ability to import song files + from PowerSong. + + **PowerSong 1.0 Song File Format:** + + The file has a number of label-field (think key-value) pairs. + + Label and Field strings: + + * Every label and field is a variable length string preceded by an + integer specifying it's byte length. + * Integer is 32-bit but is encoded in 7-bit format to save space. Thus + if length will fit in 7 bits (ie <= 127) it takes up only one byte. + + Metadata fields: + + * Every PowerSong file has a TITLE field. + * There is zero or more AUTHOR fields. + * There is always a COPYRIGHTLINE label, but its field may be empty. + This field may also contain a CCLI number: e.g. "CCLI 176263". + + Lyrics fields: + + * Each verse is contained in a PART field. + * Lines have Windows line endings ``CRLF`` (0x0d, 0x0a). + * There is no concept of verse types. + + Valid extensions for a PowerSong song file are: + + * .song + """ + + @staticmethod + def isValidSource(**kwargs): + """ + Checks if source is a PowerSong 1.0 folder: + * is a directory + * contains at least one *.song file + """ + if u'folder' in kwargs: + dir = kwargs[u'folder'] + if os.path.isdir(dir): + for file in os.listdir(dir): + if fnmatch.fnmatch(file, u'*.song'): + return True + return False + + def doImport(self): + """ + Receive either a list of files or a folder (unicode) to import. + """ + if isinstance(self.importSource, unicode): + if os.path.isdir(self.importSource): + dir = self.importSource + self.importSource = [] + for file in os.listdir(dir): + if fnmatch.fnmatch(file, u'*.song'): + self.importSource.append(os.path.join(dir, file)) + else: + self.importSource = u'' + if not self.importSource or not isinstance(self.importSource, list): + self.logError(unicode(translate('SongsPlugin.PowerSongImport', + 'No songs to import.')), + unicode(translate('SongsPlugin.PowerSongImport', + 'No %s files found.' % WizardStrings.PS))) + return + self.importWizard.progressBar.setMaximum(len(self.importSource)) + for file in self.importSource: + if self.stopImportFlag: + return + self.setDefaults() + parse_error = False + with open(file, 'rb') as song_data: + while True: + try: + label = self._readString(song_data) + if not label: + break + field = self._readString(song_data) + except ValueError: + parse_error = True + self.logError(os.path.basename(file), unicode( + translate('SongsPlugin.PowerSongImport', + 'Invalid %s file. Unexpected byte value.' + % WizardStrings.PS))) + break + else: + if label == u'TITLE': + self.title = field.replace(u'\n', u' ') + elif label == u'AUTHOR': + self.parseAuthor(field) + elif label == u'COPYRIGHTLINE': + found_copyright = True + self._parseCopyrightCCLI(field) + elif label == u'PART': + self.addVerse(field) + if parse_error: + continue + # Check that file had TITLE field + if not self.title: + self.logError(os.path.basename(file), unicode( + translate('SongsPlugin.PowerSongImport', + 'Invalid %s file. Missing "TITLE" header.' + % WizardStrings.PS))) + continue + # Check that file had COPYRIGHTLINE label + if not found_copyright: + self.logError(self.title, unicode( + translate('SongsPlugin.PowerSongImport', + 'Invalid %s file. Missing "COPYRIGHTLINE" ' + 'header.' % WizardStrings.PS))) + continue + # Check that file had at least one verse + if not self.verses: + self.logError(self.title, unicode( + translate('SongsPlugin.PowerSongImport', + 'Verses not found. Missing "PART" header.'))) + continue + if not self.finish(): + self.logError(self.title) + + def _readString(self, file_object): + """ + Reads in next variable-length string. + """ + string_len = self._read7BitEncodedInteger(file_object) + return unicode(file_object.read(string_len), u'utf-8', u'ignore') + + def _read7BitEncodedInteger(self, file_object): + """ + Reads in a 32-bit integer in compressed 7-bit format. + + Accomplished by reading the integer 7 bits at a time. The high bit + of the byte when set means to continue reading more bytes. + If the integer will fit in 7 bits (ie <= 127), it only takes up one + byte. Otherwise, it may take up to 5 bytes. + + Reference: .NET method System.IO.BinaryReader.Read7BitEncodedInt + """ + val = 0 + shift = 0 + i = 0 + while True: + # Check for corrupted stream (since max 5 bytes per 32-bit integer) + if i == 5: + raise ValueError + byte = self._readByte(file_object) + # Strip high bit and shift left + val += (byte & 0x7f) << shift + shift += 7 + high_bit_set = byte & 0x80 + if not high_bit_set: + break + i += 1 + return val + + def _readByte(self, file_object): + """ + Reads in next byte as an unsigned integer + + Note: returns 0 at end of file. + """ + byte_str = file_object.read(1) + # If read result is empty, then reached end of file + if not byte_str: + return 0 + else: + return ord(byte_str) + + def _parseCopyrightCCLI(self, field): + """ + Look for CCLI song number, and get copyright + """ + copyright, sep, ccli_no = field.rpartition(u'CCLI') + if not sep: + copyright = ccli_no + ccli_no = u'' + if copyright: + self.addCopyright(copyright.rstrip(u'\n').replace(u'\n', u' ')) + if ccli_no: + ccli_no = ccli_no.strip(u' :') + if ccli_no.isdigit(): + self.ccliNumber = ccli_no diff --git a/openlp/plugins/songs/lib/songimport.py b/openlp/plugins/songs/lib/songimport.py index 74767d793..ac6818184 100644 --- a/openlp/plugins/songs/lib/songimport.py +++ b/openlp/plugins/songs/lib/songimport.py @@ -50,6 +50,13 @@ class SongImport(QtCore.QObject): whether the authors etc already exist and add them or refer to them as necessary """ + @staticmethod + def isValidSource(**kwargs): + """ + Override this method to validate the source prior to import. + """ + pass + def __init__(self, manager, **kwargs): """ Initialise and create defaults for properties @@ -65,14 +72,16 @@ class SongImport(QtCore.QObject): self.importSource = kwargs[u'filename'] elif u'filenames' in kwargs: self.importSource = kwargs[u'filenames'] + elif u'folder' in kwargs: + self.importSource = kwargs[u'folder'] else: - raise KeyError(u'Keyword arguments "filename[s]" not supplied.') + raise KeyError( + u'Keyword arguments "filename[s]" or "folder" not supplied.') log.debug(self.importSource) self.importWizard = None self.song = None self.stopImportFlag = False self.setDefaults() - self.errorLog = [] QtCore.QObject.connect(Receiver.get_receiver(), QtCore.SIGNAL(u'openlp_stop_wizard'), self.stopImport) @@ -107,11 +116,11 @@ class SongImport(QtCore.QObject): ``filepath`` This should be the file path if ``self.importSource`` is a list - with different files. If it is not a list, but a single file (for + with different files. If it is not a list, but a single file (for instance a database), then this should be the song's title. ``reason`` - The reason, why the import failed. The string should be as + The reason why the import failed. The string should be as informative as possible. """ self.setDefaults() diff --git a/openlp/plugins/songs/lib/wowimport.py b/openlp/plugins/songs/lib/wowimport.py index 99f448736..97a11d873 100644 --- a/openlp/plugins/songs/lib/wowimport.py +++ b/openlp/plugins/songs/lib/wowimport.py @@ -71,7 +71,7 @@ class WowImport(SongImport): * ``SOH`` (0x01) - Chorus * ``STX`` (0x02) - Bridge - Blocks are seperated by two bytes. The first byte is 0x01, and the + Blocks are separated by two bytes. The first byte is 0x01, and the second byte is 0x80. Lines: @@ -126,7 +126,7 @@ class WowImport(SongImport): ('Invalid Words of Worship song file. Missing ' '"CSongDoc::CBlock" string.')))) continue - # Seek to the beging of the first block + # Seek to the beginning of the first block song_data.seek(82) for block in range(no_of_blocks): self.linesToRead = ord(song_data.read(4)[:1]) @@ -140,7 +140,7 @@ class WowImport(SongImport): block_text += self.lineText self.linesToRead -= 1 block_type = BLOCK_TYPES[ord(song_data.read(4)[:1])] - # Blocks are seperated by 2 bytes, skip them, but not if + # Blocks are separated by 2 bytes, skip them, but not if # this is the last block! if block + 1 < no_of_blocks: song_data.seek(2, os.SEEK_CUR) diff --git a/openlp/plugins/songs/lib/xml.py b/openlp/plugins/songs/lib/xml.py index fdcb1dd60..45dac95b9 100644 --- a/openlp/plugins/songs/lib/xml.py +++ b/openlp/plugins/songs/lib/xml.py @@ -33,7 +33,7 @@ The basic XML for storing the lyrics in the song database looks like this:: - + @@ -135,7 +135,7 @@ class SongXML(object): The returned list has the following format:: [[{'type': 'v', 'label': '1'}, - u"virtual slide 1[---]virtual slide 2"], + u"optional slide split 1[---]optional slide split 2"], [{'lang': 'en', 'type': 'c', 'label': '1'}, u"English chorus"]] """ self.song_xml = None @@ -317,9 +317,7 @@ class OpenLyrics(object): tags_element = None match = re.search(u'\{/?\w+\}', song.lyrics, re.UNICODE) if match: - # Reset available tags. - FormattingTags.reset_html_tags() - # Named 'formatting' - 'format' is built-in fuction in Python. + # Named 'format_' - 'format' is built-in fuction in Python. format_ = etree.SubElement(song_xml, u'format') tags_element = etree.SubElement(format_, u'tags') tags_element.set(u'application', u'OpenLP') @@ -334,18 +332,59 @@ class OpenLyrics(object): self._add_text_to_element(u'verse', lyrics, None, verse_def) if u'lang' in verse[0]: verse_element.set(u'lang', verse[0][u'lang']) - # Create a list with all "virtual" verses. - virtual_verses = cgi.escape(verse[1]) - virtual_verses = virtual_verses.split(u'[---]') - for index, virtual_verse in enumerate(virtual_verses): + # Create a list with all "optional" verses. + optional_verses = cgi.escape(verse[1]) + optional_verses = optional_verses.split(u'\n[---]\n') + start_tags = u'' + end_tags = u'' + for index, optional_verse in enumerate(optional_verses): + # Fix up missing end and start tags such as {r} or {/r}. + optional_verse = start_tags + optional_verse + start_tags, end_tags = self._get_missing_tags(optional_verse) + optional_verse += end_tags # Add formatting tags to text lines_element = self._add_text_with_tags_to_lines(verse_element, - virtual_verse, tags_element) + optional_verse, tags_element) # Do not add the break attribute to the last lines element. - if index < len(virtual_verses) - 1: + if index < len(optional_verses) - 1: lines_element.set(u'break', u'optional') return self._extract_xml(song_xml) + def _get_missing_tags(self, text): + """ + Tests the given text for not closed formatting tags and returns a tuple + consisting of two unicode strings:: + + (u'{st}{r}', u'{/r}{/st}') + + The first unicode string are the start tags (for the next slide). The + second unicode string are the end tags. + + ``text`` + The text to test. The text must **not** contain html tags, only + OpenLP formatting tags are allowed:: + + {st}{r}Text text text + """ + tags = [] + for tag in FormattingTags.get_html_tags(): + if tag[u'start tag'] == u'{br}': + continue + if text.count(tag[u'start tag']) != text.count(tag[u'end tag']): + tags.append((text.find(tag[u'start tag']), + tag[u'start tag'], tag[u'end tag'])) + # Sort the lists, so that the tags which were opened first on the first + # slide (the text we are checking) will be opened first on the next + # slide as well. + tags.sort(key=lambda tag: tag[0]) + end_tags = [] + start_tags = [] + for tag in tags: + start_tags.append(tag[1]) + end_tags.append(tag[2]) + end_tags.reverse() + return u''.join(start_tags), u''.join(end_tags) + def xml_to_song(self, xml, parse_and_temporary_save=False): """ Create and save a song from OpenLyrics format xml to the database. Since @@ -572,7 +611,8 @@ class OpenLyrics(object): for tag in FormattingTags.get_html_tags()] new_tags = [tag for tag in found_tags if tag[u'start tag'] not in existing_tag_ids] - FormattingTags.add_html_tags(new_tags, True) + FormattingTags.add_html_tags(new_tags) + FormattingTags.save_html_tags() def _process_lines_mixed_content(self, element, newlines=True): """