← Back to team overview

openlp-core team mailing list archive

Re: [Merge] lp:~kirkstover/openlp/wysiwyg into lp:openlp

 

Hi Samuel -

Thanks for your kind words!

I wrote the code myself.

I'm not sure what happened, but it appears that I pushed an earlier 
version of htmleditor (core/lib).  The custom/lib version should be the 
correct one.

Maybe you could give me an idea on how to fix it.  Would I just delete 
locally and do a commit/push?  Sorry, but I'm new to Python,
Bazaar, and source management.  Kind of sounds like a dangerous 
trifecta, huh?

Hopefully we can get this working for some future release.

Kirk

On 6/19/2014 3:43 PM, Samuel Mehrbrodt wrote:
> Review: Needs Fixing
>
> Hi Kirk,
>
> thanks for your great work and welcome aboard! It's really great to finally see a visual editor that actually works!
>
> I had a quick look at the code and added some comments in the diff.
>
> Some other comments:
> There are two files htmleditor.py. What's the difference between them? Which one is used?
> Did you write the editor yourself or is that code from somewhere else?
>
> I tested it quickly and one thing that doesn't work is using backspace to delete a character.
>
> Also you should run pep8, there are some style issues in your code.
>
> Looking forward to seeing this in a release :)
>
> Diff comments:
>
>> === modified file 'openlp/.version'
>> --- openlp/.version	2013-08-10 14:51:01 +0000
>> +++ openlp/.version	2014-06-19 13:47:40 +0000
>> @@ -1,1 +1,1 @@
>> -2.1.0-bzr2234
>> +b'2.1.0'-bzrb'2338\r\n'
> Why this change?
>
>> \ No newline at end of file
>>
>> === modified file 'openlp/core/lib/formattingtags.py'
>> --- openlp/core/lib/formattingtags.py	2014-03-17 19:05:55 +0000
>> +++ openlp/core/lib/formattingtags.py	2014-06-19 13:47:40 +0000
>> @@ -157,6 +157,11 @@
>>               'start tag': '{br}', 'start html': '<br>', 'end tag': '',
>>               'end html': '', 'protected': True,
>>               'temporary': False})
>> +        base_tags.append({
>> +            'desc': translate('OpenLP.FormatingTags', 'Image'),
>> +            'start tag': '{img}', 'start html': '<img src=',
>> +            'end tag': '{/img}', 'end html': ' />',
>> +            'protected': True, 'temporary': False})
>>           FormattingTags.add_html_tags(base_tags)
>>           FormattingTags.add_html_tags(temporary_tags)
>>           user_expands_string = str(Settings().value('formattingTags/html_tags'))
>>
>> === added file 'openlp/core/lib/htmleditor.py'
>> --- openlp/core/lib/htmleditor.py	1970-01-01 00:00:00 +0000
>> +++ openlp/core/lib/htmleditor.py	2014-06-19 13:47:40 +0000
>> @@ -0,0 +1,780 @@
>> +# -*- coding: utf-8 -*-
>> +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
>> +
>> +###############################################################################
>> +# OpenLP - Open Source Lyrics Projection                                      #
>> +# --------------------------------------------------------------------------- #
>> +# Copyright (c) 2008-2014 Raoul Snyman                                        #
>> +# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan      #
>> +# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub,      #
>> +# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer.   #
>> +# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru,          #
>> +# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith,             #
>> +# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock,              #
>> +# Frode Woldsund, Martin Zibricky, Patrick Zimmermann                         #
>> +# --------------------------------------------------------------------------- #
>> +# 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:`~openlp.core.lib.htmleditor` module provides a wysiwig editor for the editing of custom slides.
>> +The editor is implemented with QWebView using the contenteditable=true attribute.
>> +
>> +Known issues:
>> +    - Spellchecking not implemented
>> +    - Strange highlighting after editing some blocks (selecting/deselecting block removes artifacts)
>> +    - Using the scrollbar loses text input focus
>> +    - Undo
>> +"""
>> +
>> +import logging
>> +
>> +from PyQt4 import QtCore, QtGui, QtWebKit
>> +from PyQt4.QtGui import QStyle
>> +from openlp.core.lib import translate, FormattingTags
>> +#  from openlp.core.lib.ui import create_action
>> +from openlp.core.lib.htmlbuilder import build_background_css, build_lyrics_css, build_footer_css
>> +from .editimagedialog import Ui_EditImageDialog
>> +from .editimageform import EditImageForm
>> +
>> +
>> +log = logging.getLogger(__name__)
>> +
>> +
>> +class HtmlEditor(QtWebKit.QWebView):
>> +    """
>> +    A custom slide editor using QWebView in editing mode
>> +    """
>> +    def __init__(self, parent=None):
>> +#    def __init__(self, parent=None, formatting_tags_allowed=True):
>> +
>> +        """
>> +        Constructor.
>> +        """
>> +        super(HtmlEditor, self).__init__(parent)
>> +        self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
>> +#        self.formatting_tags_allowed = formatting_tags_allowed
>> +        self.setUrl(QtCore.QUrl('about:blank'))
>> +        self.zoom = 0.5                             # Default to 50%
>> +        self.background = 'Theme'                   # Default to theme background
>> +        self.scr_w = int                            # Screen width - set from the ServiceItem in set_text
>> +        self.scr_h = int                            # Screen Height - set from the ServiceItem in set_text
>> +        self.marker = '!mArKeR!'                    # Strange characters used to identify insert position
>> +        self.last_id = 1                            # The last slide id assigned
>> +        self.image_selected = None                  # The image that has focus
>> +        self.active_slide = None                    # The slide div that has focus
>> +        self.active_text = None                     # The text div that has focus
>> +        self.connect(self, QtCore.SIGNAL('selectionChanged()'), self.on_selection_changed)
>> +
>> +    def javascript(self, func, str1=None, str2=None, str3=None):
>> +        """
>> +        Format the javascript string to pass to the mainframe function "evaluateJavaScript"
>> +
>> +        :param func: A string containing the javascript function name
>> +        :param str1: Optional passed variable
>> +        :param str2: Optional passed variable
>> +        :param str3: Optional passed variable
>> +        :return result from javascript as a string
>> +        """
>> +        cmd = '%s()' % func
>> +        if str1:
>> +            cmd = '%s\'%s\')' % (cmd[:-1], str1)
>> +            if str2:
>> +                cmd = '%s, \'%s\')' % (cmd[:-1], str2)
>> +                if str3:
>> +                    cmd = '%s, \'%s\')' % (cmd[:-1], str3)
>> +        result = str(self.page().mainFrame().evaluateJavaScript(cmd))
>> +        return result
>> +
>> +    def insert_data_tag(self, tag, html):
>> +        """
>> +        Insert the data-tag into the start html string from FormattingTags
>> +
>> +        :param tag: The formatting {tag} string
>> +        :param html: The start html string
>> +        :return The start html string with data-tag="{tag}" inserted
>> +        """
>> +        tag_text = ' data-tag="%s"' % tag
>> +        if html.find(' ') > -1:
>> +            html = html.replace(' ', tag_text + ' ', 1)
>> +        else:
>> +            html = html.replace('>', tag_text + '>', 1)
>> +        return html
>> +
>> +    def text_to_html(self, text):
>> +        """
>> +        Convert special characters, and replace formatting tags with html
>> +
>> +        :param text: One or more slides in plain text with formatting tags
>> +        :return The slide text in html format wrapped in divs
>> +        """
>> +        text = text.replace(u'<', u'&lt;')
>> +        text = text.replace(u'>', u'&gt;')
>> +        self.last_id = 0
>> +        slides_html = ''
>> +        slides = text.split(u'\n[===]\n')
>> +        for slide in slides:
>> +            slide = slide.replace(u'\n', u'<br />')
>> +            for html in FormattingTags.get_html_tags():
>> +                if slide.find(html['start tag']) != -1:
>> +                    start_html = self.insert_data_tag(html['start tag'], html['start html'])
>> +                    slide = slide.replace(html['start tag'], start_html)
>> +                    if html['end tag']:
>> +                        slide = slide.replace(html['end tag'], html['end html'])
>> +            slide = self.remove_empty_tags(slide)
>> +            slides_html += self.build_slide_wrapper(slide)
>> +        return slides_html
>> +
>> +    def build_slide_wrapper(self, slide_html):
>> +        """
>> +        Create the div wrappers for a slide
>> +
>> +        :param slide_html: The slide content in html format
>> +        :return Wrapped slide content
>> +        """
>> +        self.last_id += 1
>> +        html = '<div class="slide" id="slide%d">' % self.last_id
>> +        html += '<a href="#slide%d"></a><div class="text_table">' % self.last_id
>> +        html += '<div class="lyricscell lyricsmain" id="text%d" contenteditable="true">' % self.last_id
>> +        html += '%s</div></div></div>' % slide_html
>> +        return html
>> +
>> +    def build_background_css(self, item):
>> +        """
>> +        Build the background css - use htmlbuilder unless we have an image or a transparency
>> +
>> +        :param item: ServiceItem
>> +        :return  The background css statement
>> +        """
>> +
>> +        if item.theme_data.background_type == 'image':
>> +            url_filename = QtCore.QUrl.fromLocalFile(item.theme_data.background_filename)
>> +            background = "background: %s url('%s') repeat-y" % (item.theme_data.background_border_color,
>> +                                                                url_filename.toString())
>> +        elif item.theme_data.background_type == 'transparent':
>> +            background = 'background-color: transparent'
>> +        else:
>> +            background = build_background_css(item, item.renderer.width)
>> +        return background
>> +
>> +    def set_text(self, text, item):
>> +        """
>> +        Build the html to display the custom slide in wysiwyg mode
>> +
>> +        :param text: The slide text (unicode)
>> +        :param item: ServiceItem
>> +        :return  The complete html
>> +        """
>> +
>> +        self.scr_w = item.renderer.width
>> +        self.scr_h = item.renderer.height
>> +        html = u"""
>> +<!DOCTYPE html>
>> +<html>
>> +<head>
>> +    <style>
>> +        * {
>> +            margin: 0;
>> +            padding: 0;
>> +            border: 0;
>> +        }
>> +        sup {
>> +            font-size: 0.6em;
>> +            vertical-align: top;
>> +            position: relative;
>> +            top: -0.3em;
>> +        }
>> +        .slides {
>> +            width: %dpx;
>> +            min-height: %dpx;
>> +            margin: 0px;
>> +            padding: 0px;
>> +        }
>> +        .slide {
>> +            width: %dpx;
>> +            min-height: %dpx;
>> +            %s;                             /* background or background-color */
>> +            margin: 10px;
>> +            padding: %dpx 0px 0px %dpx;
>> +            outline: black solid 1px;
>> +        }
>> +        %s                                  /* build_lyrics_css from htmlbuilder */
>> +        .lyricscell {                       /* override */
>> +            -webkit-transition: none;
>> +        }
>> +        .text_table {                       /* use this in place of lyricstable */
>> +            display: table;
>> +            background: transparent url('file:///c:/users/ace/desktop/custom_overflow.png') no-repeat;
> Absolute path here.
>
>> +            background-position: 0px %dpx;
>> +            background-size: cover;
>> +            min-height: %dpx;
>> +        }
>> +        img {
>> +            -webkit-user-select: auto !important;
>> +        }
>> +    </style>
>> +    <script type="text/javascript">
>> +        window.onload = function() {
>> +            document.getElementById("text1").focus();
>> +        }
>> +        function set_focus(id) {
>> +            /*
>> +            Set focus to element with id - QWebElement.setFocus() does NOT give text focus (blinking caret)
>> +            */
>> +            document.getElementById(id).focus();
>> +        }
>> +        function insertHTML(html) {
>> +            document.execCommand('insertHTML', false, html);
>> +        }
>> +        function get_slide_id() {
>> +            /*
>> +            Get the slide id of the slide that is being edited
>> +            */
>> +            var slide = window.getSelection().anchorNode;
>> +            while (slide) {
>> +                if (slide.className == 'slide') {
>> +                    break;
>> +                }
>> +                slide = slide.parentNode;
>> +            }
>> +            return slide.id;
>> +        }
>> +        function merge_slide(curr_slide_id) {
>> +            /*
>> +            Add the contents of the current slide to the end of the previous and delete current slide
>> +            */
>> +            var slides = document.getElementById('slides');
>> +            var curr_slide = document.getElementById(curr_slide_id);
>> +            var prev_slide = curr_slide.previousSibling;
>> +            var prev_text = prev_slide.lastChild.firstChild;
>> +            var sel = window.getSelection();
>> +            sel.modify('move', 'backward', 'documentboundary');     // select the entire slide contents
>> +            sel.modify('extend', 'forward', 'documentboundary');
>> +            var range = sel.getRangeAt(0);
>> +            var frag = range.extractContents();                     // store removed selection in a fragment
>> +            window.location.hash = '#' + prev_slide.id;             // scroll to previous slide
>> +            prev_text.focus();
>> +            sel = window.getSelection();
>> +            sel.modify('move', 'forward', 'documentboundary');      // move insertion point to end of slide
>> +            sel.getRangeAt(0).insertNode(frag);                     // paste the fragment into new slide
>> +            slides.removeChild(curr_slide);                         // delete the current slide
>> +            document.normalize();
>> +        }
>> +        function insert_slide(wrapper, id) {
>> +            /*
>> +            Split the current slide at the insertion point and create new slide element structure
>> +            */
>> +            var slides = document.getElementById('slides');         // parent div for all slides
>> +            var sel = window.getSelection();
>> +            var slide = sel.anchorNode;
>> +            while (slide) {                                         // find the current slide
>> +                if (slide.className == 'slide') {
>> +                    break;
>> +                }
>> +                slide = slide.parentNode;
>> +            }
>> +            sel.modify('extend', 'forward', 'documentboundary');    // select to the end of current slide
>> +            var range = sel.getRangeAt(0);
>> +            var frag = range.extractContents();                     // store removed selection in a fragment
>> +            var div = document.createElement('div');                // Create a temporary div
>> +            div.innerHTML = wrapper;                                // Put slide structure into div
>> +            slides.insertBefore(div.firstChild, slide.nextSibling); // Insert wrapper
>> +            window.location.hash = '#slide' + id;                   // scroll to new slide
>> +            document.getElementById('text' + id).focus();
>> +            window.getSelection().getRangeAt(0).insertNode(frag);   // paste the fragment into new slide
>> +        }
>> +        function focus_node(id) {
>> +            elem = document.getElementById(id);
>> +            range = document.createRange();
>> +            range.selectNode(elem);
>> +            sel = window.getSelection();
>> +            sel.removeAllRanges();
>> +            sel.addRange(range);
>> +        }
>> +        function insert_tag(tag_html) {
>> +            /*
>> +            Surround the current selection with the formatting tag html
>> +            */
>> +            var range = window.getSelection().getRangeAt(0);
>> +            var frag = range.createContextualFragment(tag_html);
>> +            try {
>> +                range.surroundContents(frag.firstChild);
>> +            }
>> +            catch(err) {
>> +                alert('Unable to insert tag. The selected text spans an existing tag boundary.');
>> +            }
>> +        }
>> +        function remove_tag(tag) {
>> +            /*
>> +            Search backwards from the current anchor node until the formatting {tag} is found.
>> +            Remove the tag but keep the text and all other child tags
>> +            */
>> +            var elem = window.getSelection().anchorNode;
>> +            while (elem) {
>> +                if (elem.nodeType == 1) {
>> +                    if (elem.getAttribute('data-tag') == tag) {
>> +                        var frag = document.createDocumentFragment();
>> +                        while (elem.firstChild) {
>> +                            frag.appendChild(elem.firstChild);
>> +                        }
>> +                        elem.parentNode.insertBefore(frag, elem);
>> +                        elem.parentNode.removeChild(elem);
>> +                        document.normalize();
>> +                        return;
>> +                    }
>> +                }
>> +                elem = elem.parentNode;
>> +            }
>> +            alert('Tag not found!');        // Should never happen
>> +        }
>> +        function get_path() {
>> +            /*
>> +            Find any open formatting tags before the anchor node
>> +            */
>> +            var path = '';
>> +            var attr = '';
>> +            var elem = window.getSelection().anchorNode;
>> +            while (elem) {
>> +                if (elem.nodeType == 1) {
>> +                    attr = elem.getAttribute('data-tag');
>> +                    if ((attr) && (attr != '{img}')) {
>> +                        if (path) {
>> +                            path = attr + ',' + path;
>> +                        }
>> +                        else {
>> +                            path = attr;
>> +                        }
>> +                    }
>> +                }
>> +                elem = elem.parentNode;
>> +            }
>> +            return path;
>> +        }
>> +        function get_focus_position() {
>> +            /*
>> +            Return the position of the caret in the focus node
>> +            */
>> +            return window.getSelection().focusOffset.toFixed();
>> +        }
>> +    </script>
>> +</head>
>> +<body style='width: 100%%; height: 100%%; color: %s;'>
>> +    <div id="slides" class="slides">%s</div>
>> +</body>
>> +</html>""" % (self.scr_w + 22,                  # .slides: width = screen width + border + margins
>> +              self.scr_h + 22,
>> +              self.scr_w - item.main.left(),    # .slide: width
>> +              self.scr_h - item.main.top(),     # .slide: min-height
>> +              self.build_background_css(item),  # .slide: background or background-color
>> +              item.main.top(),                  # .slide: padding-top
>> +              item.main.left(),                 # .slide: padding-left
>> +              build_lyrics_css(item),           # .lyricstable, .lyricscell, .lyricsmain from htmlbuilder
>> +              item.main.bottom(),               # .text_table: background-position (top) of overflow image
>> +              item.main.height(),               # .text_table: min-height
>> +              item.theme_data.font_main_color,  # body color used for setting caret color
>> +              self.text_to_html(text))          # slide text
>> +        self.set_zoom()
>> +        self.setHtml(html)
>> +
>> +    def get_text(self):
>> +        """
>> +        Convert all of the slides from html to plain text with formatting tags
>> +
>> +        return: A string containing all slides in plain text
>> +        """
>> +        text = ''
>> +        slides = self.page().mainFrame().documentElement().findAll('div.lyricscell.lyricsmain').toList()
>> +        for slide in slides:
>> +            if text:
>> +                text += '\n[===]\n'                         # create an insert marker for every slide after the first
>> +            while True:
>> +                elem = slide.findFirst('[data-tag]')        # find a QWebElement with a data-tag attribute
>> +                if elem.isNull():
>> +                    break
>> +                tag = elem.attribute('data-tag')            # get the tag name from the data-tag
>> +                if tag == '{img}':                          # replace <img src="filename" /> with {img}filename{/img}
>> +                    img = elem.firstChild()                 # the img element is wrapped in a span
>> +                    tag_str = '{img}src="' + img.attribute('src') + '" style="' + img.attribute('style') + '"{/img}'
>> +                    img.removeFromDocument()
>> +                else:                                       # replace <?>xxx</?> with {tag}xxx{/tag}
>> +                    tag_str = '%s%s{/%s' % (tag, elem.toInnerXml(), tag[1:])
>> +                elem.replace(tag_str)
>> +            text += slide.toPlainText()
>> +        return text
>> +
>> +    def remove_empty_tags(self, slide_html):
>> +        """
>> +        Iterate through slide html to remove empty tags, both simple and nested
>> +
>> +        :param slide_html: A string containing the slide contents in html format
>> +        :return slide contents without empty tags
>> +        """
>> +        empty_tags = []
>> +        for html in FormattingTags.get_html_tags():
>> +            if html['end tag']:
>> +                empty_tags.append(self.insert_data_tag(html['start tag'], html['start html']) + html['end html'])
>> +        empty_found = True
>> +        while empty_found:
>> +            empty_found = False
>> +            for empty_tag in empty_tags:
>> +                if slide_html.find(empty_tag) > -1:
>> +                    slide_html = slide_html.replace(empty_tag, '')
>> +                    empty_found = True
>> +                    break
>> +        return slide_html
>> +
>> +    def get_path(self):
>> +        """
>> +        Get formatting tags in path from the selection anchor node
>> +
>> +        :return str: formatting tags delimited with commas
>> +        """
>> +        path = self.javascript('get_path')
>> +        return path
>> +
>> +    def split(self):
>> +        """
>> +        Insert our marker into current selection and check resulting text to determine if it is surrounded by line
>> +        breaks.  Replace marker with [---] optional split.
>> +        """
>> +        self.javascript('insertHTML', self.marker)
>> +        html = self.active_text.toInnerXml()
>> +        text = self.active_text.toPlainText()
>> +        if text.find('\n' + self.marker) == -1:
>> +            html = html.replace(self.marker, '<br>' + self.marker)
>> +        if text.find(self.marker + '\n') == -1:
>> +            html = html.replace(self.marker, self.marker + '<br>')
>> +        html = html.replace(self.marker, '[---]')
>> +        self.active_text.setInnerXml(html)
>> +        self.javascript('set_focus', self.active_text.attribute('id'))
>> +
>> +    def insert(self):
>> +        """
>> +        Split the slide at the current selection and create a new slide.  Scroll new slide into view and sets focus.
>> +        Note:  Open formatting tags are added to the new slide when the new slide doesn't have that tag at the start
>> +        of the string.
>> +        """
>> +        path = self.javascript('get_path')
>> +        old_slide = self.active_text                                # save the current slide pointer
>> +        wrapper = self.build_slide_wrapper('')                      # create an empty slide structure
>> +        self.javascript('insert_slide', wrapper, self.last_id)      # insert new slide into document
>> +        slide_html = self.remove_empty_tags(old_slide.toInnerXml())
>> +        old_slide.setInnerXml(slide_html)
>> +        slide = self.page().mainFrame().findFirstElement('#text' + str(self.last_id))
>> +        slide_html = self.remove_empty_tags(slide.toInnerXml())
>> +        if path:                                                    # There may have been open formatting tags at
>> +            path_tags = path.split(',')                             # the point of split
>> +            new_tags_start = ''
>> +            new_tags_end = ''
>> +            tag_pos = 0
>> +            for tag in path_tags:
>> +                for html in FormattingTags.get_html_tags():
>> +                    if tag == html['start tag']:
>> +                        new_tag = self.insert_data_tag(html['start tag'], html['start html'])
>> +                        if slide_html.find(new_tag) == tag_pos:     # Tag already exists
>> +                            tag_pos += len(new_tag)                 # Adjust the position for next tag search
>> +                        else:
>> +                            new_tags_start += new_tag
>> +                            new_tags_end += html['end html']
>> +                        break
>> +            if new_tags_start:
>> +                slide_html = new_tags_start + slide_html + new_tags_end
>> +        slide.setInnerXml(slide_html)
>> +
>> +    def set_zoom(self):
>> +        if self.zoom == 0.0:    # set zoom to fill the body of the QWebView
>> +            ratio_x = round(((self.width() - QStyle.PM_ScrollBarExtent - 10) /
>> +                             (self.scr_w + 22 + QStyle.PM_ScrollBarExtent)) - 0.004, 2)
>> +            if self.zoomFactor() != ratio_x:
>> +                self.setZoomFactor(ratio_x)
>> +        else:
>> +            self.setZoomFactor(self.zoom)
>> +
>> +    def get_text_div(self, slide_div):
>> +        """
>> +        Return the text div wrapper element for a given slide with an id of 'slideXXX'
>> +
>> +        :param QWebElement slide_div: The slide div wrapper
>> +        :return QWebElement The slide text wrapper
>> +        """
>> +        text_id = '#text' + slide_div.attribute('id')[5:]    # Skip over 'slide' to get numeric id
>> +        text_div = self.page().mainFrame().findFirstElement(text_id)
>> +        return text_div
>> +
>> +    def image_clicked(self, pos_x, pos_y):
>> +        """
>> +        Determine if the mouse click was on an img element.  If so, focus the img node
>> +        :param pos_x: from event.pos().x()
>> +        :param pos_y: from event.pos().y()
>> +        :return True if clicked
>> +        """
>> +        img_list = self.page().mainFrame().findAllElements('img').toList()
>> +        rel_x = self.page().mainFrame().scrollBarValue(QtCore.Qt.Horizontal) + pos_x
>> +        rel_y = self.page().mainFrame().scrollBarValue(QtCore.Qt.Vertical) + pos_y
>> +        self.image_selected = None
>> +        for img in img_list:
>> +            if img.geometry().contains(rel_x, rel_y, True):
>> +                img.setAttribute('id', 'img1')
>> +                self.javascript('focus_node', 'img1')
>> +                img.setAttribute('id', '')
>> +                self.image_selected = img
>> +                break
>> +        return self.image_selected
>> +
>> +    def on_selection_changed(self):
>> +        """
>> +        Set the QWebElements active_slide and active_text to point to the div wrappers of the current selection anchor
>> +        """
>> +        slide_id = self.javascript('get_slide_id')
>> +        self.active_slide = self.page().mainFrame().findFirstElement('#' + slide_id)
>> +        self.active_text = self.get_text_div(self.active_slide)
>> +
>> +    def keyPressEvent(self, event):
>> +        """
>> +        If the enter key, set shift "on" to generate a <br>
>> +        """
>> +        if event.key() == QtCore.Qt.Key_Return:
>> +            event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress, event.key(), QtCore.Qt.ShiftModifier, event.text())
>> +        QtWebKit.QWebView.keyPressEvent(self, event)
>> +
>> +    def resizeEvent(self, event):
>> +        QtWebKit.QWebView.resizeEvent(self, event)
>> +        self.set_zoom()
>> +
>> +    def mousePressEvent(self, event):
>> +        """
>> +        Handle mouse clicks within the QWebView
>> +        """
>> +        if self.image_clicked(event.pos().x(), event.pos().y()):
>> +            return
>> +        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)
>> +        QtWebKit.QWebView.mousePressEvent(self, event)
>> +
>> +    def zoom_menu_item(self, menu, text, zoom):
>> +        action = MenuAction(text, menu)
>> +        action.setCheckable(True)
>> +        action.setChecked(self.zoom == zoom)
>> +        action.menu_action.connect(self.on_zoom)
>> +        menu.addAction(action)
>> +
>> +    def color_menu_item(self, menu, color):
>> +        action = MenuAction(color, menu)
>> +        action.setCheckable(True)
>> +        action.setChecked(self.background == color)
>> +        action.menu_action.connect(self.on_background)
>> +        menu.addAction(action)
>> +
>> +    def customContextMenuRequested(self, pos):
>> +        popup_menu = QtGui.QMenu(self)
>> +        if self.image_selected:
>> +            action = MenuAction('Change Image...', popup_menu)
>> +            action.menu_action.connect(self.on_change_image)
>> +            popup_menu.addAction(action)
>> +#        if self.formatting_tags_allowed:
>> +        else:
>> +            action = MenuAction('Insert Image...', popup_menu)
>> +            action.menu_action.connect(self.on_insert_image)
>> +            popup_menu.addAction(action)
>> +            if not self.selectedText():
>> +                path = self.get_path()
>> +                if path:
>> +                    remove_tag_menu = QtGui.QMenu('Remove Formatting Tag')
>> +                    for html in FormattingTags.get_html_tags():
>> +                        if path.find(html['start tag']) > -1:
>> +                            action = MenuAction(html['desc'], remove_tag_menu)
>> +                            action.menu_action.connect(self.on_remove_tag)
>> +                            remove_tag_menu.addAction(action)
>> +                    popup_menu.addMenu(remove_tag_menu)
>> +            else:
>> +                tag_menu = QtGui.QMenu('Insert Formatting Tag')
>> +                tag_system_menu = QtGui.QMenu('System')
>> +                tag_custom_menu = QtGui.QMenu('Custom')
>> +                for html in FormattingTags.get_html_tags():
>> +                    if html['start tag'] != '{img}':
>> +                        if html['protected']:
>> +                            active_menu = tag_system_menu
>> +                        else:
>> +                            active_menu = tag_custom_menu
>> +                        action = MenuAction(html['desc'], active_menu)
>> +                        action.menu_action.connect(self.on_insert_tag)
>> +                        active_menu.addAction(action)
>> +                tag_menu.addMenu(tag_system_menu)
>> +                tag_menu.addMenu(tag_custom_menu)
>> +                popup_menu.addMenu(tag_menu)
>> +        prev_slide_found = not self.active_slide.previousSibling().isNull()
>> +        next_slide_found = not self.active_slide.nextSibling().isNull()
>> +        if prev_slide_found:
>> +            action = MenuAction('Merge With Previous Slide', popup_menu)
>> +            action.menu_action.connect(self.on_merge)
>> +            popup_menu.addAction(action)
>> +            action = MenuAction('Move Slide Up', popup_menu)
>> +            action.menu_action.connect(self.on_move_up)
>> +            popup_menu.addAction(action)
>> +        if next_slide_found:
>> +            action = MenuAction('Move Slide Down', popup_menu)
>> +            action.menu_action.connect(self.on_move_down)
>> +            popup_menu.addAction(action)
>> +        if prev_slide_found or next_slide_found:
>> +            action = MenuAction('Delete Slide', popup_menu)
>> +            action.menu_action.connect(self.on_delete)
>> +            popup_menu.addAction(action)
>> +
>> +        zoom_menu = QtGui.QMenu('Scale Slide')
>> +        self.zoom_menu_item(zoom_menu, 'Fit Width', 0.0)
>> +        self.zoom_menu_item(zoom_menu, 'Actual Size', 1.0)
>> +        self.zoom_menu_item(zoom_menu, '75%', 0.75)
>> +        self.zoom_menu_item(zoom_menu, '50%', 0.50)
>> +        self.zoom_menu_item(zoom_menu, '25%', 0.25)
>> +        popup_menu.addMenu(zoom_menu)
>> +        bg_menu = QtGui.QMenu('Set Background')
>> +        self.color_menu_item(bg_menu, 'Theme')
>> +        self.color_menu_item(bg_menu, 'Black')
>> +        self.color_menu_item(bg_menu, 'White')
>> +        self.color_menu_item(bg_menu, 'Red')
>> +        self.color_menu_item(bg_menu, 'Green')
>> +        self.color_menu_item(bg_menu, 'Blue')
>> +        popup_menu.addMenu(bg_menu)
>> +
>> +        popup_menu.exec(self.mapToGlobal(pos))
>> +
>> +    def on_change_image(self):
>> +        """
>> +        Display an open file dialog and set the image source to the selected file name
>> +        """
>> +        img_name = QtCore.QUrl(self.image_selected.attribute('src'))
>> +        path_name = QtCore.QDir(img_name.toLocalFile())
>> +        file_name = QtGui.QFileDialog.getOpenFileName(self,
>> +                                                      'Select Image File',
>> +                                                      path_name.absolutePath(),
>> +                                                      'Images (*.png *.jpg)')
> Maybe more image types can be allowed here?
>
>> +        if file_name:
>> +            img_name = QtCore.QUrl(file_name)
>> +            self.image_selected.setAttribute('src', img_name.toString())
>> +
>> +    def on_insert_image(self):
>> +        file_name = QtGui.QFileDialog.getOpenFileName( self,
>> +                                                      'Select Image File',
>> +                                                      '',
>> +                                                      'Images (*.png *.jpg)')
> Same here.
>
>> +        if file_name:
>> +            img_name = QtCore.QUrl(file_name)
>> +            self.javascript('insertHTML', u'<span id="new_span" data-tag="{img}"></span>');
>> +            elem = self.page().mainFrame().findFirstElement('#new_span')
>> +            if elem.isNull():
>> +                print("Insert failed")
>> +            elem.removeAttribute('id')
>> +            elem.setInnerXml('<img src="%s" style="" />' % img_name.toString())
>> +
>> +#            self.javascript('insertHTML', u'<span data-tag="{img}"><img_src="%s" /></span>' % img_name.toString())
>> +
>> +    def on_merge(self):
>> +        """
>> +        Add the contents of the current slide to the previous slide, then delete current slide
>> +        """
>> +        self.javascript('merge_slide', self.active_slide.attribute('id'))
>> +        slide_html = self.remove_empty_tags(self.active_text.toInnerXml())
>> +        self.active_text.setInnerXml(slide_html)
>> +
>> +    def on_move_up(self):
>> +        """
>> +        Swap the contents of the current slide with the previous slide
>> +        """
>> +        prev_slide = self.active_slide.previousSibling()
>> +        prev_text = self.get_text_div(prev_slide)
>> +        html = prev_text.toInnerXml()
>> +        prev_text.setInnerXml(self.active_text.toInnerXml())
>> +        self.active_text.setInnerXml(html)
>> +        self.page().mainFrame().scrollToAnchor(prev_slide.attribute('id'))
>> +        self.javascript('set_focus', prev_text.attribute('id'))
>> +
>> +    def on_move_down(self):
>> +        """
>> +        Swap the contents of the current slide with the next slide
>> +        """
>> +        next_slide = self.active_slide.nextSibling()
>> +        next_text = self.get_text_div(next_slide)
>> +        html = next_text.toInnerXml()
>> +        next_text.setInnerXml(self.active_text.toInnerXml())
>> +        self.active_text.setInnerXml(html)
>> +        self.page().mainFrame().scrollToAnchor(next_slide.attribute('id'))
>> +        self.javascript('set_focus', next_text.attribute('id'))
>> +
>> +    def on_delete(self):
>> +        """
>> +        Delete the current slide
>> +        """
>> +        next_slide = self.active_slide.previousSibling()
>> +        if next_slide.isNull():
>> +            next_slide = self.active_slide.nextSibling()
>> +        next_text = self.get_text_div(next_slide)
>> +        self.active_slide.removeFromDocument()
>> +        self.page().mainFrame().scrollToAnchor(next_slide.attribute('id'))
>> +        self.javascript('set_focus', next_text.attribute('id'))
>> +
>> +    def on_zoom(self, zoom):
>> +        if zoom == 'Fit Width':
>> +            self.zoom = 0.0
>> +        elif zoom == 'Actual Size':
>> +            self.zoom = 1.0
>> +        else:
>> +            self.zoom = float(str(zoom[0:2])) / 100.0
>> +        self.set_zoom()
>> +
>> +    def on_background(self, color):
>> +        self.background = color
>> +        if color == 'Theme':
>> +            color = ''
>> +        slides = self.page().mainFrame().documentElement().findAll('div.slide')
>> +        for slide in slides:
>> +            slide.setStyleProperty('background', color)
>> +
>> +    def on_insert_tag(self, tag_desc):
>> +        """
>> +        Build the html from the formatting tag and wrap it around the current selection
>> +        """
>> +        for html in FormattingTags.get_html_tags():
>> +            if tag_desc == html['desc']:
>> +                if html['start tag'] == '{img}':
>> +                    self.on_insert_image()
>> +                else:
>> +                    tag_html = '%s%s' % (self.insert_data_tag(html['start tag'], html['start html']),
>> +                                         html['end html'])
>> +                    self.javascript('insert_tag', tag_html)
>> +
>> +    def on_remove_tag(self, tag_desc):
>> +        """
>> +        Remove the first matching formatting tag in path, starting from caret and searching backwards
>> +        :param tag_desc:
>> +        """
>> +        for html in FormattingTags.get_html_tags():
>> +            if tag_desc == html['desc']:
>> +                tag = html['start tag']
>> +                self.javascript('remove_tag', tag)
>> +                break
>> +
>> +
>> +class MenuAction(QtGui.QAction):
>> +    """
>> +    A special QAction that returns the text in a signal.
>> +    """
>> +    menu_action = QtCore.pyqtSignal(str)
>> +
>> +    def __init__(self, *args):
>> +        """
>> +        Constructor
>> +        """
>> +        super(MenuAction, self).__init__(*args)
>> +        self.triggered.connect(lambda x: self.menu_action.emit(self.text()))
>>
>> === modified file 'openlp/core/lib/renderer.py'
>> --- openlp/core/lib/renderer.py	2014-04-29 11:04:19 +0000
>> +++ openlp/core/lib/renderer.py	2014-06-19 13:47:40 +0000
>> @@ -71,6 +71,7 @@
>>           self.web = QtWebKit.QWebView()
>>           self.web.setVisible(False)
>>           self.web_frame = self.web.page().mainFrame()
>> +        self.temp_tags = []
>>           Registry().register_function('theme_update_global', self.set_global_theme)
>>   
>>       def bootstrap_initialise(self):
>> @@ -194,11 +195,25 @@
>>           self._set_theme(item_theme_name)
>>           self.item_theme_name = item_theme_name
>>   
>> +    def generate_html(self, item):
>> +        """
>> +        Generate the html for a service item
>> +
>> +        :param item:  The service item
>> +        """
>> +        theme_data, main, footer = self.pre_render()
>> +        item.theme_data = theme_data
>> +        item.main = main
>> +        item.footer = footer
>> +        item.render()
>> +
>> +
>> +
>>       def generate_preview(self, theme_data, force_page=False):
>>           """
>>           Generate a preview of a theme.
>>   
>> -        :param theme_data:  The theme to generated a preview for.
>> +        :param item:  The service item to use
>>           :param force_page: Flag to tell message lines per page need to be generated.
>>           """
>>           # save value for use in format_slide
>> @@ -246,6 +261,10 @@
>>               pages = self._paginate_slide_words(text.split('\n'), line_end)
>>           # Songs and Custom
>>           elif item.is_capable(ItemCapabilities.CanSoftBreak):
>> +
>> +            text = self._create_img_tags(text, self.page_width,
>> +                                         self.page_height - (item.theme_data.font_main_size * 2))
>> +
>>               pages = []
>>               if '[---]' in text:
>>                   # Remove two or more option slide breaks next to each other (causing infinite loop).
>> @@ -309,6 +328,54 @@
>>               new_pages.append(page)
>>           return new_pages
>>   
>> +    def _create_img_tags(self, text, max_width, max_height):
>> +        """
>> +        Create a temporary image tag for every '{img}' tag in text and changes text
>> +            from: {img}"file:/filename..." style="width:100px;...{/img}
>> +            to: {temp tag}{/temp tag}
>> +        Adds max-width and max-height to style string to always fit within a slide - even when a '\n' is appended
>> +
>> +        :param text: slide text
>> +        :param max_width: maximum width of image
>> +        :param max_height: maximum height of image
>> +        """
>> +        if self.temp_tags:
>> +            self._remove_temp_tags()
>> +        next_tag = 0
>> +        while True:
>> +            start_idx = text.find('{img}')
>> +            if start_idx == -1:
>> +                break
>> +            end_idx = text.find('{/img}')
>> +            next_tag += 1
>> +            new_tag = '{$%d}' % next_tag
>> +            end_tag = '{/$%d}' % next_tag
>> +            tag_html = '<img src=%smax-width:%dpx;max-height:%dpx;"' % (text[start_idx + 5:end_idx - 1],
>> +                                                                        max_width,
>> +                                                                        max_height)
>> +            self.temp_tags.append({'start tag': new_tag,
>> +                                   'desc': 'Temporary Image Tag',
>> +                                   'start html': tag_html,
>> +                                   'end tag': end_tag,
>> +                                   'end html': ' />',
>> +                                   'protected': True,
>> +                                   'temporary': True})
>> +            text = text.replace(text[start_idx:end_idx + 6], new_tag + end_tag, 1)
>> +        if self.temp_tags:
>> +            FormattingTags.add_html_tags(self.temp_tags)
>> +        return text
>> +
>> +    def _remove_temp_tags(self):
>> +        """
>> +        Remove the temporary image tags from FormattingTags
>> +        """
>> +        for tag in self.temp_tags:
>> +            for idx, html in enumerate(FormattingTags.get_html_tags()):
>> +                if html['temporary'] and html['start tag'] == tag['start tag']:
>> +                    FormattingTags.remove_html_tag(idx)
>> +                    break
>> +        self.temp_tags = []
>> +
>>       def _calculate_default(self):
>>           """
>>           Calculate the default dimensions of the screen.
>>
>> === modified file 'openlp/core/ui/slidecontroller.py'
>> --- openlp/core/ui/slidecontroller.py	2014-05-02 06:33:18 +0000
>> +++ openlp/core/ui/slidecontroller.py	2014-06-19 13:47:40 +0000
>> @@ -1226,7 +1226,7 @@
>>           if event.timerId() == self.timer_id:
>>               self.on_slide_selected_next(self.play_slides_loop.isChecked())
>>   
>> -    def on_edit_song(self):
>> +    def on_edit_song(self, field=None):
>>           """
>>           From the preview display requires the service Item to be editied
>>           """
>> @@ -1235,7 +1235,7 @@
>>           if new_item:
>>               self.add_service_item(new_item)
>>   
>> -    def on_preview_add_to_service(self):
>> +    def on_preview_add_to_service(self, field=None):
>>           """
>>           From the preview display request the Item to be added to service
>>           """
>>
>> === modified file 'openlp/plugins/custom/forms/editcustomform.py'
>> --- openlp/plugins/custom/forms/editcustomform.py	2014-05-01 17:49:43 +0000
>> +++ openlp/plugins/custom/forms/editcustomform.py	2014-06-19 13:47:40 +0000
>> @@ -33,6 +33,7 @@
>>   
>>   from openlp.core.common import Registry, translate
>>   from openlp.core.lib.ui import critical_error_message_box, find_and_set_in_combo_box
>> +# from openlp.core.lib import serviceitem
>>   from openlp.plugins.custom.lib import CustomXMLBuilder, CustomXMLParser
>>   from openlp.plugins.custom.lib.db import CustomSlide
>>   from .editcustomdialog import Ui_CustomEditDialog
>> @@ -40,7 +41,6 @@
>>   
>>   log = logging.getLogger(__name__)
>>   
>> -
>>   class EditCustomForm(QtGui.QDialog, Ui_CustomEditDialog):
>>       """
>>       Class documentation goes here.
>> @@ -57,6 +57,7 @@
>>           self.setupUi(self)
>>           # Create other objects and forms.
>>           self.edit_slide_form = EditCustomSlideForm(self)
>> +
>>           # Connecting signals and slots
>>           self.preview_button.clicked.connect(self.on_preview_button_clicked)
>>           self.add_button.clicked.connect(self.on_add_button_clicked)
>> @@ -148,10 +149,24 @@
>>               self.slide_list_view.insertItem(selected_row + 1, qw)
>>               self.slide_list_view.setCurrentRow(selected_row + 1)
>>   
>> +    def update_service_item(self):
>> +        """
>> +        Update the service item to reflect the currently selected theme and credit text.  The service item will be used
>> +        by the HtmlEditor for WYSIWYG display
>> +        """
>> +        item = self.edit_slide_form.service_item
>> +        item.raw_footer = self.credit_edit.text()
>> +        item.update_theme(self.theme_combo_box.currentText())
>> +        theme_data, main, footer = item.renderer.pre_render()
>> +        item.theme_data = theme_data
>> +        item.main = main
>> +        item.footer = footer
>> +
>>       def on_add_button_clicked(self):
>>           """
>>           Add a new blank slide.
>>           """
>> +        self.update_service_item()
>>           self.edit_slide_form.set_text('')
>>           if self.edit_slide_form.exec_():
>>               self.slide_list_view.addItems(self.edit_slide_form.get_text())
>> @@ -160,6 +175,7 @@
>>           """
>>           Edit the currently selected slide.
>>           """
>> +        self.update_service_item()
>>           self.edit_slide_form.set_text(self.slide_list_view.currentItem().text())
>>           if self.edit_slide_form.exec_():
>>               self.update_slide_list(self.edit_slide_form.get_text())
>> @@ -168,6 +184,7 @@
>>           """
>>           Edits all slides.
>>           """
>> +        self.update_service_item()
>>           slide_text = ''
>>           for row in range(self.slide_list_view.count()):
>>               item = self.slide_list_view.item(row)
>>
>> === modified file 'openlp/plugins/custom/forms/editcustomslidedialog.py'
>> --- openlp/plugins/custom/forms/editcustomslidedialog.py	2014-05-03 15:01:43 +0000
>> +++ openlp/plugins/custom/forms/editcustomslidedialog.py	2014-06-19 13:47:40 +0000
>> @@ -27,32 +27,62 @@
>>   # Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
>>   ###############################################################################
>>   
>> -from PyQt4 import QtGui
>> +from PyQt4 import QtCore, QtGui, QtWebKit
>>   
>>   from openlp.core.common import UiStrings, translate
>>   from openlp.core.lib import SpellTextEdit, build_icon
>>   from openlp.core.lib.ui import create_button, create_button_box
>> +from openlp.plugins.custom.lib.htmleditor import HtmlEditor
>>   
>>   
>>   class Ui_CustomSlideEditDialog(object):
>> -    def setupUi(self, custom_slide_edit_dialog):
>> -        custom_slide_edit_dialog.setObjectName('custom_slide_edit_dialog')
>> -        custom_slide_edit_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg'))
>> -        custom_slide_edit_dialog.resize(350, 300)
>> -        self.dialog_layout = QtGui.QVBoxLayout(custom_slide_edit_dialog)
>> -        self.slide_text_edit = SpellTextEdit(self)
>> -        self.slide_text_edit.setObjectName('slide_text_edit')
>> -        self.dialog_layout.addWidget(self.slide_text_edit)
>> -        self.split_button = create_button(custom_slide_edit_dialog, 'splitButton', icon=':/general/general_add.png')
>> -        self.insert_button = create_button(custom_slide_edit_dialog, 'insertButton',
>> -                                           icon=':/general/general_add.png')
>> -        self.button_box = create_button_box(custom_slide_edit_dialog, 'button_box', ['cancel', 'save'],
>> -                                            [self.split_button, self.insert_button])
>> +    def setupUi(self, edit_custom_slide_dialog):
>> +        edit_custom_slide_dialog.setObjectName("edit_custom_slide_dialog")
>> +        edit_custom_slide_dialog.resize(600, 450)
>> +        edit_custom_slide_dialog.setModal(True)
>> +        self.dialog_layout = QtGui.QVBoxLayout(edit_custom_slide_dialog)
>> +        self.dialog_layout.setObjectName("dialog_layout")
>> +        self.editor_tab_widget = QtGui.QTabWidget(edit_custom_slide_dialog)
>> +        self.editor_tab_widget.setObjectName("editor_tab_widget")
>> +        self.editor_tab_widget.setMinimumSize(QtCore.QSize(400, 300))
>> +        self.tag_editor_tab = QtGui.QWidget()
>> +        self.tag_editor_tab.setObjectName("tag_editor_tab")
>> +        self.tag_editor_layout = QtGui.QVBoxLayout(self.tag_editor_tab)
>> +        self.tag_editor_layout.setObjectName("tag_editor_layout")
>> +        self.plain_text_edit = SpellTextEdit(self.tag_editor_tab)
>> +        self.plain_text_edit.setObjectName("plain_text_edit")
>> +        self.tag_editor_layout.addWidget(self.plain_text_edit)
>> +        self.editor_tab_widget.addTab(self.tag_editor_tab, "")
>> +        self.visual_editor_tab = QtGui.QWidget()
>> +        self.visual_editor_tab.setObjectName("visual_editor_tab")
>> +        self.visual_editor_layout = QtGui.QVBoxLayout(self.visual_editor_tab)
>> +        self.visual_editor_layout.setObjectName("visual_editor_layout")
>> +        self.slide_html_edit = HtmlEditor(self.visual_editor_tab)
>> +        self.slide_html_edit.setObjectName("slide_html_edit")
>> +        self.visual_editor_layout.addWidget(self.slide_html_edit)
>> +        self.msg_label = QtGui.QLabel(self.visual_editor_tab)
>> +        self.msg_label.setObjectName("msg_label")
>> +        self.visual_editor_layout.addWidget(self.msg_label)
>> +        self.editor_tab_widget.addTab(self.visual_editor_tab, "")
>> +        self.dialog_layout.addWidget(self.editor_tab_widget)
>> +        spacerItem = QtGui.QSpacerItem(20, 6, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Fixed)
>> +        self.dialog_layout.addItem(spacerItem)
>> +        self.button_layout = QtGui.QHBoxLayout()
>> +        self.button_layout.setObjectName('button_layout')
>> +        self.split_button = create_button(edit_custom_slide_dialog, 'splitButton', icon=':/general/general_add.png')
>> +        self.insert_button = create_button(edit_custom_slide_dialog, 'insertButton',icon=':/general/general_add.png')
>> +        self.button_layout.addWidget(self.split_button)
>> +        self.button_layout.addWidget(self.insert_button)
>> +        self.button_layout.addStretch()
>> +        self.dialog_layout.addLayout(self.button_layout)
>> +        self.button_box = create_button_box(edit_custom_slide_dialog, 'button_box', ['cancel', 'save'])
>>           self.dialog_layout.addWidget(self.button_box)
>> -        self.retranslateUi(custom_slide_edit_dialog)
>> +        self.retranslateUi(edit_custom_slide_dialog)
>>   
>> -    def retranslateUi(self, custom_slide_edit_dialog):
>> -        custom_slide_edit_dialog.setWindowTitle(translate('CustomPlugin.EditVerseForm', 'Edit Slide'))
>> +    def retranslateUi(self, edit_custom_slide_dialog):
>> +        edit_custom_slide_dialog.setWindowTitle(translate('CustomPlugin.EditCustomForm', "Edit Slide"))
>> +        self.editor_tab_widget.setTabText(0, translate('CustomPlugin.EditCustomForm', "Tag Editor"))
>> +        self.editor_tab_widget.setTabText(1, translate('CustomPlugin.EditCustomForm', "Visual Editor"))
>>           self.split_button.setText(UiStrings().Split)
>>           self.split_button.setToolTip(UiStrings().SplitToolTip)
>>           self.insert_button.setText(translate('CustomPlugin.EditCustomForm', 'Insert Slide'))
>>
>> === modified file 'openlp/plugins/custom/forms/editcustomslideform.py'
>> --- openlp/plugins/custom/forms/editcustomslideform.py	2014-04-12 20:19:22 +0000
>> +++ openlp/plugins/custom/forms/editcustomslideform.py	2014-06-19 13:47:40 +0000
>> @@ -28,9 +28,8 @@
>>   ###############################################################################
>>   
>>   import logging
>> -
>> -from PyQt4 import QtGui
>> -
>> +from PyQt4 import QtGui, QtCore
>> +from openlp.core.common import Registry, translate
>>   from .editcustomslidedialog import Ui_CustomSlideEditDialog
>>   
>>   log = logging.getLogger(__name__)
>> @@ -40,7 +39,7 @@
>>       """
>>       Class documentation goes here.
>>       """
>> -    log.info('Custom Verse Editor loaded')
>> +    log.info('Custom Slide Editor loaded')
>>   
>>       def __init__(self, parent=None):
>>           """
>> @@ -48,40 +47,96 @@
>>           """
>>           super(EditCustomSlideForm, self).__init__(parent)
>>           self.setupUi(self)
>> +        self.service_item = None
>> +        self.background_message_shown = False             # Only show the background image message once
>>           # Connecting signals and slots
>>           self.insert_button.clicked.connect(self.on_insert_button_clicked)
>>           self.split_button.clicked.connect(self.on_split_button_clicked)
>> +        self.connect(self.slide_html_edit, QtCore.SIGNAL('selectionChanged()'), self.on_selection_changed)
>> +        self.connect(self.editor_tab_widget, QtCore.SIGNAL('currentChanged(int)'), self.on_tab_changed)
>>   
>>       def set_text(self, text):
>>           """
>> -        Set the text for slide_text_edit.
>> +        Set the text for plain_text_edit or the slide_html_edit depending on current editor tab
>>   
>>           :param text: The text (unicode).
>>           """
>> -        self.slide_text_edit.clear()
>> -        if text:
>> -            self.slide_text_edit.setPlainText(text)
>> -        self.slide_text_edit.setFocus()
>> +        if self.editor_tab_widget.currentIndex() == 0:
>> +            self.plain_text_edit.clear()
>> +            if text:
>> +                self.plain_text_edit.setPlainText(text)
>> +            self.plain_text_edit.setFocus()
>> +        else:
>> +            self.slide_html_edit.set_text(text, self.service_item)
>> +            self.slide_html_edit.setFocus()
>>   
>>       def get_text(self):
>>           """
>>           Returns a list with all slides.
>>           """
>> -        return self.slide_text_edit.toPlainText().split('\n[===]\n')
>> +
>> +        if self.editor_tab_widget.currentIndex() == 0:
>> +            text = self.plain_text_edit.toPlainText()
>> +        else:
>> +            text = self.slide_html_edit.get_text()
>> +        return text.split('\n[===]\n')
>> +
>> +    def on_tab_changed(self, newTab):
>> +        if newTab == 0:
>> +            self.plain_text_edit.setPlainText(self.slide_html_edit.get_text())
>> +            self.slide_html_edit.hide()
>> +            self.plain_text_edit.show()
>> +            self.plain_text_edit.setFocus()
>> +        else:
>> +            self.slide_html_edit.set_text(self.plain_text_edit.toPlainText(), self.service_item)
>> +            self.plain_text_edit.hide()
>> +            self.slide_html_edit.show()
>> +            self.slide_html_edit.setFocus()
>> +
>> +    def on_selection_changed(self):
>> +        msg = '<strong>%s: </strong>' % translate('CustomPlugin.EditCustomSlideForm', 'Path')
>> +        if self.slide_html_edit.selectedHtml() == '':
>> +            path = self.slide_html_edit.get_path()
>> +            if path:
>> +                msg += path.replace(',', ' ')
>> +            else:
>> +                msg += '{}'
>> +        else:
>> +            msg += '(%s)' % translate('CustomPlugin.EditCustomSlideForm', 'selection')
>> +        msg += ' <strong>%s: </strong>%d%%' % (translate('CustomPlugin.EditCustomSlideForm', 'Zoom'),
>> +                                               int(self.slide_html_edit.zoomFactor() * 100))
>> +        if self.slide_html_edit.zoomFactor() != 1.0:
>> +            msg += ' (%s)' % translate('CustomPlugin.EditCustomSlideForm', 'use actual size for true representation')
>> +        self.msg_label.setText(msg)
>> +        if not self.background_message_shown and self.slide_html_edit.has_background_image():
>> +            self.background_message_shown = True
>> +            caption = translate('CustomPlugin.EditCustomSlideForm', 'Background Image')
>> +            msg = translate('CustomPlugin.EditCustomSlideForm', 'This slide contains a background image.\n\n'
>> +                                                                'To select a background image, hold down the CTRL key '
>> +                                                                'while clicking the image.')
>> +            Registry().get('main_window').information_message(caption, msg)
>>   
>>       def on_insert_button_clicked(self):
>>           """
>>           Adds a slide split at the cursor.
>>           """
>> -        self.insert_single_line_text_at_cursor('[===]')
>> -        self.slide_text_edit.setFocus()
>> +        if self.editor_tab_widget.currentIndex() == 0:
>> +            self.insert_single_line_text_at_cursor('[===]')
>> +            self.plain_text_edit.setFocus()
>> +        else:
>> +            self.slide_html_edit.insert()
>> +            self.slide_html_edit.setFocus()
>>   
>>       def on_split_button_clicked(self):
>>           """
>>           Adds an optional split at cursor.
>>           """
>> -        self.insert_single_line_text_at_cursor('[---]')
>> -        self.slide_text_edit.setFocus()
>> +        if self.editor_tab_widget.currentIndex() == 0:
>> +            self.insert_single_line_text_at_cursor('[---]')
>> +            self.plain_text_edit.setFocus()
>> +        else:
>> +            self.slide_html_edit.split()
>> +            self.slide_html_edit.setFocus()
>>   
>>       def insert_single_line_text_at_cursor(self, text):
>>           """
>> @@ -89,10 +144,10 @@
>>   
>>           :param text: The text to be inserted
>>           """
>> -        full_text = self.slide_text_edit.toPlainText()
>> -        position = self.slide_text_edit.textCursor().position()
>> +        full_text = self.plain_text_edit.toPlainText()
>> +        position = self.plain_text_edit.textCursor().position()
>>           if position and full_text[position - 1] != '\n':
>>               text = '\n' + text
>>           if position == len(full_text) or full_text[position] != '\n':
>>               text += '\n'
>> -        self.slide_text_edit.insertPlainText(text)
>> +        self.plain_text_edit.insertPlainText(text)
>>
>> === added file 'openlp/plugins/custom/forms/editimagedialog.py'
>> --- openlp/plugins/custom/forms/editimagedialog.py	1970-01-01 00:00:00 +0000
>> +++ openlp/plugins/custom/forms/editimagedialog.py	2014-06-19 13:47:40 +0000
>> @@ -0,0 +1,345 @@
>> +# -*- coding: utf-8 -*-
>> +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
>> +
>> +###############################################################################
>> +# OpenLP - Open Source Lyrics Projection                                      #
>> +# --------------------------------------------------------------------------- #
>> +# Copyright (c) 2008-2014 Raoul Snyman                                        #
>> +# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan      #
>> +# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub,      #
>> +# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer.   #
>> +# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru,          #
>> +# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith,             #
>> +# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock,              #
>> +# Frode Woldsund, Martin Zibricky, Patrick Zimmermann                         #
>> +# --------------------------------------------------------------------------- #
>> +# 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                          #
>> +###############################################################################
>> +
>> +from PyQt4 import QtCore, Qt, QtGui
>> +
>> +from openlp.core.lib import build_icon, translate
>> +from openlp.core.lib.ui import UiStrings, create_button_box
>> +
>> +
>> +class Ui_EditImageDialog(object):
>> +    def setupUi(self, edit_image_dialog):
>> +        edit_image_dialog.setObjectName("edit_image_dialog")
>> +        edit_image_dialog.resize(589, 327)
>> +        edit_image_dialog.setModal(True)
>> +        self.dialog_layout = QtGui.QVBoxLayout(edit_image_dialog)
>> +        self.dialog_layout.setSpacing(12)
>> +        self.dialog_layout.setObjectName("dialog_layout")
>> +        # image group box
>> +        self.image_group_box = QtGui.QGroupBox(edit_image_dialog)
>> +        sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.MinimumExpanding, QtGui.QSizePolicy.Preferred)
>> +        sizePolicy.setHorizontalStretch(0)
>> +        sizePolicy.setVerticalStretch(0)
>> +        sizePolicy.setHeightForWidth(self.image_group_box.sizePolicy().hasHeightForWidth())
>> +        self.image_group_box.setSizePolicy(sizePolicy)
>> +        self.image_group_box.setObjectName("image_group_box")
>> +        self.image_layout = QtGui.QHBoxLayout(self.image_group_box)
>> +        self.image_layout.setSpacing(8)
>> +        self.image_layout.setObjectName("image_layout")
>> +        self.thumbnail_label = QtGui.QLabel(self.image_group_box)
>> +        sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed)
>> +        sizePolicy.setHorizontalStretch(0)
>> +        sizePolicy.setVerticalStretch(0)
>> +        sizePolicy.setHeightForWidth(self.thumbnail_label.sizePolicy().hasHeightForWidth())
>> +        self.thumbnail_label.setSizePolicy(sizePolicy)
>> +        self.thumbnail_label.setMinimumSize(QtCore.QSize(48, 48))
>> +        self.thumbnail_label.setMaximumSize(QtCore.QSize(48, 48))
>> +        self.thumbnail_label.setObjectName("thumbnail_label")
>> +        self.image_layout.addWidget(self.thumbnail_label)
>> +        self.image_line_edit = QtGui.QLineEdit(self.image_group_box)
>> +        self.image_line_edit.setReadOnly(True)
>> +        self.image_line_edit.setObjectName("image_line_edit")
>> +        self.image_layout.addWidget(self.image_line_edit)
>> +        self.image_push_button = QtGui.QPushButton(self.image_group_box)
>> +        sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Fixed)
>> +        sizePolicy.setHorizontalStretch(0)
>> +        sizePolicy.setVerticalStretch(0)
>> +        sizePolicy.setHeightForWidth(self.image_push_button.sizePolicy().hasHeightForWidth())
>> +        self.image_push_button.setSizePolicy(sizePolicy)
>> +        self.image_push_button.setObjectName("image_push_button")
>> +        self.image_layout.addWidget(self.image_push_button)
>> +        self.dialog_layout.addWidget(self.image_group_box)
>> +        # properties layout - contains size, style, spacing and border group boxes
>> +        self.properties_layout = QtGui.QHBoxLayout()
>> +        self.properties_layout.setSpacing(12)
>> +        self.properties_layout.setContentsMargins(-1, 12, -1, 0)
>> +        self.properties_layout.setObjectName("properties_layout")
>> +        # size group box
>> +        self.size_group_box = QtGui.QGroupBox(edit_image_dialog)
>> +        sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.MinimumExpanding, QtGui.QSizePolicy.MinimumExpanding)
>> +        sizePolicy.setHorizontalStretch(0)
>> +        sizePolicy.setVerticalStretch(0)
>> +        sizePolicy.setHeightForWidth(self.size_group_box.sizePolicy().hasHeightForWidth())
>> +        self.size_group_box.setSizePolicy(sizePolicy)
>> +        self.size_group_box.setObjectName("size_group_box")
>> +        self.size_layout = QtGui.QGridLayout(self.size_group_box)
>> +        self.size_layout.setVerticalSpacing(10)
>> +        self.size_layout.setObjectName("size_layout")
>> +        self.width_label = QtGui.QLabel(self.size_group_box)
>> +        self.width_label.setObjectName("width_label")
>> +        self.size_layout.addWidget(self.width_label, 0, 0, 1, 1)
>> +        self.width_spin_box = QtGui.QSpinBox(self.size_group_box)
>> +        sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Fixed)
>> +        sizePolicy.setHorizontalStretch(0)
>> +        sizePolicy.setVerticalStretch(0)
>> +        sizePolicy.setHeightForWidth(self.width_spin_box.sizePolicy().hasHeightForWidth())
>> +        self.width_spin_box.setSizePolicy(sizePolicy)
>> +        self.width_spin_box.setMinimum(1)
>> +        self.width_spin_box.setMaximum(9999)
>> +        self.width_spin_box.setProperty("value", 9999)
>> +        self.width_spin_box.setObjectName("width_spin_box")
>> +        self.size_layout.addWidget(self.width_spin_box, 0, 1, 1, 1)
>> +        self.height_label = QtGui.QLabel(self.size_group_box)
>> +        self.height_label.setObjectName("height_label")
>> +        self.size_layout.addWidget(self.height_label, 1, 0, 1, 1)
>> +        self.height_spin_box = QtGui.QSpinBox(self.size_group_box)
>> +        self.height_spin_box.setMinimum(1)
>> +        self.height_spin_box.setMaximum(9999)
>> +        self.height_spin_box.setProperty("value", 9999)
>> +        self.height_spin_box.setObjectName("height_spin_box")
>> +        self.size_layout.addWidget(self.height_spin_box, 1, 1, 1, 1)
>> +        self.proportional_check_box = QtGui.QCheckBox(self.size_group_box)
>> +        self.proportional_check_box.setObjectName("proportional_check_box")
>> +        self.size_layout.addWidget(self.proportional_check_box, 2, 1, 1, 1)
>> +        self.reset_push_button = QtGui.QPushButton(self.size_group_box)
>> +        sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Fixed)
>> +        sizePolicy.setHorizontalStretch(0)
>> +        sizePolicy.setVerticalStretch(0)
>> +        sizePolicy.setHeightForWidth(self.reset_push_button.sizePolicy().hasHeightForWidth())
>> +        self.reset_push_button.setSizePolicy(sizePolicy)
>> +        self.reset_push_button.setObjectName("reset_push_button")
>> +        self.size_layout.addWidget(self.reset_push_button, 3, 0, 1, 2)
>> +        self.properties_layout.addWidget(self.size_group_box)
>> +        # style group box
>> +        self.style_group_box = QtGui.QGroupBox(edit_image_dialog)
>> +        sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.MinimumExpanding, QtGui.QSizePolicy.Preferred)
>> +        sizePolicy.setHorizontalStretch(0)
>> +        sizePolicy.setVerticalStretch(0)
>> +        sizePolicy.setHeightForWidth(self.style_group_box.sizePolicy().hasHeightForWidth())
>> +        self.style_group_box.setSizePolicy(sizePolicy)
>> +        self.style_group_box.setObjectName("style_group_box")
>> +        self.style_layout = QtGui.QGridLayout(self.style_group_box)
>> +        self.style_layout.setVerticalSpacing(10)
>> +        self.style_layout.setObjectName("style_layout")
>> +        self.align_label = QtGui.QLabel(self.style_group_box)
>> +        self.align_label.setObjectName("align_label")
>> +        self.style_layout.addWidget(self.align_label, 0, 0, 1, 1)
>> +        self.align_combo_box = QtGui.QComboBox(self.style_group_box)
>> +        self.align_combo_box.setObjectName("align_combo_box")
>> +        self.align_combo_box.addItem("")
>> +        self.align_combo_box.addItem("")
>> +        self.align_combo_box.addItem("")
>> +        self.align_combo_box.addItem("")
>> +        self.align_combo_box.addItem("")
>> +        self.align_combo_box.addItem("")
>> +        self.align_combo_box.addItem("")
>> +        self.align_combo_box.addItem("")
>> +        self.align_combo_box.addItem("")
>> +        self.style_layout.addWidget(self.align_combo_box, 0, 1, 1, 1)
>> +        self.opacity_label = QtGui.QLabel(self.style_group_box)
>> +        self.opacity_label.setObjectName("opacity_label")
>> +        self.style_layout.addWidget(self.opacity_label, 1, 0, 1, 1)
>> +        self.opacity_horizontal_slider = QtGui.QSlider(self.style_group_box)
>> +        self.opacity_horizontal_slider.setMinimum(10)
>> +        self.opacity_horizontal_slider.setMaximum(100)
>> +        self.opacity_horizontal_slider.setSingleStep(10)
>> +        self.opacity_horizontal_slider.setProperty("value", 100)
>> +        self.opacity_horizontal_slider.setSliderPosition(100)
>> +        self.opacity_horizontal_slider.setOrientation(QtCore.Qt.Horizontal)
>> +        self.opacity_horizontal_slider.setInvertedAppearance(False)
>> +        self.opacity_horizontal_slider.setTickPosition(QtGui.QSlider.NoTicks)
>> +        self.opacity_horizontal_slider.setTickInterval(10)
>> +        self.opacity_horizontal_slider.setObjectName("opacity_horizontal_slider")
>> +        self.style_layout.addWidget(self.opacity_horizontal_slider, 1, 1, 1, 1)
>> +        self.shadow_label = QtGui.QLabel(self.style_group_box)
>> +        self.shadow_label.setObjectName("shadow_label")
>> +        self.style_layout.addWidget(self.shadow_label, 2, 0, 1, 1)
>> +        self.shadow_horizontal_slider = QtGui.QSlider(self.style_group_box)
>> +        self.shadow_horizontal_slider.setMaximum(50)
>> +        self.shadow_horizontal_slider.setOrientation(QtCore.Qt.Horizontal)
>> +        self.shadow_horizontal_slider.setTickPosition(QtGui.QSlider.NoTicks)
>> +        self.shadow_horizontal_slider.setTickInterval(5)
>> +        self.shadow_horizontal_slider.setObjectName("shadow_horizontal_slider")
>> +        self.style_layout.addWidget(self.shadow_horizontal_slider, 2, 1, 1, 1)
>> +        self.blur_label = QtGui.QLabel(self.style_group_box)
>> +        self.blur_label.setObjectName("blur_label")
>> +        self.style_layout.addWidget(self.blur_label, 3, 0, 1, 1)
>> +        self.blur_horizontal_slider = QtGui.QSlider(self.style_group_box)
>> +        self.blur_horizontal_slider.setMaximum(50)
>> +        self.blur_horizontal_slider.setOrientation(QtCore.Qt.Horizontal)
>> +        self.blur_horizontal_slider.setTickPosition(QtGui.QSlider.NoTicks)
>> +        self.blur_horizontal_slider.setTickInterval(5)
>> +        self.blur_horizontal_slider.setObjectName("blur_horizontal_slider")
>> +        self.style_layout.addWidget(self.blur_horizontal_slider, 3, 1, 1, 1)
>> +        self.properties_layout.addWidget(self.style_group_box)
>> +        # spacing group box
>> +        self.spacing_group_box = QtGui.QGroupBox(edit_image_dialog)
>> +        sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.MinimumExpanding, QtGui.QSizePolicy.Preferred)
>> +        sizePolicy.setHorizontalStretch(0)
>> +        sizePolicy.setVerticalStretch(0)
>> +        sizePolicy.setHeightForWidth(self.spacing_group_box.sizePolicy().hasHeightForWidth())
>> +        self.spacing_group_box.setSizePolicy(sizePolicy)
>> +        self.spacing_group_box.setObjectName("spacing_group_box")
>> +        self.spacing_layout = QtGui.QGridLayout(self.spacing_group_box)
>> +        self.spacing_layout.setVerticalSpacing(10)
>> +        self.spacing_layout.setObjectName("spacing_layout")
>> +        self.top_spin_box = QtGui.QSpinBox(self.spacing_group_box)
>> +        self.top_spin_box.setMaximum(9999)
>> +        self.top_spin_box.setProperty("value", 9999)
>> +        self.top_spin_box.setObjectName("top_spin_box")
>> +        self.spacing_layout.addWidget(self.top_spin_box, 6, 1, 1, 1)
>> +        self.top_label = QtGui.QLabel(self.spacing_group_box)
>> +        self.top_label.setObjectName("top_label")
>> +        self.spacing_layout.addWidget(self.top_label, 6, 0, 1, 1)
>> +        self.right_spin_box = QtGui.QSpinBox(self.spacing_group_box)
>> +        self.right_spin_box.setMaximum(9999)
>> +        self.right_spin_box.setProperty("value", 9999)
>> +        self.right_spin_box.setObjectName("right_spin_box")
>> +        self.spacing_layout.addWidget(self.right_spin_box, 2, 1, 1, 1)
>> +        self.bottom_label = QtGui.QLabel(self.spacing_group_box)
>> +        self.bottom_label.setObjectName("bottom_label")
>> +        self.spacing_layout.addWidget(self.bottom_label, 7, 0, 1, 1)
>> +        self.bottom_spin_box = QtGui.QSpinBox(self.spacing_group_box)
>> +        self.bottom_spin_box.setMaximum(9999)
>> +        self.bottom_spin_box.setProperty("value", 9999)
>> +        self.bottom_spin_box.setObjectName("bottom_spin_box")
>> +        self.spacing_layout.addWidget(self.bottom_spin_box, 7, 1, 1, 1)
>> +        self.right_label = QtGui.QLabel(self.spacing_group_box)
>> +        self.right_label.setObjectName("right_label")
>> +        self.spacing_layout.addWidget(self.right_label, 2, 0, 1, 1)
>> +        self.left_label = QtGui.QLabel(self.spacing_group_box)
>> +        self.left_label.setObjectName("left_label")
>> +        self.spacing_layout.addWidget(self.left_label, 1, 0, 1, 1)
>> +        self.left_spin_box = QtGui.QSpinBox(self.spacing_group_box)
>> +        self.left_spin_box.setMaximum(9999)
>> +        self.left_spin_box.setProperty("value", 9999)
>> +        self.left_spin_box.setObjectName("left_spin_box")
>> +        self.spacing_layout.addWidget(self.left_spin_box, 1, 1, 1, 1)
>> +        self.properties_layout.addWidget(self.spacing_group_box)
>> +        # border group box
>> +        self.border_group_box = QtGui.QGroupBox(edit_image_dialog)
>> +        sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.MinimumExpanding, QtGui.QSizePolicy.Preferred)
>> +        sizePolicy.setHorizontalStretch(0)
>> +        sizePolicy.setVerticalStretch(0)
>> +        sizePolicy.setHeightForWidth(self.border_group_box.sizePolicy().hasHeightForWidth())
>> +        self.border_group_box.setSizePolicy(sizePolicy)
>> +        self.border_group_box.setObjectName("border_group_box")
>> +        self.border_layout = QtGui.QGridLayout(self.border_group_box)
>> +        self.border_layout.setVerticalSpacing(10)
>> +        self.border_layout.setObjectName("border_layout")
>> +        self.border_color_frame = QtGui.QFrame(self.border_group_box)
>> +        sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.MinimumExpanding, QtGui.QSizePolicy.Fixed)
>> +        sizePolicy.setHorizontalStretch(0)
>> +        sizePolicy.setVerticalStretch(0)
>> +        sizePolicy.setHeightForWidth(self.border_color_frame.sizePolicy().hasHeightForWidth())
>> +        self.border_color_frame.setSizePolicy(sizePolicy)
>> +        self.border_color_frame.setMinimumSize(QtCore.QSize(16, 16))
>> +        self.border_color_frame.setStyleSheet("background-color: #FF0000;")
>> +        self.border_color_frame.setFrameShape(QtGui.QFrame.StyledPanel)
>> +        self.border_color_frame.setFrameShadow(QtGui.QFrame.Sunken)
>> +        self.border_color_frame.setObjectName("border_color_frame")
>> +        self.border_layout.addWidget(self.border_color_frame, 2, 0, 1, 1)
>> +        self.border_type_label = QtGui.QLabel(self.border_group_box)
>> +        self.border_type_label.setObjectName("border_type_label")
>> +        self.border_layout.addWidget(self.border_type_label, 0, 0, 1, 1)
>> +        self.border_color_push_button = QtGui.QPushButton(self.border_group_box)
>> +        self.border_color_push_button.setObjectName("border_color_push_button")
>> +        self.border_layout.addWidget(self.border_color_push_button, 2, 1, 1, 1)
>> +        self.radius_horizontal_slider = QtGui.QSlider(self.border_group_box)
>> +        self.radius_horizontal_slider.setMaximum(50)
>> +        self.radius_horizontal_slider.setPageStep(5)
>> +        self.radius_horizontal_slider.setOrientation(QtCore.Qt.Horizontal)
>> +        self.radius_horizontal_slider.setObjectName("radius_horizontal_slider")
>> +        self.border_layout.addWidget(self.radius_horizontal_slider, 4, 1, 1, 1)
>> +        self.radius_label = QtGui.QLabel(self.border_group_box)
>> +        self.radius_label.setObjectName("radius_label")
>> +        self.border_layout.addWidget(self.radius_label, 4, 0, 1, 1)
>> +        self.border_type_combo_box = QtGui.QComboBox(self.border_group_box)
>> +        self.border_type_combo_box.setObjectName("border_type_combo_box")
>> +        self.border_type_combo_box.addItem("")
>> +        self.border_type_combo_box.addItem("")
>> +        self.border_type_combo_box.addItem("")
>> +        self.border_type_combo_box.addItem("")
>> +        self.border_type_combo_box.addItem("")
>> +        self.border_type_combo_box.addItem("")
>> +        self.border_type_combo_box.addItem("")
>> +        self.border_type_combo_box.addItem("")
>> +        self.border_type_combo_box.addItem("")
>> +        self.border_layout.addWidget(self.border_type_combo_box, 0, 1, 1, 1)
>> +        self.border_width_label = QtGui.QLabel(self.border_group_box)
>> +        self.border_width_label.setObjectName("border_width_label")
>> +        self.border_layout.addWidget(self.border_width_label, 3, 0, 1, 1)
>> +        self.border_width_horizontal_slider = QtGui.QSlider(self.border_group_box)
>> +        self.border_width_horizontal_slider.setMinimum(1)
>> +        self.border_width_horizontal_slider.setMaximum(50)
>> +        self.border_width_horizontal_slider.setPageStep(5)
>> +        self.border_width_horizontal_slider.setOrientation(QtCore.Qt.Horizontal)
>> +        self.border_width_horizontal_slider.setObjectName("border_width_horizontal_slider")
>> +        self.border_layout.addWidget(self.border_width_horizontal_slider, 3, 1, 1, 1)
>> +        self.properties_layout.addWidget(self.border_group_box)
>> +        self.dialog_layout.addLayout(self.properties_layout)
>> +        spacerItem = QtGui.QSpacerItem(20, 12, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Fixed)
>> +        self.dialog_layout.addItem(spacerItem)
>> +        # button box
>> +        self.button_box = create_button_box(edit_image_dialog, 'button_box', ['cancel', 'ok'])
>> +        self.dialog_layout.addWidget(self.button_box)
>> +        self.retranslateUi(edit_image_dialog)
>> +
>> +    def retranslateUi(self, edit_image_dialog):
>> +        edit_image_dialog.setWindowTitle(translate('CustomPlugin.EditImageForm', "Edit Image"))
>> +        self.image_group_box.setTitle(translate('CustomPlugin.EditImageForm', "Image"))
>> +        self.image_push_button.setText(translate('CustomPlugin.EditImageForm', "Select Image..."))
>> +        self.size_group_box.setTitle(translate('CustomPlugin.EditImageForm', "Size"))
>> +        self.width_label.setText(translate('CustomPlugin.EditImageForm', "Width"))
>> +        self.height_label.setText(translate('CustomPlugin.EditImageForm', "Height"))
>> +        self.proportional_check_box.setText(translate('CustomPlugin.EditImageForm', "Proportional"))
>> +        self.reset_push_button.setText(translate('CustomPlugin.EditImageForm', "Reset"))
>> +        self.style_group_box.setTitle(translate('CustomPlugin.EditImageForm', "Style"))
>> +        self.align_label.setText(translate('CustomPlugin.EditImageForm', "Align"))
>> +        self.align_combo_box.setItemText(0, translate('CustomPlugin.EditImageForm', "None"))
>> +        self.align_combo_box.setItemText(1, translate('CustomPlugin.EditImageForm', "Left"))
>> +        self.align_combo_box.setItemText(2, translate('CustomPlugin.EditImageForm', "Right"))
>> +        self.align_combo_box.setItemText(3, translate('CustomPlugin.EditImageForm', "Center"))
>> +        self.align_combo_box.setItemText(4, translate('CustomPlugin.EditImageForm', "Block"))
>> +        self.align_combo_box.setItemText(5, translate('CustomPlugin.EditImageForm', "Text Top"))
>> +        self.align_combo_box.setItemText(6, translate('CustomPlugin.EditImageForm', "Text Middle"))
>> +        self.align_combo_box.setItemText(7, translate('CustomPlugin.EditImageForm', "Text Bottom"))
>> +        self.align_combo_box.setItemText(8, translate('CustomPlugin.EditImageForm', "Background"))
>> +        self.opacity_label.setText(translate('CustomPlugin.EditImageForm', "Opacity"))
>> +        self.shadow_label.setText(translate('CustomPlugin.EditImageForm', "Shadow"))
>> +        self.blur_label.setText(translate('CustomPlugin.EditImageForm', "Blur"))
>> +        self.spacing_group_box.setTitle(translate('CustomPlugin.EditImageForm', "Spacing"))
>> +        self.top_label.setText(translate('CustomPlugin.EditImageForm', "Top"))
>> +        self.right_label.setText(translate('CustomPlugin.EditImageForm', "Right"))
>> +        self.bottom_label.setText(translate('CustomPlugin.EditImageForm', "Bottom"))
>> +        self.left_label.setText(translate('CustomPlugin.EditImageForm', "Left"))
>> +        self.border_group_box.setTitle(translate('CustomPlugin.EditImageForm', "Border"))
>> +        self.border_type_label.setText(translate('CustomPlugin.EditImageForm', "Type"))
>> +        self.border_type_combo_box.setItemText(0, translate('CustomPlugin.EditImageForm', "None"))
>> +        self.border_type_combo_box.setItemText(1, translate('CustomPlugin.EditImageForm', "Solid"))
>> +        self.border_type_combo_box.setItemText(2, translate('CustomPlugin.EditImageForm', "Dotted"))
>> +        self.border_type_combo_box.setItemText(3, translate('CustomPlugin.EditImageForm', "Dashed"))
>> +        self.border_type_combo_box.setItemText(4, translate('CustomPlugin.EditImageForm', "Double"))
>> +        self.border_type_combo_box.setItemText(5, translate('CustomPlugin.EditImageForm', "Groove"))
>> +        self.border_type_combo_box.setItemText(6, translate('CustomPlugin.EditImageForm', "Ridge"))
>> +        self.border_type_combo_box.setItemText(7, translate('CustomPlugin.EditImageForm', "Inset"))
>> +        self.border_type_combo_box.setItemText(8, translate('CustomPlugin.EditImageForm', "Outset"))
>> +        self.border_width_label.setText(translate('CustomPlugin.EditImageForm', "Width"))
>> +        self.border_color_push_button.setText(translate('CustomPlugin.EditImageForm', "Color..."))
>> +        self.radius_label.setText(translate('CustomPlugin.EditImageForm', "Radius"))
>>
>> === added file 'openlp/plugins/custom/forms/editimagedialog.ui'
>> --- openlp/plugins/custom/forms/editimagedialog.ui	1970-01-01 00:00:00 +0000
>> +++ openlp/plugins/custom/forms/editimagedialog.ui	2014-06-19 13:47:40 +0000
>> @@ -0,0 +1,605 @@
>> +<?xml version="1.0" encoding="UTF-8"?>
>> +<ui version="4.0">
>> + <class>EditImageDialog</class>
>> + <widget class="QDialog" name="EditImageDialog">
>> +  <property name="geometry">
>> +   <rect>
>> +    <x>0</x>
>> +    <y>0</y>
>> +    <width>629</width>
>> +    <height>308</height>
>> +   </rect>
>> +  </property>
>> +  <property name="windowTitle">
>> +   <string>Dialog</string>
>> +  </property>
>> +  <layout class="QVBoxLayout" name="verticalLayout">
>> +   <item>
>> +    <widget class="QGroupBox" name="ImageGroupBox">
>> +     <property name="sizePolicy">
>> +      <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
>> +       <horstretch>0</horstretch>
>> +       <verstretch>0</verstretch>
>> +      </sizepolicy>
>> +     </property>
>> +     <property name="title">
>> +      <string>Image</string>
>> +     </property>
>> +     <layout class="QHBoxLayout" name="horizontalLayout">
>> +      <item>
>> +       <layout class="QHBoxLayout" name="ImageLayout">
>> +        <property name="spacing">
>> +         <number>8</number>
>> +        </property>
>> +        <property name="margin">
>> +         <number>8</number>
>> +        </property>
>> +        <item>
>> +         <widget class="QLineEdit" name="ImageLineEdit"/>
>> +        </item>
>> +        <item>
>> +         <widget class="QPushButton" name="ImagePushButton">
>> +          <property name="sizePolicy">
>> +           <sizepolicy hsizetype="Minimum" vsizetype="Fixed">
>> +            <horstretch>0</horstretch>
>> +            <verstretch>0</verstretch>
>> +           </sizepolicy>
>> +          </property>
>> +          <property name="text">
>> +           <string>Select Image...</string>
>> +          </property>
>> +         </widget>
>> +        </item>
>> +       </layout>
>> +      </item>
>> +     </layout>
>> +    </widget>
>> +   </item>
>> +   <item>
>> +    <layout class="QHBoxLayout" name="PropertiesLayout" stretch="0,0,0,0">
>> +     <property name="spacing">
>> +      <number>8</number>
>> +     </property>
>> +     <property name="topMargin">
>> +      <number>12</number>
>> +     </property>
>> +     <property name="bottomMargin">
>> +      <number>0</number>
>> +     </property>
>> +     <item>
>> +      <widget class="QGroupBox" name="SizeGroupBox">
>> +       <property name="sizePolicy">
>> +        <sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
>> +         <horstretch>0</horstretch>
>> +         <verstretch>0</verstretch>
>> +        </sizepolicy>
>> +       </property>
>> +       <property name="title">
>> +        <string>Size</string>
>> +       </property>
>> +       <layout class="QHBoxLayout" name="horizontalLayout_2">
>> +        <item>
>> +         <layout class="QFormLayout" name="SizeFormLayout">
>> +          <property name="horizontalSpacing">
>> +           <number>8</number>
>> +          </property>
>> +          <property name="verticalSpacing">
>> +           <number>8</number>
>> +          </property>
>> +          <property name="margin">
>> +           <number>8</number>
>> +          </property>
>> +          <item row="0" column="0">
>> +           <widget class="QLabel" name="WidthLabel">
>> +            <property name="text">
>> +             <string>Width</string>
>> +            </property>
>> +           </widget>
>> +          </item>
>> +          <item row="0" column="1">
>> +           <widget class="QSpinBox" name="WidthSpinBox">
>> +            <property name="sizePolicy">
>> +             <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
>> +              <horstretch>0</horstretch>
>> +              <verstretch>0</verstretch>
>> +             </sizepolicy>
>> +            </property>
>> +            <property name="minimum">
>> +             <number>1</number>
>> +            </property>
>> +            <property name="maximum">
>> +             <number>9999</number>
>> +            </property>
>> +            <property name="value">
>> +             <number>9999</number>
>> +            </property>
>> +           </widget>
>> +          </item>
>> +          <item row="1" column="0">
>> +           <widget class="QLabel" name="HeightLabel">
>> +            <property name="text">
>> +             <string>Height</string>
>> +            </property>
>> +           </widget>
>> +          </item>
>> +          <item row="1" column="1">
>> +           <widget class="QSpinBox" name="HeightSpinBox">
>> +            <property name="minimum">
>> +             <number>1</number>
>> +            </property>
>> +            <property name="maximum">
>> +             <number>9999</number>
>> +            </property>
>> +            <property name="value">
>> +             <number>9999</number>
>> +            </property>
>> +           </widget>
>> +          </item>
>> +          <item row="2" column="0">
>> +           <widget class="QLabel" name="RatioLabel">
>> +            <property name="text">
>> +             <string>Ratio</string>
>> +            </property>
>> +           </widget>
>> +          </item>
>> +          <item row="2" column="1">
>> +           <widget class="QCheckBox" name="RatioCheckBox">
>> +            <property name="text">
>> +             <string/>
>> +            </property>
>> +           </widget>
>> +          </item>
>> +          <item row="3" column="1">
>> +           <widget class="QPushButton" name="ResetPushButton">
>> +            <property name="text">
>> +             <string>Reset</string>
>> +            </property>
>> +           </widget>
>> +          </item>
>> +         </layout>
>> +        </item>
>> +       </layout>
>> +      </widget>
>> +     </item>
>> +     <item>
>> +      <widget class="QGroupBox" name="StyleGroupBox">
>> +       <property name="sizePolicy">
>> +        <sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
>> +         <horstretch>0</horstretch>
>> +         <verstretch>0</verstretch>
>> +        </sizepolicy>
>> +       </property>
>> +       <property name="title">
>> +        <string>Style</string>
>> +       </property>
>> +       <layout class="QHBoxLayout" name="horizontalLayout_3">
>> +        <item>
>> +         <layout class="QFormLayout" name="StyleFormLayout">
>> +          <property name="horizontalSpacing">
>> +           <number>8</number>
>> +          </property>
>> +          <property name="verticalSpacing">
>> +           <number>8</number>
>> +          </property>
>> +          <property name="margin">
>> +           <number>8</number>
>> +          </property>
>> +          <item row="0" column="0">
>> +           <widget class="QLabel" name="AlignLabel">
>> +            <property name="text">
>> +             <string>Align</string>
>> +            </property>
>> +           </widget>
>> +          </item>
>> +          <item row="0" column="1">
>> +           <widget class="QComboBox" name="AlignComboBox">
>> +            <item>
>> +             <property name="text">
>> +              <string>None</string>
>> +             </property>
>> +            </item>
>> +            <item>
>> +             <property name="text">
>> +              <string>Left</string>
>> +             </property>
>> +            </item>
>> +            <item>
>> +             <property name="text">
>> +              <string>Right</string>
>> +             </property>
>> +            </item>
>> +            <item>
>> +             <property name="text">
>> +              <string>Center</string>
>> +             </property>
>> +            </item>
>> +            <item>
>> +             <property name="text">
>> +              <string>Block</string>
>> +             </property>
>> +            </item>
>> +            <item>
>> +             <property name="text">
>> +              <string>Text Top</string>
>> +             </property>
>> +            </item>
>> +            <item>
>> +             <property name="text">
>> +              <string>Text Middle</string>
>> +             </property>
>> +            </item>
>> +            <item>
>> +             <property name="text">
>> +              <string>Text Bottom</string>
>> +             </property>
>> +            </item>
>> +            <item>
>> +             <property name="text">
>> +              <string>Background</string>
>> +             </property>
>> +            </item>
>> +           </widget>
>> +          </item>
>> +          <item row="1" column="0">
>> +           <widget class="QLabel" name="OpacityLabel">
>> +            <property name="text">
>> +             <string>Opacity</string>
>> +            </property>
>> +           </widget>
>> +          </item>
>> +          <item row="1" column="1">
>> +           <widget class="QSlider" name="OpacityHorizontalSlider">
>> +            <property name="minimum">
>> +             <number>10</number>
>> +            </property>
>> +            <property name="maximum">
>> +             <number>100</number>
>> +            </property>
>> +            <property name="value">
>> +             <number>100</number>
>> +            </property>
>> +            <property name="sliderPosition">
>> +             <number>100</number>
>> +            </property>
>> +            <property name="orientation">
>> +             <enum>Qt::Horizontal</enum>
>> +            </property>
>> +            <property name="invertedAppearance">
>> +             <bool>false</bool>
>> +            </property>
>> +            <property name="tickPosition">
>> +             <enum>QSlider::NoTicks</enum>
>> +            </property>
>> +            <property name="tickInterval">
>> +             <number>10</number>
>> +            </property>
>> +           </widget>
>> +          </item>
>> +          <item row="2" column="0">
>> +           <widget class="QLabel" name="LeftBackgroundLabel">
>> +            <property name="text">
>> +             <string>Left</string>
>> +            </property>
>> +           </widget>
>> +          </item>
>> +          <item row="2" column="1">
>> +           <widget class="QSpinBox" name="LeftBackgroundSpinBox">
>> +            <property name="maximum">
>> +             <number>9999</number>
>> +            </property>
>> +            <property name="value">
>> +             <number>9999</number>
>> +            </property>
>> +           </widget>
>> +          </item>
>> +          <item row="3" column="0">
>> +           <widget class="QLabel" name="TopBackgroundLabel">
>> +            <property name="text">
>> +             <string>Top</string>
>> +            </property>
>> +           </widget>
>> +          </item>
>> +          <item row="3" column="1">
>> +           <widget class="QSpinBox" name="TopBackgroundSpinBox">
>> +            <property name="maximum">
>> +             <number>9999</number>
>> +            </property>
>> +            <property name="value">
>> +             <number>9999</number>
>> +            </property>
>> +           </widget>
>> +          </item>
>> +         </layout>
>> +        </item>
>> +       </layout>
>> +      </widget>
>> +     </item>
>> +     <item>
>> +      <widget class="QGroupBox" name="SpacingGroupBox">
>> +       <property name="sizePolicy">
>> +        <sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
>> +         <horstretch>0</horstretch>
>> +         <verstretch>0</verstretch>
>> +        </sizepolicy>
>> +       </property>
>> +       <property name="title">
>> +        <string>Spacing</string>
>> +       </property>
>> +       <layout class="QHBoxLayout" name="horizontalLayout_4">
>> +        <item>
>> +         <layout class="QFormLayout" name="SpacingFormLayout">
>> +          <property name="horizontalSpacing">
>> +           <number>8</number>
>> +          </property>
>> +          <property name="verticalSpacing">
>> +           <number>8</number>
>> +          </property>
>> +          <property name="margin">
>> +           <number>8</number>
>> +          </property>
>> +          <item row="0" column="0">
>> +           <widget class="QLabel" name="TopLabel">
>> +            <property name="text">
>> +             <string>Top</string>
>> +            </property>
>> +           </widget>
>> +          </item>
>> +          <item row="0" column="1">
>> +           <widget class="QSpinBox" name="TopSpinBox">
>> +            <property name="maximum">
>> +             <number>9999</number>
>> +            </property>
>> +            <property name="value">
>> +             <number>9999</number>
>> +            </property>
>> +           </widget>
>> +          </item>
>> +          <item row="1" column="0">
>> +           <widget class="QLabel" name="RightLabel">
>> +            <property name="text">
>> +             <string>Right</string>
>> +            </property>
>> +           </widget>
>> +          </item>
>> +          <item row="1" column="1">
>> +           <widget class="QSpinBox" name="RightSpinBox">
>> +            <property name="maximum">
>> +             <number>9999</number>
>> +            </property>
>> +            <property name="value">
>> +             <number>9999</number>
>> +            </property>
>> +           </widget>
>> +          </item>
>> +          <item row="2" column="0">
>> +           <widget class="QLabel" name="BottomLabel">
>> +            <property name="text">
>> +             <string>Bottom</string>
>> +            </property>
>> +           </widget>
>> +          </item>
>> +          <item row="2" column="1">
>> +           <widget class="QSpinBox" name="BottomSpinBox">
>> +            <property name="maximum">
>> +             <number>9999</number>
>> +            </property>
>> +            <property name="value">
>> +             <number>9999</number>
>> +            </property>
>> +           </widget>
>> +          </item>
>> +          <item row="3" column="0">
>> +           <widget class="QLabel" name="LeftLabel">
>> +            <property name="text">
>> +             <string>Left</string>
>> +            </property>
>> +           </widget>
>> +          </item>
>> +          <item row="3" column="1">
>> +           <widget class="QSpinBox" name="LeftSpinBox">
>> +            <property name="maximum">
>> +             <number>9999</number>
>> +            </property>
>> +            <property name="value">
>> +             <number>9999</number>
>> +            </property>
>> +           </widget>
>> +          </item>
>> +         </layout>
>> +        </item>
>> +       </layout>
>> +      </widget>
>> +     </item>
>> +     <item>
>> +      <widget class="QGroupBox" name="BorderGroupBox">
>> +       <property name="sizePolicy">
>> +        <sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
>> +         <horstretch>0</horstretch>
>> +         <verstretch>0</verstretch>
>> +        </sizepolicy>
>> +       </property>
>> +       <property name="title">
>> +        <string>Border</string>
>> +       </property>
>> +       <layout class="QHBoxLayout" name="horizontalLayout_5">
>> +        <item>
>> +         <layout class="QFormLayout" name="BorderFormLayout">
>> +          <property name="horizontalSpacing">
>> +           <number>8</number>
>> +          </property>
>> +          <property name="verticalSpacing">
>> +           <number>8</number>
>> +          </property>
>> +          <property name="margin">
>> +           <number>8</number>
>> +          </property>
>> +          <item row="0" column="0">
>> +           <widget class="QLabel" name="BorderTypeLabel">
>> +            <property name="text">
>> +             <string>Type</string>
>> +            </property>
>> +           </widget>
>> +          </item>
>> +          <item row="0" column="1">
>> +           <widget class="QComboBox" name="BorderTypeComboBox">
>> +            <item>
>> +             <property name="text">
>> +              <string>None</string>
>> +             </property>
>> +            </item>
>> +            <item>
>> +             <property name="text">
>> +              <string>Solid</string>
>> +             </property>
>> +            </item>
>> +            <item>
>> +             <property name="text">
>> +              <string>Dotted</string>
>> +             </property>
>> +            </item>
>> +            <item>
>> +             <property name="text">
>> +              <string>Dashed</string>
>> +             </property>
>> +            </item>
>> +            <item>
>> +             <property name="text">
>> +              <string>Double</string>
>> +             </property>
>> +            </item>
>> +            <item>
>> +             <property name="text">
>> +              <string>Groove</string>
>> +             </property>
>> +            </item>
>> +            <item>
>> +             <property name="text">
>> +              <string>Ridge</string>
>> +             </property>
>> +            </item>
>> +            <item>
>> +             <property name="text">
>> +              <string>Inset</string>
>> +             </property>
>> +            </item>
>> +            <item>
>> +             <property name="text">
>> +              <string>Outset</string>
>> +             </property>
>> +            </item>
>> +           </widget>
>> +          </item>
>> +          <item row="1" column="0">
>> +           <widget class="QLabel" name="BorderWidthLabel">
>> +            <property name="text">
>> +             <string>Width</string>
>> +            </property>
>> +           </widget>
>> +          </item>
>> +          <item row="1" column="1">
>> +           <widget class="QSpinBox" name="BorderWidthSpinBox">
>> +            <property name="value">
>> +             <number>99</number>
>> +            </property>
>> +           </widget>
>> +          </item>
>> +          <item row="2" column="1">
>> +           <widget class="QPushButton" name="BorderColorPushButton">
>> +            <property name="text">
>> +             <string>Color...</string>
>> +            </property>
>> +           </widget>
>> +          </item>
>> +          <item row="2" column="0">
>> +           <widget class="QFrame" name="border_color_frame">
>> +            <property name="sizePolicy">
>> +             <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
>> +              <horstretch>0</horstretch>
>> +              <verstretch>0</verstretch>
>> +             </sizepolicy>
>> +            </property>
>> +            <property name="minimumSize">
>> +             <size>
>> +              <width>21</width>
>> +              <height>21</height>
>> +             </size>
>> +            </property>
>> +            <property name="styleSheet">
>> +             <string notr="true">background-color: #FF0000;</string>
>> +            </property>
>> +            <property name="frameShape">
>> +             <enum>QFrame::StyledPanel</enum>
>> +            </property>
>> +            <property name="frameShadow">
>> +             <enum>QFrame::Raised</enum>
>> +            </property>
>> +           </widget>
>> +          </item>
>> +         </layout>
>> +        </item>
>> +       </layout>
>> +      </widget>
>> +     </item>
>> +    </layout>
>> +   </item>
>> +   <item>
>> +    <spacer name="VerticalSpacer">
>> +     <property name="orientation">
>> +      <enum>Qt::Vertical</enum>
>> +     </property>
>> +     <property name="sizeHint" stdset="0">
>> +      <size>
>> +       <width>20</width>
>> +       <height>40</height>
>> +      </size>
>> +     </property>
>> +    </spacer>
>> +   </item>
>> +   <item>
>> +    <widget class="QDialogButtonBox" name="ButtonBox">
>> +     <property name="orientation">
>> +      <enum>Qt::Horizontal</enum>
>> +     </property>
>> +     <property name="standardButtons">
>> +      <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
>> +     </property>
>> +    </widget>
>> +   </item>
>> +  </layout>
>> + </widget>
>> + <resources/>
>> + <connections>
>> +  <connection>
>> +   <sender>ButtonBox</sender>
>> +   <signal>accepted()</signal>
>> +   <receiver>EditImageDialog</receiver>
>> +   <slot>accept()</slot>
>> +   <hints>
>> +    <hint type="sourcelabel">
>> +     <x>248</x>
>> +     <y>254</y>
>> +    </hint>
>> +    <hint type="destinationlabel">
>> +     <x>157</x>
>> +     <y>274</y>
>> +    </hint>
>> +   </hints>
>> +  </connection>
>> +  <connection>
>> +   <sender>ButtonBox</sender>
>> +   <signal>rejected()</signal>
>> +   <receiver>EditImageDialog</receiver>
>> +   <slot>reject()</slot>
>> +   <hints>
>> +    <hint type="sourcelabel">
>> +     <x>316</x>
>> +     <y>260</y>
>> +    </hint>
>> +    <hint type="destinationlabel">
>> +     <x>286</x>
>> +     <y>274</y>
>> +    </hint>
>> +   </hints>
>> +  </connection>
>> + </connections>
>> +</ui>
>>
>> === added file 'openlp/plugins/custom/forms/editimageform.py'
>> --- openlp/plugins/custom/forms/editimageform.py	1970-01-01 00:00:00 +0000
>> +++ openlp/plugins/custom/forms/editimageform.py	2014-06-19 13:47:40 +0000
>> @@ -0,0 +1,489 @@
>> +# -*- coding: utf-8 -*-
>> +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
>> +
>> +###############################################################################
>> +# OpenLP - Open Source Lyrics Projection                                      #
>> +# --------------------------------------------------------------------------- #
>> +# Copyright (c) 2008-2014 Raoul Snyman                                        #
>> +# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan      #
>> +# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub,      #
>> +# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer.   #
>> +# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru,          #
>> +# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith,             #
>> +# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock,              #
>> +# Frode Woldsund, Martin Zibricky, Patrick Zimmermann                         #
>> +# --------------------------------------------------------------------------- #
>> +# 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:`~openlp.plugins.custom.forms.editimageform` module contains the form
>> +used to select and style images.
>> +"""
>> +
>> +import logging
>> +
>> +from PyQt4 import QtCore, Qt, QtGui
>> +from openlp.core.common import translate
>> +from openlp.core.lib.ui import critical_error_message_box
>> +from openlp.plugins.custom.forms.editimagedialog import Ui_EditImageDialog
>> +
>> +log = logging.getLogger(__name__)
>> +
>> +
>> +class AlignStyle(object):
>> +    """
>> +    Class for css alignment style - 1:1 relationship with alignment combo box
>> +    """
>> +    Align = ['',                                                               # None
>> +             ' float: left;',                                                  # Left
>> +             ' float: right;',                                                 # Right
>> +             ' display: block; margin-left: auto; margin-right: auto;',        # Center
>> +             ' display: block;',                                               # Block
>> +             ' vertical-align: text-top;',                                     # Text Top
>> +             ' vertical-align: text-middle;',                                  # Text Middle
>> +             ' vertical-align: text-bottom;',                                  # Text Bottom
>> +             ' position: absolute;']                                           # Background
>> +    Left = 1
>> +    Right = 2
>> +    Center = 3
>> +    Block = 4
>> +    Text_Top = 5
>> +    Text_Middle = 6
>> +    Text_Bottom = 7
>> +    Background = 8
>> +
>> +
>> +class BorderStyle(object):
>> +    """
>> +    Class for css border style - 1:1 relationship with border combo box
>> +    """
>> +    Border = ['',                               # None
>> +              ' border-style: solid;',
>> +              ' border-style: dotted;',
>> +              ' border-style: dashed;',
>> +              ' border-style: double;',
>> +              ' border-style: groove;',
>> +              ' border-style: ridge;',
>> +              ' border-style: inset;',
>> +              ' border-style: outset;']
>> +    Solid = 1
>> +    Dotted = 2
>> +    Dashed = 3
>> +    Double = 4
>> +    Groove = 5
>> +    Ridge = 6
>> +    Inset = 7
>> +    Outset = 8
>> +
>> +
>> +class EditImageForm(QtGui.QDialog, Ui_EditImageDialog):
>> +    """
>> +    Class to display the image editor
>> +    """
>> +    log.info('%s EditImageForm loaded', __name__)
>> +
>> +    def __init__(self, parent=None):
>> +        """
>> +        Constructor
>> +        """
>> +        super(EditImageForm, self).__init__(parent)
>> +        self.setupUi(self)
>> +        self.image_width = int
>> +        self.image_height = int
>> +        self.max_width = int
>> +        self.max_height = int
>> +        self.last_path = ''
>> +        self.border_color = Qt.QColor()
>> +        self.align_style = AlignStyle()
>> +        self.border_style = BorderStyle()
>> +        # Connecting signals and slots
>> +        self.image_push_button.clicked.connect(self.on_image_push_button)
>> +        self.reset_push_button.clicked.connect(self.on_reset_push_button)
>> +        self.align_combo_box.currentIndexChanged.connect(self.on_align_combo_box)
>> +        self.border_type_combo_box.currentIndexChanged.connect(self.on_border_type_combo_box)
>> +        self.border_color_push_button.clicked.connect(self.on_color_push_button)
>> +        self.width_spin_box.valueChanged.connect(self.on_width_spin_box)
>> +        self.height_spin_box.valueChanged.connect(self.on_height_spin_box)
>> +        self.proportional_check_box.stateChanged.connect(self.on_proportional_check_box)
>> +
>> +    def set_image(self, max_width, max_height, src=None, style=None):
>> +        """
>> +        Initialize the dialog to default values and set to style values if present
>> +
>> +        :param max_width: Width of slide text area
>> +        :param max_height: Height of slide text area
>> +        :param src: string containing image filename
>> +        :param style: string containing image information
>> +        """
>> +        self.max_width = max_width
>> +        self.max_height = max_height
>> +        self.image_width = 0
>> +        self.image_height = 0
>> +        self.thumbnail_label.hide()
>> +        self.image_line_edit.setText('')
>> +        self.proportional_check_box.setCheckState(Qt.Qt.Unchecked)
>> +        self.width_spin_box.setValue(0)
>> +        self.height_spin_box.setValue(0)
>> +        self.align_combo_box.setCurrentIndex(0)
>> +        self.opacity_horizontal_slider.setValue(100)
>> +        self.shadow_horizontal_slider.setValue(0)
>> +        self.blur_horizontal_slider.setValue(0)
>> +        self.top_spin_box.setValue(0)
>> +        self.right_spin_box.setValue(0)
>> +        self.bottom_spin_box.setValue(0)
>> +        self.left_spin_box.setValue(0)
>> +        self.border_type_combo_box.setCurrentIndex(0)
>> +        self.border_width_horizontal_slider.setValue(1)
>> +        self.border_color = Qt.QColor(255, 255, 255, 255)
>> +        self.radius_horizontal_slider.setValue(0)
>> +        self.set_frame_color()
>> +        self.size_group_box.setEnabled(False)
>> +        self.style_group_box.setEnabled(False)
>> +        self.spacing_group_box.setEnabled(False)
>> +        self.border_group_box.setEnabled(False)
>> +        if src:
>> +            self.setWindowTitle(translate('CustomPlugin.EditImageForm', "Edit Image"))
>> +            if self.open_image(src):
>> +                if style:
>> +                    self.parse_style(style)
>> +        else:
>> +            self.setWindowTitle(translate('CustomPlugin.EditImageForm', "Insert Image"))
>> +        self.image_push_button.setFocus()
>> +
>> +    def set_frame_color(self):
>> +        self.border_color_frame.setStyleSheet('background-color: #%02X%02X%02X;' % (self.border_color.red(),
>> +                                                                                    self.border_color.green(),
>> +                                                                                    self.border_color.blue()))
>> +
>> +    def make_rgba_string(self):
>> +        """
>> +        Build an rgba string from the background color
>> +        """
>> +        text = 'rgba(%d,%d,%d,%1.1f)' % (self.border_color.red(),
>> +                                         self.border_color.green(),
>> +                                         self.border_color.blue(),
>> +                                         self.border_color.alphaF())
>> +        return text
>> +
>> +    def get_image_name(self):
>> +        """
>> +        Get the image file name
>> +        """
>> +        url_name = 'file:///' + self.image_line_edit.text()
>> +        return url_name
>> +
>> +    def get_image_style(self):
>> +        """
>> +        Build a style string based on the dialog selections
>> +        """
>> +        text = 'width: %dpx; height: %dpx;' %\
>> +               (self.width_spin_box.value(),
>> +                self.height_spin_box.value())
>> +        i = self.align_combo_box.currentIndex()
>> +        if i > 0:
>> +            text += self.align_style.Align[i]
>> +            if i == self.align_style.Background:
>> +                text += ' left: 0px; top: 0px; z-index: -1;'
>> +        if self.opacity_horizontal_slider.value() != 100:
>> +            text += ' opacity: %1.1f;' % (self.opacity_horizontal_slider.value() / 100.0)
>> +        if self.shadow_horizontal_slider.value() or self.blur_horizontal_slider.value():
>> +            text += ' box-shadow: 0px 0px %dpx %dpx rgba(0,0,0,0.5);' %\
>> +                    (self.blur_horizontal_slider.value(),
>> +                    self.shadow_horizontal_slider.value())
>> +        if self.top_spin_box.value():
>> +            text += ' margin-top: %dpx;' % self.top_spin_box.value()
>> +        if self.right_spin_box.value():
>> +            text += ' margin-right: %dpx;' % self.right_spin_box.value()
>> +        if self.bottom_spin_box.value():
>> +            text += ' margin-bottom: %dpx;' % self.bottom_spin_box.value()
>> +        if self.left_spin_box.value():
>> +            text += ' margin-left: %dpx;' % self.left_spin_box.value()
>> +        i = self.border_type_combo_box.currentIndex()
>> +        if i > 0:
>> +            text += '%s border-width: %dpx; border-color: %s;' %\
>> +                    (self.border_style.Border[i],
>> +                     self.border_width_horizontal_slider.value(),
>> +                     self.make_rgba_string())
>> +            if self.radius_horizontal_slider.value():
>> +                text += ' border-radius: %dpx;' % self.radius_horizontal_slider.value()
>> +        text += ' max-width: %dpx; max-height: %dpx;' %\
>> +                (self.max_width,
>> +                 self.max_height)
>> +        return text
>> +
>> +    def get_tag(self):
>> +        """
>> +        Returns the tag text when using the tag editor
>> +        """
>> +        text = '{img}"%s" style="%s"{/img}' % (self.get_image_name(), self.get_image_style())
>> +        return text
>> +
>> +    def open_image(self, img_name):
>> +        """
>> +        Open the image and retrieve its properties
>> +
>> +        :param img_name:
>> +        """
>> +        file_name = QtCore.QUrl(img_name).toLocalFile()
>> +        self.last_path = QtCore.QFileInfo(file_name).absolutePath()
>> +        img = QtGui.QImage(file_name)
>> +        if img.isNull():
>> +            msg = translate('CustomPlugin.EditImageForm',
>> +                            'Unable to open the image:') + '\n\n' + file_name
>> +            critical_error_message_box(title=translate('CustomPlugin.EditImageForm', 'Image Open Error'), message=msg)
>> +        else:
>> +            self.enable_dialog(file_name, img)
>> +        return not img.isNull()
>> +
>> +    def enable_dialog(self, file_name, img):
>> +        """
>> +        Enable the dialog widgets since we have a valid image
>> +        """
>> +        thumbnail = img.scaled(Qt.QSize(48, 48), Qt.Qt.KeepAspectRatio)
>> +        self.thumbnail_label.setPixmap(QtGui.QPixmap().fromImage(thumbnail))
>> +        self.thumbnail_label.show()
>> +        self.image_line_edit.setText(file_name)
>> +        self.image_width = img.width()
>> +        self.image_height = img.height()
>> +        self.size_group_box.setEnabled(True)
>> +        self.style_group_box.setEnabled(True)
>> +        self.spacing_group_box.setEnabled(True)
>> +        self.border_group_box.setEnabled(True)
>> +        self.on_reset_push_button()
>> +        self.on_align_combo_box()
>> +        self.on_border_type_combo_box()
>> +
>> +    def validate(self):
>> +        """
>> +        Verify that image has been selected and fits into slide text area
>> +
>> +        :return True if valid
>> +        """
>> +        valid = True
>> +        if not self.image_line_edit.text():
>> +            valid = False
>> +            msg = translate('CustomPlugin.EditImageForm', 'Select an image file.')
>> +            critical_error_message_box(title=translate('CustomPlugin.EditImageForm', 'No Image Selected'),
>> +                                       message=msg)
>> +            self.image_push_button.setFocus()
>> +        else:
>> +            if self.border_type_combo_box.currentIndex() > 0:
>> +                border = self.border_width_horizontal_slider.value() * 2
>> +            else:
>> +                border = 0
>> +            total_width = (self.width_spin_box.value() +
>> +                           border +
>> +                           self.left_spin_box.value() +
>> +                           self.right_spin_box.value())
>> +            total_height = (self.height_spin_box.value() +
>> +                            border +
>> +                            self.top_spin_box.value() +
>> +                            self.bottom_spin_box.value())
>> +            if total_width > self.max_width or total_height > self.max_height:
>> +                valid = False
>> +                msg = translate('CustomPlugin.EditImageForm', 'The image is too large for the slide area.\n\n'
>> +                                                              'Width: %d    Maximum: %d\n'
>> +                                                              'Height: %d    Maximum: %d\n\n'
>> +                                                              '(spacing and border width included)' %
>> +                                                              (total_width, self.max_width,
>> +                                                               total_height, self.max_height))
>> +                critical_error_message_box(title=translate('CustomPlugin.EditImageForm', 'Image Size Error'),
>> +                                           message=msg)
>> +        return valid
>> +
>> +    def accept(self):
>> +        """
>> +        Close dialog when validated
>> +        """
>> +        if self.validate():
>> +            QtGui.QDialog.accept(self)
>> +
>> +    @staticmethod
>> +    def parse(text, start_tag, end_tag):
>> +        item = ''
>> +        start = text.find(start_tag)
>> +        if start > -1:
>> +            start += len(start_tag)
>> +            end = text.find(end_tag, start)
>> +            if end > -1:
>> +                item = text[start:end]
>> +                if item.find(';') > -1:     # Did we jump attributes?
>> +                    item = ''
>> +        return item
>> +
>> +    def parse_style(self, style):
>> +        """
>> +        Parse the CSS style string to set the dialog values
>> +
>> +        :param style:
>> +        """
>> +        parse_error = False
>> +        style = ' ' + style
>> +        try:
>> +            text = self.parse(style, ' width: ', 'px;')
>> +            self.width_spin_box.setValue(int(text))
>> +            text = self.parse(style, ' height: ', 'px;')
>> +            self.height_spin_box.setValue(int(text))
>> +            for i, align in enumerate(self.align_style.Align):
>> +                if align:                                                           # Skip over 'None'
>> +                    if i == self.align_style.Center and style.find(' margin-left: auto;') > -1:
>> +                        self.align_combo_box.setCurrentIndex(i)
>> +                        break
>> +                    elif style.find(align) > -1:
>> +                        self.align_combo_box.setCurrentIndex(i)
>> +                        break
>> +            text = self.parse(style, ' opacity: ', ';')
>> +            if text:
>> +                self.opacity_horizontal_slider.setValue(int(float(text) * 100))
>> +            text = self.parse(style, ' box-shadow: 0px 0px', ' rgba')
>> +            if text:
>> +                blur = self.parse(text, ' ', 'px')
>> +                self.blur_horizontal_slider.setValue(int(blur))
>> +                shadow = self.parse(text, 'px ', 'px')
>> +                self.shadow_horizontal_slider.setValue(int(shadow))
>> +            text = self.parse(style, ' margin-top: ', 'px;')
>> +            if text:
>> +                self.top_spin_box.setValue(int(text))
>> +            text = self.parse(style, ' margin-right: ', 'px;')
>> +            if text:
>> +                self.right_spin_box.setValue(int(text))
>> +            text = self.parse(style, ' margin-bottom: ', 'px;')
>> +            if text:
>> +                self.bottom_spin_box.setValue(int(text))
>> +            text = self.parse(style, ' margin-left: ', 'px;')
>> +            if text:
>> +                self.left_spin_box.setValue(int(text))
>> +            for i, border in enumerate(self.border_style.Border):
>> +                if border:                                                          # Skip over 'None'
>> +                    if style.find(border) > -1:
>> +                        self.border_type_combo_box.setCurrentIndex(i)
>> +                        text = self.parse(style, ' border-width: ', 'px;')
>> +                        self.border_width_horizontal_slider.setValue(int(text))
>> +                        border_color = self.parse(style, ' border-color: rgba(', ');')
>> +                        rgba = border_color.split(',')
>> +                        red = int(rgba[0])
>> +                        green = int(rgba[1])
>> +                        blue = int(rgba[2])
>> +                        alpha = float(rgba[3])
>> +                        self.border_color.setRed(red)
>> +                        self.border_color.setGreen(green)
>> +                        self.border_color.setBlue(blue)
>> +                        self.border_color.setAlphaF(alpha)
>> +                        self.set_frame_color()
>> +                        text = self.parse(style, ' border-radius: ', 'px;')
>> +                        if text:
>> +                            self.radius_horizontal_slider.setValue(int(text))
>> +                        break
>> +        except ValueError:
>> +            parse_error = True
>> +        except IndexError:
>> +            parse_error = True
>> +        if parse_error:
>> +            msg = translate('CustomPlugin.EditImageForm',
>> +                            'Error(s) found in the formatting tag. Default values were used.')
>> +            critical_error_message_box(title=translate('CustomPlugin.EditImageForm', 'Invalid Tag'),
>> +                                       message=msg)
>> +
>> +    def on_image_push_button(self):
>> +        """
>> +        Display open file dialog and try to open the image
>> +        """
>> +        dlg_title = translate('CustomPlugin.EditImageForm', 'Select Image')
>> +        dlg_types = translate('CustomPlugin.EditImageForm', 'Images') + ' (*.bmp *.gif *.jpg *.jpeg *.png)'
> Maybe this list of image types should be shared somewhere.
>
>> +        file_dialog = QtGui.QFileDialog(self)
>> +        file_name = file_dialog.getOpenFileName(self, dlg_title, self.last_path, dlg_types)
>> +        if file_name:
>> +            self.last_path = QtCore.QFileInfo(file_name).absolutePath()
>> +            img = QtGui.QImage(file_name)
>> +            if img.isNull():
>> +                msg = translate('CustomPlugin.EditImageForm', 'The image cannot be opened.')
>> +                critical_error_message_box(title=translate('CustomPlugin.EditImageForm', 'Image Open Error'),
>> +                                           message=msg)
>> +            else:
>> +                self.enable_dialog(file_name, img)
>> +
>> +    def on_reset_push_button(self):
>> +        """
>> +        Sets the size spin boxes to the actual image size
>> +        """
>> +        checked = self.proportional_check_box.isChecked()
>> +        if checked:
>> +            self.proportional_check_box.setCheckState(Qt.Qt.Unchecked)            # Disable scaling
>> +        self.width_spin_box.setValue(self.image_width)
>> +        self.height_spin_box.setValue(self.image_height)
>> +        if checked:
>> +            self.proportional_check_box.setCheckState(Qt.Qt.Checked)              # Restore scaling
>> +
>> +    def on_color_push_button(self):
>> +        color_dialog = QtGui.QColorDialog()
>> +        new_color = color_dialog.getColor(self.border_color, self, '', QtGui.QColorDialog.ShowAlphaChannel)
>> +        if new_color.isValid():
>> +            self.border_color = new_color
>> +            self.set_frame_color()
>> +
>> +    def on_align_combo_box(self):
>> +        """
>> +        Set the enabled status of the widgets based on the align selection
>> +        """
>> +        self.left_spin_box.setEnabled(True)
>> +        self.right_spin_box.setEnabled(True)
>> +        self.top_spin_box.setEnabled(True)
>> +        self.bottom_spin_box.setEnabled(True)
>> +        if self.align_combo_box.currentIndex() == self.align_style.Background:
>> +            self.right_spin_box.setValue(0)
>> +            self.right_spin_box.setEnabled(False)
>> +            self.bottom_spin_box.setValue(0)
>> +            self.bottom_spin_box.setEnabled(False)
>> +        elif self.align_combo_box.currentIndex() == self.align_style.Center:
>> +            self.left_spin_box.setValue(0)
>> +            self.left_spin_box.setEnabled(False)
>> +            self.right_spin_box.setValue(0)
>> +            self.right_spin_box.setEnabled(False)
>> +
>> +    def on_border_type_combo_box(self):
>> +        """
>> +        Set the enabled status of the width spin box and color button based on border selection
>> +        """
>> +        enable = self.border_type_combo_box.currentIndex() > 0
>> +        self.border_width_horizontal_slider.setEnabled(enable)
>> +        self.border_color_push_button.setEnabled(enable)
>> +        self.radius_horizontal_slider.setEnabled(enable)
>> +
>> +    def on_width_spin_box(self):
>> +        """
>> +        Set the height as a ratio of original size
>> +        """
>> +        if self.proportional_check_box.isChecked():
>> +            self.height_spin_box.valueChanged.disconnect()
>> +            ratio = self.image_height / self.image_width
>> +            self.height_spin_box.setValue(int(self.width_spin_box.value() * ratio))
>> +            self.height_spin_box.valueChanged.connect(self.on_height_spin_box)
>> +
>> +    def on_height_spin_box(self):
>> +        """
>> +        Set the width as a ratio of original size
>> +        """
>> +        if self.proportional_check_box.isChecked():
>> +            self.width_spin_box.valueChanged.disconnect()
>> +            ratio = self.image_width / self.image_height
>> +            self.width_spin_box.setValue(int(self.height_spin_box.value() * ratio))
>> +            self.width_spin_box.valueChanged.connect(self.on_width_spin_box)
>> +
>> +    def on_proportional_check_box(self):
>> +        """
>> +        Change the image height when checked
>> +        """
>> +        if self.proportional_check_box.isChecked():
>> +            if self.width_spin_box.value() != self.image_width | self.height_spin_box.value() != self.image_height:
>> +                self.on_width_spin_box()
>>
>> === added file 'openlp/plugins/custom/lib/htmleditor.py'
>> --- openlp/plugins/custom/lib/htmleditor.py	1970-01-01 00:00:00 +0000
>> +++ openlp/plugins/custom/lib/htmleditor.py	2014-06-19 13:47:40 +0000
>> @@ -0,0 +1,1768 @@
>> +# -*- coding: utf-8 -*-
>> +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
>> +
>> +###############################################################################
>> +# OpenLP - Open Source Lyrics Projection                                      #
>> +# --------------------------------------------------------------------------- #
>> +# Copyright (c) 2008-2014 Raoul Snyman                                        #
>> +# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan      #
>> +# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub,      #
>> +# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer.   #
>> +# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru,          #
>> +# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith,             #
>> +# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock,              #
>> +# Frode Woldsund, Martin Zibricky, Patrick Zimmermann                         #
>> +# --------------------------------------------------------------------------- #
>> +# 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:`~openlp.plugins.custom.lib.htmleditor` module provides a wysiwig editor for the editing of custom slides,
>> +and adds support for images. The editor is implemented with QWebView using the contenteditable=true attribute.
>> +
>> +Known issues:
>> +    - Spellchecking not implemented
>> +    - Strange highlighting after editing some blocks (selecting/deselecting block removes artifacts)
>> +    - Using the scrollbar loses text input focus
>> +"""
>> +
>> +import logging
>> +
>> +from PyQt4 import QtCore, QtGui, QtWebKit
>> +from PyQt4.QtGui import QStyle
>> +from openlp.core import Registry
>> +from openlp.core.lib import translate, FormattingTags
>> +from openlp.core.lib.ui import critical_error_message_box
>> +from openlp.core.lib.htmlbuilder import build_background_css, build_lyrics_css
>> +from openlp.plugins.custom.forms.editimageform import EditImageForm
>> +
>> +log = logging.getLogger(__name__)
>> +
>> +
>> +class HtmlEditor(QtWebKit.QWebView):
>> +    """
>> +    A custom slide editor using QWebView in editing mode
>> +    """
>> +    def __init__(self, parent=None):
>> +        """
>> +        Constructor
>> +        """
>> +        super(HtmlEditor, self).__init__(parent)
>> +        self.frame = self.page().mainFrame()
>> +        self.java = self.frame.evaluateJavaScript
>> +        self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
>> +        self.setAcceptDrops(False)
>> +        self.setUrl(QtCore.QUrl('about:blank'))
>> +        # self.page().settings().setAttribute(QtWebKit.QWebSettings.DeveloperExtrasEnabled, True)
>> +        # self.inspector = QtWebKit.QWebInspector(self)
>> +        # self.inspector.setPage(self.page())
>> +        # self.inspector.show()
>> +        self.clipboard = Registry().get('main_window').clipboard
>> +        self.clipboard_owned = False                    # True if the clipboard data is ours
>> +        self.zoom = 0.0                                 # Default to window width
>> +        self.background = 'Theme'                       # Default to theme background
>> +        self.background_color = QtGui.QColor(QtCore.Qt.white)  # Default to white background override color
>> +        self.screen_width = 0                           # Screen width - set from the ServiceItem in set_text
>> +        self.screen_height = 0                          # Screen Height - ''
>> +        self.main_width = 0                             # Text area width - ''
>> +        self.main_height = 0                            # Text area height - ''
>> +        self.max_image_width = 0                        # Maximum image width for current theme
>> +        self.max_image_height = 0                       # Maximum image height for current theme
>> +        self.marker = '!mArKeR!'                        # Strange characters used to identify insert position
>> +        self.last_id = 0                                # The last element id assigned
>> +        self.typing = False                             # Used as a semaphore to ignore selection changed when typing
>> +        self.image_selected = None                      # The image that has focus
>> +        self.active_slide = None                        # The slide div that has focus
>> +        self.active_text = None                         # The text div that has focus
>> +        self.text_undo_command = None                   # The currently active text undo command
>> +        self.undo_stack = QtGui.QUndoStack(self)        # Use our own undo stack instead of the QWebView's
>> +        self.edit_image_form = EditImageForm(self)      # Image dialog form
>> +        # Signals and slots
>> +        self.connect(self, QtCore.SIGNAL('selectionChanged()'), self.on_selection_changed)
>> +        self.connect(self, QtCore.SIGNAL('customContextMenuRequested(const QPoint&)'),
>> +                     self.customContextMenuRequested)
>> +        self.connect(self.clipboard, QtCore.SIGNAL('dataChanged()'), self.on_clipboard_changed)
>> +
>> +    def insert_data_tag(self, tag, html):
>> +        """
>> +        Insert the data-tag into the start html string from FormattingTags
>> +
>> +        :param tag: The formatting {tag} string
>> +        :param html: The start html string
>> +        :return The start html string with id and data-tag="{tag}" inserted
>> +        """
>> +        self.last_id += 1
>> +        tag_text = u' id="elem%d" data-tag="%s"' % (self.last_id, tag)
>> +        if html.find(u' ') > -1:
>> +            html = html.replace(u' ', tag_text + u' ', 1)
>> +        else:
>> +            html = html.replace(u'>', tag_text + u'>', 1)
>> +        return html
>> +
>> +    def remove_empty_tags(self, slide_text):
>> +        """
>> +        Iterate through slide text to remove empty tags, both simple and nested
>> +
>> +        :param slide_text: A string containing the slide contents in text format
>> +        :return slide contents without empty tags
>> +        """
>> +        empty_tags = []
>> +        for html in FormattingTags.get_html_tags():
>> +            if html['end tag']:
>> +                empty_tags.append(html['start tag'] + html['end tag'])
>> +        empty_found = True
>> +        while empty_found:
>> +            empty_found = False
>> +            for empty_tag in empty_tags:
>> +                if slide_text.find(empty_tag) > -1:
>> +                    slide_text = slide_text.replace(empty_tag, '')
>> +                    empty_found = True
>> +                    break
>> +        return slide_text
>> +
>> +    def text_to_html(self, text):
>> +        """
>> +        Convert special characters, and replace formatting tags with html
>> +
>> +        :param text: One or more slides in plain text with formatting tags
>> +        :return The slide text in html format wrapped in divs
>> +        """
>> +        if not text:    # Build an empty slide
>> +            return self.build_slide_wrapper('')
>> +        text = self.remove_empty_tags(text)
>> +        text = text.replace(u'<', u'&lt;')
>> +        text = text.replace(u'>', u'&gt;')
>> +        slides_html = ''
>> +        slides = text.split(u'\n[===]\n')
>> +        for slide in slides:
>> +            slide = slide.replace(u'\n', u'<br />')
>> +            for html in FormattingTags.get_html_tags():
>> +                start_pos = 0
>> +                while True:
>> +                    start_pos = slide.find(html['start tag'], start_pos)
>> +                    if start_pos == -1:
>> +                        break
>> +                    start_html = self.insert_data_tag(html['start tag'], html['start html'])
>> +                    slide = slide[0:start_pos] + start_html + slide[start_pos + len(html['start tag']):]
>> +                    start_pos += len(start_html)
>> +                if html['end tag']:
>> +                    if html['end tag'] == '{/img}':                 # add max-width and height to style string
>> +                        slide = slide.replace('"{/img}', ' max-width: %dpx; max-height: %dpx;" />' %
>> +                                              (self.max_image_width, self.max_image_height))
>> +                    else:
>> +                        slide = slide.replace(html['end tag'], html['end html'])
>> +            slides_html += self.build_slide_wrapper(slide)
>> +        return slides_html
>> +
>> +    def build_slide_wrapper(self, slide_html):
>> +        """
>> +        Create the div wrappers for a slide
>> +
>> +        :param slide_html: The slide content in html format
>> +        :return Wrapped slide content
>> +        """
>> +        self.last_id += 1
>> +        html = u'<div class="slide" id="slide%d">' % self.last_id
>> +        html += u'<a href="#slide%d"></a><div class="text_table">' % self.last_id
>> +        html += u'<div class="lyricscell lyricsmain" id="text%d" contenteditable="true">' % self.last_id
>> +        html += u'%s</div></div></div>' % slide_html
>> +        return html
>> +
>> +    def build_background_css(self, item):
>> +        """
>> +        Build the background css - use htmlbuilder unless we have an image or a transparency
>> +
>> +        :param item: ServiceItem
>> +        :return  The background css statement
>> +        """
>> +        if item.theme_data.background_type == 'image':
>> +            url_filename = QtCore.QUrl.fromLocalFile(item.theme_data.background_filename)
>> +            background = u"background: %s url('%s') repeat-y" % (item.theme_data.background_border_color,
>> +                                                                 url_filename.toString())
>> +        elif item.theme_data.background_type == 'transparent':
>> +            background = u'background-color: transparent'
>> +        else:
>> +            background = build_background_css(item, item.renderer.width)
>> +        return background
>> +
>> +    def set_text(self, text, item):
>> +        """
>> +        Build the html to display the custom slide in wysiwyg mode
>> +
>> +        :param text: The slide text (unicode)
>> +        :param item: ServiceItem
>> +        :return  The complete html
>> +        """
>> +        self.undo_stack.clear()
>> +        self.last_id = 0
>> +        self.background = 'Theme'
>> +        self.screen_width = item.renderer.width
>> +        self.screen_height = item.renderer.height
>> +        if item.theme_data.font_main_override:
>> +            self.main_width = item.theme_data.font_main_width -\
>> +                item.theme_data.font_main_outline_size -\
>> +                item.theme_data.font_main_shadow_size
>> +            self.main_height = item.theme_data.font_main_height -\
>> +                item.theme_data.font_main_outline_size -\
>> +                item.theme_data.font_main_shadow_size
>> +        else:
>> +            self.main_width = item.main.width() - 1
>> +            self.main_height = item.main.height() - 1
>> +        self.max_image_width = self.main_width
>> +        self.max_image_height = self.main_height - (item.theme_data.font_main_size * 2)
>> +        html = u"""<!DOCTYPE html>
>> +<html>
>> +<head>
>> +    <style>
>> +        * {
>> +            margin: 0;
>> +            padding: 0;
>> +            border: 0;
>> +        }
>> +        [contenteditable="true"]:focus {
>> +            outline: none;
>> +        }
>> +        sup {
>> +            font-size: 0.6em;
>> +            vertical-align: top;
>> +            position: relative;
>> +            top: -0.3em;
>> +        }
>> +        .slides {
>> +            width: %dpx;
>> +            min-height: %dpx;
>> +            margin: 0px;
>> +            padding: 0px;
>> +            z-index: -5;
>> +        }
>> +        .slide {
>> +            width: %dpx;
>> +            min-height: %dpx;
>> +            %s;                             /* background or background-color */
>> +            margin: 10px;
>> +            padding: %dpx 0px 0px %dpx;
>> +            outline: black solid 1px;
>> +            z-index: -4;
>> +            overflow-x: hidden;
>> +        }
>> +        %s                                  /* build_lyrics_css from htmlbuilder */
>> +        .lyricscell {                       /* override */
>> +            -webkit-transition: none;
>> +            z-index: 5;
>> +            position:relative;
>> +            top: 0px;
>> +        }
>> +        .text_table {                       /* use this in place of lyricstable */
>> +            display: table;
>> +            /* background: transparent url('file:///c:/users/ace/desktop/custom_overflow.png') no-repeat; */
>> +            /* background: transparent url(':/custom/custom_overflow.png') no-repeat; */
>> +            background: transparent url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAA1JREFUCNdjaGhoqAcABQQCALTD+iYAAAAASUVORK5CYII=') no-repeat;
>> +            background-position: 0px %dpx;
>> +            background-size: cover;
>> +            min-height: %dpx;
>> +            overflow: hidden;
>> +        }
>> +    </style>
>> +    <script type="text/javascript">
>> +        function setFocus(id) {
>> +            /*
>> +            Set focus to element with id - QWebElement.setFocus() does NOT give keyboard focus (blinking caret)
>> +            */
>> +            document.getElementById(id).focus();
>> +        }
>> +        function insertHTML(html) {
>> +            /*
>> +            Insert html string at caret/selection
>> +            */
>> +            document.execCommand('insertHTML', false, html);
>> +        }
>> +        function getSlideId() {
>> +            /*
>> +            Get the slide id of the slide that is being edited
>> +            */
>> +            var slide = window.getSelection().anchorNode;
>> +            while (slide) {
>> +                if (slide.className == 'slide') {
>> +                    return slide.id;
>> +                }
>> +                slide = slide.parentNode;
>> +            }
>> +            return 'None';
>> +        }
>> +        function mergeSlide(currSlideId) {
>> +            /*
>> +            Add the contents of the current slide to the end of the previous and delete current slide
>> +            */
>> +            var slides = document.getElementById('slides');
>> +            var currSlide = document.getElementById(currSlideId);
>> +            var prevSlide = currSlide.previousSibling;
>> +            var prevText = prevSlide.lastChild.firstChild;
>> +            var sel = window.getSelection();
>> +            sel.modify('move', 'backward', 'documentboundary');     // select the entire slide contents
>> +            sel.modify('extend', 'forward', 'documentboundary');
>> +            var range = sel.getRangeAt(0);
>> +            var frag = range.extractContents();                     // store removed selection in a fragment
>> +            window.location.hash = '#' + prevSlide.id;              // scroll to previous slide
>> +            prevText.focus();
>> +            sel = window.getSelection();
>> +            sel.modify('move', 'forward', 'documentboundary');      // move insertion point to end of slide
>> +            sel.getRangeAt(0).insertNode(frag);                     // paste the fragment into new slide
>> +            slides.removeChild(currSlide);                          // delete the current slide
>> +        }
>> +        function walkTheDOM(node, func) {
>> +            /*
>> +            Recursive DOM walker - based on code from Douglas Crockford
>> +            */
>> +            func(node);
>> +            node = node.firstChild;
>> +            while (node) {
>> +                walkTheDOM(node, func);
>> +                node = node.nextSibling;
>> +            }
>> +        }
>> +        function removeEmptyTags(slideNode) {
>> +            /*
>> +            Remove all elements that have empty text nodes
>> +            */
>> +            var elemIds = [];
>> +            walkTheDOM(slideNode, function (node) {
>> +                if (node.nodeType == 1 && node.hasAttribute('data-tag') &&
>> +                    node.tagName != 'IMG' && node.textContent == '') {
>> +                    elemIds.push(node.id);
>> +                }
>> +            });
>> +            for (var i=0;i<elemIds.length;i++) {
>> +                var elem = document.getElementById(elemIds[i]);
>> +                if (elem) {
>> +                    elem.parentNode.removeChild(elem);
>> +                }
>> +            }
>> +        }
>> +        function insertSlide(wrapper, id) {
>> +            /*
>> +            Split the current slide at the insertion point and create new slide element structure
>> +            */
>> +            var slides = document.getElementById('slides');         // parent div for all slides
>> +            var sel = window.getSelection();
>> +            var slide = sel.anchorNode;
>> +            var pathIds = [];
>> +            var pathTags = [];
>> +            while (slide) {                                         // find the current slide
>> +                if (slide.className == 'slide') {
>> +                    break;
>> +                }
>> +                if (slide.nodeType == 1 && slide.hasAttribute('data-tag')) {
>> +                    pathIds.push(slide.id);                         // store the path id's
>> +                    pathTags.push(slide.getAttribute('data-tag'));  // store the path tags
>> +                }
>> +                slide = slide.parentNode;
>> +            }
>> +            sel.modify('extend', 'forward', 'documentboundary');    // select to the end of current slide
>> +            var selText = sel.toString();
>> +            var range = sel.getRangeAt(0);
>> +            var frag = range.extractContents();                     // store removed selection in a fragment
>> +            removeEmptyTags(slide);                                 // extract may have left elements without text
>> +            var lastId = id;
>> +            if (selText == '') {                                    // if the slide insertion point doesn't have any
>> +                pathTags = [];                                      // text after the caret, do not carry tags forward
>> +            }
>> +            else {
>> +                while (pathIds.length) {                            // determine if the fragment contains path elements
>> +                    var pathId = pathIds.shift();
>> +                    var elem = frag.querySelector('#' + pathId);
>> +                    if (!elem) {                                    // doesn't have element, return the remaining
>> +                        break;                                      // path tags
>> +                    }
>> +                    lastId += 1;
>> +                    elem.id = 'elem' + lastId;                      // set the frag element id to a unique value
>> +                    pathTags.shift();                               // remove the path tag from the open tag list
>> +                }
>> +            }
>> +            var div = document.createElement('div');                // Create a temporary div
>> +            div.innerHTML = wrapper;                                // Put slide structure into div
>> +            slides.insertBefore(div.firstChild, slide.nextSibling); // Insert wrapper
>> +            window.location.hash = '#slide' + id;                   // scroll to new slide
>> +            document.getElementById('text' + id).focus();
>> +            window.getSelection().getRangeAt(0).insertNode(frag);   // paste the fragment into new slide
>> +            return lastId.toString() + '/' + pathTags.toString();
>> +        }
>> +        function cutCopy(doCut) {
>> +            /*
>> +            Cut or copy the current selection and return it and the open formatting tags
>> +            */
>> +            var sel = window.getSelection();
>> +            var slide = sel.anchorNode;
>> +            var pathIds = [];
>> +            var pathTags = [];
>> +            while (slide) {                                         // find the current slide
>> +                if (slide.className == 'slide') {
>> +                    break;
>> +                }
>> +                if (slide.nodeType == 1 && slide.hasAttribute('data-tag') && slide.tagName != 'IMG') {
>> +                    pathIds.push(slide.id);                         // store the path id's
>> +                    pathTags.push(slide.getAttribute('data-tag'));  // store the path tags
>> +                }
>> +                slide = slide.parentNode;
>> +            }
>> +            var range = sel.getRangeAt(0);
>> +            if (doCut) {
>> +                var frag = range.extractContents();
>> +                removeEmptyTags(slide);                             // Extraction may have left empty tags
>> +            }
>> +            else {                                                  // Clone the selection's contents - we'll take
>> +                var frag = range.cloneContents();                   // care of the duplicate id's in Python
>> +            }
>> +            while (pathIds.length) {                                // determine if the fragment contains path elements
>> +                var pathId = pathIds.shift();                       // FIFO
>> +                var elem = frag.querySelector('#' + pathId);        // search the fragment for the path id
>> +                if (!elem) {                                        // doesn't have element, return the remaining
>> +                    break;                                          // path tags
>> +                }
>> +                pathTags.shift();                                   // path id found, remove the path tag and try again
>> +            }
>> +            var div = document.createElement('div');                // Create an empty div
>> +            div.appendChild(frag);                                  // and append the fragment
>> +            var html = div.innerHTML;                               // Extract the html
>> +            return pathTags.toString() + '/' + html;
>> +        }
>> +        function focusNode(id) {
>> +            /*
>> +            Select the img element
>> +            */
>> +            var elem = document.getElementById(id);
>> +            var range = document.createRange();
>> +            range.selectNode(elem);
>> +            var sel = window.getSelection();
>> +            sel.removeAllRanges();
>> +            sel.addRange(range);
>> +        }
>> +        function saveSelection() {
>> +            /*
>> +            Return a comma delimited string of the current selection, modifying it if it's right-to-left
>> +            */
>> +            var sel = window.getSelection();
>> +            var range = document.createRange();
>> +            range.setStart(sel.anchorNode, sel.anchorOffset);
>> +            range.setEnd(sel.focusNode, sel.focusOffset);
>> +            if (range.collapsed) {                                  // The current selection was right-to-left,
>> +                range.setStart(sel.focusNode, sel.focusOffset);     // change it to a left-to-right so that the
>> +                range.setEnd(sel.anchorNode, sel.anchorOffset);     // range.surroundContents method will work
>> +                sel.removeAllRanges();
>> +                sel.addRange(range);
>> +            }
>> +            else {
>> +                range.detach();
>> +            }
>> +            var saveSel = {};
>> +            var elem = sel.anchorNode;
>> +            if (elem.nodeType == 1) {
>> +                saveSel.anchorPosition = sel.anchorOffset;
>> +                saveSel.anchorOffset = 0;
>> +            }
>> +            else {
>> +                saveSel.anchorPosition = 0;
>> +                saveSel.anchorOffset = sel.anchorOffset;
>> +                while (elem.previousSibling) {
>> +                    saveSel.anchorPosition += 1;
>> +                    elem = elem.previousSibling;
>> +                }
>> +                elem = elem.parentNode;
>> +            }
>> +            saveSel.anchorId = elem.id;
>> +            elem = sel.focusNode;
>> +            if (elem.nodeType == 1) {
>> +                saveSel.focusPosition = sel.focusOffset;
>> +                saveSel.focusOffset = 0;
>> +            }
>> +            else {
>> +                saveSel.focusPosition = 0;
>> +                saveSel.focusOffset = sel.focusOffset;
>> +                while (elem.previousSibling) {
>> +                    saveSel.focusPosition += 1;
>> +                    elem = elem.previousSibling;
>> +                }
>> +                elem = elem.parentNode;
>> +            }
>> +            saveSel.focusId = elem.id;
>> +            return saveSel.anchorId + ',' + saveSel.anchorPosition + ',' + saveSel.anchorOffset + ',' +
>> +                   saveSel.focusId + ',' + saveSel.focusPosition + ',' + saveSel.focusOffset;
>> +        }
>> +        function restoreSelection(anchorId, anchorPosition, anchorOffset, focusId, focusPosition, focusOffset) {
>> +            /*
>> +            Restore the window selection object to a previous state
>> +            */
>> +            var anchorElem = document.getElementById(anchorId);
>> +            anchorElem = anchorElem.firstChild;
>> +            var i = anchorPosition.valueOf();
>> +            while (i > 0) {
>> +                anchorElem = anchorElem.nextSibling;
>> +                i -= 1;
>> +            }
>> +            var focusElem = document.getElementById(focusId);
>> +            focusElem = focusElem.firstChild;
>> +            i = focusPosition.valueOf();
>> +            while (i > 0) {
>> +                focusElem = focusElem.nextSibling;
>> +                i -= 1;
>> +            }
>> +            var sel = window.getSelection();
>> +            var range = document.createRange();
>> +            range.setStart(anchorElem, anchorOffset.valueOf());
>> +            range.setEnd(focusElem, focusOffset.valueOf());
>> +            sel.removeAllRanges();
>> +            sel.addRange(range);
>> +        }
>> +        function insertTag(tagHtml) {
>> +            /*
>> +            Surround the current selection with the formatting tag html.  Operation will fail if the new tag
>> +            spans a partial existing tag
>> +            */
>> +            var range = window.getSelection().getRangeAt(0);
>> +            var frag = range.createContextualFragment(tagHtml);
>> +            try {
>> +                range.surroundContents(frag.firstChild);
>> +            }
>> +            catch(err) {
>> +                return false;
>> +            }
>> +            return true;
>> +        }
>> +        function removeTag(tag) {
>> +            /*
>> +            Search backwards from the current anchor node until the formatting {tag} is found.
>> +            Remove the tag but keep the text and all other child tags
>> +            */
>> +            var elem = window.getSelection().anchorNode;
>> +            while (elem) {
>> +                if (elem.nodeType == 1) {
>> +                    if (elem.getAttribute('data-tag') == tag) {
>> +                        var frag = document.createDocumentFragment();
>> +                        while (elem.firstChild) {
>> +                            frag.appendChild(elem.firstChild);
>> +                        }
>> +                        elem.parentNode.insertBefore(frag, elem);
>> +                        elem.parentNode.removeChild(elem);
>> +                        return;
>> +                    }
>> +                }
>> +                elem = elem.parentNode;
>> +            }
>> +            alert('Tag not found!');        // Should never happen
>> +        }
>> +        function getPath() {
>> +            /*
>> +            Find any open formatting tags before the anchor node and return in comma delimited string
>> +            */
>> +            var path = '';
>> +            var elem = window.getSelection().anchorNode;
>> +            while (elem) {
>> +                if (elem.nodeType == 1 && elem.hasAttribute('data-tag') && elem.tagName != 'IMG') {
>> +                    var attr = elem.getAttribute('data-tag');
>> +                    if (path) {
>> +                        path = attr + ',' + path;
>> +                    }
>> +                    else {
>> +                        path = attr;
>> +                    }
>> +                }
>> +                elem = elem.parentNode;
>> +            }
>> +            return path;
>> +        }
>> +    </script>
>> +</head>
>> +<body style='width: 100%%; height: 100%%; color: %s;'>
>> +    <div id="slides" class="slides">%s</div>
>> +</body>
>> +</html>""" % (self.screen_width + 22,                   # .slides: width = screen width + border + margins
>> +              self.screen_height + 22,                  # .slides: height
>> +              self.screen_width - item.main.left(),     # .slide: width
>> +              self.screen_height - item.main.top(),     # .slide: min-height
>> +              self.build_background_css(item),          # .slide: background or background-color
>> +              item.main.top(),                          # .slide: padding-top
>> +              item.main.left(),                         # .slide: padding-left
>> +              build_lyrics_css(item),                   # .lyricstable, .lyricscell, .lyricsmain from htmlbuilder
>> +              item.main.bottom(),                       # .text_table: background-position (top) of overflow image
>> +              self.main_height,                         # .text_table: min-height
>> +              item.theme_data.font_main_color,          # body color used for setting caret color
>> +              self.text_to_html(text))                  # slide text
>> +        self.set_zoom()
>> +        self.setHtml(html)
>> +        elem = self.frame.findFirstElement('.lyricscell.lyricsmain')
>> +        self.java('setFocus("%s")' % elem.attribute('id'))
>> +
>> +    def get_text(self):
>> +        """
>> +        Convert all of the slides from html to plain text with formatting tags
>> +
>> +        return: A string containing all slides in plain text
>> +        """
>> +        text = ''
>> +        slides = self.frame.documentElement().findAll('div.lyricscell.lyricsmain').toList()
>> +        for slide in slides:
>> +            if text:
>> +                text += u'\n[===]\n'                        # create an insert marker for every slide after the first
>> +            while True:
>> +                elem = slide.findFirst(u'[data-tag]')       # find a QWebElement with a data-tag attribute
>> +                if elem.isNull():
>> +                    break
>> +                tag = elem.attribute(u'data-tag')           # get the tag name from the data-tag
>> +                if tag == u'{img}':                         # replace <img src="filename" /> with {img}filename{/img}
>> +                    img_style = elem.attribute('style')
>> +                    img_style = img_style[:img_style.find(u' max-width:')]    # strip out max-width and height
>> +                    tag_str = u'<span>{img}"%s" style="%s"{/img}</span>' %\
>> +                              (elem.attribute('src'), img_style)
>> +                    elem.setOuterXml(tag_str)
>> +                else:                                       # replace <?>xxx</?> with {tag}xxx{/tag}
>> +                    tag_str = u'%s%s{/%s' % (tag, elem.toInnerXml(), tag[1:])
>> +                    elem.replace(tag_str)
>> +            text += slide.toPlainText()
>> +        return text
>> +
>> +    def get_path(self):
>> +        """
>> +        Get formatting tags in path from the selection anchor node
>> +
>> +        :return str: formatting tags delimited with commas
>> +        """
>> +        return str(self.java('getPath()'))
>> +
>> +    def do_split(self):
>> +        """
>> +        Insert our marker into current selection and check resulting text to determine if it is surrounded by line
>> +        breaks.  Replace marker with [---] optional split.
>> +        """
>> +        self.java('insertHTML("%s")' % self.marker)
>> +        html = self.active_text.toInnerXml()
>> +        text = self.active_text.toPlainText()
>> +        if text.find(u'\n' + self.marker) == -1:
>> +            html = html.replace(self.marker, u'<br />' + self.marker)
>> +        if text.find(self.marker + u'\n') == -1:
>> +            html = html.replace(self.marker, self.marker + u'<br />')
>> +        html = html.replace(self.marker, u'[---]')
>> +        self.active_text.setInnerXml(html)
>> +        self.java('setFocus("%s")' % self.active_text.attribute('id'))
>> +
>> +    def split(self):
>> +        """
>> +        Create UndoCommand and do_split()
>> +        """
>> +        cmd = MethodUndoCommand(self, self.do_split, translate('CustomPlugin.Editor', 'Split Slide'))
>> +        self.undo_stack.push(cmd)
>> +
>> +    def do_insert(self):
>> +        """
>> +        Split the slide at the current selection and create a new slide.  Scroll new slide into view and sets focus.
>> +        Note:  Open formatting tags are added to the new slide
>> +        """
>> +        wrapper = self.build_slide_wrapper('')                      # create an empty slide structure
>> +        save_id = self.last_id                                      # Save the id of the new slide
>> +        result = str(self.java("""insertSlide('%s', %d)""" %        # insert new slide into document
>> +                     (wrapper, self.last_id)))
>> +        results = result.split('/', 1)
>> +        self.last_id = int(results[0])                              # javascript function may have modified
>> +        path = results[1]
>> +        if path:                                                    # Open formatting tags found
>> +            path_tags = path.split(',')
>> +            new_tags_start = ''
>> +            new_tags_end = ''
>> +            for tag in path_tags:
>> +                for html in FormattingTags.get_html_tags():
>> +                    if tag == html['start tag']:
>> +                        new_tags_start += self.insert_data_tag(html['start tag'], html['start html'])
>> +                        new_tags_end += html['end html']
>> +                        break
>> +            slide = self.frame.findFirstElement(u'#text%d' % save_id)
>> +            slide_html = new_tags_start + slide.toInnerXml() + new_tags_end
>> +            slide.setInnerXml(slide_html)
>> +            self.java('setFocus("elem%d")' % save_id)
>> +        if self.background != 'Theme':
>> +            self.active_slide.setStyleProperty(u'background', self.background)
>> +
>> +    def insert(self):
>> +        """
>> +        Create UndoCommand and do_insert()
>> +        """
>> +        cmd = MethodUndoCommand(self, self.do_insert, translate('CustomPlugin.Editor', 'Insert Slide'))
>> +        self.undo_stack.push(cmd)
>> +
>> +    def set_zoom(self):
>> +        """
>> +        Set the zoom factor of the editor
>> +        """
>> +        if self.zoom == 0.0:                                        # set zoom to fill the body of the QWebView
>> +            ratio_x = round(((self.width() - QStyle.PM_ScrollBarExtent - 10) /
>> +                             (self.screen_width + 22 + QStyle.PM_ScrollBarExtent)) - 0.004, 2)
>> +            if self.zoomFactor() != ratio_x:
>> +                self.setZoomFactor(ratio_x)
>> +        else:
>> +            self.setZoomFactor(self.zoom)
>> +        self.selectionChanged.emit()                                # Update the zoom label
>> +
>> +    def get_text_div(self, slide_div):
>> +        """
>> +        Return the text div wrapper element for a given slide with an id of 'slideXXX'
>> +
>> +        :param QWebElement slide_div: The slide div wrapper
>> +        :return QWebElement The slide text wrapper
>> +        """
>> +        text_id = u'#text' + slide_div.attribute('id')[5:]          # Skip over 'slide' to get numeric id
>> +        text_div = self.frame.findFirstElement(text_id)
>> +        return text_div
>> +
>> +    def image_clicked(self, ctrl_key_down, pos_x, pos_y):
>> +        """
>> +        Determine if the mouse click was on an img element.  If so, focus the img node
>> +
>> +        :param ctrl_key_down: True if CTRL was pushed
>> +        :param pos_x: from event.pos().x()
>> +        :param pos_y: from event.pos().y()
>> +        :return True if image under cursor
>> +        """
>> +        img_list = self.frame.findAllElements('img').toList()
>> +        rel_x = self.frame.scrollBarValue(QtCore.Qt.Horizontal) + pos_x
>> +        rel_y = self.frame.scrollBarValue(QtCore.Qt.Vertical) + pos_y
>> +        self.image_selected = None
>> +        for img in img_list:
>> +            if img.geometry().contains(rel_x, rel_y, True):
>> +                if img.attribute('style').find('position: absolute;') > -1:     # We have a background image
>> +                    if not ctrl_key_down:
>> +                        break                                                   # Only select if Ctrl key is down
>> +                self.java('focusNode("%s")' % img.attribute('id'))
>> +                self.image_selected = img
>> +                break
>> +        return self.image_selected
>> +
>> +    def has_background_image(self):
>> +        """
>> +        Determine if the current slide has a background image
>> +        """
>> +        if self.active_text:
>> +            return self.active_text.toInnerXml().find('position: absolute;') > -1
>> +        else:
>> +            return False
>> +
>> +    def on_selection_changed(self):
>> +        """
>> +        Set the QWebElements active_slide and active_text to point to the div wrappers of the current selection anchor
>> +        """
>> +        if not self.typing:
>> +            self.text_undo_command = None       # Any typing must use a new text undo command after a selection change
>> +        slide_id = str(self.java('getSlideId()'))
>> +        if slide_id == 'None':
>> +            self.active_slide = None
>> +            self.active_slide = None
>> +        else:
>> +            self.active_slide = self.frame.findFirstElement('#' + slide_id)
>> +            self.active_text = self.get_text_div(self.active_slide)
>> +
>> +    def on_clipboard_changed(self):
>> +        """
>> +        Set the ownership flag to False - only paste HTML if we put it there
>> +        """
>> +        self.clipboard_owned = False
>> +
>> +    def keyPressEvent(self, event):
>> +        """
>> +        Intercept key press - send only the navigation key events to the web view
>> +        """
>> +        if event.matches(QtGui.QKeySequence.Cut):
>> +            self.on_cut(translate('CustomPlugin.Editor', 'Cut'))
>> +        elif event.matches(QtGui.QKeySequence.Copy):
>> +            self.on_copy()
>> +        elif event.matches(QtGui.QKeySequence.Paste):
>> +            self.on_paste_html(translate('CustomPlugin.Editor', 'Paste HTML'))
>> +        elif event.matches(QtGui.QKeySequence.SelectAll):
>> +            self.on_select_all()
>> +        elif event.matches(QtGui.QKeySequence.Delete):
>> +            save_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress, event.key(), event.modifiers(), event.text())
>> +            cmd = MethodUndoCommand(self, QtWebKit.QWebView.keyPressEvent,
>> +                                    translate('CustomPlugin.Editor', 'Delete'), self, save_event)
>> +            self.undo_stack.push(cmd)
>> +        elif event.matches(QtGui.QKeySequence.Back):
>> +            save_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress, event.key(), event.modifiers(), event.text())
>> +            cmd = MethodUndoCommand(self, QtWebKit.QWebView.keyPressEvent,
>> +                                    translate('CustomPlugin.Editor', 'Backspace'), self, save_event)
>> +            self.undo_stack.push(cmd)
>> +        elif event.matches(QtGui.QKeySequence.Undo):
>> +            self.undo_stack.undo()
>> +        elif event.matches(QtGui.QKeySequence.Redo):
>> +            self.undo_stack.redo()
>> +        elif event.key() in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
>> +            # Set the shift key to force a <br> insertion
>> +            save_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress, QtCore.Qt.Key_Return, QtCore.Qt.ShiftModifier, '\n')
>> +            cmd = MethodUndoCommand(self, QtWebKit.QWebView.keyPressEvent,
>> +                                    translate('CustomPlugin.Editor', 'New Line'), self, save_event)
>> +            self.undo_stack.push(cmd)
>> +        elif event.key() == QtCore.Qt.Key_PageUp:
>> +            # Handle this ourselves to prevent crash
>> +            self.frame.scroll(0, -self.height())
>> +        elif event.key() == QtCore.Qt.Key_PageDown:
>> +            # Handle this ourselves to prevent crash
>> +            self.frame.scroll(0, self.height())
>> +        elif event.text():
>> +            text = event.text()
>> +            if text == '<':
>> +                text = u'&lt;'
>> +            elif text == '>':
>> +                text = u'&gt;'
>> +            elif text == "'":
>> +                text = u'&#39;'
>> +            elif text == '"':
>> +                text = u'&quot;'
>> +            if text == ' ' and self.text_undo_command:
>> +                self.text_undo_command = None               # Don't append text on a word break
>> +            if self.text_undo_command:
>> +                self.text_undo_command.append_text(text)
>> +            else:
>> +                self.text_undo_command = TextUndoCommand(self, translate('CustomPlugin.Editor', 'Typing'), text)
>> +                self.undo_stack.push(self.text_undo_command)
>> +        elif event.key() in (QtCore.Qt.Key_Left, QtCore.Qt.Key_Right, QtCore.Qt.Key_Up, QtCore.Qt.Key_Down,
>> +                             QtCore.Qt.Key_Home, QtCore.Qt.Key_End):
>> +            QtWebKit.QWebView.keyPressEvent(self, event)
>> +
>> +    def resizeEvent(self, event):
>> +        """
>> +        Window size changed, the zoom factor may need changing
>> +        """
>> +        QtWebKit.QWebView.resizeEvent(self, event)
>> +        if self.screen_width > 0:
>> +            self.set_zoom()
>> +
>> +    def mousePressEvent(self, event):
>> +        """
>> +        Handle mouse clicks within the QWebView
>> +        """
>> +        if self.frame.scrollBarGeometry(QtCore.Qt.Vertical).contains(event.pos()) or\
>> +           self.frame.scrollBarGeometry(QtCore.Qt.Horizontal).contains(event.pos()):
>> +            # ToDo: The scrollbar was clicked - need a solution for the caret which has stopped blinking...
>> +            pass
>> +        elif self.image_clicked(event.modifiers() & QtCore.Qt.CTRL, event.pos().x(), event.pos().y()):
>> +            # An image was clicked, don't send mouse press to parent
>> +            return
>> +        elif 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)
>> +        QtWebKit.QWebView.mousePressEvent(self, event)
>> +
>> +    def wheelEvent(self, event):
>> +        """
>> +        Override the event to create a page scroll.  Without the override, WebView moves to the top/bottom
>> +        """
>> +        if event.delta() > 0:               # page up
>> +            delta = -self.height()
>> +        else:                               # page down
>> +            delta = self.height()
>> +        self.frame.scroll(0, delta)
>> +
>> +    def customContextMenuRequested(self, pos):
>> +        popup_menu = QtGui.QMenu(self)
>> +        # Undo/Redo
>> +        action = self.undo_stack.createUndoAction(popup_menu)
>> +        action.setShortcut(QtGui.QKeySequence.Undo)
>> +        popup_menu.addAction(action)
>> +        action = self.undo_stack.createRedoAction(popup_menu)
>> +        action.setShortcut(QtGui.QKeySequence.Redo)
>> +        popup_menu.addAction(action)
>> +        popup_menu.addSeparator()
>> +        # Cut/Copy/Paste/Select Slide
>> +        action = MenuAction(translate('CustomPlugin.Editor', 'Cut'), popup_menu)
>> +        action.setShortcut(QtGui.QKeySequence.Cut)
>> +        action.menu_action.connect(self.on_cut)
>> +        action.setEnabled(self.selectedHtml() != '')
>> +        popup_menu.addAction(action)
>> +        action = MenuAction(translate('CustomPlugin.Editor', 'Copy'), popup_menu)
>> +        action.setShortcut(QtGui.QKeySequence.Copy)
>> +        action.menu_action.connect(self.on_copy)
>> +        action.setEnabled(self.selectedHtml() != '')
>> +        popup_menu.addAction(action)
>> +        mime = self.clipboard.mimeData()
>> +        action = MenuAction(translate('CustomPlugin.Editor', 'Paste HTML'), popup_menu)
>> +        action.setShortcut(QtGui.QKeySequence.Paste)
>> +        action.setEnabled(self.clipboard_owned)
>> +        action.menu_action.connect(self.on_paste_html)
>> +        popup_menu.addAction(action)
>> +        action = MenuAction(translate('CustomPlugin.Editor', 'Paste Text'), popup_menu)
>> +        action.setEnabled(mime.hasText())
>> +        action.menu_action.connect(self.on_paste_text)
>> +        popup_menu.addAction(action)
>> +        action = MenuAction(translate('CustomPlugin.Editor', 'Select Slide'), popup_menu)
>> +        action.setShortcut(QtGui.QKeySequence.SelectAll)
>> +        action.menu_action.connect(self.on_select_all)
>> +        popup_menu.addAction(action)
>> +        popup_menu.addSeparator()
>> +        # Insert punctuation
>> +        punctuation_menu = QtGui.QMenu(translate('CustomPlugin.Editor', 'Insert Punctuation'))
>> +        action = MenuAction('– ndash', punctuation_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        punctuation_menu.addAction(action)
>> +        action = MenuAction('— mdash', punctuation_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        punctuation_menu.addAction(action)
>> +        action = MenuAction('¡ iexcl', punctuation_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        punctuation_menu.addAction(action)
>> +        action = MenuAction('¿ iquest', punctuation_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        punctuation_menu.addAction(action)
>> +        action = MenuAction('“ ldquo', punctuation_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        punctuation_menu.addAction(action)
>> +        action = MenuAction('” rdquo', punctuation_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        punctuation_menu.addAction(action)
>> +        action = MenuAction('‘ lsquo', punctuation_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        punctuation_menu.addAction(action)
>> +        action = MenuAction('’ rsquo', punctuation_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        punctuation_menu.addAction(action)
>> +        action = MenuAction('« laquo', punctuation_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        punctuation_menu.addAction(action)
>> +        action = MenuAction('» raquo', punctuation_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        punctuation_menu.addAction(action)
>> +        action = MenuAction('_ nbsp', punctuation_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        punctuation_menu.addAction(action)
>> +        popup_menu.addMenu(punctuation_menu)
>> +        # Insert symbol
>> +        symbol_menu = QtGui.QMenu(translate('CustomPlugin.Editor', 'Insert Symbol'))
>> +        action = MenuAction('• bull', symbol_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        symbol_menu.addAction(action)
>> +        action = MenuAction('¢ cent', symbol_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        symbol_menu.addAction(action)
>> +        action = MenuAction('© copy', symbol_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        symbol_menu.addAction(action)
>> +        action = MenuAction('° deg', symbol_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        symbol_menu.addAction(action)
>> +        action = MenuAction('÷ divide', symbol_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        symbol_menu.addAction(action)
>> +        action = MenuAction('€ euro', symbol_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        symbol_menu.addAction(action)
>> +        action = MenuAction('· middot', symbol_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        symbol_menu.addAction(action)
>> +        action = MenuAction('¶ para', symbol_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        symbol_menu.addAction(action)
>> +        action = MenuAction('± plusmn', symbol_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        symbol_menu.addAction(action)
>> +        action = MenuAction('£ pound', symbol_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        symbol_menu.addAction(action)
>> +        action = MenuAction('® reg', symbol_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        symbol_menu.addAction(action)
>> +        action = MenuAction('§ sect', symbol_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        symbol_menu.addAction(action)
>> +        action = MenuAction('™ trade', symbol_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        symbol_menu.addAction(action)
>> +        action = MenuAction('¥ yen', symbol_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        symbol_menu.addAction(action)
>> +        popup_menu.addMenu(symbol_menu)
>> +        # Insert diacritic
>> +        diacritic_menu = QtGui.QMenu(translate('CustomPlugin.Editor', 'Insert Diacritic'))
>> +        action = MenuAction('á aacute', diacritic_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        diacritic_menu.addAction(action)
>> +        action = MenuAction('Á Aacute', diacritic_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        diacritic_menu.addAction(action)
>> +        action = MenuAction('à agrave', diacritic_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        diacritic_menu.addAction(action)
>> +        action = MenuAction('À Agrave', diacritic_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        diacritic_menu.addAction(action)
>> +        action = MenuAction('â acirc', diacritic_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        diacritic_menu.addAction(action)
>> +        action = MenuAction('Â Acirc', diacritic_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        diacritic_menu.addAction(action)
>> +        action = MenuAction('å aring', diacritic_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        diacritic_menu.addAction(action)
>> +        action = MenuAction('Å Aring', diacritic_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        diacritic_menu.addAction(action)
>> +        action = MenuAction('ã atilde', diacritic_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        diacritic_menu.addAction(action)
>> +        action = MenuAction('Ã Atilde', diacritic_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        diacritic_menu.addAction(action)
>> +        action = MenuAction('ä auml', diacritic_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        diacritic_menu.addAction(action)
>> +        action = MenuAction('Ä Auml', diacritic_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        diacritic_menu.addAction(action)
>> +        action = MenuAction('æ aelig', diacritic_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        diacritic_menu.addAction(action)
>> +        action = MenuAction('Æ AElig', diacritic_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        diacritic_menu.addAction(action)
>> +        action = MenuAction('ç ccedil', diacritic_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        diacritic_menu.addAction(action)
>> +        action = MenuAction('Ç Ccedil', diacritic_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        diacritic_menu.addAction(action)
>> +        action = MenuAction('é eacute', diacritic_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        diacritic_menu.addAction(action)
>> +        action = MenuAction('É Eacute', diacritic_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        diacritic_menu.addAction(action)
>> +        action = MenuAction('è egrave', diacritic_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        diacritic_menu.addAction(action)
>> +        action = MenuAction('È Egrave', diacritic_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        diacritic_menu.addAction(action)
>> +        action = MenuAction('ê ecirc', diacritic_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        diacritic_menu.addAction(action)
>> +        action = MenuAction('Ê Ecirc', diacritic_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        diacritic_menu.addAction(action)
>> +        action = MenuAction('ë euml', diacritic_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        diacritic_menu.addAction(action)
>> +        action = MenuAction('Ë Euml', diacritic_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        diacritic_menu.addAction(action)
>> +        action = MenuAction('í iacute', diacritic_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        diacritic_menu.addAction(action)
>> +        action = MenuAction('Í Iacute', diacritic_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        diacritic_menu.addAction(action)
>> +        action = MenuAction('ì igrave', diacritic_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        diacritic_menu.addAction(action)
>> +        action = MenuAction('Ì Igrave', diacritic_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        diacritic_menu.addAction(action)
>> +        action = MenuAction('î icirc', diacritic_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        diacritic_menu.addAction(action)
>> +        action = MenuAction('Î Icirc', diacritic_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        diacritic_menu.addAction(action)
>> +        action = MenuAction('ï iuml', diacritic_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        diacritic_menu.addAction(action)
>> +        action = MenuAction('Ï Iuml', diacritic_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        diacritic_menu.addAction(action)
>> +        action = MenuAction('ñ ntilde', diacritic_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        diacritic_menu.addAction(action)
>> +        action = MenuAction('Ñ Ntilde', diacritic_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        diacritic_menu.addAction(action)
>> +        action = MenuAction('ó oacute', diacritic_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        diacritic_menu.addAction(action)
>> +        action = MenuAction('Ó Oacute', diacritic_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        diacritic_menu.addAction(action)
>> +        action = MenuAction('ò ograve', diacritic_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        diacritic_menu.addAction(action)
>> +        action = MenuAction('Ò Ograve', diacritic_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        diacritic_menu.addAction(action)
>> +        action = MenuAction('ô ocirc', diacritic_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        diacritic_menu.addAction(action)
>> +        action = MenuAction('Ô Ocirc', diacritic_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        diacritic_menu.addAction(action)
>> +        action = MenuAction('ø oslash', diacritic_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        diacritic_menu.addAction(action)
>> +        action = MenuAction('Ø Oslash', diacritic_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        diacritic_menu.addAction(action)
>> +        action = MenuAction('õ otilde', diacritic_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        diacritic_menu.addAction(action)
>> +        action = MenuAction(' Õ Otilde', diacritic_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        diacritic_menu.addAction(action)
>> +        action = MenuAction('ö ouml', diacritic_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        diacritic_menu.addAction(action)
>> +        action = MenuAction('Ö Ouml', diacritic_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        diacritic_menu.addAction(action)
>> +        action = MenuAction('ß szlig', diacritic_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        diacritic_menu.addAction(action)
>> +        action = MenuAction('ú uacute', diacritic_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        diacritic_menu.addAction(action)
>> +        action = MenuAction('Ú Uacute', diacritic_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        diacritic_menu.addAction(action)
>> +        action = MenuAction('ù ugrave', diacritic_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        diacritic_menu.addAction(action)
>> +        action = MenuAction('Ù Ugrave', diacritic_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        diacritic_menu.addAction(action)
>> +        action = MenuAction('û ucirc', diacritic_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        diacritic_menu.addAction(action)
>> +        action = MenuAction('Û Ucirc', diacritic_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        diacritic_menu.addAction(action)
>> +        action = MenuAction('ü uuml', diacritic_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        diacritic_menu.addAction(action)
>> +        action = MenuAction('Ü Uuml', diacritic_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        diacritic_menu.addAction(action)
>> +        action = MenuAction('ÿ yuml', diacritic_menu)
>> +        action.menu_action.connect(self.on_insert_html_character)
>> +        diacritic_menu.addAction(action)
>> +        popup_menu.addMenu(diacritic_menu)
>> +        popup_menu.addSeparator()
>> +        # Insert/Remove formatting tags
>> +        if self.selectedText():
>> +            tag_menu = QtGui.QMenu(translate('CustomPlugin.Editor', 'Insert Formatting Tag'))
>> +            tag_system_menu = QtGui.QMenu(translate('CustomPlugin.Editor', 'System'))
>> +            tag_custom_menu = QtGui.QMenu(translate('CustomPlugin.Editor', 'Custom'))
>> +            for html in FormattingTags.get_html_tags():
>> +                if html['start tag'] != '{img}':
>> +                    if html['protected']:
>> +                        active_menu = tag_system_menu
>> +                    else:
>> +                        active_menu = tag_custom_menu
>> +                    action = MenuAction(html['desc'], active_menu)
>> +                    action.menu_action.connect(self.on_insert_tag)
>> +                    active_menu.addAction(action)
>> +            tag_menu.addMenu(tag_system_menu)
>> +            tag_menu.addMenu(tag_custom_menu)
>> +            popup_menu.addMenu(tag_menu)
>> +            action = MenuAction(translate('CustomPlugin.Editor', 'Remove Formatting Tag'), popup_menu)
>> +            action.setEnabled(False)
>> +            popup_menu.addAction(action)
>> +        else:
>> +            action = MenuAction(translate('CustomPlugin.Editor', 'Insert Formatting Tag'), popup_menu)
>> +            action.setEnabled(False)
>> +            popup_menu.addAction(action)
>> +            path = self.get_path()
>> +            if path:
>> +                remove_tag_menu = QtGui.QMenu(translate('CustomPlugin.Editor', 'Remove Formatting Tag'))
>> +                for html in FormattingTags.get_html_tags():
>> +                    if path.find(html['start tag']) > -1:
>> +                        action = MenuAction(html['desc'], remove_tag_menu)
>> +                        action.menu_action.connect(self.on_remove_tag)
>> +                        remove_tag_menu.addAction(action)
>> +                popup_menu.addMenu(remove_tag_menu)
>> +            else:
>> +                action = MenuAction(translate('CustomPlugin.Editor', 'Remove Formatting Tag'), popup_menu)
>> +                action.setEnabled(False)
>> +                popup_menu.addAction(action)
>> +        popup_menu.addSeparator()
>> +        # Insert/Edit Image
>> +        action = MenuAction(translate('CustomPlugin.Editor', 'Insert Image...'), popup_menu)
>> +        action.setEnabled(self.image_selected is None)
>> +        action.menu_action.connect(self.on_insert_image)
>> +        popup_menu.addAction(action)
>> +        action = MenuAction(translate('CustomPlugin.Editor', 'Edit Image...'), popup_menu)
>> +        action.setEnabled(self.image_selected is not None)
>> +        action.menu_action.connect(self.on_edit_image)
>> +        popup_menu.addAction(action)
>> +        popup_menu.addSeparator()
>> +        # Merge/Up/Down/Delete
>> +        prev_slide_found = not self.active_slide.previousSibling().isNull()
>> +        next_slide_found = not self.active_slide.nextSibling().isNull()
>> +        action = MenuAction(translate('CustomPlugin.Editor', 'Merge With Previous Slide'), popup_menu)
>> +        action.setEnabled(prev_slide_found)
>> +        action.menu_action.connect(self.on_merge)
>> +        popup_menu.addAction(action)
>> +        action = MenuAction(translate('CustomPlugin.Editor', 'Move Slide Up'), popup_menu)
>> +        action.setEnabled(prev_slide_found)
>> +        action.menu_action.connect(self.on_move_up)
>> +        popup_menu.addAction(action)
>> +        action = MenuAction(translate('CustomPlugin.Editor', 'Move Slide Down'), popup_menu)
>> +        action.setEnabled(next_slide_found)
>> +        action.menu_action.connect(self.on_move_down)
>> +        popup_menu.addAction(action)
>> +        action = MenuAction(translate('CustomPlugin.Editor', 'Delete Slide'), popup_menu)
>> +        action.setEnabled(prev_slide_found or next_slide_found)
>> +        action.menu_action.connect(self.on_delete)
>> +        popup_menu.addAction(action)
>> +        popup_menu.addSeparator()
>> +        # Zoom/Background
>> +        zoom_menu = QtGui.QMenu(translate('CustomPlugin.Editor', 'Scale Slide'))
>> +        action = MenuAction(translate('CustomPlugin.Editor', 'Fit Width'), zoom_menu)
>> +        action.setCheckable(True)
>> +        action.setChecked(self.zoom == 0.0)
>> +        action.menu_action.connect(self.on_zoom_fit_width)
>> +        zoom_menu.addAction(action)
>> +        action = MenuAction(translate('CustomPlugin.Editor', 'Actual Size'), zoom_menu)
>> +        action.setCheckable(True)
>> +        action.setChecked(self.zoom == 1.0)
>> +        action.menu_action.connect(self.on_zoom_actual_size)
>> +        zoom_menu.addAction(action)
>> +        zoom = 75
>> +        while zoom > 0:
>> +            action = MenuAction('%d%%' % zoom, zoom_menu)
>> +            action.setCheckable(True)
>> +            action.setChecked(zoom / 100 == self.zoom)
>> +            action.menu_action.connect(self.on_zoom_percent)
>> +            zoom_menu.addAction(action)
>> +            zoom -= 25
>> +        popup_menu.addMenu(zoom_menu)
>> +        bg_menu = QtGui.QMenu(translate('CustomPlugin.Editor', 'Set Background'))
>> +        action = MenuAction(translate('CustomPlugin.Editor', 'Theme'), bg_menu)
>> +        action.setCheckable(True)
>> +        action.setChecked(self.background == 'Theme')
>> +        action.menu_action.connect(self.on_background_theme)
>> +        bg_menu.addAction(action)
>> +        action = MenuAction(translate('CustomPlugin.Editor', 'Override Color'), bg_menu)
>> +        action.setCheckable(True)
>> +        action.setChecked(self.background != 'Theme')
>> +        action.menu_action.connect(self.on_background_color)
>> +        bg_menu.addAction(action)
>> +        popup_menu.addMenu(bg_menu)
>> +        popup_menu.exec(self.mapToGlobal(pos))
>> +
>> +    def create_unique_ids(self, html):
>> +        """
>> +        Generate new id's for every element in html
>> +
>> +        :param html:
>> +        """
>> +        start_pos = 0
>> +        while True:
>> +            start_pos = html.find(u' id="elem', start_pos)
>> +            if start_pos == -1:
>> +                break
>> +            start_pos += len(u' id="')
>> +            end_pos = html.find(u'"', start_pos)
>> +            elem_id = html[start_pos:end_pos]
>> +            self.last_id += 1
>> +            html = html.replace(elem_id, u'elem%d' % self.last_id, 1)
>> +        return html
>> +
>> +    def add_path_tags(self, html_str, path):
>> +        """
>> +        Add open formatting tags to the html string
>> +
>> +        :param html_str:
>> +        :param path:
>> +        """
>> +        new_tags_start = ''
>> +        new_tags_end = ''
>> +        path_tags = path.split(',')
>> +        for tag in path_tags:
>> +            for html in FormattingTags.get_html_tags():
>> +                if tag == html['start tag']:
>> +                    new_tags_start += self.insert_data_tag(html['start tag'], html['start html'])
>> +                    new_tags_end += html['end html']
>> +                    break
>> +        return new_tags_start + html_str + new_tags_end
>> +
>> +    def do_cut_copy(self, cut):
>> +        """
>> +        Cut or copy the selection and place the HTML and plain text in the clipboard.  The built in execCommand('cut')
>> +        cannot be used because it combines all of the styles of the selection's parent and we lose the formatting tags
>> +
>> +        :param cut: True to cut selection
>> +        """
>> +        if cut:
>> +            result = self.java('cutCopy(true)')
>> +        else:
>> +            result = self.java('cutCopy(false)')
>> +        results = result.split('/', 1)
>> +        path = str(results[0])
>> +        selected_html = str(results[1])
>> +        if not cut:
>> +            selected_html = self.create_unique_ids(selected_html)
>> +        if path:
>> +            selected_html = self.add_path_tags(selected_html, path)
>> +        mime = QtCore.QMimeData()
>> +        mime.setHtml(selected_html)
>> +        if self.selectedText():
>> +            mime.setText(self.selectedText())
>> +        self.clipboard.setMimeData(mime)
>> +        self.clipboard_owned = True
>> +
>> +    def on_cut(self, menu_text):
>> +        """
>> +        Create undo command and do_cut_copy(True)
>> +
>> +        :param menu_text:
>> +        """
>> +        cmd = MethodUndoCommand(self, self.do_cut_copy, menu_text, True)
>> +        self.undo_stack.push(cmd)
>> +
>> +    def on_copy(self):
>> +        """
>> +        Copy the selection to the clipboard
>> +        """
>> +        self.do_cut_copy(False)
>> +
>> +    def do_paste(self, text_html):
>> +        """
>> +        Insert the text or html at current caret position
>> +        """
>> +        self.java("""insertHTML('%s')""" % text_html)
>> +
>> +    def on_paste_html(self, menu_text):
>> +        """
>> +        Reformat the clipboard html data and create UndoCommand do_paste()
>> +
>> +        :param menu_text:
>> +        """
>> +        mime = self.clipboard.mimeData()
>> +        if mime.hasHtml() and self.clipboard_owned:
>> +            cmd = MethodUndoCommand(self, self.do_paste, menu_text, mime.html())
>> +            self.undo_stack.push(cmd)
>> +        else:
>> +            self.on_paste_text(translate('CustomPlugin.Editor', 'Paste Text'))
>> +
>> +    def on_paste_text(self, menu_text):
>> +        """
>> +        Get the plain text from the clipboard, reformat, and create UndoCommand do_paste()
>> +        """
>> +        mime = self.clipboard.mimeData()
>> +        if mime.hasText():
>> +            text = mime.text()
>> +            text.encode('utf-8')
>> +            text = text.replace(u'<', u'&lt;')
>> +            text = text.replace(u'>', u'&gt;')
>> +            text = text.replace(u'\n', u'<br />')
>> +            text = text.replace(u"'", u'&#39;')
>> +            text = text.replace(u'"', u'&quot;')
>> +            cmd = MethodUndoCommand(self, self.do_paste, menu_text, text)
>> +            self.undo_stack.push(cmd)
>> +
>> +    def on_select_all(self):
>> +        """
>> +        Select the contents of the currently active slide
>> +        """
>> +        self.image_selected = None
>> +        self.page().triggerAction(QtWebKit.QWebPage.SelectAll)
>> +
>> +    def on_insert_html_character(self, menu_text):
>> +        """
>> +        Insert the special character
>> +        """
>> +        param = 'insertHTML("&%s;")' % menu_text[menu_text.find(' ')+1:]
>> +        cmd = MethodUndoCommand(self, self.java, translate('CustomPlugin.Editor', 'Insert HTML Character'), param)
>> +        self.undo_stack.push(cmd)
>> +
>> +    def on_insert_image(self, menu_text):
>> +        """
>> +        Display the edit image dialog and insert image at caret or selection
>> +
>> +        :param menu_text:
>> +        """
>> +        self.edit_image_form.set_image(self.max_image_width, self.max_image_height)
>> +        if self.edit_image_form.exec_():
>> +            name = self.edit_image_form.get_image_name()
>> +            style = self.edit_image_form.get_image_style()
>> +            self.last_id += 1
>> +            param = """insertHTML('<img id="elem%d" src="%s" data-tag="{img}" style="%s" />')""" %\
>> +                    (self.last_id, name, style)
>> +            cmd = MethodUndoCommand(self, self.java, menu_text, param)
>> +            self.undo_stack.push(cmd)
>> +
>> +    def on_edit_image(self, menu_text):
>> +        """
>> +        Display the edit image dialog for the currently selected image and set the image attributes
>> +
>> +        :param menu_text:
>> +        """
>> +        self.edit_image_form.set_image(self.max_image_width, self.max_image_height,
>> +                                       self.image_selected.attribute('src'), self.image_selected.attribute('style'))
>> +        if self.edit_image_form.exec_():
>> +            name = self.edit_image_form.get_image_name()
>> +            style = self.edit_image_form.get_image_style()
>> +            cmd = ImageUndoCommand(self, menu_text, name, style)
>> +            self.undo_stack.push(cmd)
>> +
>> +    def do_merge(self):
>> +        """
>> +        Add the contents of the current slide to the previous slide, then delete current slide
>> +        """
>> +        self.java('mergeSlide("%s")' % self.active_slide.attribute('id'))
>> +
>> +    def on_merge(self, menu_text):
>> +        """
>> +        Create undo command and do_merge()
>> +
>> +        :param menu_text:
>> +        """
>> +        cmd = MethodUndoCommand(self, self.do_merge, menu_text)
>> +        self.undo_stack.push(cmd)
>> +
>> +    def do_move_up(self):
>> +        """
>> +        Swap the contents of the current slide with the previous slide
>> +        """
>> +        prev_slide = self.active_slide.previousSibling()
>> +        prev_text = self.get_text_div(prev_slide)
>> +        html = prev_text.toInnerXml()
>> +        prev_text.setInnerXml(self.active_text.toInnerXml())
>> +        self.active_text.setInnerXml(html)
>> +        self.frame.scrollToAnchor(prev_slide.attribute('id'))
>> +        self.java('setFocus("%s")' % prev_text.attribute('id'))
>> +
>> +    def on_move_up(self, menu_text):
>> +        """
>> +        Create undo command and do_move_up()
>> +
>> +        :param menu_text:
>> +        """
>> +        cmd = MethodUndoCommand(self, self.do_move_up, menu_text)
>> +        self.undo_stack.push(cmd)
>> +
>> +    def do_move_down(self):
>> +        """
>> +        Swap the contents of the current slide with the next slide
>> +        """
>> +        next_slide = self.active_slide.nextSibling()
>> +        next_text = self.get_text_div(next_slide)
>> +        html = next_text.toInnerXml()
>> +        next_text.setInnerXml(self.active_text.toInnerXml())
>> +        self.active_text.setInnerXml(html)
>> +        self.frame.scrollToAnchor(next_slide.attribute('id'))
>> +        self.java('setFocus("%s")' % next_text.attribute('id'))
>> +
>> +    def on_move_down(self, menu_text):
>> +        """
>> +        Create undo command and do_move_down()
>> +
>> +        :param menu_text:
>> +        """
>> +        cmd = MethodUndoCommand(self, self.do_move_down, menu_text)
>> +        self.undo_stack.push(cmd)
>> +
>> +    def do_delete(self):
>> +        """
>> +        Delete the current slide
>> +        """
>> +        next_slide = self.active_slide.previousSibling()
>> +        if next_slide.isNull():
>> +            next_slide = self.active_slide.nextSibling()
>> +        next_text = self.get_text_div(next_slide)
>> +        self.active_slide.removeFromDocument()
>> +        self.frame.scrollToAnchor(next_slide.attribute('id'))
>> +        self.java('setFocus("%s")' % next_text.attribute('id'))
>> +
>> +    def on_delete(self, menu_text):
>> +        """
>> +        Create undo command and do_delete()
>> +
>> +        :param menu_text:
>> +        """
>> +        cmd = MethodUndoCommand(self, self.do_delete, menu_text)
>> +        self.undo_stack.push(cmd)
>> +
>> +    def on_zoom_fit_width(self):
>> +        """
>> +        Set the zoom factor to fit the width of the editor window
>> +        """
>> +        self.zoom = 0.0
>> +        self.set_zoom()
>> +
>> +    def on_zoom_actual_size(self):
>> +        """
>> +        Set the zoom factor to actual size
>> +        """
>> +        self.zoom = 1.0
>> +        self.set_zoom()
>> +
>> +    def on_zoom_percent(self, menu_text):
>> +        """
>> +        Set the zoom factor of the editor to the menu_text
>> +
>> +        :param menu_text:
>> +        """
>> +        self.zoom = float(str(menu_text[0:2])) / 100.0       # Convert the menu text (less % sign) to zoom value
>> +        self.set_zoom()
>> +
>> +    def on_background_theme(self):
>> +        """
>> +        Remove the background color override
>> +        """
>> +        self.background = 'Theme'
>> +        slides = self.frame.documentElement().findAll('div.slide')
>> +        for slide in slides:
>> +            slide.setStyleProperty('background', '')
>> +
>> +    def on_background_color(self):
>> +        """
>> +        Display the color picker and override the background with selected color
>> +        """
>> +        color_dialog = QtGui.QColorDialog()
>> +        new_color = color_dialog.getColor(self.background_color, self, '')
>> +        if new_color.isValid():
>> +            self.background_color = new_color
>> +            self.background = '#%02X%02X%02X' % (new_color.red(), new_color.green(), new_color.blue())
>> +            slides = self.frame.documentElement().findAll('div.slide')
>> +            for slide in slides:
>> +                slide.setStyleProperty('background', self.background)
>> +
>> +    def do_insert_tag(self, tag_html):
>> +        """
>> +        Wrap the formatting tag html around the current selection
>> +
>> +        :param tag_html:
>> +        """
>> +        result = bool(self.java("""insertTag('%s')""" % tag_html))
>> +        if not result:
>> +            msg = translate('CustomPlugin.Editor', 'Unable to insert tag.\n\n'
>> +                                                   'The selected text spans an existing tag boundary.')
>> +            critical_error_message_box(message=msg)
>> +            self.undo_stack.setIndex(self.undo_stack.index() - 1)
>> +
>> +    def on_insert_tag(self, menu_text):
>> +        """
>> +        Build the html from the formatting tag, create undo command and do_insert_tag()
>> +
>> +        :param menu_text:
>> +        """
>> +        for html in FormattingTags.get_html_tags():
>> +            if menu_text == html['desc']:
>> +                tag_html = '%s%s' % (self.insert_data_tag(html['start tag'], html['start html']),
>> +                                     html['end html'])
>> +                undo_name = translate('CustomPlugin.Editor', 'Insert Tag: ') + menu_text
>> +                cmd = MethodUndoCommand(self, self.do_insert_tag, undo_name, tag_html)
>> +                self.undo_stack.push(cmd)
>> +                break
>> +
>> +    def on_remove_tag(self, menu_text):
>> +        """
>> +        Remove the first matching formatting tag in path, starting from caret and searching backwards
>> +
>> +        :param menu_text:
>> +        """
>> +        for html in FormattingTags.get_html_tags():
>> +            if menu_text == html['desc']:
>> +                tag = html['start tag']
>> +                param = 'removeTag("%s")' % tag
>> +                undo_name = translate('CustomPlugin.Editor', 'Remove Tag: ') + menu_text
>> +                cmd = MethodUndoCommand(self, self.java, undo_name, param)
>> +                self.undo_stack.push(cmd)
>> +                break
>> +
>> +
>> +class CustomUndoCommand(QtGui.QUndoCommand):
>> +    """
>> +    The custom base class for all of the UndoCommands pushed to the undo_stack - saves editor state and defines a
>> +    method to restore the selection, and another to set the scrollbar positions.
>> +    The undo method overrides the base class and restores the editor state.
>> +    Ancestors must implement the redo function.
>> +    """
>> +
>> +    def __init__(self, editor, description):
>> +        """
>> +        :param editor: HtmlEditor
>> +        :param description: Text to display for undo/redo
>> +        """
>> +        super(CustomUndoCommand, self).__init__(description)
>> +        self.editor = editor
>> +        # Save the current editor state
>> +        self.slides = editor.frame.findFirstElement('#slides')
>> +        self.html = self.slides.toInnerXml()
>> +        self.scroll_x = editor.frame.scrollBarValue(QtCore.Qt.Horizontal)
>> +        self.scroll_y = editor.frame.scrollBarValue(QtCore.Qt.Vertical)
>> +        self.image_selected = editor.image_selected
>> +        # QWebKit does not give access to the window.getSelection() object, so we let javascript find which text node
>> +        # is currently selected for the anchor and focus nodes, find their relative position to previous siblings,
>> +        # and retrieve their parent element's id.  We can't just store the node objects, as they will change if/when
>> +        # the innerXml is set
>> +        select_data = str(editor.java('saveSelection()'))
>> +        select_fields = select_data.split(',')
>> +        self.anchor_id = select_fields[0]
>> +        self.anchor_pos = select_fields[1]
>> +        self.anchor_offset = select_fields[2]
>> +        self.focus_id = select_fields[3]
>> +        self.focus_pos = select_fields[4]
>> +        self.focus_offset = select_fields[5]
>> +
>> +    def restore_selection(self):
>> +        """
>> +        Move the caret/selection to original slide position.  This will trigger the selection changed event
>> +        to update the active slide and text properties
>> +        """
>> +        self.editor.image_selected = self.image_selected
>> +        if self.image_selected:
>> +            self.editor.java('focusNode("%s")' % self.image_selected.attribute('id'))
>> +        else:
>> +            self.editor.java('restoreSelection("%s", "%s", "%s", "%s", "%s", "%s")' % (
>> +                self.anchor_id, self.anchor_pos, self.anchor_offset,
>> +                self.focus_id, self.focus_pos, self.focus_offset))
>> +
>> +    def set_scroll_position(self):
>> +        """
>> +        Returns scroll bars to original position.  Will not always be accurate if the zoom level or window size
>> +        was changed after the UndoCommand was created
>> +        """
>> +        self.editor.frame.setScrollBarValue(QtCore.Qt.Horizontal, self.scroll_x)
>> +        self.editor.frame.setScrollBarValue(QtCore.Qt.Vertical, self.scroll_y)
>> +
>> +    def undo(self):
>> +        """
>> +        Restore the editor to the original state
>> +        """
>> +        self.slides.setInnerXml(self.html)
>> +        self.restore_selection()
>> +        self.set_scroll_position()
>> +
>> +
>> +class MethodUndoCommand(CustomUndoCommand):
>> +    """
>> +    UndoCommand class for executing html_editor commands
>> +    """
>> +
>> +    def __init__(self, editor, command, description, param1=None, param2=None):
>> +        """
>> +        :param editor: HtmlEditor
>> +        :param command: Command to be executed
>> +        :param description: Text to display for undo/redo
>> +        :param param1: Optional parameter for the command
>> +        :param param2: Optional parameter for the command
>> +        """
>> +        super(MethodUndoCommand, self).__init__(editor, description)
>> +        self.command = command
>> +        self.param1 = param1
>> +        self.param2 = param2
>> +
>> +    def redo(self):
>> +        """
>> +        Reset the selection and execute the command
>> +        """
>> +        self.restore_selection()
>> +        self.set_scroll_position()
>> +        if self.param1:
>> +            if self.param2:
>> +                self.command(self.param1, self.param2)
>> +            else:
>> +                self.command(self.param1)
>> +        else:
>> +            self.command()
>> +
>> +
>> +class TextUndoCommand(CustomUndoCommand):
>> +    """
>> +    This undo command is used for typing.  The insertHTML fires the selection changed event, which under normal
>> +    circumstances an open text undo command is closed.  To allow for the appending of text, we set the editor's
>> +    'typing' to true
>> +    """
>> +
>> +    def __init__(self, editor, description, text):
>> +        """
>> +        :param editor: HtmlEditor
>> +        :param description: Text to display for undo/redo
>> +        :param text: The text to insert
>> +        """
>> +        super(TextUndoCommand, self).__init__(editor, description)
>> +        self.new_text = text
>> +
>> +    def redo(self):
>> +        """
>> +        Reset the selection and insert the text
>> +        """
>> +        self.restore_selection()
>> +        self.set_scroll_position()
>> +        self.editor.typing = True                          # Ignore selection changed caused by insertHTML
>> +        self.editor.text_undo_command = self
>> +        self.editor.java("""insertHTML('%s')""" % self.new_text)
>> +        self.editor.typing = False
>> +
>> +    def append_text(self, char):
>> +        """
>> +        Add the char to the new text so a new UndoCommand does not have to be created
>> +
>> +        :param char:
>> +        """
>> +        self.editor.typing = True
>> +        self.new_text += char
>> +        self.editor.java('insertHTML("%s")' % char)
>> +        self.editor.typing = False
>> +
>> +
>> +class ImageUndoCommand(CustomUndoCommand):
>> +    """
>> +    UndoCommand class for editing images
>> +    """
>> +
>> +    def __init__(self, editor, description, source, style):
>> +        """
>> +        :param editor: HtmlEditor
>> +        :param description: Text to display for undo/redo
>> +        :param source: The image source file name
>> +        :param style: The image css style
>> +        """
>> +        super(ImageUndoCommand, self).__init__(editor, description)
>> +        self.source = source
>> +        self.style = style
>> +        self.save_source = self.editor.image_selected.attribute('src')
>> +        self.save_style = self.editor.image_selected.attribute('style')
>> +
>> +    def redo(self):
>> +        """
>> +        Reset the selection and set the image attributes
>> +        """
>> +        self.restore_selection()
>> +        self.set_scroll_position()
>> +        self.image_selected.setAttribute('src', self.source)
>> +        self.image_selected.setAttribute('style', self.style)
>> +
>> +    def undo(self):
>> +        """
>> +        Reset the selection and restore the image attributes
>> +        """
>> +        self.restore_selection()
>> +        self.set_scroll_position()
>> +        self.image_selected.setAttribute('src', self.save_source)
>> +        self.image_selected.setAttribute('style', self.save_style)
>> +
>> +
>> +class MenuAction(QtGui.QAction):
>> +    """
>> +    A special QAction that returns the selected menu item text in a signal
>> +    """
>> +    menu_action = QtCore.pyqtSignal(str)
>> +
>> +    def __init__(self, *args):
>> +        """
>> +        Constructor
>> +        """
>> +        super(MenuAction, self).__init__(*args)
>> +        self.triggered.connect(lambda x: self.menu_action.emit(self.text()))
>>
>> === modified file 'openlp/plugins/custom/lib/mediaitem.py'
>> --- openlp/plugins/custom/lib/mediaitem.py	2014-03-21 21:38:08 +0000
>> +++ openlp/plugins/custom/lib/mediaitem.py	2014-06-19 13:47:40 +0000
>> @@ -33,7 +33,7 @@
>>   from sqlalchemy.sql import or_, func, and_
>>   
>>   from openlp.core.common import Registry, Settings, UiStrings, translate
>> -from openlp.core.lib import MediaManagerItem, ItemCapabilities, ServiceItemContext, PluginStatus, \
>> +from openlp.core.lib import MediaManagerItem, ItemCapabilities, ServiceItem, ServiceItemContext, PluginStatus, \
>>       check_item_selected
>>   from openlp.plugins.custom.forms.editcustomform import EditCustomForm
>>   from openlp.plugins.custom.lib import CustomXMLParser, CustomXMLBuilder
>> @@ -140,6 +140,9 @@
>>           Handle the New item event
>>           """
>>           self.edit_custom_form.load_custom(0)
>> +        service_item = ServiceItem(self.plugin)
>> +        service_item.from_plugin = True
>> +        self.edit_custom_form.edit_slide_form.service_item = service_item
>>           self.edit_custom_form.exec_()
>>           self.on_clear_text_button_click()
>>           self.on_selection_change()
>> @@ -156,6 +159,9 @@
>>           valid = self.plugin.db_manager.get_object(CustomSlide, custom_id)
>>           if valid:
>>               self.edit_custom_form.load_custom(custom_id, preview)
>> +            service_item = ServiceItem(self.plugin)
>> +            service_item.from_plugin = True
>> +            self.edit_custom_form.edit_slide_form.service_item = service_item
>>               if self.edit_custom_form.exec_() == QtGui.QDialog.Accepted:
>>                   self.remote_triggered = True
>>                   self.remote_custom = custom_id
>> @@ -173,6 +179,9 @@
>>           Edit a custom item
>>           """
>>           if check_item_selected(self.list_view, UiStrings().SelectEdit):
>> +            service_item = ServiceItem(self.plugin)
>> +            service_item.from_plugin = True
>> +            self.edit_custom_form.edit_slide_form.service_item = service_item
>>               item = self.list_view.currentItem()
>>               item_id = item.data(QtCore.Qt.UserRole)
>>               self.edit_custom_form.load_custom(item_id, False)
>>
>> === modified file 'openlp/plugins/presentations/lib/pptviewcontroller.py'
>> --- openlp/plugins/presentations/lib/pptviewcontroller.py	2014-03-29 19:56:20 +0000
>> +++ openlp/plugins/presentations/lib/pptviewcontroller.py	2014-06-19 13:47:40 +0000
>> @@ -35,6 +35,7 @@
>>       from ctypes.wintypes import RECT
>>   
>>   from openlp.core.utils import AppLocation
>> +
>>   from openlp.core.lib import ScreenList
>>   from .presentationcontroller import PresentationController, PresentationDocument
>>   
>> @@ -86,8 +87,10 @@
>>               if self.process:
>>                   return
>>               log.debug('start PPTView')
>> +
>>               dll_path = os.path.join(AppLocation.get_directory(AppLocation.AppDir),
>>                                       'plugins', 'presentations', 'lib', 'pptviewlib', 'pptviewlib.dll')
>> +
>>               self.process = cdll.LoadLibrary(dll_path)
>>               if log.isEnabledFor(logging.DEBUG):
>>                   self.process.SetDebug(1)
>>
>> === modified file 'openlp/plugins/presentations/presentationplugin.py'
>> --- openlp/plugins/presentations/presentationplugin.py	2014-04-12 20:19:22 +0000
>> +++ openlp/plugins/presentations/presentationplugin.py	2014-06-19 13:47:40 +0000
>> @@ -47,9 +47,9 @@
>>                           'presentations/enable_pdf_program': QtCore.Qt.Unchecked,
>>                           'presentations/pdf_program': '',
>>                           'presentations/Impress': QtCore.Qt.Checked,
>> -                        'presentations/Powerpoint': QtCore.Qt.Checked,
>> -                        'presentations/Powerpoint Viewer': QtCore.Qt.Checked,
>> -                        'presentations/Pdf': QtCore.Qt.Checked,
>> +                        'presentations/Powerpoint': QtCore.Qt.Unchecked,
>> +                        'presentations/Powerpoint Viewer': QtCore.Qt.Unchecked,
>> +                        'presentations/Pdf': QtCore.Qt.Unchecked,
> What are these changes about?
>
>>                           'presentations/presentations files': []
>>                           }
>>   
>>
>> === modified file 'resources/forms/editcustomslidedialog.ui'
>> --- resources/forms/editcustomslidedialog.ui	2010-10-10 13:17:01 +0000
>> +++ resources/forms/editcustomslidedialog.ui	2014-06-19 13:47:40 +0000
>> @@ -6,8 +6,8 @@
>>      <rect>
>>       <x>0</x>
>>       <y>0</y>
>> -    <width>474</width>
>> -    <height>442</height>
>> +    <width>650</width>
>> +    <height>450</height>
>>      </rect>
>>     </property>
>>     <property name="windowTitle">
>> @@ -18,7 +18,7 @@
>>       <rect>
>>        <x>8</x>
>>        <y>407</y>
>> -     <width>458</width>
>> +     <width>634</width>
>>        <height>32</height>
>>       </rect>
>>      </property>
>> @@ -29,33 +29,232 @@
>>       <set>QDialogButtonBox::Cancel|QDialogButtonBox::Save</set>
>>      </property>
>>     </widget>
>> -  <widget class="QTextEdit" name="VerseTextEdit">
>> +  <widget class="QTabWidget" name="EditorTabWidget">
>>      <property name="geometry">
>>       <rect>
>>        <x>8</x>
>>        <y>8</y>
>> -     <width>458</width>
>> -     <height>349</height>
>> -    </rect>
>> -   </property>
>> -  </widget>
>> -  <widget class="QPushButton" name="SplitButton">
>> -   <property name="geometry">
>> -    <rect>
>> -     <x>380</x>
>> -     <y>370</y>
>> -     <width>85</width>
>> -     <height>27</height>
>> -    </rect>
>> -   </property>
>> -   <property name="toolTip">
>> -    <string extracomment="Add new slide split"/>
>> -   </property>
>> -   <property name="text">
>> -    <string>Split Slide</string>
>> -   </property>
>> +     <width>634</width>
>> +     <height>391</height>
>> +    </rect>
>> +   </property>
>> +   <property name="currentIndex">
>> +    <number>1</number>
>> +   </property>
>> +   <widget class="QWidget" name="TagEditorTab">
>> +    <attribute name="title">
>> +     <string>Tag Editor</string>
>> +    </attribute>
>> +    <widget class="QWidget" name="horizontalLayoutWidget">
>> +     <property name="geometry">
>> +      <rect>
>> +       <x>10</x>
>> +       <y>320</y>
>> +       <width>611</width>
>> +       <height>40</height>
>> +      </rect>
>> +     </property>
>> +     <layout class="QHBoxLayout" name="TagButtonLayout" stretch="0,0,0">
>> +      <property name="spacing">
>> +       <number>8</number>
>> +      </property>
>> +      <property name="margin">
>> +       <number>0</number>
>> +      </property>
>> +      <item>
>> +       <widget class="QPushButton" name="TagSplitButton">
>> +        <property name="text">
>> +         <string>Optional Split</string>
>> +        </property>
>> +        <property name="icon">
>> +         <iconset resource="../images/openlp-2.qrc">
>> +          <normaloff>:/general/general_add.png</normaloff>:/general/general_add.png</iconset>
>> +        </property>
>> +       </widget>
>> +      </item>
>> +      <item>
>> +       <widget class="QPushButton" name="TagInsertButton">
>> +        <property name="text">
>> +         <string>Insert</string>
>> +        </property>
>> +        <property name="icon">
>> +         <iconset resource="../images/openlp-2.qrc">
>> +          <normaloff>:/general/general_add.png</normaloff>:/general/general_add.png</iconset>
>> +        </property>
>> +       </widget>
>> +      </item>
>> +      <item>
>> +       <spacer name="VerseTypeSpacer">
>> +        <property name="orientation">
>> +         <enum>Qt::Horizontal</enum>
>> +        </property>
>> +        <property name="sizeHint" stdset="0">
>> +         <size>
>> +          <width>40</width>
>> +          <height>20</height>
>> +         </size>
>> +        </property>
>> +       </spacer>
>> +      </item>
>> +     </layout>
>> +    </widget>
>> +    <widget class="QPlainTextEdit" name="plainTextEdit">
>> +     <property name="geometry">
>> +      <rect>
>> +       <x>10</x>
>> +       <y>10</y>
>> +       <width>611</width>
>> +       <height>291</height>
>> +      </rect>
>> +     </property>
>> +    </widget>
>> +   </widget>
>> +   <widget class="QWidget" name="VisualEditorTag">
>> +    <attribute name="title">
>> +     <string>Visual Editor</string>
>> +    </attribute>
>> +    <widget class="QWebView" name="CustomWebView">
>> +     <property name="geometry">
>> +      <rect>
>> +       <x>8</x>
>> +       <y>8</y>
>> +       <width>612</width>
>> +       <height>261</height>
>> +      </rect>
>> +     </property>
>> +     <property name="sizePolicy">
>> +      <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
>> +       <horstretch>0</horstretch>
>> +       <verstretch>0</verstretch>
>> +      </sizepolicy>
>> +     </property>
>> +     <property name="minimumSize">
>> +      <size>
>> +       <width>200</width>
>> +       <height>200</height>
>> +      </size>
>> +     </property>
>> +     <property name="focusPolicy">
>> +      <enum>Qt::StrongFocus</enum>
>> +     </property>
>> +     <property name="url">
>> +      <url>
>> +       <string>about:blank</string>
>> +      </url>
>> +     </property>
>> +    </widget>
>> +    <widget class="QWidget" name="horizontalLayoutWidget_2">
>> +     <property name="geometry">
>> +      <rect>
>> +       <x>10</x>
>> +       <y>320</y>
>> +       <width>611</width>
>> +       <height>40</height>
>> +      </rect>
>> +     </property>
>> +     <layout class="QHBoxLayout" name="VisualButtonLayout" stretch="0,0,0">
>> +      <property name="spacing">
>> +       <number>8</number>
>> +      </property>
>> +      <property name="margin">
>> +       <number>0</number>
>> +      </property>
>> +      <item>
>> +       <widget class="QPushButton" name="VisualSplitButton">
>> +        <property name="text">
>> +         <string>Optional Split</string>
>> +        </property>
>> +        <property name="icon">
>> +         <iconset resource="../images/openlp-2.qrc">
>> +          <normaloff>:/general/general_add.png</normaloff>:/general/general_add.png</iconset>
>> +        </property>
>> +       </widget>
>> +      </item>
>> +      <item>
>> +       <widget class="QPushButton" name="VisualInsertButton">
>> +        <property name="text">
>> +         <string>Insert</string>
>> +        </property>
>> +        <property name="icon">
>> +         <iconset resource="../images/openlp-2.qrc">
>> +          <normaloff>:/general/general_add.png</normaloff>:/general/general_add.png</iconset>
>> +        </property>
>> +       </widget>
>> +      </item>
>> +      <item>
>> +       <spacer name="VisualButtonSpacer">
>> +        <property name="orientation">
>> +         <enum>Qt::Horizontal</enum>
>> +        </property>
>> +        <property name="sizeHint" stdset="0">
>> +         <size>
>> +          <width>40</width>
>> +          <height>20</height>
>> +         </size>
>> +        </property>
>> +       </spacer>
>> +      </item>
>> +     </layout>
>> +     <zorder>VisualInsertButton</zorder>
>> +     <zorder>VisualSplitButton</zorder>
>> +    </widget>
>> +    <widget class="QWidget" name="horizontalLayoutWidget_3">
>> +     <property name="geometry">
>> +      <rect>
>> +       <x>10</x>
>> +       <y>280</y>
>> +       <width>611</width>
>> +       <height>40</height>
>> +      </rect>
>> +     </property>
>> +     <layout class="QHBoxLayout" name="PathLayout" stretch="0,0">
>> +      <property name="spacing">
>> +       <number>8</number>
>> +      </property>
>> +      <property name="margin">
>> +       <number>0</number>
>> +      </property>
>> +      <item>
>> +       <widget class="QLabel" name="PathLabel">
>> +        <property name="text">
>> +         <string>Path:</string>
>> +        </property>
>> +       </widget>
>> +      </item>
>> +      <item>
>> +       <spacer name="PathSpacer">
>> +        <property name="orientation">
>> +         <enum>Qt::Horizontal</enum>
>> +        </property>
>> +        <property name="sizeHint" stdset="0">
>> +         <size>
>> +          <width>40</width>
>> +          <height>20</height>
>> +         </size>
>> +        </property>
>> +       </spacer>
>> +      </item>
>> +     </layout>
>> +    </widget>
>> +   </widget>
>>     </widget>
>>    </widget>
>> + <customwidgets>
>> +  <customwidget>
>> +   <class>QWebView</class>
>> +   <extends>QWidget</extends>
>> +   <header>QtWebKit/QWebView</header>
>> +  </customwidget>
>> + </customwidgets>
>> + <tabstops>
>> +  <tabstop>EditorTabWidget</tabstop>
>> +  <tabstop>TagSplitButton</tabstop>
>> +  <tabstop>TagInsertButton</tabstop>
>> +  <tabstop>buttonBox</tabstop>
>> +  <tabstop>VisualInsertButton</tabstop>
>> +  <tabstop>CustomWebView</tabstop>
>> +  <tabstop>VisualSplitButton</tabstop>
>> + </tabstops>
>>    <resources>
>>     <include location="../images/openlp-2.qrc"/>
>>    </resources>
>>
>> === added file 'resources/forms/editimagedialog.ui'
>> --- resources/forms/editimagedialog.ui	1970-01-01 00:00:00 +0000
>> +++ resources/forms/editimagedialog.ui	2014-06-19 13:47:40 +0000
>> @@ -0,0 +1,624 @@
>> +<?xml version="1.0" encoding="UTF-8"?>
>> +<ui version="4.0">
>> + <class>edit_image_dialog</class>
>> + <widget class="QDialog" name="edit_image_dialog">
>> +  <property name="geometry">
>> +   <rect>
>> +    <x>0</x>
>> +    <y>0</y>
>> +    <width>589</width>
>> +    <height>327</height>
>> +   </rect>
>> +  </property>
>> +  <property name="windowTitle">
>> +   <string>Edit Image</string>
>> +  </property>
>> +  <layout class="QVBoxLayout" name="dialog_layout">
>> +   <property name="spacing">
>> +    <number>12</number>
>> +   </property>
>> +   <item>
>> +    <widget class="QGroupBox" name="image_group_box">
>> +     <property name="sizePolicy">
>> +      <sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
>> +       <horstretch>0</horstretch>
>> +       <verstretch>0</verstretch>
>> +      </sizepolicy>
>> +     </property>
>> +     <property name="title">
>> +      <string>Image</string>
>> +     </property>
>> +     <layout class="QHBoxLayout" name="image_layout">
>> +      <property name="spacing">
>> +       <number>8</number>
>> +      </property>
>> +      <item>
>> +       <widget class="QLabel" name="thumbnail_label">
>> +        <property name="sizePolicy">
>> +         <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
>> +          <horstretch>0</horstretch>
>> +          <verstretch>0</verstretch>
>> +         </sizepolicy>
>> +        </property>
>> +        <property name="minimumSize">
>> +         <size>
>> +          <width>48</width>
>> +          <height>48</height>
>> +         </size>
>> +        </property>
>> +        <property name="maximumSize">
>> +         <size>
>> +          <width>48</width>
>> +          <height>48</height>
>> +         </size>
>> +        </property>
>> +        <property name="frameShape">
>> +         <enum>QFrame::NoFrame</enum>
>> +        </property>
>> +        <property name="text">
>> +         <string/>
>> +        </property>
>> +       </widget>
>> +      </item>
>> +      <item>
>> +       <widget class="QLineEdit" name="image_line_edit">
>> +        <property name="focusPolicy">
>> +         <enum>Qt::NoFocus</enum>
>> +        </property>
>> +        <property name="readOnly">
>> +         <bool>true</bool>
>> +        </property>
>> +       </widget>
>> +      </item>
>> +      <item>
>> +       <widget class="QPushButton" name="image_push_button">
>> +        <property name="sizePolicy">
>> +         <sizepolicy hsizetype="Minimum" vsizetype="Fixed">
>> +          <horstretch>0</horstretch>
>> +          <verstretch>0</verstretch>
>> +         </sizepolicy>
>> +        </property>
>> +        <property name="text">
>> +         <string>Select Image...</string>
>> +        </property>
>> +       </widget>
>> +      </item>
>> +     </layout>
>> +    </widget>
>> +   </item>
>> +   <item>
>> +    <layout class="QHBoxLayout" name="properties_layout" stretch="0,0,0,0">
>> +     <property name="spacing">
>> +      <number>12</number>
>> +     </property>
>> +     <property name="topMargin">
>> +      <number>12</number>
>> +     </property>
>> +     <property name="bottomMargin">
>> +      <number>0</number>
>> +     </property>
>> +     <item>
>> +      <widget class="QGroupBox" name="size_group_box">
>> +       <property name="sizePolicy">
>> +        <sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
>> +         <horstretch>0</horstretch>
>> +         <verstretch>0</verstretch>
>> +        </sizepolicy>
>> +       </property>
>> +       <property name="title">
>> +        <string>Size</string>
>> +       </property>
>> +       <layout class="QGridLayout" name="size_layout">
>> +        <property name="verticalSpacing">
>> +         <number>10</number>
>> +        </property>
>> +        <item row="0" column="0">
>> +         <widget class="QLabel" name="width_label">
>> +          <property name="text">
>> +           <string>Width</string>
>> +          </property>
>> +         </widget>
>> +        </item>
>> +        <item row="0" column="1">
>> +         <widget class="QSpinBox" name="width_spin_box">
>> +          <property name="sizePolicy">
>> +           <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
>> +            <horstretch>0</horstretch>
>> +            <verstretch>0</verstretch>
>> +           </sizepolicy>
>> +          </property>
>> +          <property name="minimum">
>> +           <number>1</number>
>> +          </property>
>> +          <property name="maximum">
>> +           <number>9999</number>
>> +          </property>
>> +          <property name="value">
>> +           <number>9999</number>
>> +          </property>
>> +         </widget>
>> +        </item>
>> +        <item row="1" column="0">
>> +         <widget class="QLabel" name="height_label">
>> +          <property name="text">
>> +           <string>Height</string>
>> +          </property>
>> +         </widget>
>> +        </item>
>> +        <item row="1" column="1">
>> +         <widget class="QSpinBox" name="height_spin_box">
>> +          <property name="minimum">
>> +           <number>1</number>
>> +          </property>
>> +          <property name="maximum">
>> +           <number>9999</number>
>> +          </property>
>> +          <property name="value">
>> +           <number>9999</number>
>> +          </property>
>> +         </widget>
>> +        </item>
>> +        <item row="2" column="1">
>> +         <widget class="QCheckBox" name="proportional_check_box">
>> +          <property name="text">
>> +           <string>Proportional</string>
>> +          </property>
>> +         </widget>
>> +        </item>
>> +        <item row="3" column="0" colspan="2">
>> +         <widget class="QPushButton" name="reset_push_button">
>> +          <property name="sizePolicy">
>> +           <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
>> +            <horstretch>0</horstretch>
>> +            <verstretch>0</verstretch>
>> +           </sizepolicy>
>> +          </property>
>> +          <property name="text">
>> +           <string>Reset</string>
>> +          </property>
>> +         </widget>
>> +        </item>
>> +       </layout>
>> +      </widget>
>> +     </item>
>> +     <item>
>> +      <widget class="QGroupBox" name="style_group_box">
>> +       <property name="sizePolicy">
>> +        <sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
>> +         <horstretch>0</horstretch>
>> +         <verstretch>0</verstretch>
>> +        </sizepolicy>
>> +       </property>
>> +       <property name="title">
>> +        <string>Style</string>
>> +       </property>
>> +       <layout class="QGridLayout" name="style_layout">
>> +        <property name="verticalSpacing">
>> +         <number>10</number>
>> +        </property>
>> +        <item row="1" column="0">
>> +         <widget class="QLabel" name="opacity_label">
>> +          <property name="text">
>> +           <string>Opacity</string>
>> +          </property>
>> +         </widget>
>> +        </item>
>> +        <item row="2" column="1">
>> +         <widget class="QSlider" name="shadow_horizontal_slider">
>> +          <property name="maximum">
>> +           <number>50</number>
>> +          </property>
>> +          <property name="pageStep">
>> +           <number>5</number>
>> +          </property>
>> +          <property name="orientation">
>> +           <enum>Qt::Horizontal</enum>
>> +          </property>
>> +         </widget>
>> +        </item>
>> +        <item row="2" column="0">
>> +         <widget class="QLabel" name="shadow_label">
>> +          <property name="text">
>> +           <string>Shadow</string>
>> +          </property>
>> +         </widget>
>> +        </item>
>> +        <item row="0" column="0">
>> +         <widget class="QLabel" name="align_label">
>> +          <property name="text">
>> +           <string>Align</string>
>> +          </property>
>> +         </widget>
>> +        </item>
>> +        <item row="1" column="1">
>> +         <widget class="QSlider" name="opacity_horizontal_slider">
>> +          <property name="minimum">
>> +           <number>10</number>
>> +          </property>
>> +          <property name="maximum">
>> +           <number>100</number>
>> +          </property>
>> +          <property name="singleStep">
>> +           <number>10</number>
>> +          </property>
>> +          <property name="value">
>> +           <number>100</number>
>> +          </property>
>> +          <property name="sliderPosition">
>> +           <number>100</number>
>> +          </property>
>> +          <property name="orientation">
>> +           <enum>Qt::Horizontal</enum>
>> +          </property>
>> +          <property name="invertedAppearance">
>> +           <bool>false</bool>
>> +          </property>
>> +          <property name="tickPosition">
>> +           <enum>QSlider::NoTicks</enum>
>> +          </property>
>> +          <property name="tickInterval">
>> +           <number>10</number>
>> +          </property>
>> +         </widget>
>> +        </item>
>> +        <item row="0" column="1">
>> +         <widget class="QComboBox" name="align_combo_box">
>> +          <item>
>> +           <property name="text">
>> +            <string>None</string>
>> +           </property>
>> +          </item>
>> +          <item>
>> +           <property name="text">
>> +            <string>Left</string>
>> +           </property>
>> +          </item>
>> +          <item>
>> +           <property name="text">
>> +            <string>Right</string>
>> +           </property>
>> +          </item>
>> +          <item>
>> +           <property name="text">
>> +            <string>Center</string>
>> +           </property>
>> +          </item>
>> +          <item>
>> +           <property name="text">
>> +            <string>Block</string>
>> +           </property>
>> +          </item>
>> +          <item>
>> +           <property name="text">
>> +            <string>Text Top</string>
>> +           </property>
>> +          </item>
>> +          <item>
>> +           <property name="text">
>> +            <string>Text Middle</string>
>> +           </property>
>> +          </item>
>> +          <item>
>> +           <property name="text">
>> +            <string>Text Bottom</string>
>> +           </property>
>> +          </item>
>> +          <item>
>> +           <property name="text">
>> +            <string>Background</string>
>> +           </property>
>> +          </item>
>> +         </widget>
>> +        </item>
>> +        <item row="3" column="0">
>> +         <widget class="QLabel" name="blur_label">
>> +          <property name="text">
>> +           <string>Blur</string>
>> +          </property>
>> +         </widget>
>> +        </item>
>> +        <item row="3" column="1">
>> +         <widget class="QSlider" name="blur_horizontal_slider">
>> +          <property name="maximum">
>> +           <number>50</number>
>> +          </property>
>> +          <property name="pageStep">
>> +           <number>5</number>
>> +          </property>
>> +          <property name="orientation">
>> +           <enum>Qt::Horizontal</enum>
>> +          </property>
>> +         </widget>
>> +        </item>
>> +       </layout>
>> +      </widget>
>> +     </item>
>> +     <item>
>> +      <widget class="QGroupBox" name="spacing_group_box">
>> +       <property name="sizePolicy">
>> +        <sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
>> +         <horstretch>0</horstretch>
>> +         <verstretch>0</verstretch>
>> +        </sizepolicy>
>> +       </property>
>> +       <property name="title">
>> +        <string>Spacing</string>
>> +       </property>
>> +       <layout class="QGridLayout" name="spacing_layout">
>> +        <property name="verticalSpacing">
>> +         <number>10</number>
>> +        </property>
>> +        <item row="6" column="1">
>> +         <widget class="QSpinBox" name="top_spin_box">
>> +          <property name="maximum">
>> +           <number>9999</number>
>> +          </property>
>> +          <property name="value">
>> +           <number>9999</number>
>> +          </property>
>> +         </widget>
>> +        </item>
>> +        <item row="6" column="0">
>> +         <widget class="QLabel" name="top_label">
>> +          <property name="text">
>> +           <string>Top</string>
>> +          </property>
>> +         </widget>
>> +        </item>
>> +        <item row="2" column="1">
>> +         <widget class="QSpinBox" name="right_spin_box">
>> +          <property name="maximum">
>> +           <number>9999</number>
>> +          </property>
>> +          <property name="value">
>> +           <number>9999</number>
>> +          </property>
>> +         </widget>
>> +        </item>
>> +        <item row="7" column="0">
>> +         <widget class="QLabel" name="bottom_label">
>> +          <property name="text">
>> +           <string>Bottom</string>
>> +          </property>
>> +         </widget>
>> +        </item>
>> +        <item row="7" column="1">
>> +         <widget class="QSpinBox" name="bottom_spin_box">
>> +          <property name="maximum">
>> +           <number>9999</number>
>> +          </property>
>> +          <property name="value">
>> +           <number>9999</number>
>> +          </property>
>> +         </widget>
>> +        </item>
>> +        <item row="2" column="0">
>> +         <widget class="QLabel" name="right_label">
>> +          <property name="text">
>> +           <string>Right</string>
>> +          </property>
>> +         </widget>
>> +        </item>
>> +        <item row="1" column="0">
>> +         <widget class="QLabel" name="left_label">
>> +          <property name="text">
>> +           <string>Left</string>
>> +          </property>
>> +         </widget>
>> +        </item>
>> +        <item row="1" column="1">
>> +         <widget class="QSpinBox" name="left_spin_box">
>> +          <property name="maximum">
>> +           <number>9999</number>
>> +          </property>
>> +          <property name="value">
>> +           <number>9999</number>
>> +          </property>
>> +         </widget>
>> +        </item>
>> +       </layout>
>> +      </widget>
>> +     </item>
>> +     <item>
>> +      <widget class="QGroupBox" name="border_group_box">
>> +       <property name="sizePolicy">
>> +        <sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
>> +         <horstretch>0</horstretch>
>> +         <verstretch>0</verstretch>
>> +        </sizepolicy>
>> +       </property>
>> +       <property name="title">
>> +        <string>Border</string>
>> +       </property>
>> +       <layout class="QGridLayout" name="border_layout">
>> +        <property name="verticalSpacing">
>> +         <number>10</number>
>> +        </property>
>> +        <item row="2" column="0">
>> +         <widget class="QFrame" name="border_color_frame">
>> +          <property name="sizePolicy">
>> +           <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
>> +            <horstretch>0</horstretch>
>> +            <verstretch>0</verstretch>
>> +           </sizepolicy>
>> +          </property>
>> +          <property name="minimumSize">
>> +           <size>
>> +            <width>16</width>
>> +            <height>16</height>
>> +           </size>
>> +          </property>
>> +          <property name="styleSheet">
>> +           <string notr="true">background-color: #FF0000;</string>
>> +          </property>
>> +          <property name="frameShape">
>> +           <enum>QFrame::StyledPanel</enum>
>> +          </property>
>> +          <property name="frameShadow">
>> +           <enum>QFrame::Sunken</enum>
>> +          </property>
>> +         </widget>
>> +        </item>
>> +        <item row="0" column="0">
>> +         <widget class="QLabel" name="border_type_label">
>> +          <property name="text">
>> +           <string>Type</string>
>> +          </property>
>> +         </widget>
>> +        </item>
>> +        <item row="2" column="1">
>> +         <widget class="QPushButton" name="border_color_push_button">
>> +          <property name="text">
>> +           <string>Color...</string>
>> +          </property>
>> +         </widget>
>> +        </item>
>> +        <item row="4" column="1">
>> +         <widget class="QSlider" name="radius_horizontal_slider">
>> +          <property name="maximum">
>> +           <number>50</number>
>> +          </property>
>> +          <property name="pageStep">
>> +           <number>5</number>
>> +          </property>
>> +          <property name="orientation">
>> +           <enum>Qt::Horizontal</enum>
>> +          </property>
>> +         </widget>
>> +        </item>
>> +        <item row="4" column="0">
>> +         <widget class="QLabel" name="radius_label">
>> +          <property name="text">
>> +           <string>Radius</string>
>> +          </property>
>> +         </widget>
>> +        </item>
>> +        <item row="0" column="1">
>> +         <widget class="QComboBox" name="border_type_combo_box">
>> +          <item>
>> +           <property name="text">
>> +            <string>None</string>
>> +           </property>
>> +          </item>
>> +          <item>
>> +           <property name="text">
>> +            <string>Solid</string>
>> +           </property>
>> +          </item>
>> +          <item>
>> +           <property name="text">
>> +            <string>Dotted</string>
>> +           </property>
>> +          </item>
>> +          <item>
>> +           <property name="text">
>> +            <string>Dashed</string>
>> +           </property>
>> +          </item>
>> +          <item>
>> +           <property name="text">
>> +            <string>Double</string>
>> +           </property>
>> +          </item>
>> +          <item>
>> +           <property name="text">
>> +            <string>Groove</string>
>> +           </property>
>> +          </item>
>> +          <item>
>> +           <property name="text">
>> +            <string>Ridge</string>
>> +           </property>
>> +          </item>
>> +          <item>
>> +           <property name="text">
>> +            <string>Inset</string>
>> +           </property>
>> +          </item>
>> +          <item>
>> +           <property name="text">
>> +            <string>Outset</string>
>> +           </property>
>> +          </item>
>> +         </widget>
>> +        </item>
>> +        <item row="3" column="0">
>> +         <widget class="QLabel" name="border_width_label">
>> +          <property name="text">
>> +           <string>Width</string>
>> +          </property>
>> +         </widget>
>> +        </item>
>> +        <item row="3" column="1">
>> +         <widget class="QSlider" name="border_width_horizontal_slider">
>> +          <property name="minimum">
>> +           <number>1</number>
>> +          </property>
>> +          <property name="maximum">
>> +           <number>50</number>
>> +          </property>
>> +          <property name="pageStep">
>> +           <number>5</number>
>> +          </property>
>> +          <property name="orientation">
>> +           <enum>Qt::Horizontal</enum>
>> +          </property>
>> +         </widget>
>> +        </item>
>> +       </layout>
>> +      </widget>
>> +     </item>
>> +    </layout>
>> +   </item>
>> +   <item>
>> +    <spacer name="vertical_spacer">
>> +     <property name="orientation">
>> +      <enum>Qt::Vertical</enum>
>> +     </property>
>> +     <property name="sizeType">
>> +      <enum>QSizePolicy::Fixed</enum>
>> +     </property>
>> +     <property name="sizeHint" stdset="0">
>> +      <size>
>> +       <width>20</width>
>> +       <height>12</height>
>> +      </size>
>> +     </property>
>> +    </spacer>
>> +   </item>
>> +   <item>
>> +    <widget class="QDialogButtonBox" name="button_box">
>> +     <property name="orientation">
>> +      <enum>Qt::Horizontal</enum>
>> +     </property>
>> +     <property name="standardButtons">
>> +      <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
>> +     </property>
>> +    </widget>
>> +   </item>
>> +  </layout>
>> + </widget>
>> + <tabstops>
>> +  <tabstop>image_line_edit</tabstop>
>> +  <tabstop>image_push_button</tabstop>
>> +  <tabstop>width_spin_box</tabstop>
>> +  <tabstop>height_spin_box</tabstop>
>> +  <tabstop>proportional_check_box</tabstop>
>> +  <tabstop>reset_push_button</tabstop>
>> +  <tabstop>align_combo_box</tabstop>
>> +  <tabstop>opacity_horizontal_slider</tabstop>
>> +  <tabstop>shadow_horizontal_slider</tabstop>
>> +  <tabstop>blur_horizontal_slider</tabstop>
>> +  <tabstop>left_spin_box</tabstop>
>> +  <tabstop>right_spin_box</tabstop>
>> +  <tabstop>top_spin_box</tabstop>
>> +  <tabstop>bottom_spin_box</tabstop>
>> +  <tabstop>border_type_combo_box</tabstop>
>> +  <tabstop>border_color_push_button</tabstop>
>> +  <tabstop>border_width_horizontal_slider</tabstop>
>> +  <tabstop>radius_horizontal_slider</tabstop>
>> +  <tabstop>button_box</tabstop>
>> + </tabstops>
>> + <resources/>
>> + <connections/>
>> +</ui>
>>
>> === added file 'resources/images/custom_overflow.png'
>> Binary files resources/images/custom_overflow.png	1970-01-01 00:00:00 +0000 and resources/images/custom_overflow.png	2014-06-19 13:47:40 +0000 differ
>


-- 
https://code.launchpad.net/~kirkstover/openlp/wysiwyg/+merge/223745
Your team OpenLP Core is requested to review the proposed merge of lp:~kirkstover/openlp/wysiwyg into lp:openlp.


Follow ups

References