diff --git a/openlp/core/lib/__init__.py b/openlp/core/lib/__init__.py index b76179c2c..721b6ae23 100644 --- a/openlp/core/lib/__init__.py +++ b/openlp/core/lib/__init__.py @@ -39,40 +39,40 @@ log = logging.getLogger(__name__) html_expands = [] html_expands.append({u'desc':u'Red', u'start tag':u'{r}', \ - u'start html':u'', \ - u'end tag':u'{/r}', u'end html':u'', \ + u'start html':u'', \ + u'end tag':u'{/r}', u'end html':u'', \ u'protected':False}) html_expands.append({u'desc':u'Black', u'start tag':u'{b}', \ - u'start html':u'', \ - u'end tag':u'{/b}', u'end html':u'', \ + u'start html':u'', \ + u'end tag':u'{/b}', u'end html':u'', \ u'protected':False}) html_expands.append({u'desc':u'Blue', u'start tag':u'{bl}', \ - u'start html':u'', \ - u'end tag':u'{/bl}', u'end html':u'', \ + u'start html':u'', \ + u'end tag':u'{/bl}', u'end html':u'', \ u'protected':False}) html_expands.append({u'desc':u'Yellow', u'start tag':u'{y}', \ - u'start html':u'', \ - u'end tag':u'{/y}', u'end html':u'', \ + u'start html':u'', \ + u'end tag':u'{/y}', u'end html':u'', \ u'protected':False}) html_expands.append({u'desc':u'Green', u'start tag':u'{g}', \ - u'start html':u'', \ - u'end tag':u'{/g}', u'end html':u'', \ + u'start html':u'', \ + u'end tag':u'{/g}', u'end html':u'', \ u'protected':False}) html_expands.append({u'desc':u'Pink', u'start tag':u'{pk}', \ - u'start html':u'', \ - u'end tag':u'{/pk}', u'end html':u'', \ + u'start html':u'', \ + u'end tag':u'{/pk}', u'end html':u'', \ u'protected':False}) html_expands.append({u'desc':u'Orange', u'start tag':u'{o}', \ - u'start html':u'', \ - u'end tag':u'{/o}', u'end html':u'', \ + u'start html':u'', \ + u'end tag':u'{/o}', u'end html':u'', \ u'protected':False}) html_expands.append({u'desc':u'Purple', u'start tag':u'{pp}', \ - u'start html':u'', \ - u'end tag':u'{/pp}', u'end html':u'', \ + u'start html':u'', \ + u'end tag':u'{/pp}', u'end html':u'', \ u'protected':False}) html_expands.append({u'desc':u'White', u'start tag':u'{w}', \ - u'start html':u'', \ - u'end tag':u'{/w}', u'end html':u'', \ + u'start html':u'', \ + u'end tag':u'{/w}', u'end html':u'', \ u'protected':False}) html_expands.append({u'desc':u'Superscript', u'start tag':u'{su}', \ u'start html':u'', \ diff --git a/openlp/core/lib/htmlbuilder.py b/openlp/core/lib/htmlbuilder.py index 332d23844..d03b7f7cc 100644 --- a/openlp/core/lib/htmlbuilder.py +++ b/openlp/core/lib/htmlbuilder.py @@ -24,6 +24,8 @@ # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### +from PyQt4 import QtWebKit + from openlp.core.lib import image_to_byte HTMLSRC = u""" @@ -39,7 +41,7 @@ HTMLSRC = u""" body { background-color: black; } -.dim { +.size { position: absolute; left: 0px; top: 0px; @@ -51,6 +53,9 @@ body { background-color: black; display: none; } +#image { + z-index:1; +} #video { z-index:2; } @@ -136,8 +141,12 @@ body { } document.getElementById('black').style.display = black; document.getElementById('lyricsmain').style.visibility = lyrics; - document.getElementById('lyricsoutline').style.visibility = lyrics; - document.getElementById('lyricsshadow').style.visibility = lyrics; + outline = document.getElementById('lyricsoutline') + if(outline!=null) + outline.style.visibility = lyrics; + shadow = document.getElementById('lyricsshadow') + if(shadow!=null) + shadow.style.visibility = lyrics; document.getElementById('footer').style.visibility = lyrics; var vid = document.getElementById('video'); if(vid.src != ''){ @@ -156,7 +165,7 @@ body { return 0; } if(position == ''){ - position = window.getComputedStyle(text, '').verticalAlign; + position = getComputedStyle(text, '').verticalAlign; } switch(position) { @@ -181,111 +190,62 @@ body { } function show_text(newtext){ - var text1 = document.getElementById('lyricsmain'); - var texto1 = document.getElementById('lyricsoutline'); - var texts1 = document.getElementById('lyricsshadow'); - if(!transition){ - text1.innerHTML = newtext; - texto1.innerHTML = newtext; - texts1.innerHTML = newtext; - return; - } - var text2 = document.getElementById('lyricsmain2'); - var texto2 = document.getElementById('lyricsoutline2'); - var texts2 = document.getElementById('lyricsshadow2'); - if((text2.style.opacity == '')||(parseFloat(text2.style.opacity) < 0.5)) - { - text2.innerHTML = text1.innerHTML; - text2.style.opacity = text1.style.opacity; - texto2.innerHTML = text1.innerHTML; - texto2.style.opacity = text1.style.opacity; - texts2.innerHTML = text1.innerHTML; - texts2.style.opacity = text1.style.opacity; - } - text1.style.opacity = 0; - text1.innerHTML = newtext; - texto1.style.opacity = 0; - texto1.innerHTML = newtext; - texts1.style.opacity = 0; - texts1.innerHTML = newtext; - // For performance reasons, we'll not animate the shadow for now - texts2.style.opacity = 0; if(timer != null) clearTimeout(timer); - timer = setTimeout('text_fade()', 50); + text_fade('lyricsmain', newtext); + text_fade('lyricsoutline', newtext); + text_fade('lyricsshadow', newtext); + if(text_opacity()==1) return; + timer = setTimeout(function(){ + show_text(newtext); + }, 100); } - function text_fade(){ - var text1 = document.getElementById('lyricsmain'); - var texto1 = document.getElementById('lyricsoutline'); - var texts1 = document.getElementById('lyricsshadow'); - var text2 = document.getElementById('lyricsmain2'); - var texto2 = document.getElementById('lyricsoutline2'); - var texts2 = document.getElementById('lyricsshadow2'); - if(parseFloat(text1.style.opacity) < 1){ - text1.style.opacity = parseFloat(text1.style.opacity) + 0.1; - texto1.style.opacity = parseFloat(texto1.style.opacity) + 0.1; - // Don't animate shadow (performance) - //texts1.style.opacity = parseFloat(texts1.style.opacity) + 0.1; + function text_fade(id, newtext){ + /* + Using -webkit-transition: opacity 1s linear; would have been preferred + but it isn't currently quick enough when animating multiple layers of + large areas of large text. Therefore do it manually as best we can. + Hopefully in the future we can revisit and do more interesting + transitions using -webkit-transition and -webkit-transform. + However we need to ensure interrupted transitions (quickly change 2 + slides) still looks pretty and is zippy. + */ + var text = document.getElementById(id); + if(text==null) return; + if(!transition){ + text.innerHTML = newtext; + return; } - if(parseFloat(text2.style.opacity) > 0){ - text2.style.opacity = parseFloat(text2.style.opacity) - 0.1; - texto2.style.opacity = parseFloat(texto2.style.opacity) - 0.1; - // Don't animate shadow (performance) - //texts2.style.opacity = parseFloat(texts2.style.opacity) - 0.1; - } - if((parseFloat(text1.style.opacity) < 1) || - (parseFloat(text2.style.opacity) > 0)){ - t = setTimeout('text_fade()', 50); + if(newtext==text.innerHTML){ + text.style.opacity = parseFloat(text.style.opacity) + 0.3; + if(text.style.opacity>0.7) + text.style.opacity = 1; } else { - text1.style.opacity = 1; - texto1.style.opacity = 1; - texts1.style.opacity = 1; - text2.style.opacity = 0; - texto2.style.opacity = 0; - texts2.style.opacity = 0; + text.style.opacity = parseFloat(text.style.opacity) - 0.3; + if(text.style.opacity<=0.1){ + text.innerHTML = newtext; + } } } + function text_opacity(){ + var text = document.getElementById('lyricsmain'); + return getComputedStyle(text, '').opacity; + } + function show_text_complete(){ - return (document.getElementById('lyricsmain').style.opacity == 1); + return (text_opacity()==1); } - - - -
- - -
- - -
- - -
- - -
- - -
- + + +%s - -
- +
+ """ @@ -300,7 +260,13 @@ def build_html(item, screen, alert, islive): Current display information `alert` Alert display display information + `islive` + Item is going live, rather than preview/theme building """ + try: + webkitvers = float(QtWebKit.qWebKitVersion()) + except AttributeError: + webkitvers = 0 width = screen[u'size'].width() height = screen[u'size'].height() theme = item.themedata @@ -309,83 +275,144 @@ def build_html(item, screen, alert, islive): else: image = u'' html = HTMLSRC % (width, height, - build_alert(alert, width), - build_footer(item), - build_lyrics(item), + build_alert_css(alert, width), + build_footer_css(item), + build_lyrics_css(item, webkitvers), u'true' if theme and theme.display_slideTransition and islive \ else u'false', - image) + image, + build_lyrics_html(item, webkitvers)) return html -def build_lyrics(item): +def build_lyrics_css(item, webkitvers): """ - Build the video display div + Build the video display css `item` Service Item containing theme and location information + + `webkitvers` + The version of qtwebkit we're using + """ style = """ - .lyricscommon { position: absolute; %s } - .lyricstable { z-index:4; %s } - .lyricsoutlinetable { z-index:3; %s } - .lyricsshadowtable { z-index:2; %s } - .lyrics { %s } - .lyricsoutline { %s } - .lyricsshadow { %s } +.lyricstable { + z-index:4; + position: absolute; + display: table; + %s +} +.lyricscell { + display:table-cell; + word-wrap: break-word; + %s +} +.lyricsmain { +%s +} +.lyricsoutline { +%s +} +.lyricsshadow { +%s +} """ theme = item.themedata - lyricscommon = u'' lyricstable = u'' - outlinetable = u'' - shadowtable = u'' lyrics = u'' - outline = u'display: none;' - shadow = u'display: none;' + lyricsmain = u'' + outline = u'' + shadow = u'' if theme: - lyricscommon = u'width: %spx; height: %spx; word-wrap: break-word; ' \ - u'font-family: %s; font-size: %spt; color: %s; line-height: %d%%;' \ - % (item.main.width(), item.main.height(), theme.font_main_name, - theme.font_main_proportion, theme.font_main_color, - 100 + int(theme.font_main_line_adjustment)) lyricstable = u'left: %spx; top: %spx;' % \ (item.main.x(), item.main.y()) - outlinetable = u'left: %spx; top: %spx;' % \ - (item.main.x(), item.main.y()) - shadowtable = u'left: %spx; top: %spx;' % \ - (item.main.x() + float(theme.display_shadow_size), - item.main.y() + float(theme.display_shadow_size)) - align = u'' if theme.display_horizontalAlign == 2: - align = u'text-align:center;' + align = u'center' elif theme.display_horizontalAlign == 1: - align = u'text-align:right;' + align = u'right' else: - align = u'text-align:left;' + align = u'left' if theme.display_verticalAlign == 2: - valign = u'vertical-align:bottom;' + valign = u'bottom' elif theme.display_verticalAlign == 1: - valign = u'vertical-align:middle;' + valign = u'middle' else: - valign = u'vertical-align:top;' - lyrics = u'%s %s' % (align, valign) + valign = u'top' + lyrics = u'width: %spx; height: %spx; text-align: %s; ' \ + 'vertical-align: %s; font-family: %s; font-size: %spt; ' \ + 'color: %s; line-height: %d%%;' % \ + (item.main.width(), item.main.height(), align, valign, + theme.font_main_name, theme.font_main_proportion, + theme.font_main_color, 100 + int(theme.font_main_line_adjustment)) + # For performance reasons we want to show as few DIV's as possible, + # especially when animating/transitions. + # However some bugs in older versions of qtwebkit mean we need to + # perform workarounds and add extra divs. Only do these when needed. + # + # Before 533.3 the webkit-text-fill colour wasn't displayed, only the + # stroke (outline) color. So put stroke layer underneath the main text. + # + # Before 534.4 the webkit-text-stroke was sometimes out of alignment + # with the fill, or normal text. letter-spacing=1 is workaround + # https://bugs.webkit.org/show_bug.cgi?id=44403 + # + # Before 534.4 the text-shadow didn't get displayed when + # webkit-text-stroke was used. So use an offset text layer underneath. + # https://bugs.webkit.org/show_bug.cgi?id=19728 if theme.display_outline: - lyricscommon += u' letter-spacing: 1px;' - outline = u'-webkit-text-stroke: %sem %s; ' % \ + if webkitvers < 534.3: + lyrics += u' letter-spacing: 1px;' + outline = u' -webkit-text-stroke: %sem %s; ' \ + '-webkit-text-fill-color: %s; ' % \ (float(theme.display_outline_size) / 16, - theme.display_outline_color) - if theme.display_shadow: + theme.display_outline_color, theme.font_main_color) + if webkitvers >= 533.3: + lyricsmain += outline + if theme.display_shadow and webkitvers < 534.3: shadow = u'-webkit-text-stroke: %sem %s; ' \ - u'-webkit-text-fill-color: %s; ' % \ + u'-webkit-text-fill-color: %s; ' \ + u' padding-left: %spx; padding-top: %spx' % \ (float(theme.display_outline_size) / 16, - theme.display_shadow_color, theme.display_shadow_color) - else: - if theme.display_shadow: - shadow = u'color: %s;' % (theme.display_shadow_color) - lyrics_html = style % (lyricscommon, lyricstable, outlinetable, - shadowtable, lyrics, outline, shadow) - return lyrics_html + theme.display_shadow_color, theme.display_shadow_color, + theme.display_shadow_size, theme.display_shadow_size) + if theme.display_shadow and \ + (not theme.display_outline or webkitvers >= 534.3): + lyricsmain += u' text-shadow: %s %spx %spx;' % \ + (theme.display_shadow_color, theme.display_shadow_size, + theme.display_shadow_size) + lyrics_css = style % (lyricstable, lyrics, lyricsmain, outline, shadow) + return lyrics_css + +def build_lyrics_html(item, webkitvers): + """ + Build the HTML required to show the lyrics -def build_footer(item): + `item` + Service Item containing theme and location information + + `webkitvers` + The version of qtwebkit we're using + """ + # Bugs in some versions of QtWebKit mean we sometimes need additional + # divs for outline and shadow, since the CSS doesn't work. + # To support vertical alignment middle and bottom, nested div's using + # display:table/display:table-cell are required for each lyric block. + lyrics = u'' + theme = item.themedata + if webkitvers < 534.4 and theme and theme.display_outline: + lyrics += u'
' \ + u'
' + if webkitvers < 533.3: + lyrics += u'
' \ + u'
' + lyrics += u'
' \ + u'
' + return lyrics + +def build_footer_css(item): """ Build the display of the item footer @@ -416,7 +443,7 @@ def build_footer(item): theme.font_footer_proportion, theme.font_footer_color, align) return lyrics_html -def build_alert(alertTab, width): +def build_alert_css(alertTab, width): """ Build the display of the footer @@ -424,7 +451,7 @@ def build_alert(alertTab, width): Details from the Alert tab for fonts etc """ style = """ - width: %s; + width: %spx; vertical-align: %s; font-family: %s; font-size: %spt; diff --git a/openlp/core/lib/renderer.py b/openlp/core/lib/renderer.py index 433018c23..41e547800 100644 --- a/openlp/core/lib/renderer.py +++ b/openlp/core/lib/renderer.py @@ -47,26 +47,13 @@ class Renderer(object): Initialise the renderer. """ self._rect = None - self._debug = False - self._display_shadow_size_footer = 0 - self._display_outline_size_footer = 0 self.theme_name = None self._theme = None self._bg_image_filename = None self.frame = None - self.frame_opaque = None self.bg_frame = None self.bg_image = None - def set_debug(self, debug): - """ - Set the debug mode of the renderer. - - ``debug`` - The debug mode. - """ - self._debug = debug - def set_theme(self, theme): """ Set the theme to be used. @@ -159,7 +146,7 @@ class Renderer(object): doc = QtGui.QTextDocument() doc.setPageSize(QtCore.QSizeF(self._rect.width(), self._rect.height())) df = doc.defaultFont() - df.setPixelSize(self._theme.font_main_proportion) + df.setPointSize(self._theme.font_main_proportion) df.setFamily(self._theme.font_main_name) main_weight = 50 if self._theme.font_main_weight == u'Bold': @@ -188,7 +175,7 @@ class Renderer(object): # Text too long so gone to next mage if layout.pageCount() != 1: formatted.append(shell % old_html_text) - temp_text = line + temp_text = line + line_end old_html_text = temp_text formatted.append(shell % old_html_text) log.debug(u'format_slide - End') diff --git a/openlp/core/lib/rendermanager.py b/openlp/core/lib/rendermanager.py index 9caabdc69..6be26bd82 100644 --- a/openlp/core/lib/rendermanager.py +++ b/openlp/core/lib/rendermanager.py @@ -93,6 +93,7 @@ class RenderManager(object): """ self.global_theme = global_theme self.theme_level = theme_level + self.themedata = None def set_service_theme(self, service_theme): """ @@ -102,6 +103,7 @@ class RenderManager(object): The service-level theme to be set. """ self.service_theme = service_theme + self.themedata = None def set_override_theme(self, theme, overrideLevels=False): """ @@ -111,6 +113,10 @@ class RenderManager(object): ``theme`` The name of the song-level theme. None means the service item wants to use the given value. + + ``overrideLevels`` + Used to force the theme data passed in to be used. + """ log.debug(u'set override theme to %s', theme) theme_level = self.theme_level @@ -137,6 +143,7 @@ class RenderManager(object): if self.theme != self.renderer.theme_name or self.themedata is None \ or overrideLevels: log.debug(u'theme is now %s', self.theme) + # Force the theme to be the one passed in. if overrideLevels: self.themedata = theme else: diff --git a/openlp/core/ui/maindisplay.py b/openlp/core/ui/maindisplay.py index e1ec90d7a..8410e037d 100644 --- a/openlp/core/ui/maindisplay.py +++ b/openlp/core/ui/maindisplay.py @@ -115,8 +115,11 @@ class MainDisplay(DisplayWidget): self.screen = self.screens.current self.setVisible(False) self.setGeometry(self.screen[u'size']) - self.webView = QtWebKit.QWebView(self) - self.webView.setGeometry(0, 0, self.screen[u'size'].width(), \ + self.scene = QtGui.QGraphicsScene() + self.setScene(self.scene) + self.webView = QtWebKit.QGraphicsWebView() + self.scene.addItem(self.webView) + self.webView.resize(self.screen[u'size'].width(), \ self.screen[u'size'].height()) self.page = self.webView.page() self.frame = self.page.mainFrame() @@ -319,9 +322,6 @@ class MainDisplay(DisplayWidget): # Make display show up if in single screen mode if self.isLive: self.setVisible(True) - # save preview for debugging - if log.isEnabledFor(logging.DEBUG): - preview.save(u'temp.png', u'png') return preview def buildHtml(self, serviceItem): diff --git a/openlp/core/ui/servicemanager.py b/openlp/core/ui/servicemanager.py index f2961b717..c7d3c497f 100644 --- a/openlp/core/ui/servicemanager.py +++ b/openlp/core/ui/servicemanager.py @@ -574,7 +574,7 @@ class ServiceManager(QtGui.QWidget): * An osd which is a pickle of the service items * All image, presentation and video files needed to run the service. """ - log.debug(u'onSaveService') + log.debug(u'onSaveService %s' % quick) if not quick or self.isNew: filename = QtGui.QFileDialog.getSaveFileName(self, translate('OpenLP.ServiceManager', 'Save Service'), @@ -755,6 +755,7 @@ class ServiceManager(QtGui.QWidget): """ Set the theme for the current service """ + log.debug(u'onThemeComboBoxSelected') self.service_theme = unicode(self.themeComboBox.currentText()) self.parent.RenderManager.set_service_theme(self.service_theme) QtCore.QSettings().setValue( @@ -767,6 +768,7 @@ class ServiceManager(QtGui.QWidget): The theme may have changed in the settings dialog so make sure the theme combo box is in the correct state. """ + log.debug(u'themeChange') if self.parent.RenderManager.theme_level == ThemeLevel.Global: self.toolbar.actions[u'ThemeLabel'].setVisible(False) self.toolbar.actions[u'ThemeWidget'].setVisible(False) @@ -779,6 +781,7 @@ class ServiceManager(QtGui.QWidget): Rebuild the service list as things have changed and a repaint is the easiest way to do this. """ + log.debug(u'regenerateServiceItems') # force reset of renderer as theme data has changed self.parent.RenderManager.themedata = None if self.serviceItems: @@ -800,6 +803,7 @@ class ServiceManager(QtGui.QWidget): ``item`` Service Item to be added """ + log.debug(u'addServiceItem') sitem = self.findServiceItem()[0] item.render() if replace: diff --git a/openlp/plugins/bibles/lib/csvbible.py b/openlp/plugins/bibles/lib/csvbible.py index 7c0ba6b2b..cc981059c 100644 --- a/openlp/plugins/bibles/lib/csvbible.py +++ b/openlp/plugins/bibles/lib/csvbible.py @@ -51,9 +51,9 @@ class CSVBible(BibleDB): if u'booksfile' not in kwargs: raise KeyError(u'You have to supply a file to import books from.') self.booksfile = kwargs[u'booksfile'] - if u'versesfile' not in kwargs: + if u'versefile' not in kwargs: raise KeyError(u'You have to supply a file to import verses from.') - self.versesfile = kwargs[u'versesfile'] + self.versesfile = kwargs[u'versefile'] QtCore.QObject.connect(Receiver.get_receiver(), QtCore.SIGNAL(u'bibles_stop_import'), self.stop_import) diff --git a/openlp/plugins/bibles/lib/mediaitem.py b/openlp/plugins/bibles/lib/mediaitem.py index ab38b5f83..e7850c65c 100644 --- a/openlp/plugins/bibles/lib/mediaitem.py +++ b/openlp/plugins/bibles/lib/mediaitem.py @@ -47,7 +47,6 @@ class BibleListView(BaseListWithDnD): self.parent().onListViewResize(event.size().width(), event.size().width()) - class BibleMediaItem(MediaManagerItem): """ This is the custom media manager item for Bibles. @@ -466,19 +465,27 @@ class BibleMediaItem(MediaManagerItem): def generateSlideData(self, service_item, item=None): """ - Generates and formats the slides for the service item. + Generates and formats the slides for the service item as well as the + service item's title. """ log.debug(u'generating slide data') items = self.listView.selectedIndexes() if len(items) == 0: return False + has_dual_bible = False bible_text = u'' old_chapter = u'' raw_footer = [] raw_slides = [] - service_item.add_capability(ItemCapabilities.AllowsPreview) - service_item.add_capability(ItemCapabilities.AllowsLoop) - service_item.add_capability(ItemCapabilities.AllowsAdditions) + for item in items: + bitem = self.listView.item(item.row()) + reference = bitem.data(QtCore.Qt.UserRole) + if isinstance(reference, QtCore.QVariant): + reference = reference.toPyObject() + dual_bible = self._decodeQtObject(reference, 'dual_bible') + if dual_bible: + has_dual_bible = True + break # Let's loop through the main lot, and assemble our verses. for item in items: bitem = self.listView.item(item.row()) @@ -491,7 +498,7 @@ class BibleMediaItem(MediaManagerItem): bible = self._decodeQtObject(reference, 'bible') version = self._decodeQtObject(reference, 'version') copyright = self._decodeQtObject(reference, 'copyright') - #permission = self._decodeQtObject(reference, 'permission') + permission = self._decodeQtObject(reference, 'permission') text = self._decodeQtObject(reference, 'text') dual_bible = self._decodeQtObject(reference, 'dual_bible') if dual_bible: @@ -499,70 +506,57 @@ class BibleMediaItem(MediaManagerItem): 'dual_version') dual_copyright = self._decodeQtObject(reference, 'dual_copyright') - #dual_permission = self._decodeQtObject(reference, - # 'dual_permission') + dual_permission = self._decodeQtObject(reference, + 'dual_permission') dual_text = self._decodeQtObject(reference, 'dual_text') - if self.parent.settings_tab.display_style == 1: - verse_text = self.formatVerse(old_chapter, chapter, verse, - u'{su}(', u'){/su}') - elif self.parent.settings_tab.display_style == 2: - verse_text = self.formatVerse(old_chapter, chapter, verse, - u'{su}{', u'}{/su}') - elif self.parent.settings_tab.display_style == 3: - verse_text = self.formatVerse(old_chapter, chapter, verse, - u'{su}[', u']{/su}') - else: - verse_text = self.formatVerse(old_chapter, chapter, verse, - u'{su}', u'{/su}') - old_chapter = chapter - footer = u'%s (%s %s)' % (book, version, copyright) - # If not found add to footer + verse_text = self.formatVerse(old_chapter, chapter, verse) + footer = u'%s (%s %s %s)' % (book, version, copyright, permission) if footer not in raw_footer: raw_footer.append(footer) - if dual_bible: - footer = u'%s (%s %s)' % (book, dual_version, - dual_copyright) - # If not found add second version and copyright to footer. - if footer not in raw_footer: - raw_footer.append(footer) - bible_text = u'%s %s \n\n %s %s' % (verse_text, text, - verse_text, dual_text) - raw_slides.append(bible_text) - bible_text = u'' - else: - # If we are 'Verse Per Line' then force a new line. - if self.parent.settings_tab.layout_style == 1: - text = text + u'\n' - else: - # split the line but do not replace line breaks in renderer - service_item.add_capability(ItemCapabilities.NoLineBreaks) - text = text + u'\n' - bible_text = u'%s %s %s' % (bible_text, verse_text, text) - # If we are 'Verse Per Slide' then create a new slide. - if self.parent.settings_tab.layout_style == 0: - raw_slides.append(bible_text) - bible_text = u'' - # If we are not 'Verse Per Slide' we have to make sure, that we - # add more verses. - else: - if item.row() < len(items) - 1: - bitem = items[item.row() + 1] - reference = bitem.data(QtCore.Qt.UserRole) - if isinstance(reference, QtCore.QVariant): - reference = reference.toPyObject() - bible_new = self._decodeQtObject(reference, 'bible') - dual_bible_new = self._decodeQtObject(reference, - 'dual_bible') - if dual_bible_new: - raw_slides.append(bible_text) - bible_text = u'' - elif bible != bible_new: - raw_slides.append(bible_text) - bible_text = u'' - else: + if has_dual_bible: + if dual_bible: + footer = u'%s (%s %s %s)' % (book, dual_version, + dual_copyright, dual_permission) + if footer not in raw_footer: + raw_footer.append(footer) + # If there is an old bible_text we have to add it. + if bible_text: raw_slides.append(bible_text) bible_text = u'' - # service item title + bible_text = u'%s %s\n\n%s %s' % (verse_text, text, + verse_text, dual_text) + raw_slides.append(bible_text) + bible_text = u'' + elif self.parent.settings_tab.layout_style == 0: + bible_text = u'%s %s' % (verse_text, text) + raw_slides.append(bible_text) + bible_text = u'' + else: + bible_text = u'%s %s %s\n' % (bible_text, verse_text, text) + # If we are 'Verse Per Slide' then create a new slide. + elif self.parent.settings_tab.layout_style == 0: + bible_text = u'%s %s' % (verse_text, text) + raw_slides.append(bible_text) + bible_text = u'' + # If we are 'Verse Per Line' then force a new line. + elif self.parent.settings_tab.layout_style == 1: + bible_text = u'%s %s %s\n' % (bible_text, verse_text, text) + # We have to be 'Continuous'. + else: + bible_text = u'%s %s %s\n' % (bible_text, verse_text, text) + old_chapter = chapter + # If there are no more items we check whether we have to add bible_text. + if bible_text: + raw_slides.append(bible_text) + bible_text = u'' + # Service Item: Capabilities + if self.parent.settings_tab.layout_style == 2 and not has_dual_bible: + # split the line but do not replace line breaks in renderer + service_item.add_capability(ItemCapabilities.NoLineBreaks) + service_item.add_capability(ItemCapabilities.AllowsPreview) + service_item.add_capability(ItemCapabilities.AllowsLoop) + service_item.add_capability(ItemCapabilities.AllowsAdditions) + # Service Item: Title if not service_item.title: if dual_bible: service_item.title = u'%s (%s, %s) %s' % (book, version, @@ -573,7 +567,7 @@ class BibleMediaItem(MediaManagerItem): translate('BiblesPlugin.MediaItem', 'etc')) == -1: service_item.title = u'%s, %s' % (service_item.title, translate('BiblesPlugin.MediaItem', 'etc')) - # item theme + # Service Item: Theme if len(self.parent.settings_tab.bible_theme) == 0: service_item.theme = None else: @@ -587,14 +581,20 @@ class BibleMediaItem(MediaManagerItem): service_item.raw_footer = raw_footer return True - def formatVerse(self, old_chapter, chapter, verse, opening, closing): - verse_text = opening - if old_chapter != chapter: - verse_text += chapter + u':' - elif not self.parent.settings_tab.show_new_chapters: - verse_text += chapter + u':' - verse_text += verse - verse_text += closing + def formatVerse(self, old_chapter, chapter, verse): + if not self.parent.settings_tab.show_new_chapters or \ + old_chapter != chapter: + verse_text = chapter + u':' + verse + else: + verse_text = verse + if self.parent.settings_tab.display_style == 1: + verse_text = u'{su}(' + verse_text + u'){/su}' + elif self.parent.settings_tab.display_style == 2: + verse_text = u'{su}{' + verse_text + u'}{/su}' + elif self.parent.settings_tab.display_style == 3: + verse_text = u'{su}[' + verse_text + u']{/su}' + else: + verse_text = u'{su}' + verse_text + u'{/su}' return verse_text def reloadBibles(self): @@ -639,14 +639,14 @@ class BibleMediaItem(MediaManagerItem): for i in range(int(range_from), int(range_to) + 1): combo.addItem(unicode(i)) - def displayResults(self, bible, dual_bible=None): + def displayResults(self, bible, dual_bible=u''): """ Displays the search results in the media manager. All data needed for further action is saved for/in each row. """ version = self.parent.manager.get_meta_data(bible, u'Version') copyright = self.parent.manager.get_meta_data(bible, u'Copyright') - #permission = self.parent.manager.get_meta_data(bible, u'Permissions') + permission = self.parent.manager.get_meta_data(bible, u'Permissions') if dual_bible: dual_version = self.parent.manager.get_meta_data(dual_bible, u'Version') @@ -654,43 +654,41 @@ class BibleMediaItem(MediaManagerItem): u'Copyright') dual_permission = self.parent.manager.get_meta_data(dual_bible, u'Permissions') - if dual_permission: - dual_permission = dual_permission.value - else: + if not dual_permission: dual_permission = u'' # We count the number of rows which are maybe already present. start_count = self.listView.count() for count, verse in enumerate(self.search_results): if dual_bible: vdict = { - 'book':QtCore.QVariant(verse.book.name), - 'chapter':QtCore.QVariant(verse.chapter), - 'verse':QtCore.QVariant(verse.verse), - 'bible':QtCore.QVariant(bible), - 'version':QtCore.QVariant(version.value), - 'copyright':QtCore.QVariant(copyright.value), - #'permission':QtCore.QVariant(permission.value), - 'text':QtCore.QVariant(verse.text), - 'dual_bible':QtCore.QVariant(dual_bible), - 'dual_version':QtCore.QVariant(dual_version.value), - 'dual_copyright':QtCore.QVariant(dual_copyright.value), - #'dual_permission':QtCore.QVariant(dual_permission), - 'dual_text':QtCore.QVariant( + 'book': QtCore.QVariant(verse.book.name), + 'chapter': QtCore.QVariant(verse.chapter), + 'verse': QtCore.QVariant(verse.verse), + 'bible': QtCore.QVariant(bible), + 'version': QtCore.QVariant(version.value), + 'copyright': QtCore.QVariant(copyright.value), + 'permission': QtCore.QVariant(permission.value), + 'text': QtCore.QVariant(verse.text), + 'dual_bible': QtCore.QVariant(dual_bible), + 'dual_version': QtCore.QVariant(dual_version.value), + 'dual_copyright': QtCore.QVariant(dual_copyright.value), + 'dual_permission': QtCore.QVariant(dual_permission.value), + 'dual_text': QtCore.QVariant( self.dual_search_results[count].text) } bible_text = u' %s %d:%d (%s, %s)' % (verse.book.name, verse.chapter, verse.verse, version.value, dual_version.value) else: vdict = { - 'book':QtCore.QVariant(verse.book.name), - 'chapter':QtCore.QVariant(verse.chapter), - 'verse':QtCore.QVariant(verse.verse), - 'bible':QtCore.QVariant(bible), - 'version':QtCore.QVariant(version.value), - 'copyright':QtCore.QVariant(copyright.value), - #'permission':QtCore.QVariant(permission.value), - 'text':QtCore.QVariant(verse.text), - 'dual_bible':QtCore.QVariant(dual_bible) + 'book': QtCore.QVariant(verse.book.name), + 'chapter': QtCore.QVariant(verse.chapter), + 'verse': QtCore.QVariant(verse.verse), + 'bible': QtCore.QVariant(bible), + 'version': QtCore.QVariant(version.value), + 'copyright': QtCore.QVariant(copyright.value), + 'permission': QtCore.QVariant(permission.value), + 'text': QtCore.QVariant(verse.text), + 'dual_bible': QtCore.QVariant(dual_bible) } bible_text = u' %s %d:%d (%s)' % (verse.book.name, verse.chapter, verse.verse, version.value) diff --git a/openlp/plugins/songs/forms/songimportform.py b/openlp/plugins/songs/forms/songimportform.py index 6fb6b38a0..f2a59ba81 100644 --- a/openlp/plugins/songs/forms/songimportform.py +++ b/openlp/plugins/songs/forms/songimportform.py @@ -83,6 +83,12 @@ class ImportWizardForm(QtGui.QWizard, Ui_SongImportWizard): QtCore.QObject.connect(self.wordsOfWorshipRemoveButton, QtCore.SIGNAL(u'clicked()'), self.onWordsOfWorshipRemoveButtonClicked) + QtCore.QObject.connect(self.ccliAddButton, + QtCore.SIGNAL(u'clicked()'), + self.onCCLIAddButtonClicked) + QtCore.QObject.connect(self.ccliRemoveButton, + QtCore.SIGNAL(u'clicked()'), + self.onCCLIRemoveButtonClicked) QtCore.QObject.connect(self.songsOfFellowshipAddButton, QtCore.SIGNAL(u'clicked()'), self.onSongsOfFellowshipAddButtonClicked) @@ -277,6 +283,16 @@ class ImportWizardForm(QtGui.QWizard, Ui_SongImportWizard): def onWordsOfWorshipRemoveButtonClicked(self): self.removeSelectedItems(self.wordsOfWorshipFileListWidget) + def onCCLIAddButtonClicked(self): + self.getFiles( + translate('SongsPlugin.ImportWizardForm', + 'Select CCLI Files'), + self.ccliFileListWidget + ) + + def onCCLIRemoveButtonClicked(self): + self.removeSelectedItems(self.ccliFileListWidget) + def onSongsOfFellowshipAddButtonClicked(self): self.getFiles( translate('SongsPlugin.ImportWizardForm', diff --git a/openlp/plugins/songs/lib/cclifileimport.py b/openlp/plugins/songs/lib/cclifileimport.py new file mode 100755 index 000000000..08bccef79 --- /dev/null +++ b/openlp/plugins/songs/lib/cclifileimport.py @@ -0,0 +1,310 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=80 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2010 Raoul Snyman # +# Portions copyright (c) 2008-2010 Tim Bentley, Jonathan Corwin, Michael # +# Gorven, Scott Guerrieri, Meinert Jordan, Andreas Preikschat, Christian # +# Richter, Philip Ridout, Maikel Stuivenberg, Martin Thompson, Jon Tibble, # +# Carsten Tinggaard, Frode Woldsund, Derek Scotney # +# --------------------------------------------------------------------------- # +# 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 # +############################################################################### + +import logging +import os +import chardet +import codecs + +from songimport import SongImport + +log = logging.getLogger(__name__) + +class CCLIFileImportError(Exception): + pass + +class CCLIFileImport(SongImport): + """ + The :class:`CCLIFileImport` class provides OpenLP with the + ability to import CCLI SongSelect song files in both .txt and + .usr formats. See http://www.ccli.com + """ + + def __init__(self, manager, **kwargs): + """ + Initialise the import. + + ``manager`` + The song manager for the running OpenLP installation. + ``filenames`` + The files to be imported. + """ + SongImport.__init__(self, manager) + if u'filenames' in kwargs: + self.filenames = kwargs[u'filenames'] + log.debug(self.filenames) + else: + raise KeyError(u'Keyword argument "filenames" not supplied.') + + def do_import(self): + """ + Import either a .usr or a .txt SongSelect file + """ + log.debug(u'Starting CCLI File Import') + song_total = len(self.filenames) + self.import_wizard.importProgressBar.setMaximum(song_total) + song_count = 1 + for filename in self.filenames: + self.import_wizard.incrementProgressBar( + u'Importing song %s of %s' % (song_count, song_total)) + filename = unicode(filename) + log.debug(u'Importing CCLI File: %s', filename) + lines = [] + if os.path.isfile(filename): + detect_file = open(filename, u'r') + details = chardet.detect(detect_file.read(2048)) + detect_file.close() + infile = codecs.open(filename, u'r', details['encoding']) + lines = infile.readlines() + ext = os.path.splitext(filename)[1] + if ext.lower() == ".usr": + log.info(u'SongSelect .usr format file found %s: ' , filename) + self.do_import_usr_file(lines) + elif ext.lower() == ".txt": + log.info(u'SongSelect .txt format file found %s: ', filename) + self.do_import_txt_file(lines) + else: + log.info(u'Extension %s is not valid', filename) + pass + song_count += 1 + if self.stop_import_flag: + return False + return True + + def do_import_usr_file(self, textList): + """ + The :method:`do_import_usr_file` method provides OpenLP + with the ability to import CCLI SongSelect songs in + *USR* file format + + ``textList`` + An array of strings containing the usr file content. + + **SongSelect .usr file format** + ``[File]`` + USR file format first line + ``Type=`` + Indicates the file type + e.g. *Type=SongSelect Import File* + ``Version=3.0`` + File format version + ``[S A2672885]`` + Contains the CCLI Song number e.g. *2672885* + ``Title=`` + Contains the song title (e.g. *Title=Above All*) + ``Author=`` + Contains a | delimited list of the song authors + e.g. *Author=LeBlanc, Lenny | Baloche, Paul* + ``Copyright=`` + Contains a | delimited list of the song copyrights + e.g. Copyright=1999 Integrity's Hosanna! Music | + LenSongs Publishing (Verwaltet von Gerth Medien + Musikverlag) + ``Admin=`` + Contains the song administrator + e.g. *Admin=Gerth Medien Musikverlag* + ``Themes=`` + Contains a /t delimited list of the song themes + e.g. *Themes=Cross/tKingship/tMajesty/tRedeemer* + ``Keys=`` + Contains the keys in which the music is played?? + e.g. *Keys=A* + ``Fields=`` + Contains a list of the songs fields in order /t delimited + e.g. *Fields=Vers 1/tVers 2/tChorus 1/tAndere 1* + ``Words=`` + Contains the songs various lyrics in order as shown by the + *Fields* description + e.g. *Words=Above all powers....* [/n = CR, /n/t = CRLF] + """ + log.debug(u'USR file text: %s', textList) + lyrics = [] + self.set_defaults() + for line in textList: + if line.startswith(u'Title='): + song_name = line[6:].strip() + elif line.startswith(u'Author='): + song_author = line[7:].strip() + elif line.startswith(u'Copyright='): + song_copyright = line[10:].strip() + elif line.startswith(u'[S A'): + song_ccli = line[4:-3].strip() + elif line.startswith(u'Fields='): + #Fields contain single line indicating verse, chorus, etc, + #/t delimited, same as with words field. store seperately + #and process at end. + song_fields = line[7:].strip() + elif line.startswith(u'Words='): + song_words = line[6:].strip() + #Unhandled usr keywords:Type,Version,Admin,Themes,Keys + #Process Fields and words sections + field_list = song_fields.split(u'/t') + words_list = song_words.split(u'/t') + for counter in range(0, len(field_list)): + if field_list[counter].startswith(u'Ver'): + verse_type = u'V' + elif field_list[counter].startswith(u'Ch'): + verse_type = u'C' + elif field_list[counter].startswith(u'Br'): + verse_type = u'B' + else: #Other + verse_type = u'O' + verse_text = unicode(words_list[counter]) + verse_text = verse_text.replace("/n", "\n") + if len(verse_text) > 0: + self.add_verse(verse_text, verse_type); + #Handle multiple authors + author_list = song_author.split(u'/') + if len(author_list) < 2: + author_list = song_author.split(u'|') + for author in author_list: + seperated = author.split(u',') + self.add_author(seperated[1].strip() + " " + seperated[0].strip()) + self.title = song_name + self.copyright = song_copyright + self.ccli_number = song_ccli + self.finish() + + def do_import_txt_file(self, textList): + """ + The :method:`do_import_txt_file` method provides OpenLP + with the ability to import CCLI SongSelect songs in + *TXT* file format + + ``textList`` + An array of strings containing the txt file content. + + **SongSelect .txt file format** + + ``Song Title`` + Contains the song title + + + + ``Title of following verse/chorus and number`` + e.g. Verse 1, Chorus 1 + + ``Verse/Chorus lyrics`` + + + + + + ``Title of next verse/chorus (repeats)`` + + ``Verse/Chorus lyrics`` + + + + + + ``Song CCLI Number`` + e.g. CCLI Number (e.g.CCLI-Liednummer: 2672885) + ``Song Copyright`` + e.g. © 1999 Integrity's Hosanna! Music | LenSongs Publishing + ``Song Authors`` + e.g. Lenny LeBlanc | Paul Baloche + ``Licencing info`` + e.g. For use solely with the SongSelect Terms of Use. + All rights Reserved. www.ccli.com + ``CCLI Licence number of user`` + e.g. CCL-Liedlizenznummer: 14 / CCLI License No. 14 + """ + log.debug(u'TXT file text: %s', textList) + self.set_defaults() + line_number = 0 + verse_text = u'' + song_comments = u'' + song_copyright = u''; + verse_start = False + for line in textList: + clean_line = line.strip() + if not clean_line: + if line_number==0: + continue + elif verse_start: + if verse_text: + self.add_verse(verse_text, verse_type) + verse_text = '' + verse_start = False + else: + #line_number=0, song title + if line_number==0: + song_name = clean_line + line_number += 1 + #line_number=1, verses + elif line_number==1: + #line_number=1, ccli number, first line after verses + if clean_line.startswith(u'CCLI'): + line_number += 1 + ccli_parts = clean_line.split(' ') + song_ccli = ccli_parts[len(ccli_parts)-1] + elif not verse_start: + # We have the verse descriptor + verse_desc_parts = clean_line.split(' ') + if len(verse_desc_parts) == 2: + if verse_desc_parts[0].startswith(u'Ver'): + verse_type = u'V' + elif verse_desc_parts[0].startswith(u'Ch'): + verse_type = u'C' + elif verse_desc_parts[0].startswith(u'Br'): + verse_type = u'B' + else: + verse_type = u'O' + verse_number = verse_desc_parts[1] + else: + verse_type = u'O' + verse_number = 1 + verse_start = True + else: + # We have verse content or the start of the + # last part. Add l so as to keep the CRLF + verse_text = verse_text + line + else: + #line_number=2, copyright + if line_number==2: + line_number += 1 + song_copyright = clean_line + #n=3, authors + elif line_number==3: + line_number += 1 + song_author = clean_line + #line_number=4, comments lines before last line + elif (line_number==4) and (not clean_line.startswith(u'CCL')): + song_comments = song_comments + clean_line + # split on known separators + author_list = song_author.split(u'/') + if len(author_list) < 2: + author_list = song_author.split(u'|') + #Clean spaces before and after author names + for author_name in author_list: + self.add_author(author_name.strip()) + self.title = song_name + self.copyright = song_copyright + self.ccli_number = song_ccli + self.comments = song_comments + self.finish() + diff --git a/openlp/plugins/songs/lib/importer.py b/openlp/plugins/songs/lib/importer.py index ae18f4389..5801ea44a 100644 --- a/openlp/plugins/songs/lib/importer.py +++ b/openlp/plugins/songs/lib/importer.py @@ -29,6 +29,7 @@ from olpimport import OpenLPSongImport try: from sofimport import SofImport from oooimport import OooImport + from cclifileimport import CCLIFileImport from wowimport import WowImport except ImportError: pass @@ -68,6 +69,8 @@ class SongFormat(object): return WowImport elif format == SongFormat.Generic: return OooImport + elif format == SongFormat.CCLI: + return CCLIFileImport # else: return None diff --git a/openlp/plugins/songs/lib/songimport.py b/openlp/plugins/songs/lib/songimport.py index dd83c679d..bf5079c8c 100644 --- a/openlp/plugins/songs/lib/songimport.py +++ b/openlp/plugins/songs/lib/songimport.py @@ -43,12 +43,6 @@ class SongImport(QtCore.QObject): whether the authors etc already exist and add them or refer to them as necessary """ - - COPYRIGHT_STRING = unicode(translate( - 'SongsPlugin.SongImport', 'copyright')) - COPYRIGHT_SYMBOL = unicode(translate( - 'SongsPlugin.SongImport', '\xa9')) - def __init__(self, manager): """ Initialise and create defaults for properties @@ -58,11 +52,11 @@ class SongImport(QtCore.QObject): """ self.manager = manager self.stop_import_flag = False + self.set_defaults() QtCore.QObject.connect(Receiver.get_receiver(), QtCore.SIGNAL(u'songs_stop_import'), self.stop_import) - self.setDefaults() - - def setDefaults(self): + + def set_defaults(self): self.title = u'' self.song_number = u'' self.alternate_title = u'' @@ -78,6 +72,10 @@ class SongImport(QtCore.QObject): self.verses = [] self.versecount = 0 self.choruscount = 0 + self.copyright_string = unicode(translate( + 'SongsPlugin.SongImport', 'copyright')) + self.copyright_symbol = unicode(translate( + 'SongsPlugin.SongImport', '\xa9')) def stop_import(self): """ @@ -163,8 +161,7 @@ class SongImport(QtCore.QObject): def parse_author(self, text): """ Add the author. OpenLP stores them individually so split by 'and', '&' - and comma. - However need to check for 'Mr and Mrs Smith' and turn it to + and comma. However need to check for 'Mr and Mrs Smith' and turn it to 'Mr Smith' and 'Mrs Smith'. """ for author in text.split(u','): @@ -241,7 +238,7 @@ class SongImport(QtCore.QObject): """ All fields have been set to this song. Write it away """ - if len(self.authors) == 0: + if not self.authors: self.authors.append(u'Author unknown') self.commit_song()