diff --git a/openlp/core/lib/__init__.py b/openlp/core/lib/__init__.py index b76179c2c..1f911491f 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'', \ @@ -316,6 +316,7 @@ def expand_tags(text): text = text.replace(tag[u'end tag'], tag[u'end html']) return text +from spelltextedit import SpellTextEdit from eventreceiver import Receiver from settingsmanager import SettingsManager from plugin import PluginStatus, Plugin @@ -324,7 +325,8 @@ from settingstab import SettingsTab from serviceitem import ServiceItem from serviceitem import ServiceItemType from serviceitem import ItemCapabilities -from htmlbuilder import build_html +from htmlbuilder import build_html, build_lyrics_format_css, \ + build_lyrics_outline_css from toolbar import OpenLPToolbar from dockwidget import OpenLPDockWidget from theme import ThemeLevel, ThemeXML diff --git a/openlp/core/lib/htmlbuilder.py b/openlp/core/lib/htmlbuilder.py index 00468608a..a39c3dac5 100644 --- a/openlp/core/lib/htmlbuilder.py +++ b/openlp/core/lib/htmlbuilder.py @@ -24,8 +24,13 @@ # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### +import logging +from PyQt4 import QtWebKit + from openlp.core.lib import image_to_byte +log = logging.getLogger(__name__) + HTMLSRC = u""" @@ -37,9 +42,9 @@ HTMLSRC = u""" border: 0; } body { - background-color: black; + %s; } -.dim { +.size { position: absolute; left: 0px; top: 0px; @@ -51,6 +56,9 @@ body { background-color: black; display: none; } +#image { + z-index:1; +} #video { z-index:2; } @@ -136,8 +144,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 +168,7 @@ body { return 0; } if(position == ''){ - position = window.getComputedStyle(text, '').verticalAlign; + position = getComputedStyle(text, '').verticalAlign; } switch(position) { @@ -181,116 +193,67 @@ 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 - -
- +
+ """ -def build_html(item, screen, alert): +def build_html(item, screen, alert, islive): """ Build the full web paged structure for display @@ -300,92 +263,245 @@ def build_html(item, screen, alert): Current display information `alert` Alert display display information + `islive` + Item is going live, rather than preview/theme building """ width = screen[u'size'].width() height = screen[u'size'].height() theme = item.themedata + webkitvers = webkit_version() if item.bg_frame: image = u'data:image/png;base64,%s' % image_to_byte(item.bg_frame) else: image = u'' - html = HTMLSRC % (width, height, - build_alert(alert, width), - build_footer(item), - build_lyrics(item), - u'true' if theme and theme.display_slideTransition \ + html = HTMLSRC % (build_background_css(item, width, height), + width, height, + 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 webkit_version(): """ - Build the video display div + Return the Webkit version in use. + Note method added relatively recently, so return 0 if prior to this + """ + try: + webkitvers = float(QtWebKit.qWebKitVersion()) + log.debug(u'Webkit version = %s' % webkitvers) + except AttributeError: + webkitvers = 0 + return webkitvers + +def build_background_css(item, width, height): + """ + Build the background css `item` Service Item containing theme and location information + + """ + width = int(width) / 2 + theme = item.themedata + background = u'background-color: black' + if theme: + if theme.background_type == u'solid': + background = u'background-color: %s' % theme.background_color + else: + if theme.background_direction == u'horizontal': + background = \ + u'background: ' \ + u'-webkit-gradient(linear, left top, left bottom, ' \ + 'from(%s), to(%s))' % (theme.background_startColor, + theme.background_endColor) + elif theme.background_direction == u'vertical': + background = \ + u'background: -webkit-gradient(linear, left top, ' \ + u'right top, from(%s), to(%s))' % \ + (theme.background_startColor, theme.background_endColor) + else: + background = \ + u'background: -webkit-gradient(radial, %s 50%%, 100, %s ' \ + u'50%%, %s, from(%s), to(%s))' % (width, width, width, + theme.background_startColor, theme.background_endColor) + return background + +def build_lyrics_css(item, webkitvers): + """ + Build the lyrics 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: %spx; 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;' - elif theme.display_horizontalAlign == 1: - align = u'text-align:right;' + lyrics = build_lyrics_format_css(theme, item.main.width(), + item.main.height()) + # 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 webkitvers >= 533.3: + lyricsmain += build_lyrics_outline_css(theme) else: - align = u'text-align:left;' - if theme.display_verticalAlign == 2: - valign = u'vertical-align:bottom;' - elif theme.display_verticalAlign == 1: - valign = u'vertical-align:middle;' - else: - valign = u'vertical-align:top;' - lyrics = u'%s %s' % (align, valign) - if theme.display_outline: - lyricscommon += u' letter-spacing: 1px;' - outline = u'-webkit-text-stroke: %sem %s; ' % \ - (float(theme.display_outline_size) / 16, - theme.display_outline_color) - if theme.display_shadow: - shadow = u'-webkit-text-stroke: %sem %s; ' \ - u'-webkit-text-fill-color: %s; ' % \ - (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 + outline = build_lyrics_outline_css(theme) + if theme.display_shadow: + if theme.display_outline and webkitvers < 534.3: + shadow = u'padding-left: %spx; padding-top: %spx ' % \ + (theme.display_shadow_size, theme.display_shadow_size) + shadow += build_lyrics_outline_css(theme, True) + else: + 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_outline_css(theme, is_shadow=False): + """ + Build the css which controls the theme outline + Also used by renderer for splitting verses -def build_footer(item): + `theme` + Object containing theme information + + `is_shadow` + If true, use the shadow colors instead + """ + if theme.display_outline: + size = float(theme.display_outline_size) / 16 + if is_shadow: + fill_color = theme.display_shadow_color + outline_color = theme.display_shadow_color + else: + fill_color = theme.font_main_color + outline_color = theme.display_outline_color + return u' -webkit-text-stroke: %sem %s; ' \ + u'-webkit-text-fill-color: %s; ' % (size, outline_color, fill_color) + else: + return u'' + +def build_lyrics_format_css(theme, width, height): + """ + Build the css which controls the theme format + Also used by renderer for splitting verses + + `theme` + Object containing theme information + + `width` + Width of the lyrics block + + `height` + Height of the lyrics block + + """ + if theme.display_horizontalAlign == 2: + align = u'center' + elif theme.display_horizontalAlign == 1: + align = u'right' + else: + align = u'left' + if theme.display_verticalAlign == 2: + valign = u'bottom' + elif theme.display_verticalAlign == 1: + valign = u'middle' + else: + valign = u'top' + lyrics = u'white-space:pre-wrap; word-wrap: break-word; ' \ + 'text-align: %s; vertical-align: %s; font-family: %s; ' \ + 'font-size: %spt; color: %s; line-height: %d%%; ' \ + 'margin:0; padding:0; width: %spx; height: %spx; ' % \ + (align, valign, theme.font_main_name, theme.font_main_proportion, + theme.font_main_color, 100 + int(theme.font_main_line_adjustment), + width, height) + if theme.display_outline: + if webkit_version() < 534.3: + lyrics += u' letter-spacing: 1px;' + if theme.font_main_italics: + lyrics += u' font-style:italic; ' + if theme.font_main_weight == u'Bold': + lyrics += u' font-weight:bold; ' + return lyrics + +def build_lyrics_html(item, webkitvers): + """ + Build the HTML required to show the lyrics + + `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 @@ -398,7 +514,7 @@ def build_footer(item): width: %spx; height: %spx; font-family: %s; - font-size: %spx; + font-size: %spt; color: %s; text-align: %s; """ @@ -416,7 +532,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,10 +540,10 @@ 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: %spx; + font-size: %spt; color: %s; background-color: %s; """ diff --git a/openlp/core/lib/renderer.py b/openlp/core/lib/renderer.py index 433018c23..330c434c5 100644 --- a/openlp/core/lib/renderer.py +++ b/openlp/core/lib/renderer.py @@ -29,9 +29,10 @@ format it for the output display. """ import logging -from PyQt4 import QtGui, QtCore +from PyQt4 import QtGui, QtCore, QtWebKit -from openlp.core.lib import resize_image, expand_tags +from openlp.core.lib import resize_image, expand_tags, \ + build_lyrics_format_css, build_lyrics_outline_css log = logging.getLogger(__name__) @@ -47,26 +48,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. @@ -155,42 +143,35 @@ class Renderer(object): for verse in verses_text: lines = verse.split(u'\n') for line in lines: - text.append(line) - doc = QtGui.QTextDocument() - doc.setPageSize(QtCore.QSizeF(self._rect.width(), self._rect.height())) - df = doc.defaultFont() - df.setPixelSize(self._theme.font_main_proportion) - df.setFamily(self._theme.font_main_name) - main_weight = 50 - if self._theme.font_main_weight == u'Bold': - main_weight = 75 - df.setWeight(main_weight) - doc.setDefaultFont(df) - layout = doc.documentLayout() + text.append(line) + web = QtWebKit.QWebView() + web.resize(self._rect.width(), self._rect.height()) + web.setVisible(False) + frame = web.page().mainFrame() + # Adjust width and height to account for shadow. outline done in css + width = self._rect.width() - int(self._theme.display_shadow_size) + height = self._rect.height() - int(self._theme.display_shadow_size) + shell = u'' \ + u'
' % \ + (build_lyrics_format_css(self._theme, width, height), + build_lyrics_outline_css(self._theme)) formatted = [] - if self._theme.font_main_weight == u'Bold' and \ - self._theme.font_main_italics: - shell = u'{p}{st}{it}%s{/it}{/st}{/p}' - elif self._theme.font_main_weight == u'Bold' and \ - not self._theme.font_main_italics: - shell = u'{p}{st}%s{/st}{/p}' - elif self._theme.font_main_italics: - shell = u'{p}{it}%s{/it}{/p}' - else: - shell = u'{p}%s{/p}' - temp_text = u'' - old_html_text = u'' + html_text = u'' + styled_text = u'' + js_height = 'document.getElementById("main").scrollHeight' for line in text: - # mark line ends - temp_text = temp_text + line + line_end - html_text = shell % expand_tags(temp_text) - doc.setHtml(html_text) - # Text too long so gone to next mage - if layout.pageCount() != 1: - formatted.append(shell % old_html_text) - temp_text = line - old_html_text = temp_text - formatted.append(shell % old_html_text) + styled_line = expand_tags(line) + line_end + styled_text += styled_line + html = shell + styled_text + u'
' + web.setHtml(html) + # Text too long so go to next page + text_height = int(frame.evaluateJavaScript(js_height).toString()) + if text_height > height: + formatted.append(html_text) + html_text = u'' + styled_text = styled_line + html_text += line + line_end + formatted.append(html_text) log.debug(u'format_slide - End') return formatted @@ -203,44 +184,47 @@ class Renderer(object): self.bg_frame = QtGui.QImage(self.frame.width(), self.frame.height(), QtGui.QImage.Format_ARGB32_Premultiplied) log.debug(u'render background %s start', self._theme.background_type) - painter = QtGui.QPainter() - painter.begin(self.bg_frame) if self._theme.background_type == u'solid': - painter.fillRect(self.frame.rect(), - QtGui.QColor(self._theme.background_color)) + self.bg_frame = None +# painter.fillRect(self.frame.rect(), +# QtGui.QColor(self._theme.background_color)) elif self._theme.background_type == u'gradient': + self.bg_frame = None # gradient - gradient = None - if self._theme.background_direction == u'horizontal': - w = int(self.frame.width()) / 2 - # vertical - gradient = QtGui.QLinearGradient(w, 0, w, self.frame.height()) - elif self._theme.background_direction == u'vertical': - h = int(self.frame.height()) / 2 - # Horizontal - gradient = QtGui.QLinearGradient(0, h, self.frame.width(), h) - else: - w = int(self.frame.width()) / 2 - h = int(self.frame.height()) / 2 - # Circular - gradient = QtGui.QRadialGradient(w, h, w) - gradient.setColorAt(0, - QtGui.QColor(self._theme.background_startColor)) - gradient.setColorAt(1, - QtGui.QColor(self._theme.background_endColor)) - painter.setBrush(QtGui.QBrush(gradient)) - rect_path = QtGui.QPainterPath() - max_x = self.frame.width() - max_y = self.frame.height() - rect_path.moveTo(0, 0) - rect_path.lineTo(0, max_y) - rect_path.lineTo(max_x, max_y) - rect_path.lineTo(max_x, 0) - rect_path.closeSubpath() - painter.drawPath(rect_path) +# gradient = None +# if self._theme.background_direction == u'horizontal': +# w = int(self.frame.width()) / 2 +# # vertical +# gradient = QtGui.QLinearGradient(w, 0, w, self.frame.height()) +# elif self._theme.background_direction == u'vertical': +# h = int(self.frame.height()) / 2 +# # Horizontal +# gradient = QtGui.QLinearGradient(0, h, self.frame.width(), h) +# else: +# w = int(self.frame.width()) / 2 +# h = int(self.frame.height()) / 2 +# # Circular +# gradient = QtGui.QRadialGradient(w, h, w) +# gradient.setColorAt(0, +# QtGui.QColor(self._theme.background_startColor)) +# gradient.setColorAt(1, +# QtGui.QColor(self._theme.background_endColor)) +# painter.setBrush(QtGui.QBrush(gradient)) +# rect_path = QtGui.QPainterPath() +# max_x = self.frame.width() +# max_y = self.frame.height() +# rect_path.moveTo(0, 0) +# rect_path.lineTo(0, max_y) +# rect_path.lineTo(max_x, max_y) +# rect_path.lineTo(max_x, 0) +# rect_path.closeSubpath() +# painter.drawPath(rect_path) +# painter.end() elif self._theme.background_type == u'image': # image + painter = QtGui.QPainter() + painter.begin(self.bg_frame) painter.fillRect(self.frame.rect(), QtCore.Qt.black) if self.bg_image: painter.drawImage(0, 0, self.bg_image) - painter.end() + painter.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/lib/serviceitem.py b/openlp/core/lib/serviceitem.py index 134df0c42..0e8625ce7 100644 --- a/openlp/core/lib/serviceitem.py +++ b/openlp/core/lib/serviceitem.py @@ -160,9 +160,9 @@ class ServiceItem(object): self.themedata = self.render_manager.renderer._theme for slide in self._raw_frames: before = time.time() - formated = self.render_manager \ + formatted = self.render_manager \ .format_slide(slide[u'raw_slide'], line_break) - for page in formated: + for page in formatted: self._display_frames.append( {u'title': clean_tags(page), u'text': clean_tags(page.rstrip()), diff --git a/openlp/core/lib/spelltextedit.py b/openlp/core/lib/spelltextedit.py new file mode 100644 index 000000000..7d227079b --- /dev/null +++ b/openlp/core/lib/spelltextedit.py @@ -0,0 +1,155 @@ +# -*- 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 # +# --------------------------------------------------------------------------- # +# 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 re +import sys +try: + import enchant + enchant_available = True +except ImportError: + enchant_available = False + +# based on code from +# http://john.nachtimwald.com/2009/08/22/qplaintextedit-with-in-line-spell-check/ + +from PyQt4 import QtCore, QtGui +from openlp.core.lib import html_expands, translate, context_menu_action + +class SpellTextEdit(QtGui.QPlainTextEdit): + def __init__(self, *args): + QtGui.QPlainTextEdit.__init__(self, *args) + # Default dictionary based on the current locale. + if enchant_available: + self.dict = enchant.Dict() + self.highlighter = Highlighter(self.document()) + self.highlighter.setDict(self.dict) + + def mousePressEvent(self, event): + if event.button() == QtCore.Qt.RightButton: + # Rewrite the mouse event to a left button event so the cursor is + # moved to the location of the pointer. + event = QtGui.QMouseEvent(QtCore.QEvent.MouseButtonPress, + event.pos(), QtCore.Qt.LeftButton, QtCore.Qt.LeftButton, + QtCore.Qt.NoModifier) + QtGui.QPlainTextEdit.mousePressEvent(self, event) + + def contextMenuEvent(self, event): + popup_menu = self.createStandardContextMenu() + # Select the word under the cursor. + cursor = self.textCursor() + # only select text if not already selected + if not cursor.hasSelection(): + cursor.select(QtGui.QTextCursor.WordUnderCursor) + self.setTextCursor(cursor) + # Check if the selected word is misspelled and offer spelling + # suggestions if it is. + if enchant_available and self.textCursor().hasSelection(): + text = unicode(self.textCursor().selectedText()) + if not self.dict.check(text): + spell_menu = QtGui.QMenu(translate('OpenLP.SpellTextEdit', + 'Spelling Suggestions')) + for word in self.dict.suggest(text): + action = SpellAction(word, spell_menu) + action.correct.connect(self.correctWord) + spell_menu.addAction(action) + # Only add the spelling suggests to the menu if there are + # suggestions. + if len(spell_menu.actions()) != 0: + popup_menu.insertSeparator(popup_menu.actions()[0]) + popup_menu.insertMenu(popup_menu.actions()[0], spell_menu) + tag_menu = QtGui.QMenu(translate('OpenLP.SpellTextEdit', + 'Formatting Tags')) + for html in html_expands: + action = SpellAction( html[u'desc'], tag_menu) + action.correct.connect(self.htmlTag) + tag_menu.addAction(action) + popup_menu.insertSeparator(popup_menu.actions()[0]) + popup_menu.insertMenu(popup_menu.actions()[0], tag_menu) + popup_menu.exec_(event.globalPos()) + + def correctWord(self, word): + """ + Replaces the selected text with word. + """ + cursor = self.textCursor() + cursor.beginEditBlock() + cursor.removeSelectedText() + cursor.insertText(word) + cursor.endEditBlock() + + def htmlTag(self, tag): + """ + Replaces the selected text with word. + """ + for html in html_expands: + if tag == html[u'desc']: + cursor = self.textCursor() + if self.textCursor().hasSelection(): + text = cursor.selectedText() + cursor.beginEditBlock() + cursor.removeSelectedText() + cursor.insertText(html[u'start tag']) + cursor.insertText(text) + cursor.insertText(html[u'end tag']) + cursor.endEditBlock() + else: + cursor = self.textCursor() + cursor.insertText(html[u'start tag']) + cursor.insertText(html[u'end tag']) + +class Highlighter(QtGui.QSyntaxHighlighter): + + WORDS = u'(?iu)[\w\']+' + + def __init__(self, *args): + QtGui.QSyntaxHighlighter.__init__(self, *args) + self.dict = None + + def setDict(self, dict): + self.dict = dict + + def highlightBlock(self, text): + if not self.dict: + return + text = unicode(text) + format = QtGui.QTextCharFormat() + format.setUnderlineColor(QtCore.Qt.red) + format.setUnderlineStyle(QtGui.QTextCharFormat.SpellCheckUnderline) + for word_object in re.finditer(self.WORDS, text): + if not self.dict.check(word_object.group()): + self.setFormat(word_object.start(), + word_object.end() - word_object.start(), format) + +class SpellAction(QtGui.QAction): + """ + A special QAction that returns the text in a signal. + """ + correct = QtCore.pyqtSignal(unicode) + + def __init__(self, *args): + QtGui.QAction.__init__(self, *args) + self.triggered.connect(lambda x: self.correct.emit( + unicode(self.text()))) diff --git a/openlp/core/ui/__init__.py b/openlp/core/ui/__init__.py index dad79cb7b..33ba25046 100644 --- a/openlp/core/ui/__init__.py +++ b/openlp/core/ui/__init__.py @@ -27,141 +27,6 @@ The :mod:`ui` module provides the core user interface for OpenLP """ -# http://john.nachtimwald.com/2009/08/22/qplaintextedit-with-in-line-spell-check/ - -import re -import sys -try: - import enchant - enchant_available = True -except ImportError: - enchant_available = False - -from PyQt4 import QtCore, QtGui -from openlp.core.lib import html_expands, translate, context_menu_action - -class SpellTextEdit(QtGui.QPlainTextEdit): - def __init__(self, *args): - QtGui.QPlainTextEdit.__init__(self, *args) - # Default dictionary based on the current locale. - if enchant_available: - self.dict = enchant.Dict() - self.highlighter = Highlighter(self.document()) - self.highlighter.setDict(self.dict) - - def mousePressEvent(self, event): - if event.button() == QtCore.Qt.RightButton: - # Rewrite the mouse event to a left button event so the cursor is - # moved to the location of the pointer. - event = QtGui.QMouseEvent(QtCore.QEvent.MouseButtonPress, - event.pos(), QtCore.Qt.LeftButton, QtCore.Qt.LeftButton, - QtCore.Qt.NoModifier) - QtGui.QPlainTextEdit.mousePressEvent(self, event) - - def contextMenuEvent(self, event): - popup_menu = self.createStandardContextMenu() - # Select the word under the cursor. - cursor = self.textCursor() - cursor.select(QtGui.QTextCursor.WordUnderCursor) - self.setTextCursor(cursor) - # Check if the selected word is misspelled and offer spelling - # suggestions if it is. - if enchant_available and self.textCursor().hasSelection(): - text = unicode(self.textCursor().selectedText()) - if not self.dict.check(text): - spell_menu = QtGui.QMenu(translate('OpenLP.SpellTextEdit', - 'Spelling Suggestions')) - for word in self.dict.suggest(text): - action = SpellAction(word, spell_menu) - action.correct.connect(self.correctWord) - spell_menu.addAction(action) - # Only add the spelling suggests to the menu if there are - # suggestions. - if len(spell_menu.actions()) != 0: - popup_menu.insertSeparator(popup_menu.actions()[0]) - popup_menu.insertMenu(popup_menu.actions()[0], spell_menu) - tag_menu = QtGui.QMenu(translate('OpenLP.SpellTextEdit', - 'Formatting Tags')) - for html in html_expands: - action = SpellAction( html[u'desc'], tag_menu) - action.correct.connect(self.htmlTag) - tag_menu.addAction(action) - popup_menu.insertSeparator(popup_menu.actions()[0]) - popup_menu.insertMenu(popup_menu.actions()[0], tag_menu) - - popup_menu.exec_(event.globalPos()) - - def correctWord(self, word): - """ - Replaces the selected text with word. - """ - cursor = self.textCursor() - cursor.beginEditBlock() - - cursor.removeSelectedText() - cursor.insertText(word) - - cursor.endEditBlock() - - def htmlTag(self, tag): - """ - Replaces the selected text with word. - """ - for html in html_expands: - if tag == html[u'desc']: - cursor = self.textCursor() - if self.textCursor().hasSelection(): - text = cursor.selectedText() - cursor.beginEditBlock() - cursor.removeSelectedText() - cursor.insertText(html[u'start tag']) - cursor.insertText(text) - cursor.insertText(html[u'end tag']) - cursor.endEditBlock() - else: - cursor = self.textCursor() - cursor.insertText(html[u'start tag']) - cursor.insertText(html[u'end tag']) - -class Highlighter(QtGui.QSyntaxHighlighter): - - WORDS = u'(?iu)[\w\']+' - - def __init__(self, *args): - QtGui.QSyntaxHighlighter.__init__(self, *args) - - self.dict = None - - def setDict(self, dict): - self.dict = dict - - def highlightBlock(self, text): - if not self.dict: - return - - text = unicode(text) - - format = QtGui.QTextCharFormat() - format.setUnderlineColor(QtCore.Qt.red) - format.setUnderlineStyle(QtGui.QTextCharFormat.SpellCheckUnderline) - - for word_object in re.finditer(self.WORDS, text): - if not self.dict.check(word_object.group()): - self.setFormat(word_object.start(), - word_object.end() - word_object.start(), format) - -class SpellAction(QtGui.QAction): - """ - A special QAction that returns the text in a signal. - """ - correct = QtCore.pyqtSignal(unicode) - - def __init__(self, *args): - QtGui.QAction.__init__(self, *args) - - self.triggered.connect(lambda x: self.correct.emit( - unicode(self.text()))) - class HideMode(object): """ This is basically an enumeration class which specifies the mode of a Bible. diff --git a/openlp/core/ui/maindisplay.py b/openlp/core/ui/maindisplay.py index 98b7a84ec..2c9472414 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() @@ -135,7 +138,7 @@ class MainDisplay(DisplayWidget): painter_image = QtGui.QPainter() painter_image.begin(self.black) painter_image.fillRect(self.black.rect(), QtCore.Qt.black) - #Build the initial frame. + # Build the initial frame. initialFrame = QtGui.QImage( self.screens.current[u'size'].width(), self.screens.current[u'size'].height(), @@ -152,12 +155,12 @@ class MainDisplay(DisplayWidget): splash_image) serviceItem = ServiceItem() serviceItem.bg_frame = initialFrame - self.webView.setHtml(build_html(serviceItem, self.screen, \ - self.parent.alertTab)) + self.webView.setHtml(build_html(serviceItem, self.screen, + self.parent.alertTab, self.isLive)) self.initialFrame = True - self.show() # To display or not to display? if not self.screen[u'primary']: + self.show() self.primary = False else: self.primary = True @@ -297,13 +300,14 @@ class MainDisplay(DisplayWidget): Generates a preview of the image displayed. """ log.debug(u'preview for %s', self.isLive) - # Wait for the fade to finish before geting the preview. - # Important otherwise preview will have incorrect text if at all ! - if self.serviceItem.themedata and \ - self.serviceItem.themedata.display_slideTransition: - while self.frame.evaluateJavaScript(u'show_text_complete()') \ - .toString() == u'false': - Receiver.send_message(u'openlp_process_events') + if self.isLive: + # Wait for the fade to finish before geting the preview. + # Important otherwise preview will have incorrect text if at all ! + if self.serviceItem.themedata and \ + self.serviceItem.themedata.display_slideTransition: + while self.frame.evaluateJavaScript(u'show_text_complete()') \ + .toString() == u'false': + Receiver.send_message(u'openlp_process_events') # Wait for the webview to update before geting the preview. # Important otherwise first preview will miss the background ! while not self.loaded: @@ -318,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): @@ -332,7 +333,8 @@ class MainDisplay(DisplayWidget): self.loaded = False self.initialFrame = False self.serviceItem = serviceItem - html = build_html(self.serviceItem, self.screen, self.parent.alertTab) + html = build_html(self.serviceItem, self.screen, self.parent.alertTab,\ + self.isLive) self.webView.setHtml(html) if serviceItem.foot_text and serviceItem.foot_text: self.footer(serviceItem.foot_text) 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/core/ui/thememanager.py b/openlp/core/ui/thememanager.py index 5cd989a2a..11130293c 100644 --- a/openlp/core/ui/thememanager.py +++ b/openlp/core/ui/thememanager.py @@ -295,7 +295,7 @@ class ThemeManager(QtGui.QWidget): path = unicode(path) if path: SettingsManager.set_last_dir(self.settingsSection, path, 1) - themePath = os.path.join(path, theme + u'.thz') + themePath = os.path.join(path, theme + u'.otz') zip = None try: zip = zipfile.ZipFile(themePath, u'w') 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/custom/forms/editcustomdialog.py b/openlp/plugins/custom/forms/editcustomdialog.py index d8557b5e2..84a310cb9 100644 --- a/openlp/plugins/custom/forms/editcustomdialog.py +++ b/openlp/plugins/custom/forms/editcustomdialog.py @@ -26,8 +26,7 @@ from PyQt4 import QtCore, QtGui -from openlp.core.lib import build_icon, translate -from openlp.core.ui import SpellTextEdit +from openlp.core.lib import build_icon, translate, SpellTextEdit class Ui_CustomEditDialog(object): def setupUi(self, customEditDialog): diff --git a/openlp/plugins/songs/forms/editversedialog.py b/openlp/plugins/songs/forms/editversedialog.py index 64d56322f..43dc8b96d 100644 --- a/openlp/plugins/songs/forms/editversedialog.py +++ b/openlp/plugins/songs/forms/editversedialog.py @@ -26,8 +26,7 @@ from PyQt4 import QtCore, QtGui -from openlp.core.lib import build_icon, translate -from openlp.core.ui import SpellTextEdit +from openlp.core.lib import build_icon, translate, SpellTextEdit from openlp.plugins.songs.lib import VerseType class Ui_EditVerseDialog(object): diff --git a/openlp/plugins/songs/forms/songimportform.py b/openlp/plugins/songs/forms/songimportform.py index 68bba299d..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) @@ -160,7 +166,7 @@ class ImportWizardForm(QtGui.QWizard, Ui_SongImportWizard): self.openSongAddButton.setFocus() return False elif source_format == SongFormat.WordsOfWorship: - if self.wordsOfWorshipListWidget.count() == 0: + if self.wordsOfWorshipFileListWidget.count() == 0: QtGui.QMessageBox.critical(self, translate('SongsPlugin.ImportWizardForm', 'No Words of Worship Files Selected'), @@ -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', @@ -315,6 +331,7 @@ class ImportWizardForm(QtGui.QWizard, Ui_SongImportWizard): pass def setDefaults(self): + self.restart() self.formatComboBox.setCurrentIndex(0) self.openLP2FilenameEdit.setText(u'') self.openLP1FilenameEdit.setText(u'') 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 7410a5184..5801ea44a 100644 --- a/openlp/plugins/songs/lib/importer.py +++ b/openlp/plugins/songs/lib/importer.py @@ -29,6 +29,8 @@ from olpimport import OpenLPSongImport try: from sofimport import SofImport from oooimport import OooImport + from cclifileimport import CCLIFileImport + from wowimport import WowImport except ImportError: pass @@ -63,8 +65,12 @@ class SongFormat(object): return OpenSongImport elif format == SongFormat.SongsOfFellowship: return SofImport + elif format == SongFormat.WordsOfWorship: + 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 60cdfd969..bf5079c8c 100644 --- a/openlp/plugins/songs/lib/songimport.py +++ b/openlp/plugins/songs/lib/songimport.py @@ -76,7 +76,7 @@ class SongImport(QtCore.QObject): 'SongsPlugin.SongImport', 'copyright')) self.copyright_symbol = unicode(translate( 'SongsPlugin.SongImport', '\xa9')) - + def stop_import(self): """ Sets the flag for importers to stop their import @@ -129,13 +129,13 @@ class SongImport(QtCore.QObject): def process_verse_text(self, text): lines = text.split(u'\n') - if text.lower().find(self.copyright_string) >= 0 \ - or text.lower().find(self.copyright_symbol) >= 0: + if text.lower().find(COPYRIGHT_STRING) >= 0 \ + or text.lower().find(COPYRIGHT_SYMBOL) >= 0: copyright_found = False for line in lines: if (copyright_found or - line.lower().find(self.copyright_string) >= 0 or - line.lower().find(self.copyright_symbol) >= 0): + line.lower().find(COPYRIGHT_STRING) >= 0 or + line.lower().find(COPYRIGHT_SYMBOL) >= 0): copyright_found = True self.add_copyright(line) else: @@ -300,6 +300,7 @@ class SongImport(QtCore.QObject): topic = Topic.populate(name=topictext) song.topics.append(topic) self.manager.save_object(song) + self.setDefaults() def print_song(self): """ diff --git a/openlp/plugins/songs/lib/wowimport.py b/openlp/plugins/songs/lib/wowimport.py new file mode 100644 index 000000000..dd09034d1 --- /dev/null +++ b/openlp/plugins/songs/lib/wowimport.py @@ -0,0 +1,173 @@ +# -*- 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 # +# --------------------------------------------------------------------------- # +# 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:`wowimport` module provides the functionality for importing Words of +Worship songs into the OpenLP database. +""" +import os +import logging + +from openlp.plugins.songs.lib.songimport import SongImport + +BLOCK_TYPES = (u'V', u'C', u'B') + +log = logging.getLogger(__name__) + +class WowImport(SongImport): + """ + The :class:`WowImport` class provides the ability to import song files from + Words of Worship. + + Words Of Worship Song File Format + ````````````````````````````````` + + The Words Of Worship song file format is as follows: + + * The song title is the file name minus the extension. + * The song has a header, a number of blocks, followed by footer containing + the author and the copyright. + * A block can be a verse, chorus or bridge. + + File Header: + Bytes are counted from one, i.e. the first byte is byte 1. These bytes, + up to the 56 byte, can change but no real meaning has been found. The + 56th byte specifies how many blocks there are. The first block starts + with byte 83 after the "CSongDoc::CBlock" declaration. + + Blocks: + Each block has a starting header, some lines of text, and an ending + footer. Each block starts with 4 bytes, the first byte specifies how + many lines are in that block, the next three bytes are null bytes. + + Each block ends with 4 bytes, the first of which defines what type of + block it is, and the rest which are null bytes: + + * ``NUL`` (\x00) - Verse + * ``SOH`` (\x01) - Chorus + * ``STX`` (\x02) - Bridge + + Blocks are seperated by two bytes. The first byte is ``SOH`` (\x01), + and the second byte is ``€`` (\x80). + + Lines: + Each line starts with a byte which specifies how long that line is, + the line text, and ends with a null byte. + + + Footer: + The footer follows on after the last block, the first byte specifies + the length of the author text, followed by the author text, if + this byte is null, then there is no author text. The byte after the + author text specifies the length of the copyright text, followed + by the copyright text. + + The file is ended with four null bytes. + + Valid extensions for a Words of Worship song file are: + + * .wsg + * .wow-song + """ + + def __init__(self, master_manager, **kwargs): + """ + Initialise the import. + + ``master_manager`` + The song manager for the running OpenLP installation. + """ + SongImport.__init__(self, master_manager) + self.master_manager = master_manager + if kwargs.has_key(u'filename'): + self.import_source = kwargs[u'filename'] + if kwargs.has_key(u'filenames'): + self.import_source = kwargs[u'filenames'] + log.debug(self.import_source) + + def do_import(self): + """ + Recieve a single file, or a list of files to import. + """ + + if isinstance(self.import_source, list): + self.import_wizard.importProgressBar.setMaximum( + len(self.import_source)) + for file in self.import_source: + # TODO: check that it is a valid words of worship file (could + # check header for WoW File Song Word) + self.author = u'' + self.copyright= u'' + # Get the song title + self.file_name = os.path.split(file)[1] + self.import_wizard.incrementProgressBar( + "Importing %s" % (self.file_name), 0) + self.title = self.file_name.rpartition(u'.')[0] + self.songData = open(file, 'rb') + # Seek to byte which stores number of blocks in the song + self.songData.seek(56) + self.no_of_blocks = ord(self.songData.read(1)) + # Seek to the beging of the first block + self.songData.seek(82) + for block in range(self.no_of_blocks): + self.lines_to_read = ord(self.songData.read(1)) + # Skip 3 nulls to the beginnig of the 1st line + self.songData.seek(3, os.SEEK_CUR) + self.block_text = u'' + while self.lines_to_read: + self.length_of_line = ord(self.songData.read(1)) + self.line_text = unicode( + self.songData.read(self.length_of_line), u'cp1252') + self.songData.seek(1, os.SEEK_CUR) + if self.block_text != u'': + self.block_text += u'\n' + self.block_text += self.line_text + self.lines_to_read -= 1 + self.block_type = BLOCK_TYPES[ord(self.songData.read(1))] + # Skip 3 nulls at the end of the block + self.songData.seek(3, os.SEEK_CUR) + # Blocks are seperated by 2 bytes, skip them, but not if + # this is the last block! + if (block + 1) < self.no_of_blocks: + self.songData.seek(2, os.SEEK_CUR) + self.add_verse(self.block_text, self.block_type) + # Now to extact the author + self.author_length = ord(self.songData.read(1)) + if self.author_length != 0: + self.author = unicode( + self.songData.read(self.author_length), u'cp1252') + # Finally the copyright + self.copyright_length = ord(self.songData.read(1)) + if self.copyright_length != 0: + self.copyright = unicode( + self.songData.read(self.copyright_length), u'cp1252') + self.parse_author(self.author) + self.add_copyright(self.copyright) + self.songData.close() + self.finish() + self.import_wizard.incrementProgressBar( + "Importing %s" % (self.file_name)) + return True +