← Back to team overview

openlp-core team mailing list archive

[Merge] lp:~marmyshev/openlp/transitions into lp:openlp

 

Dmitriy Marmyshev has proposed merging lp:~marmyshev/openlp/transitions into lp:openlp.

Requested reviews:
  OpenLP Core (openlp-core)
Related bugs:
  Bug #763067 in OpenLP: "Transition is not used when "blanking to ...""
  https://bugs.launchpad.net/openlp/+bug/763067
  Bug #1080604 in OpenLP: "Transitions works not smoothly"
  https://bugs.launchpad.net/openlp/+bug/1080604

For more details, see:
https://code.launchpad.net/~marmyshev/openlp/transitions/+merge/256730

New transition system. New effects, added transitions for background, images and blanking.
no tests.
just to review all changes.
-- 
Your team OpenLP Core is requested to review the proposed merge of lp:~marmyshev/openlp/transitions into lp:openlp.
=== modified file 'openlp/core/common/settings.py'
--- openlp/core/common/settings.py	2015-02-21 13:08:56 +0000
+++ openlp/core/common/settings.py	2015-04-18 08:51:32 +0000
@@ -296,6 +296,7 @@
         'themes/last directory export': '',
         'themes/last directory import': '',
         'themes/theme level': ThemeLevel.Song,
+        'themes/background transitions': '',
         'themes/wrap footer': False,
         'user interface/live panel': True,
         'user interface/live splitter geometry': QtCore.QByteArray(),

=== modified file 'openlp/core/lib/__init__.py'
--- openlp/core/lib/__init__.py	2015-04-02 09:04:56 +0000
+++ openlp/core/lib/__init__.py	2015-04-18 08:51:32 +0000
@@ -323,7 +323,8 @@
 from .pluginmanager import PluginManager
 from .settingstab import SettingsTab
 from .serviceitem import ServiceItem, ServiceItemType, ItemCapabilities
-from .htmlbuilder import build_html, build_lyrics_format_css, build_lyrics_outline_css
+from .htmlbuilder import build_html_display, build_lyrics_format_css, build_lyrics_outline_css, build_html_text, \
+    build_html_image, build_html_background, build_html_footer
 from .toolbar import OpenLPToolbar
 from .dockwidget import OpenLPDockWidget
 from .imagemanager import ImageManager
@@ -332,3 +333,4 @@
 from .projector.db import ProjectorDB, Projector
 from .projector.pjlink1 import PJLink1
 from .projector.constants import PJLINK_PORT, ERROR_MSG, ERROR_STRING
+from .transitions import TransitionType

=== modified file 'openlp/core/lib/htmlbuilder.py'
--- openlp/core/lib/htmlbuilder.py	2015-01-18 13:39:21 +0000
+++ openlp/core/lib/htmlbuilder.py	2015-04-18 08:51:32 +0000
@@ -9,12 +9,12 @@
 # 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                          #
@@ -409,9 +409,7 @@
     overflow: hidden;
     -webkit-user-select: none;
 }
-body {
-    %s;
-}
+body {}
 .size {
     position: absolute;
     left: 0px;
@@ -420,9 +418,7 @@
     height: 100%%;
 }
 #black {
-    z-index: 8;
     background-color: black;
-    display: none;
 }
 #bgimage {
     z-index: 1;
@@ -430,125 +426,363 @@
 #image {
     z-index: 2;
 }
-%s
+/* %css_additions% */
 #footer {
-    position: absolute;
     z-index: 6;
-    %s
 }
-/* lyric css */
-%s
 sup {
     font-size: 0.6em;
     vertical-align: top;
     position: relative;
     top: -0.3em;
 }
+
+#screen{
+    left: 0px;
+    top: 0px;
+    height: %height%px;
+    width: %width%px;
+    overflow:hidden;
+    position:relative;
+    margin: 0;
+    padding: 0;
+    border: 0;
+}
+.background_frame{
+    z-index: 1;
+    height: %height%px;
+    width: %width%px;
+    position:absolute;
+    overflow:hidden;
+    margin:0;
+    padding:0;
+    border:0;
+}
+.frame{
+    z-index: 8;
+    height: %height%px;
+    width: %width%px;
+    position:absolute;
+    overflow:hidden;
+    margin: 0;
+    padding: 0;
+    border: 0;
+}
 </style>
 <script>
