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 0139cd001..b32e36194 100644
--- a/openlp/core/lib/imagemanager.py
+++ b/openlp/core/lib/imagemanager.py
@@ -100,6 +100,7 @@ class Image(object):
variables ``image`` and ``image_bytes`` to ``None`` and add the image object
to the queue of images to process.
"""
+ secondary_priority = 0
def __init__(self, name, path, source, background):
self.name = name
self.path = path
@@ -108,25 +109,40 @@ class Image(object):
self.priority = Priority.Normal
self.source = source
self.background = background
+ self.secondary_priority = Image.secondary_priority
+ Image.secondary_priority += 1
class PriorityQueue(Queue.PriorityQueue):
"""
Customised ``Queue.PriorityQueue``.
+
+ Each item in the queue must be tuple with three values. The first value
+ is the :class:`Image`'s ``priority`` attribute, the second value
+ the :class:`Image`'s ``secondary_priority`` attribute. The last value the
+ :class:`Image` instance itself::
+
+ (image.priority, image.secondary_priority, image)
+
+ Doing this, the :class:`Queue.PriorityQueue` will sort the images according
+ to their priorities, but also according to there number. However, the number
+ only has an impact on the result if there are more images with the same
+ priority. In such case the image which has been added earlier is privileged.
"""
def modify_priority(self, image, new_priority):
"""
Modifies the priority of the given ``image``.
``image``
- The image to remove. This should be an ``Image`` instance.
+ The image to remove. This should be an :class:`Image` instance.
``new_priority``
- The image's new priority.
+ The image's new priority. See the :class:`Priority` class for
+ priorities.
"""
self.remove(image)
image.priority = new_priority
- self.put((image.priority, image))
+ self.put((image.priority, image.secondary_priority, image))
def remove(self, image):
"""
@@ -135,8 +151,8 @@ class PriorityQueue(Queue.PriorityQueue):
``image``
The image to remove. This should be an ``Image`` instance.
"""
- if (image.priority, image) in self.queue:
- self.queue.remove((image.priority, image))
+ if (image.priority, image.secondary_priority, image) in self.queue:
+ self.queue.remove((image.priority, image.secondary_priority, image))
class ImageManager(QtCore.QObject):
@@ -261,7 +277,8 @@ class ImageManager(QtCore.QObject):
if not name in self._cache:
image = Image(name, path, source, background)
self._cache[name] = image
- self._conversion_queue.put((image.priority, image))
+ self._conversion_queue.put(
+ (image.priority, image.secondary_priority, image))
else:
log.debug(u'Image in cache %s:%s' % (name, path))
# We want only one thread.
@@ -282,7 +299,7 @@ class ImageManager(QtCore.QObject):
Actually does the work.
"""
log.debug(u'_process_cache')
- image = self._conversion_queue.get()[1]
+ image = self._conversion_queue.get()[2]
# Generate the QImage for the image.
if image.image is None:
image.image = resize_image(image.path, self.width, self.height,
diff --git a/openlp/core/lib/renderer.py b/openlp/core/lib/renderer.py
index 32ea0fb90..8694ca6b6 100644
--- a/openlp/core/lib/renderer.py
+++ b/openlp/core/lib/renderer.py
@@ -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/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/wizard.py b/openlp/core/ui/wizard.py
index 5369c9799..500d958fd 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'
diff --git a/openlp/plugins/songs/forms/songimportform.py b/openlp/plugins/songs/forms/songimportform.py
index 4a44c30ef..d5f7715ea 100644
--- a/openlp/plugins/songs/forms/songimportform.py
+++ b/openlp/plugins/songs/forms/songimportform.py
@@ -171,6 +171,12 @@ class SongImportForm(OpenLPWizard):
QtCore.QObject.connect(self.foilPresenterRemoveButton,
QtCore.SIGNAL(u'clicked()'),
self.onFoilPresenterRemoveButtonClicked)
+ QtCore.QObject.connect(self.powerSongAddButton,
+ QtCore.SIGNAL(u'clicked()'),
+ self.onPowerSongAddButtonClicked)
+ QtCore.QObject.connect(self.powerSongRemoveButton,
+ QtCore.SIGNAL(u'clicked()'),
+ self.onPowerSongRemoveButtonClicked)
def addCustomPages(self):
"""
@@ -217,6 +223,8 @@ class SongImportForm(OpenLPWizard):
self.addFileSelectItem(u'foilPresenter')
# Open Song
self.addFileSelectItem(u'openSong', u'OpenSong')
+ # PowerSong
+ self.addFileSelectItem(u'powerSong')
# SongBeamer
self.addFileSelectItem(u'songBeamer')
# Song Show Plus
@@ -264,6 +272,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(
@@ -305,6 +315,10 @@ class SongImportForm(OpenLPWizard):
translate('SongsPlugin.ImportWizardForm', 'Add Files...'))
self.dreamBeamRemoveButton.setText(
translate('SongsPlugin.ImportWizardForm', 'Remove File(s)'))
+ self.powerSongAddButton.setText(
+ translate('SongsPlugin.ImportWizardForm', 'Add Files...'))
+ self.powerSongRemoveButton.setText(
+ translate('SongsPlugin.ImportWizardForm', 'Remove File(s)'))
self.songsOfFellowshipAddButton.setText(
translate('SongsPlugin.ImportWizardForm', 'Add Files...'))
self.songsOfFellowshipRemoveButton.setText(
@@ -417,6 +431,12 @@ class SongImportForm(OpenLPWizard):
WizardStrings.YouSpecifyFile % WizardStrings.DB)
self.dreamBeamAddButton.setFocus()
return False
+ elif source_format == SongFormat.PowerSong:
+ if self.powerSongFileListWidget.count() == 0:
+ critical_error_message_box(UiStrings().NFSp,
+ WizardStrings.YouSpecifyFile % WizardStrings.PS)
+ self.powerSongAddButton.setFocus()
+ return False
elif source_format == SongFormat.SongsOfFellowship:
if self.songsOfFellowshipFileListWidget.count() == 0:
critical_error_message_box(UiStrings().NFSp,
@@ -600,6 +620,22 @@ class SongImportForm(OpenLPWizard):
"""
self.removeSelectedItems(self.dreamBeamFileListWidget)
+ def onPowerSongAddButtonClicked(self):
+ """
+ Get PowerSong song database files
+ """
+ self.getFiles(WizardStrings.OpenTypeFile % WizardStrings.PS,
+ self.powerSongFileListWidget, u'%s (*.song)'
+ % translate('SongsPlugin.ImportWizardForm',
+ 'PowerSong 1.0 Song Files')
+ )
+
+ def onPowerSongRemoveButtonClicked(self):
+ """
+ Remove selected PowerSong files from the import list
+ """
+ self.removeSelectedItems(self.powerSongFileListWidget)
+
def onSongsOfFellowshipAddButtonClicked(self):
"""
Get Songs of Fellowship song database files
@@ -717,6 +753,7 @@ class SongImportForm(OpenLPWizard):
self.wordsOfWorshipFileListWidget.clear()
self.ccliFileListWidget.clear()
self.dreamBeamFileListWidget.clear()
+ self.powerSongFileListWidget.clear()
self.songsOfFellowshipFileListWidget.clear()
self.genericFileListWidget.clear()
self.easySlidesFilenameEdit.setText(u'')
@@ -784,6 +821,12 @@ class SongImportForm(OpenLPWizard):
filenames=self.getListOfFiles(
self.dreamBeamFileListWidget)
)
+ elif source_format == SongFormat.PowerSong:
+ # Import PowerSong songs
+ importer = self.plugin.importSongs(SongFormat.PowerSong,
+ filenames=self.getListOfFiles(
+ self.powerSongFileListWidget)
+ )
elif source_format == SongFormat.SongsOfFellowship:
# Import a Songs of Fellowship RTF file
importer = self.plugin.importSongs(SongFormat.SongsOfFellowship,
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..31491398c
--- /dev/null
+++ b/openlp/plugins/songs/lib/powersongimport.py
@@ -0,0 +1,195 @@
+# -*- 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
+
+from openlp.core.lib import translate
+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
+ """
+
+ def doImport(self):
+ """
+ Receive a list of files to import.
+ """
+ if not isinstance(self.importSource, list):
+ self.logError(unicode(translate('SongsPlugin.PowerSongImport',
+ 'No files to import.')))
+ 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(file, unicode(
+ translate('SongsPlugin.PowerSongImport',
+ 'Invalid PowerSong file. Unexpected byte value.')))
+ 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(file, unicode(
+ translate('SongsPlugin.PowerSongImport',
+ 'Invalid PowerSong file. Missing "TITLE" header.')))
+ continue
+ # Check that file had COPYRIGHTLINE label
+ if not found_copyright:
+ self.logError(file, unicode(
+ translate('SongsPlugin.PowerSongImport',
+ '"%s" Invalid PowerSong file. Missing "COPYRIGHTLINE" '
+ 'header.' % self.title)))
+ continue
+ # Check that file had at least one verse
+ if not self.verses:
+ self.logError(file, unicode(
+ translate('SongsPlugin.PowerSongImport',
+ '"%s" Verses not found. Missing "PART" header.'
+ % self.title)))
+ continue
+ if not self.finish():
+ self.logError(file)
+
+ 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..9bfdce124 100644
--- a/openlp/plugins/songs/lib/songimport.py
+++ b/openlp/plugins/songs/lib/songimport.py
@@ -107,11 +107,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):
"""