-    var timer = null;
-    var transition = %s;
-    %s
-
-    function show_image(src){
-        var img = document.getElementById('image');
-        img.src = src;
-        if(src == '')
-            img.style.display = 'none';
-        else
-            img.style.display = 'block';
-    }
-
-    function show_blank(state){
-        var black = 'none';
-        var lyrics = '';
-        switch(state){
-            case 'theme':
-                lyrics = 'hidden';
-                break;
-            case 'black':
-                black = 'block';
-                break;
-            case 'desktop':
-                break;
-        }
-        document.getElementById('black').style.display = black;
-        document.getElementById('lyricsmain').style.visibility = lyrics;
-        document.getElementById('image').style.visibility = lyrics;
-        document.getElementById('footer').style.visibility = lyrics;
-    }
-
-    function show_footer(footertext){
-        document.getElementById('footer').innerHTML = footertext;
-    }
-
-    function show_text(new_text){
-        var match = /-webkit-text-fill-color:[^;\"]+/gi;
-        if(timer != null)
-            clearTimeout(timer);
-        /*
-        QtWebkit bug with outlines and justify causing outline alignment
-        problems. (Bug 859950) Surround each word with a <span> to workaround,
-        but only in this scenario.
-        */
-        var txt = document.getElementById('lyricsmain');
-        if(window.getComputedStyle(txt).textAlign == 'justify'){
-            if(window.getComputedStyle(txt).webkitTextStrokeWidth != '0px'){
-                new_text = new_text.replace(/(\s|&nbsp;)+(?![^<]*>)/g,
-                    function(match) {
-                        return '</span>' + match + '<span>';
-                    });
-                new_text = '<span>' + new_text + '</span>';
-            }
-        }
-        text_fade('lyricsmain', new_text);
-    }
-
-    function text_fade(id, new_text){
-        /*
-        Show the text.
-        */
-        var text = document.getElementById(id);
-        if(text == null) return;
-        if(!transition){
-            text.innerHTML = new_text;
-            return;
-        }
-        // Fade text out. 0.1 to minimize the time "nothing" is shown on the screen.
-        text.style.opacity = '0.1';
-        // Fade new text in after the old text has finished fading out.
-        timer = window.setTimeout(function(){_show_text(text, new_text)}, 400);
-    }
-
-    function _show_text(text, new_text) {
-        /*
-        Helper function to show the new_text delayed.
-        */
-        text.innerHTML = new_text;
-        text.style.opacity = '1';
-        // Wait until the text is completely visible. We want to save the timer id, to be able to call
-        // clearTimeout(timer) when the text has changed before finishing fading.
-        timer = window.setTimeout(function(){timer = null;}, 400);
+    var current_frame_id = 'frame1';
+    var next_frame_id = 'frame2';
+    var current_bg_frame_id = 'bg_frame1';
+    var next_bg_frame_id = 'bg_frame2';
+    var width = %width%;
+    var height = %height%;
+    var frame_timer = null;
+    var bg_timer = null;
+    var transition = 'None';
+    var bg_transition = 'None';
+    %js_additions%
+
+
+    function get_next_frame_id(background){
+        if(background == undefined) background = false;
+        if(background){
+            return next_bg_frame_id;
+        }else{
+            return next_frame_id;
+        }
+    }
+
+    function get_transition(background){
+        if(background == undefined) background = false;
+        if(background){
+            return bg_transition;
+        }else{
+            return transition;
+        }
+    }
+
+    function set_transition(value, background){
+        if(value == undefined) value = 'None';
+        if(background == undefined) background = false;
+        if(background){
+            bg_transition = value;
+        }else{
+            transition = value;
+        }
+    }
+
+    function show_next(background){
+        /*
+        Show next frame of content or background
+        */
+        var current_frame = document.getElementById(current_frame_id);
+        var next_frame = document.getElementById(next_frame_id);
+        var animation_effect = transition;
+        if(background == undefined) background = false;
+        if(background){
+            current_frame = document.getElementById(current_bg_frame_id);
+            next_frame = document.getElementById(next_bg_frame_id);
+            animation_effect = bg_transition;
+        }
+        if(frame_timer != null && !background)
+            clearTimeout(frame_timer);
+        if(bg_timer != null && background)
+            clearTimeout(bg_timer);
+        current_frame.style.opacity = 1;
+        next_frame.style.opacity = 0;
+        switch(animation_effect){
+            case 'FadeOutFadeIn':
+                animation_fadeoutin(background, current_frame, next_frame, 100);
+                change_frames(background);
+                break;
+            case 'FadeIn':
+                animation_fadein(background, current_frame, next_frame, 100);
+                change_frames(background);
+                break;
+            case 'CrossFade':
+                animation_crossfade(background, current_frame, next_frame, 100);
+                change_frames(background);
+                break;
+            case 'MoveLeft':
+                animation_move(background, current_frame, next_frame, 1, 'left');
+                change_frames(background);
+                break;
+            case 'MoveRight':
+                animation_move(background, current_frame, next_frame, 1, 'right');
+                change_frames(background);
+                break;
+            case 'MoveUp':
+                animation_move(background, current_frame, next_frame, 1, 'top');
+                change_frames(background);
+                break;
+            case 'MoveDown':
+                animation_move(background, current_frame, next_frame, 1, 'bottom');
+                change_frames(background);
+                break;
+            default:
+                no_animation(current_frame, next_frame);
+                change_frames(background);
+        }
+    }
+
+    function change_frames(background){
+        if(background){
+            var temp = current_bg_frame_id;
+            current_bg_frame_id = next_bg_frame_id;
+            next_bg_frame_id = temp;
+        }else{
+            var temp = current_frame_id;
+            current_frame_id = next_frame_id;
+            next_frame_id = temp;
+        }
+    }
+
+    function no_animation(current_frame, next_frame){
+        current_frame.style.opacity = 0;
+        next_frame.style.opacity = 1;
+    }
+
+    function animation_fadeoutin(background, current_frame, next_frame, delay){
+        /*
+        Animation at first fade out current frame, then fade in next frame
+        */
+        if(parseFloat(current_frame.style.opacity) > 0.2){
+            current_frame.style.opacity = parseFloat(current_frame.style.opacity) - 0.1;
+            if(background){
+                bg_timer = window.setTimeout(function(){animation_fadeoutin(background, current_frame, next_frame, delay)}, delay);;
+            }else{
+                frame_timer = window.setTimeout(function(){animation_fadeoutin(background, current_frame, next_frame, delay)}, delay);;
+            }
+            return;
+        }else if(parseFloat(next_frame.style.opacity) <= 1){
+            next_frame.style.opacity = parseFloat(next_frame.style.opacity) + 0.1;
+            if(background){
+                bg_timer = window.setTimeout(function(){animation_fadeoutin(background, current_frame, next_frame, delay)}, delay);;
+            }else{
+                frame_timer = window.setTimeout(function(){animation_fadeoutin(background, current_frame, next_frame, delay)}, delay);;
+            }
+            return;
+        }
+        current_frame.style.opacity = 0;
+        if(background){
+            bg_timer = null;
+        }else{
+            frame_timer = null;
+        }
+    }
+
+    function animation_fadein(background, current_frame, next_frame, delay){
+        /*
+        Animation fade in next frame, and then hide current
+        */
+        if(parseFloat(next_frame.style.opacity) <= 1){
+            next_frame.style.opacity = parseFloat(next_frame.style.opacity) + 0.1;
+            if(background){
+                bg_timer = window.setTimeout(function(){animation_fadein(background, current_frame, next_frame, delay)}, delay);;
+            }else{
+                frame_timer = window.setTimeout(function(){animation_fadein(background, current_frame, next_frame, delay)}, delay);;
+            }
+            return;
+        }
+        current_frame.style.opacity = '0';
+        if(background){
+            bg_timer = null;
+        }else{
+            frame_timer = null;
+        }
+    }
+
+
+    function animation_crossfade(background, current_frame, next_frame, delay){
+        /*
+        Animation at first fade out current frame, then fade in next frame
+        */
+        var current_opacity = parseFloat(current_frame.style.opacity);
+        var next_opacity = parseFloat(next_frame.style.opacity);
+        if(next_opacity < 1 || current_opacity >= 0.2){
+            if(next_opacity > 0.5){
+                current_frame.style.opacity = current_opacity - 0.1;
+            }
+            if(next_opacity < 1){
+                next_frame.style.opacity = next_opacity + 0.1;
+            }
+            if(background){
+                bg_timer = window.setTimeout(function(){animation_crossfade(background, current_frame, next_frame, delay)}, delay);
+            }else{
+                frame_timer = window.setTimeout(function(){animation_crossfade(background, current_frame, next_frame, delay)}, delay);
+            }
+
+            return;
+        }
+        current_frame.style.opacity = 0;
+        if(background){
+            bg_timer = null;
+        }else{
+            frame_timer = null;
+        }
+    }
+
+    function animation_move(background, current_frame, next_frame, delay, direction){
+        /*
+        Animation move current frame and next frame from right to left
+        */
+        var speed = 1;
+        var move = 0;
+        switch(direction){
+            case 'left':
+                speed = parseInt(width/10);
+                if(current_frame.style.left == '' || parseInt(current_frame.style.left) == 0){
+                    next_frame.style.left = width + 'px';
+                    current_frame.style.left = '0px';
+                }
+                move = parseInt(next_frame.style.left) - speed;
+                break;
+            case 'right':
+                speed = parseInt(width/10);
+                if(current_frame.style.right == '' || parseInt(current_frame.style.right) == 0){
+                    next_frame.style.right = width + 'px';
+                    current_frame.style.right = '0px';
+                }
+                move = parseInt(next_frame.style.right) - speed;
+                break;
+            case 'top':
+                speed = parseInt(height/10);
+                if(current_frame.style.top == '' || parseInt(current_frame.style.top) == 0){
+                    next_frame.style.top = height + 'px';
+                    current_frame.style.top = '0px';
+                }
+                move = parseInt(next_frame.style.top) - speed;
+                break;
+            case 'bottom':
+                speed = parseInt(height/10);
+                if(current_frame.style.bottom == '' || parseInt(current_frame.style.bottom) == 0){
+                    next_frame.style.bottom = height + 'px';
+                    current_frame.style.bottom = '0px';
+                }
+                move = parseInt(next_frame.style.bottom) - speed;
+                break;
+        }
+        next_frame.style.opacity = '1';
+        current_frame.style.opacity = '1';
+
+        if(move < 0){
+            move = 0;
+        }
+        switch(direction){
+            case 'left':
+                next_frame.style.left = move + 'px';
+                current_frame.style.left = parseInt(current_frame.style.left) - speed + 'px';
+                break;
+            case 'right':
+                next_frame.style.right = move + 'px';
+                current_frame.style.right = parseInt(current_frame.style.right) - speed + 'px';
+                break;
+            case 'top':
+                next_frame.style.top = move + 'px';
+                current_frame.style.top = parseInt(current_frame.style.top) - speed + 'px';
+                break;
+            case 'bottom':
+                next_frame.style.bottom = move + 'px';
+                current_frame.style.bottom = parseInt(current_frame.style.bottom) - speed + 'px';
+                break;
+        }
+        if(move > 0){
+            if(background){
+                bg_timer = window.setTimeout(function(){animation_move(background, current_frame, next_frame, delay, direction)}, delay);
+            }else{
+                frame_timer = window.setTimeout(function(){animation_move(background, current_frame, next_frame, delay, direction)}, delay);
+            }
+            return;
+        }
+        if(background){
+            bg_timer = null;
+        }else{
+            frame_timer = null;
+        }
+        current_frame.style.opacity = '0';
+        current_frame.style.left = '';
+        current_frame.style.right = '';
+        current_frame.style.top = '';
+        current_frame.style.bottom = '';
+        next_frame.style.left = '';
+        next_frame.style.right = '';
+        next_frame.style.top = '';
+        next_frame.style.bottom = '';
     }
 
     function show_text_completed(){
-        return (timer == null);
+        return (frame_timer == null && bg_timer == null);
     }
+
 </script>
+
+
 </head>
 <body>
-<img id="bgimage" class="size" %s />
-<img id="image" class="size" %s />
-%s
-<div class="lyricstable"><div id="lyricsmain" style="opacity:1" class="lyricscell lyricsmain"></div></div>
-<div id="footer" class="footer"></div>
-<div id="black" class="size"></div>
+
+<div id="screen">
+<div class="background_frame" id="bg_frame1" style="opacity: 1;"></div>
+<div class="background_frame" id="bg_frame2" style="opacity: 0;"></div>
+<div id="footer"></div>
+<div class="frame" id="frame1" style="opacity: 1;"></div>
+<div class="frame" id="frame2" style="opacity: 0;"></div>
+</div>
+
 </body>
 </html>
 """
 
-
-def build_html(item, screen, is_live, background, image=None, plugins=None):
+HTML_BACKGROUND_SRC = """<div class="size" style="%background_css%;">%bg_content%</div>"""
+
+HTML_FOOTER_SRC = """<div style="%footer_css%;">%text%</div>"""
+
+HTML_LYRICS_SRC = """<div class="lyricstable" style="%lyricstable_css%;"><div id="lyricsmain"
+style="opacity:1;%lyricsmain_css%;" class="lyricscell lyricsmain">%slide%</div></div>
+"""
+
+HTML_IMAGE_SRC = """<img id="image" class="size" %image_src% />"""
+
+def build_html_display(item, screen, is_live, background, image=None, plugins=None):
     """
     Build the full web paged structure for display
 
@@ -561,18 +795,6 @@
     """
     width = screen['size'].width()
     height = screen['size'].height()
-    theme_data = item.theme_data
-    # Image generated and poked in
-    if background:
-        bgimage_src = 'src="data:image/png;base64,%s"' % background
-    elif item.bg_image_bytes:
-        bgimage_src = 'src="data:image/png;base64,%s"' % item.bg_image_bytes
-    else:
-        bgimage_src = 'style="display:none;"'
-    if image:
-        image_src = 'src="data:image/png;base64,%s"' % image
-    else:
-        image_src = 'style="display:none;"'
     css_additions = ''
     js_additions = ''
     html_additions = ''
@@ -581,17 +803,69 @@
             css_additions += plugin.get_display_css()
             js_additions += plugin.get_display_javascript()
             html_additions += plugin.get_display_html()
-    html = HTMLSRC % (
-        build_background_css(item, width),
-        css_additions,
-        build_footer_css(item, height),
-        build_lyrics_css(item),
-        'true' if theme_data and theme_data.display_slide_transition and is_live else 'false',
-        js_additions,
-        bgimage_src,
-        image_src,
-        html_additions
-    )
+    html = HTMLSRC
+    html = html.replace('%width%', str(width))
+    html = html.replace('%height%', str(height))
+    html = html.replace('%css_additions%', css_additions)
+    html = html.replace('%js_additions%', js_additions)
+    html = html.replace('%html_additions%', html_additions)
+    return html
+
+
+def build_html_background(item, screen, background, image=None):
+    """
+    Build the content for text frame
+
+    :param item: Service Item to be displayed
+    :param screen: Current display information
+    """
+    width = screen['size'].width()
+    html = HTML_BACKGROUND_SRC
+    if background:
+        bg_content = build_html_image(background)
+    elif item.bg_image_bytes:
+        bg_content = build_html_image(item.bg_image_bytes)
+    else:
+        bg_content = ''
+    if image:
+        bg_content = build_html_image(image)
+    html = html.replace('%bg_content%', bg_content)
+    html = html.replace('%background_css%', build_background_css(item, width))
+    return html
+
+
+def build_html_footer(item, screen, text):
+    """
+    Build the content for text frame
+
+    :param slide: html content of slide
+    """
+    height = screen['size'].height()
+    html = HTML_FOOTER_SRC.replace('%text%', text)
+    html = html.replace('%footer_css%', build_footer_css(item, height))
+    return html
+
+
+def build_html_text(item, slide):
+    """
+    Build the content for text frame
+
+    :param slide: html content of slide
+    """
+    css = build_lyrics_css(item)
+    html = HTML_LYRICS_SRC.replace('%slide%', slide)
+    html = html.replace('%lyricstable_css%', css['lyricstable'])
+    html = html.replace('%lyricsmain_css%', css['lyricsmain'])
+    return html
+
+
+def build_html_image(image):
+    """
+    Build the content for text frame
+
+    :param slide: html content of slide
+    """
+    html = HTML_IMAGE_SRC.replace('%image_src%', 'src="data:image/png;base64,%s" ' % image)
     return html
 
 
@@ -625,19 +899,19 @@
         else:
             if theme.background_direction == BackgroundGradientType.to_string(BackgroundGradientType.Horizontal):
                 background = 'background: -webkit-gradient(linear, left top, left bottom, from(%s), to(%s)) fixed' \
-                    % (theme.background_start_color, theme.background_end_color)
+                             % (theme.background_start_color, theme.background_end_color)
             elif theme.background_direction == BackgroundGradientType.to_string(BackgroundGradientType.LeftTop):
                 background = 'background: -webkit-gradient(linear, left top, right bottom, from(%s), to(%s)) fixed' \
-                    % (theme.background_start_color, theme.background_end_color)
+                             % (theme.background_start_color, theme.background_end_color)
             elif theme.background_direction == BackgroundGradientType.to_string(BackgroundGradientType.LeftBottom):
                 background = 'background: -webkit-gradient(linear, left bottom, right top, from(%s), to(%s)) fixed' \
-                    % (theme.background_start_color, theme.background_end_color)
+                             % (theme.background_start_color, theme.background_end_color)
             elif theme.background_direction == BackgroundGradientType.to_string(BackgroundGradientType.Vertical):
                 background = 'background: -webkit-gradient(linear, left top, right top, from(%s), to(%s)) fixed' % \
-                    (theme.background_start_color, theme.background_end_color)
+                             (theme.background_start_color, theme.background_end_color)
             else:
-                background = 'background: -webkit-gradient(radial, %s 50%%, 100, %s 50%%, %s, from(%s), to(%s)) fixed'\
-                    % (width, width, width, theme.background_start_color, theme.background_end_color)
+                background = 'background: -webkit-gradient(radial, %s 50%%, 100, %s 50%%, %s, from(%s), to(%s)) fixed' \
+                             % (width, width, width, theme.background_start_color, theme.background_end_color)
     return background
 
 
@@ -647,36 +921,21 @@
 
     :param item: Service Item containing theme and location information
     """
-    style = """
-.lyricstable {
-    z-index: 5;
-    position: absolute;
-    display: table;
-    %s
-}
-.lyricscell {
-    display: table-cell;
-    word-wrap: break-word;
-    -webkit-transition: opacity 0.4s ease;
-    %s
-}
-.lyricsmain {
-    %s
-}
-"""
+    if hasattr(item, 'css'):
+        return item.css
     theme_data = item.theme_data
-    lyricstable = ''
-    lyrics = ''
-    lyricsmain = ''
+    lyricstable_css = 'z-index: 5; position: absolute; display: table;'
+    lyricsmain_css = 'display: table-cell; word-wrap: break-word; -webkit-transition: opacity 0.4s ease;'
     if theme_data and item.main:
-        lyricstable = 'left: %spx; top: %spx;' % (item.main.x(), item.main.y())
-        lyrics = build_lyrics_format_css(theme_data, item.main.width(), item.main.height())
-        lyricsmain += build_lyrics_outline_css(theme_data)
+        lyricstable_css += 'left: %spx; top: %spx;' % (item.main.x(), item.main.y())
+        lyricsmain_css += build_lyrics_format_css(theme_data, item.main.width(), item.main.height())
+        lyricsmain_css += build_lyrics_outline_css(theme_data)
         if theme_data.font_main_shadow:
-            lyricsmain += ' text-shadow: %s %spx %spx;' % \
-                (theme_data.font_main_shadow_color, theme_data.font_main_shadow_size, theme_data.font_main_shadow_size)
-    lyrics_css = style % (lyricstable, lyrics, lyricsmain)
-    return lyrics_css
+            lyricsmain_css += ' text-shadow: %s %spx %spx;' % \
+                          (theme_data.font_main_shadow_color, theme_data.font_main_shadow_size,
+                           theme_data.font_main_shadow_size)
+    item.css = {'lyricstable': lyricstable_css, 'lyricsmain': lyricsmain_css}
+    return item.css
 
 
 def build_lyrics_outline_css(theme_data):
@@ -719,9 +978,9 @@
              'text-align: %s; vertical-align: %s; font-family: %s; ' \
              'font-size: %spt; color: %s; line-height: %d%%; margin: 0;' \
              'padding: 0; padding-bottom: %s; padding-left: %spx; width: %spx; height: %spx; ' % \
-        (justify, align, valign, theme_data.font_main_name, theme_data.font_main_size,
-         theme_data.font_main_color, 100 + int(theme_data.font_main_line_adjustment), padding_bottom,
-         left_margin, width, height)
+             (justify, align, valign, theme_data.font_main_name, theme_data.font_main_size,
+              theme_data.font_main_color, 100 + int(theme_data.font_main_line_adjustment), padding_bottom,
+              left_margin, width, height)
     if theme_data.font_main_italics:
         lyrics += 'font-style:italic; '
     if theme_data.font_main_bold:
@@ -737,6 +996,8 @@
     :param height:
     """
     style = """
+    position: absolute;
+    z-index: 6;
     left: %spx;
     bottom: %spx;
     width: %spx;
@@ -748,7 +1009,7 @@
     """
     theme = item.theme_data
     if not theme or not item.footer:
-        return ''
+        return 'position: absolute;'
     bottom = height - int(item.footer.y()) - int(item.footer.height())
     whitespace = 'normal' if Settings().value('themes/wrap footer') else 'nowrap'
     lyrics_html = style % (item.footer.x(), bottom, item.footer.width(),

=== modified file 'openlp/core/lib/renderer.py'
--- openlp/core/lib/renderer.py	2015-01-18 13:39:21 +0000
+++ openlp/core/lib/renderer.py	2015-04-18 08:51:32 +0000
@@ -213,7 +213,7 @@
         service_item.footer = footer
         service_item.render(True)
         if not self.force_page:
-            self.display.build_html(service_item)
+            self.display.show_background(service_item)
             raw_html = service_item.get_rendered_frame(0)
             self.display.text(raw_html, False)
             preview = self.display.preview()

=== modified file 'openlp/core/lib/theme.py'
--- openlp/core/lib/theme.py	2015-01-18 13:39:21 +0000
+++ openlp/core/lib/theme.py	2015-04-18 08:51:32 +0000
@@ -140,7 +140,7 @@
     Names = ['top', 'middle', 'bottom']
 
 
-BOOLEAN_LIST = ['bold', 'italics', 'override', 'outline', 'shadow', 'slide_transition']
+BOOLEAN_LIST = ['bold', 'italics', 'override', 'outline', 'shadow']
 
 INTEGER_LIST = ['size', 'line_adjustment', 'x', 'height', 'y', 'width', 'shadow_size', 'outline_size',
                 'horizontal_align', 'vertical_align', 'wrap_style']

=== added file 'openlp/core/lib/transitions.py'
--- openlp/core/lib/transitions.py	1970-01-01 00:00:00 +0000
+++ openlp/core/lib/transitions.py	2015-04-18 08:51:32 +0000
@@ -0,0 +1,94 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2015 OpenLP Developers                                   #
+# --------------------------------------------------------------------------- #
+# 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:`transitions` module provides management functionality for a transitions
+of text on main display.
+"""
+
+import logging
+
+
+log = logging.getLogger(__name__)
+
+
+class TransitionType(object):
+    """
+    Type enumeration for transitions on main display.
+    """
+    NoTransition = 0
+    FadeOutFadeIn = 1
+    CrossFade = 2
+    FadeIn = 3
+    MoveLeft = 4
+    MoveRight = 5
+    MoveUp = 6
+    MoveDown = 7
+
+    Names = ['No transition', 'Fade-out-Fade-in', 'Cross Fade', 'Fade-in', 'Move-left', 'Move-right', 'Move-Up',
+             'Move-down']
+
+    @staticmethod
+    def to_string(transition_type):
+        """
+        Return a string representation of a transition type.
+        """
+        if transition_type == TransitionType.NoTransition:
+            return 'NoTransition'
+        elif transition_type == TransitionType.FadeOutFadeIn:
+            return 'FadeOutFadeIn'
+        elif transition_type == TransitionType.CrossFade:
+            return 'CrossFade'
+        elif transition_type == TransitionType.FadeIn:
+            return 'FadeIn'
+        elif transition_type == TransitionType.MoveLeft:
+            return 'MoveLeft'
+        elif transition_type == TransitionType.MoveRight:
+            return 'MoveRight'
+        elif transition_type == TransitionType.MoveUp:
+            return 'MoveUp'
+        elif transition_type == TransitionType.MoveDown:
+            return 'MoveDown'
+
+
+    @staticmethod
+    def from_string(type_string):
+        """
+        Return a transition type for the given string.
+        """
+        if type_string == 'NoTransition' or type_string == 'False':
+            return TransitionType.NoTransition
+        elif type_string == 'FadeOutFadeIn' or type_string == 'True':
+            return TransitionType.FadeOutFadeIn
+        elif type_string == 'CrossFade':
+            return TransitionType.CrossFade
+        elif type_string == 'FadeIn':
+            return TransitionType.FadeIn
+        elif type_string == 'MoveLeft':
+            return TransitionType.MoveLeft
+        elif type_string == 'MoveRight':
+            return TransitionType.MoveRight
+        elif type_string == 'MoveUp':
+            return TransitionType.MoveUp
+        elif type_string == 'MoveDown':
+            return TransitionType.MoveDown
+        else:
+            return TransitionType.NoTransition
\ No newline at end of file

=== modified file 'openlp/core/ui/maindisplay.py'
--- openlp/core/ui/maindisplay.py	2015-04-02 20:32:20 +0000
+++ openlp/core/ui/maindisplay.py	2015-04-18 08:51:32 +0000
@@ -41,7 +41,8 @@
     PHONON_AVAILABLE = False
 
 from openlp.core.common import Registry, RegistryProperties, OpenLPMixin, Settings, translate, is_macosx
-from openlp.core.lib import ServiceItem, ImageSource, ScreenList, build_html, expand_tags, image_to_byte
+from openlp.core.lib import ServiceItem, ImageSource, ScreenList, build_html_display, build_html_text, build_html_image,\
+    build_html_background, build_html_footer, expand_tags, image_to_byte
 from openlp.core.lib.theme import BackgroundType
 from openlp.core.ui import HideMode, AlertLocation
 
@@ -141,6 +142,8 @@
         self.screens = ScreenList()
         self.rebuild_css = False
         self.hide_mode = None
+        self.current_raw_frame = None
+        self.current_background = None
         self.override = {}
         self.retranslateUi()
         self.media_object = None
@@ -218,30 +221,13 @@
         self.screen = self.screens.current
         self.setVisible(False)
         Display.setup(self)
+        service_item = ServiceItem()
+        self.web_view.setHtml(build_html_display(service_item, self.screen, self.is_live, None,
+                              plugins=self.plugin_manager.plugins))
         if self.is_live:
-            # Build the initial frame.
-            background_color = QtGui.QColor()
-            background_color.setNamedColor(Settings().value('advanced/default color'))
-            if not background_color.isValid():
-                background_color = QtCore.Qt.white
-            image_file = Settings().value('advanced/default image')
-            splash_image = QtGui.QImage(image_file)
-            self.initial_fame = QtGui.QImage(
-                self.screen['size'].width(),
-                self.screen['size'].height(),
-                QtGui.QImage.Format_ARGB32_Premultiplied)
-            painter_image = QtGui.QPainter()
-            painter_image.begin(self.initial_fame)
-            painter_image.fillRect(self.initial_fame.rect(), background_color)
-            painter_image.drawImage(
-                (self.screen['size'].width() - splash_image.width()) // 2,
-                (self.screen['size'].height() - splash_image.height()) // 2,
-                splash_image)
-            service_item = ServiceItem()
-            service_item.bg_image_bytes = image_to_byte(self.initial_fame)
-            self.web_view.setHtml(build_html(service_item, self.screen, self.is_live, None,
-                                  plugins=self.plugin_manager.plugins))
             self._hide_mouse()
+            self.set_background_transitions()
+            self.show_logoscreen()
 
     def text(self, slide, animate=True):
         """
@@ -250,19 +236,21 @@
         :param slide: The slide text to be displayed
         :param animate: Perform transitions if applicable when setting the text
         """
+        if self.current_raw_frame == slide:
+            return
+        self.current_raw_frame = slide
         # Wait for the webview to update before displaying text.
+        # FIXME: change to show_complete() JS
         while not self.web_loaded:
             self.application.process_events()
         self.setGeometry(self.screen['size'])
-        if animate:
-            self.frame.evaluateJavaScript('show_text("%s")' % slide.replace('\\', '\\\\').replace('\"', '\\\"'))
-        else:
-            # This exists for https://bugs.launchpad.net/openlp/+bug/1016843
-            # For unknown reasons if evaluateJavaScript is called
-            # from the themewizard, then it causes a crash on
-            # Windows if there are many items in the service to re-render.
-            # Setting the div elements direct seems to solve the issue
-            self.frame.findFirstElement("#lyricsmain").setInnerXml(slide)
+        html = build_html_text(self.service_item, slide)
+        next_frame_id = self.frame.evaluateJavaScript('get_next_frame_id();')
+        if not next_frame_id:
+            next_frame_id = 'frame1'
+        self.frame.findFirstElement('#'+next_frame_id).setInnerXml(html)
+        if not self.hide_mode:
+            self.frame.evaluateJavaScript('show_next();')
 
     def alert(self, text, location):
         """
@@ -306,7 +294,7 @@
             return False
         self.override['image'] = path
         self.override['theme'] = self.service_item.theme_data.background_filename
-        self.image(path)
+        self.show_background(self.service_item)
         # Update the preview frame.
         if self.is_live:
             self.live_controller.update_preview()
@@ -320,6 +308,9 @@
         :param path: The path to the image to be displayed. **Note**, the path is only passed to identify the image.
         If the image has changed it has to be re-added to the image manager.
         """
+        if self.current_raw_frame == path:
+            return
+        self.current_raw_frame = path
         image = self.image_manager.get_image_bytes(path, ImageSource.ImagePlugin)
         self.controller.media_controller.media_reset(self.controller)
         self.display_image(image)
@@ -332,10 +323,15 @@
         """
         self.setGeometry(self.screen['size'])
         if image:
-            js = 'show_image("data:image/png;base64,%s");' % image
+            html = build_html_image(image)
         else:
-            js = 'show_image("");'
-        self.frame.evaluateJavaScript(js)
+            html = ''
+        next_frame_id = self.frame.evaluateJavaScript('get_next_frame_id()')
+        if not next_frame_id:
+            next_frame_id = 'frame1'
+        self.frame.findFirstElement('#'+next_frame_id).setInnerXml(html)
+        if not self.hide_mode:
+            self.frame.evaluateJavaScript('show_next()')
 
     def reset_image(self):
         """
@@ -358,11 +354,11 @@
         was_visible = self.isVisible()
         self.application.process_events()
         # We must have a service item to preview.
-        if self.is_live and hasattr(self, 'service_item'):
+        if self.is_live and hasattr(self, 'service_item') and not self.hide_mode:
             # Wait for the fade to finish before geting the preview.
             # Important otherwise preview will have incorrect text if at all!
             if self.service_item.theme_data and self.service_item.theme_data.display_slide_transition:
-                while not self.frame.evaluateJavaScript('show_text_completed()'):
+                while not self.frame.evaluateJavaScript('show_text_completed();'):
                     self.application.process_events()
         # Wait for the webview to update before getting the preview.
         # Important otherwise first preview will miss the background !
@@ -383,14 +379,13 @@
                     self.setVisible(True)
         return QtGui.QPixmap.grabWidget(self)
 
-    def build_html(self, service_item, image_path=''):
+    def show_background(self, service_item, image_path=''):
         """
         Store the service_item and build the new HTML from it. Add the HTML to the display
 
         :param service_item: The Service item to be used
         :param image_path: Where the image resides.
         """
-        self.web_loaded = False
         self.initial_fame = None
         self.service_item = service_item
         background = None
@@ -407,8 +402,7 @@
             else:
                 # replace the background
                 background = self.image_manager.get_image_bytes(self.override['image'], ImageSource.ImagePlugin)
-        self.set_transparency(self.service_item.theme_data.background_type ==
-                              BackgroundType.to_string(BackgroundType.Transparent))
+        self.set_transparency(self.is_live)
         if self.service_item.theme_data.background_filename:
             self.service_item.bg_image_bytes = self.image_manager.get_image_bytes(
                 self.service_item.theme_data.background_filename, ImageSource.Theme)
@@ -416,9 +410,27 @@
             image_bytes = self.image_manager.get_image_bytes(image_path, ImageSource.ImagePlugin)
         else:
             image_bytes = None
-        html = build_html(self.service_item, self.screen, self.is_live, background, image_bytes,
-                          plugins=self.plugin_manager.plugins)
-        self.web_view.setHtml(html)
+        if self.current_background != image_path \
+                and self.current_background != self.service_item.theme_data.background_filename:
+            if image_path:
+                self.current_background = image_path
+            elif self.service_item.theme_data.background_filename:
+                self.current_background = self.service_item.theme_data.background_filename
+            else:
+                self.current_background = None
+            if self.service_item.theme_data.background_type == BackgroundType.to_string(BackgroundType.Transparent):
+                html = ''
+            else:
+                html = build_html_background(self.service_item, self.screen, background, image_bytes)
+            next_frame_id = self.frame.evaluateJavaScript('get_next_frame_id(true);')
+            if not next_frame_id:
+                next_frame_id = 'bg_frame1'
+            self.frame.findFirstElement('#'+next_frame_id).setInnerXml(html)
+            self.frame.evaluateJavaScript('show_next(true);')
+        # FIXME: reset self.current_raw_frame if we change theme
+        self.current_raw_frame = None
+        if self.is_live:
+            self.set_transitions(self.service_item)
         if service_item.foot_text:
             self.footer(service_item.foot_text)
         # if was hidden keep it hidden
@@ -429,14 +441,45 @@
                 self.hide_display(self.hide_mode)
         self._hide_mouse()
 
+    def set_background_transitions(self):
+        """
+        set transitions for changing background frames
+
+        """
+        transitions = Settings().value('themes/background transitions')
+        self.frame.evaluateJavaScript('set_transition(\''+transitions+'\',true);')
+
+    def set_transitions(self, item):
+        """
+        set transitions for changing screen frames
+
+        :param item: Service Item to be displayed
+        """
+        theme_data = item.theme_data
+        if self.is_live and theme_data and theme_data.display_slide_transition:
+            self.frame.evaluateJavaScript('set_transition(\''+theme_data.display_slide_transition+'\',false);')
+        else:
+            self.frame.evaluateJavaScript('set_transition(\'None\',false);')
+
+    def hide_background(self):
+        """
+        Display the Footer
+
+        :param text: footer text to be displayed
+        """
+        html = ''
+        next_frame_id = self.frame.evaluateJavaScript('get_next_frame_id(true)')
+        self.frame.findFirstElement('#'+next_frame_id).setInnerXml(html)
+        self.frame.evaluateJavaScript('show_next(true)')
+
     def footer(self, text):
         """
         Display the Footer
 
         :param text: footer text to be displayed
         """
-        js = 'show_footer(\'' + text.replace('\\', '\\\\').replace('\'', '\\\'') + '\')'
-        self.frame.evaluateJavaScript(js)
+        html = build_html_footer(self.service_item, self.screen, text)
+        self.frame.findFirstElement('#footer').setInnerXml(html)
 
     def hide_display(self, mode=HideMode.Screen):
         """
@@ -449,19 +492,71 @@
             # Only make visible if setting enabled.
             if not Settings().value('core/display on monitor'):
                 return
+        if self.hide_mode == mode:
+            return
         if mode == HideMode.Screen:
-            self.frame.evaluateJavaScript('show_blank("desktop");')
+            self.hide_content()
+            self.hide_background()
+            if hasattr(self, 'service_item'):
+                self.footer('')
+            # FIXME: add time counter to hide display in 10 sec anyway
+            while not self.frame.evaluateJavaScript('show_text_completed();'):
+                self.application.process_events()
             self.setVisible(False)
         elif mode == HideMode.Blank or self.initial_fame:
-            self.frame.evaluateJavaScript('show_blank("black");')
+            self.show_blackscreen()
         else:
-            self.frame.evaluateJavaScript('show_blank("theme");')
+            self.hide_content()
         if mode != HideMode.Screen:
             if self.isHidden():
                 self.setVisible(True)
                 self.web_view.setVisible(True)
         self.hide_mode = mode
 
+    def show_blackscreen(self):
+        """
+        Show black layer over the top of all other layers
+        """
+        html = '<div class="frame" id="black"></div>'
+        next_frame_id = self.frame.evaluateJavaScript('get_next_frame_id()')
+        self.frame.findFirstElement('#'+next_frame_id).setInnerXml(html)
+        self.frame.evaluateJavaScript('show_next()')
+
+    def show_logoscreen(self):
+        """
+        Show logo image layer over the top of all other layers
+        """
+        # Build the initial frame.
+        background_color = QtGui.QColor()
+        background_color.setNamedColor(Settings().value('advanced/default color'))
+        if not background_color.isValid():
+            background_color = QtCore.Qt.white
+        image_file = Settings().value('advanced/default image')
+        splash_image = QtGui.QImage(image_file)
+        self.initial_fame = QtGui.QImage(
+            self.screen['size'].width(),
+            self.screen['size'].height(),
+            QtGui.QImage.Format_ARGB32_Premultiplied)
+        painter_image = QtGui.QPainter()
+        painter_image.begin(self.initial_fame)
+        painter_image.fillRect(self.initial_fame.rect(), background_color)
+        painter_image.drawImage(
+            (self.screen['size'].width() - splash_image.width()) // 2,
+            (self.screen['size'].height() - splash_image.height()) // 2,
+            splash_image)
+        image = image_to_byte(self.initial_fame)
+        self.display_image(image)
+
+    def hide_content(self):
+        """
+        Hides current content on screen: text, images.
+        Background frames not hides.
+        """
+        html = ''
+        next_frame_id = self.frame.evaluateJavaScript('get_next_frame_id()')
+        self.frame.findFirstElement('#'+next_frame_id).setInnerXml(html)
+        self.frame.evaluateJavaScript('show_next()')
+
     def show_display(self):
         """
         Show the stored layers so the screen reappears as it was originally.
@@ -471,7 +566,14 @@
             # Only make visible if setting enabled.
             if not Settings().value('core/display on monitor'):
                 return
-        self.frame.evaluateJavaScript('show_blank("show");')
+        if hasattr(self, 'service_item') and self.service_item.foot_text:
+            self.footer(self.service_item.foot_text)
+        if self.hide_mode == HideMode.Screen:
+            if hasattr(self, 'service_item'):
+                self.show_background(self.service_item)
+        if self.hide_mode:
+            # FIXME: set current self.current_raw_frame to next frame
+            self.frame.evaluateJavaScript('show_next();')
         if self.isHidden():
             self.setVisible(True)
         self.hide_mode = None

=== modified file 'openlp/core/ui/media/mediacontroller.py'
--- openlp/core/ui/media/mediacontroller.py	2015-04-03 18:31:19 +0000
+++ openlp/core/ui/media/mediacontroller.py	2015-04-18 08:51:32 +0000
@@ -581,8 +581,9 @@
         else:
             self.media_volume(controller, controller.media_info.volume)
         if status:
+            #TODO: add support for bg video
             if not controller.media_info.is_background:
-                display.frame.evaluateJavaScript('show_blank("desktop");')
+                display.hide_background()
             self.current_media_players[controller.controller_type].set_visible(display, True)
             # Flash needs to be played and will not AutoPlay
             if controller.media_info.is_flash:
@@ -642,7 +643,7 @@
         log.debug('media_stop')
         display = self._define_display(controller)
         if controller.controller_type in self.current_media_players:
-            display.frame.evaluateJavaScript('show_blank("black");')
+            display.show_blackscreen()
             self.current_media_players[controller.controller_type].stop(display)
             self.current_media_players[controller.controller_type].set_visible(display, False)
             controller.seek_slider.setSliderPosition(0)

=== modified file 'openlp/core/ui/slidecontroller.py'
--- openlp/core/ui/slidecontroller.py	2015-03-18 22:02:51 +0000
+++ openlp/core/ui/slidecontroller.py	2015-04-18 08:51:32 +0000
@@ -33,7 +33,7 @@
 from openlp.core.common import Registry, RegistryProperties, Settings, SlideLimits, UiStrings, translate, \
     RegistryMixin, OpenLPMixin
 from openlp.core.lib import OpenLPToolbar, ItemCapabilities, ServiceItem, ImageSource, ServiceItemAction, \
-    ScreenList, build_icon, build_html
+    ScreenList, build_icon, build_html_display
 from openlp.core.ui import HideMode, MainDisplay, Display, DisplayControllerType
 from openlp.core.lib.ui import create_action
 from openlp.core.utils.actions import ActionList, CategoryOrder
@@ -592,7 +592,7 @@
         self.preview_widget.screen_size_changed(self.ratio)
         self.preview_display.setup()
         service_item = ServiceItem()
-        self.preview_display.web_view.setHtml(build_html(service_item, self.preview_display.screen, None, self.is_live,
+        self.preview_display.web_view.setHtml(build_html_display(service_item, self.preview_display.screen, None, self.is_live,
                                               plugins=self.plugin_manager.plugins))
         self.media_controller.setup_display(self.preview_display, True)
         if self.service_item:
@@ -883,11 +883,7 @@
                         self.image_manager.get_image_bytes(frame['path'], ImageSource.ImagePlugin)
         self.preview_widget.replace_service_item(self.service_item, width, slide_no)
         self.enable_tool_bar(service_item)
-        # Pass to display for viewing.
-        # Postpone image build, we need to do this later to avoid the theme
-        # flashing on the screen
-        if not self.service_item.is_image():
-            self.display.build_html(self.service_item)
+        self.display.show_background(self.service_item)
         if service_item.is_media():
             self.on_media_start(service_item)
         self.slide_selected(True)
@@ -1089,10 +1085,7 @@
                 if self.service_item.is_text():
                     self.display.text(to_display)
                 else:
-                    if start:
-                        self.display.build_html(self.service_item, to_display)
-                    else:
-                        self.display.image(to_display)
+                    self.display.image(to_display)
                     # reset the store used to display first image
                     self.service_item.bg_image_bytes = None
             self.selected_row = row

=== modified file 'openlp/core/ui/themeform.py'
--- openlp/core/ui/themeform.py	2015-01-18 13:39:21 +0000
+++ openlp/core/ui/themeform.py	2015-04-18 08:51:32 +0000
@@ -33,6 +33,7 @@
 from openlp.core.ui import ThemeLayoutForm
 from openlp.core.utils import get_images_filter, is_not_image_file
 from .themewizard import Ui_ThemeWizard
+from openlp.core.lib.transitions import TransitionType
 
 log = logging.getLogger(__name__)
 
@@ -88,6 +89,7 @@
         self.main_font_combo_box.activated.connect(self.calculate_lines)
         self.footer_font_combo_box.activated.connect(self.update_theme)
         self.footer_size_spin_box.valueChanged.connect(self.update_theme)
+        self.transitions_combo_box.currentIndexChanged.connect(self.on_transitions_combo_box_current_index_changed)
 
     def set_defaults(self):
         """
@@ -133,7 +135,7 @@
         self.area_position_page.registerField('footer_position_height', self.footer_height_spin_box)
         self.background_page.registerField('horizontal', self.horizontal_combo_box)
         self.background_page.registerField('vertical', self.vertical_combo_box)
-        self.background_page.registerField('slide_transition', self.transitions_check_box)
+        self.background_page.registerField('slide_transition', self.transitions_combo_box)
         self.background_page.registerField('name', self.theme_name_edit)
 
     def calculate_lines(self):
@@ -365,7 +367,7 @@
         """
         self.setField('horizontal', self.theme.display_horizontal_align)
         self.setField('vertical', self.theme.display_vertical_align)
-        self.setField('slide_transition', self.theme.display_slide_transition)
+        self.setField('slide_transition', TransitionType.from_string(self.theme.display_slide_transition))
 
     def set_preview_page_values(self):
         """
@@ -464,6 +466,14 @@
         """
         self.theme.font_footer_color = color
 
+    def on_transitions_combo_box_current_index_changed(self, index):
+        """
+        Background gradient Combo box has changed.
+        """
+        if self.update_theme_allowed:
+            self.theme.display_slide_transition = TransitionType.to_string(index)
+            self.set_alignment_page_values()
+
     def update_theme(self):
         """
         Update the theme object from the UI for fields not already updated
@@ -495,7 +505,7 @@
         # position page
         self.theme.display_horizontal_align = self.horizontal_combo_box.currentIndex()
         self.theme.display_vertical_align = self.vertical_combo_box.currentIndex()
-        self.theme.display_slide_transition = self.field('slide_transition')
+        self.theme.display_slide_transition = TransitionType.to_string(self.transitions_combo_box.currentIndex())
 
     def accept(self):
         """

=== modified file 'openlp/core/ui/themestab.py'
--- openlp/core/ui/themestab.py	2015-01-18 13:39:21 +0000
+++ openlp/core/ui/themestab.py	2015-04-18 08:51:32 +0000
@@ -29,6 +29,7 @@
 from openlp.core.common import Registry, Settings, ThemeLevel, UiStrings, translate
 from openlp.core.lib import SettingsTab
 from openlp.core.lib.ui import find_and_set_in_combo_box
+from openlp.core.lib.transitions import TransitionType
 
 
 class ThemesTab(SettingsTab):
@@ -66,6 +67,15 @@
         self.universal_group_box.setObjectName('universal_group_box')
         self.universal_group_box_layout = QtGui.QVBoxLayout(self.universal_group_box)
         self.universal_group_box_layout.setObjectName('universal_group_box_layout')
+        self.background_transitions_label = QtGui.QLabel(self.universal_group_box)
+        self.background_transitions_label.setObjectName('background_transitions_label')
+        self.universal_group_box_layout.addWidget(self.background_transitions_label)
+        self.background_transitions_combo_box = QtGui.QComboBox(self.universal_group_box)
+        self.background_transitions_combo_box.addItems(TransitionType.Names)
+        self.background_transitions_combo_box.setSizeAdjustPolicy(QtGui.QComboBox.AdjustToMinimumContentsLength)
+        self.background_transitions_combo_box.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Fixed)
+        self.background_transitions_combo_box.setObjectName('background_transitions_combo_box')
+        self.universal_group_box_layout.addWidget(self.background_transitions_combo_box)
         self.wrap_footer_check_box = QtGui.QCheckBox(self.universal_group_box)
         self.wrap_footer_check_box.setObjectName('wrap_footer_check_box')
         self.universal_group_box_layout.addWidget(self.wrap_footer_check_box)
@@ -114,6 +124,10 @@
         self.tab_title_visible = UiStrings().Themes
         self.global_group_box.setTitle(translate('OpenLP.ThemesTab', 'Global Theme'))
         self.universal_group_box.setTitle(translate('OpenLP.ThemesTab', 'Universal Settings'))
+        self.background_transitions_label.setText(translate('OpenLP.ThemesTab', 'Background Transitions:'))
+        for index in range(len(TransitionType.Names)):
+            self.background_transitions_combo_box.setItemText(index, translate('OpenLP.Transitions',
+                                                                               TransitionType.Names[index]))
         self.wrap_footer_check_box.setText(translate('OpenLP.ThemesTab', '&Wrap footer text'))
         self.level_group_box.setTitle(translate('OpenLP.ThemesTab', 'Theme Level'))
         self.song_level_radio_button.setText(translate('OpenLP.ThemesTab', 'S&ong Level'))
@@ -139,6 +153,8 @@
         settings.beginGroup(self.settings_section)
         self.theme_level = settings.value('theme level')
         self.global_theme = settings.value('global theme')
+        self.background_transitions_combo_box.setCurrentIndex(
+            TransitionType.from_string(settings.value('background transitions')))
         self.wrap_footer_check_box.setChecked(settings.value('wrap footer'))
         settings.endGroup()
         if self.theme_level == ThemeLevel.Global:
@@ -157,6 +173,8 @@
         settings.setValue('theme level', self.theme_level)
         settings.setValue('global theme', self.global_theme)
         settings.setValue('wrap footer', self.wrap_footer_check_box.isChecked())
+        settings.setValue('background transitions',
+                          TransitionType.to_string(self.background_transitions_combo_box.currentIndex()))
         settings.endGroup()
         self.renderer.set_theme_level(self.theme_level)
         if self.tab_visited:

=== modified file 'openlp/core/ui/themewizard.py'
--- openlp/core/ui/themewizard.py	2015-01-18 13:39:21 +0000
+++ openlp/core/ui/themewizard.py	2015-04-18 08:51:32 +0000
@@ -28,6 +28,7 @@
 from openlp.core.lib import build_icon, ColorButton
 from openlp.core.lib.theme import HorizontalType, BackgroundType, BackgroundGradientType
 from openlp.core.lib.ui import add_welcome_page, create_valign_selection_widgets
+from openlp.core.lib.transitions import TransitionType
 
 
 class Ui_ThemeWizard(object):
@@ -257,9 +258,10 @@
         self.alignment_layout.addRow(self.vertical_label, self.vertical_combo_box)
         self.transitions_label = QtGui.QLabel(self.alignment_page)
         self.transitions_label.setObjectName('transitions_label')
-        self.transitions_check_box = QtGui.QCheckBox(self.alignment_page)
-        self.transitions_check_box.setObjectName('transitions_check_box')
-        self.alignment_layout.addRow(self.transitions_label, self.transitions_check_box)
+        self.transitions_combo_box = QtGui.QComboBox(self.alignment_page)
+        self.transitions_combo_box.addItems(TransitionType.Names)
+        self.transitions_combo_box.setObjectName('transitions_combo_box')
+        self.alignment_layout.addRow(self.transitions_label, self.transitions_combo_box)
         self.alignment_layout.setItem(3, QtGui.QFormLayout.LabelRole, self.spacer)
         theme_wizard.addPage(self.alignment_page)
         # Area Position Page
@@ -458,6 +460,8 @@
         self.horizontal_combo_box.setItemText(HorizontalType.Center, translate('OpenLP.ThemeWizard', 'Center'))
         self.horizontal_combo_box.setItemText(HorizontalType.Justify, translate('OpenLP.ThemeWizard', 'Justify'))
         self.transitions_label.setText(translate('OpenLP.ThemeWizard', 'Transitions:'))
+        for index in range(len(TransitionType.Names)):
+            self.transitions_combo_box.setItemText(index, translate('OpenLP.Transitions', TransitionType.Names[index]))
         self.area_position_page.setTitle(translate('OpenLP.ThemeWizard', 'Output Area Locations'))
         self.area_position_page.setSubTitle(translate('OpenLP.ThemeWizard', 'Allows you to change and move the'
                                                       ' Main and Footer areas.'))

=== modified file 'tests/functional/openlp_core_lib/test_htmlbuilder.py'
--- tests/functional/openlp_core_lib/test_htmlbuilder.py	2014-07-24 21:57:16 +0000
+++ tests/functional/openlp_core_lib/test_htmlbuilder.py	2015-04-18 08:51:32 +0000
@@ -7,7 +7,7 @@
 from PyQt4 import QtCore
 
 from openlp.core.common import Settings
-from openlp.core.lib.htmlbuilder import build_html, build_background_css, build_lyrics_css, build_lyrics_outline_css, \
+from openlp.core.lib.htmlbuilder import build_html_display, build_background_css, build_lyrics_css, build_lyrics_outline_css, \
     build_lyrics_format_css, build_footer_css
 from openlp.core.lib.theme import HorizontalType, VerticalType
 from tests.functional import MagicMock, patch
@@ -240,7 +240,7 @@
             plugins = [plugin]
 
             # WHEN: Create the html.
-            html = build_html(item, screen, is_live, background, plugins=plugins)
+            html = build_html_display(item, screen, is_live, background, plugins=plugins)
 
             # THEN: The returned html should match.
             self.assertEqual(html, HTML, 'The returned html should match')


Follow ups